docs(auth): spec for per-Agent identity, login & user management (HT-54) - #69
Conversation
Replaces HT-51's single operator password with real per-Agent accounts per that ticket's mandate. Free AGPL core = username/password via an auth-provider seam; Google SSO / magic-link / passkey are marketplace modules registering against the seam (the concrete AGPL §7 boundary, HT-5). agents + agent_auth_identities (one Agent, many login methods); assignee graduates 'me' -> Agent FK; Admin/Agent roles; /setup first admin; invite-primary + temp-password provisioning; session carries sub. UX modelled on FreeScout (black-box only), rendered in our design system. Fable-authored, independently Fable-reviewed against charter + codebase; all 9 review findings applied (see changelog). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe pull request adds an Agents and Authentication specification covering Agent identity models, provider-based authentication, engine and web flows, session trust, provisioning, security requirements, rollout changes, and scope boundaries. ChangesAgents and authentication
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant Agent
participant Web
participant Engine
participant Database
Agent->>Web: Submit credentials
Web->>Engine: POST /auth/verify
Engine->>Database: Resolve provider identity
Database-->>Engine: Agent identity and status
Engine-->>Web: Session cookie v2 or generic 401
Web->>Engine: Request with asserted Agent ID
Engine->>Database: Re-check Agent status
Engine-->>Web: Authorized response
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@specs/auth/agents-and-auth.md`:
- Around line 211-216: Update specs/auth/agents-and-auth.md lines 211-216 to
require an advisory transaction lock or a serializable transaction with retry
around active-admin demotion, disabling, and deletion, while retaining the count
predicate as an additional guard. Update specs/auth/agents-and-auth.md lines
230-234 to apply the same serialization to first-admin bootstrap so concurrent
requests cannot create multiple initial Agents; retain the NOT EXISTS predicate
as an additional guard.
- Around line 42-46: Update the Agent definition in the terminology section to
describe an Agent directly as a human member of the support staff who operates
the inbox, removing the “user” characterization. Preserve the distinction that
Agents are human support staff, Assistants are AI actors, and “Users” is
reserved only for explicitly referenced FreeScout terminology.
- Around line 355-363: Define temporary-password lifecycle state for the
fallback authentication path, including an expiry or forced-change indicator and
the enforcement point during authentication. Require an Agent using a temporary
password to change it before normal authenticated access, and clear the
temporary state after a successful change; update the temp-password flow and
first-login activation behavior accordingly.
- Around line 73-78: Resolve the lifecycle contradiction across the Agent status
and authentication sections: choose one model and apply it consistently. Either
create temporary-password Agents as active with an explicit
required-password-change state, or allow password-authenticated invited Agents
and atomically promote them during `/auth/verify`; update the definitions,
provisioning paths, login behavior, and first-login activation requirements to
match the chosen model.
- Around line 86-96: Enforce the one-password-identity-per-agent invariant in
the schema and core identity service. Add a partial unique index on
agent_auth_identities.agent_id for rows where provider = 'password', and update
the core identity service to reject or prevent creating a second password
identity for the same agent while preserving existing behavior for other
providers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c9449784-6b86-49d9-a0d5-0b0533f74a21
📒 Files selected for processing (1)
specs/auth/agents-and-auth.md
- admin-set-password Agents are created 'active' outright; 'invited' is exclusively the invite path's status (resolves the lifecycle contradiction) - rename the fallback honestly: admin-set password, not temporary; forced password change deferred to §11 - one password identity per Agent enforced by a partial unique index + the identity service - /setup and last-admin mutations serialized via pg_advisory_xact_lock; guard predicates kept as inner guards (READ COMMITTED races) - §2 vocabulary wording Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
specs/auth/agents-and-auth.md (1)
87-103: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winEnforce the provider-specific
secret_hashinvariant.The schema permits
provider='password'with aNULLhash and non-password providers with arbitrary hashes, even though the contract definessecret_hashas password-only. Add aCHECKconstraint or explicitly require the identity service to reject these malformed rows before they reach authentication/reset flows.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@specs/auth/agents-and-auth.md` around lines 87 - 103, Add a schema CHECK constraint to agent_auth_identities enforcing that provider='password' requires a non-NULL secret_hash, while every non-password provider requires secret_hash to be NULL. Keep the existing uniqueness constraints and indexes unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@specs/auth/agents-and-auth.md`:
- Around line 268-271: Update the agent lifecycle documentation covering PATCH
/agents/{id} and /password to restrict status changes to defined valid
transitions, reserve invited → active with password creation for atomic invite
acceptance, and reject password writes for invited Agents. Ensure the documented
rules prevent arbitrary statuses, disabling an invited Agent, or adding a
password while status remains invited, while preserving the existing
provisioning behavior.
- Around line 268-271: Update the POST /api/v1/agents provisioning contract and
its corresponding section around the additional referenced lines to require a
configured EmailSender before accepting sendInvite=true; reject the request
before Agent insertion when no sender exists, or require the password-based
admin fallback instead. Preserve the existing invited/no-password behavior when
an EmailSender is configured and the active/password behavior for the
alternative path.
---
Outside diff comments:
In `@specs/auth/agents-and-auth.md`:
- Around line 87-103: Add a schema CHECK constraint to agent_auth_identities
enforcing that provider='password' requires a non-NULL secret_hash, while every
non-password provider requires secret_hash to be NULL. Keep the existing
uniqueness constraints and indexes unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 55cc99ca-f5bf-472f-b39b-c19bcc149451
📒 Files selected for processing (1)
specs/auth/agents-and-auth.md
…ule per-endpoint (HT-54) - §3.4: agent_mailbox_access modelled in this migration (TJ decision), schema-only, nothing consults it in v1 - §8: acting-Agent header rule made precise — required on /agents/*, /auth/me, PUT .../assignee; other inbox endpoints stay bearer-only this increment (no third breaking change), disabled-Agent consequence stated - §12: decision 4 confirmed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…led only (HT-54) invited exits solely via invite acceptance (or delete/re-create); password writes on an invited Agent are refused. Closes the incoherent states an unconstrained status field permitted (CodeRabbit round 2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@specs/auth/agents-and-auth.md`:
- Around line 498-502: The draft.3 changelog entry has an incomplete causal
phrase. Update the sentence in the draft.3 bullet to state that the closed
incoherent states were permitted by an unconstrained `status` field, preserving
the existing technical details and meaning.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6e9514ff-f8a2-43f7-9340-7ce66246722a
📒 Files selected for processing (1)
specs/auth/agents-and-auth.md
Restores PR references the changelog consolidation dropped. The previous commit message claimed "ticket and PR references are kept as navigable provenance" while the diff deleted PR #82, #88, #69 and #85 from the consolidated entries. Claim and diff now agree. Rewrites 28 further sites the earlier passes missed. They used phrasings the patterns did not cover -- "an earlier revision", "the first draft wrongly hung", "retracted here rather than quietly patched", "corrected there in this same review round", "review round 2". Each is restated forward and keeps its technical lesson: store/mailboxes.ts "Why not tell the operator to disconnect and retry? Because that instruction would be false: ..." providers/inbound-email.ts "Why not return a NormalizedInboundEmail? That puts the parse inside the provider ..." gmail-reconcile.ts "Why not just ack? The tempting reasoning is ..." Also adds a supersession note to decision point 4 in agents-and-auth.md, which still recorded "no scoping behavior or UI" while section 3.4 manages real grants. The recorded decision is left intact; the note points at what superseded it. Two artifacts from the previous pass fixed: an orphaned closing paren in substrate-v1.md, and a substitution that made a date the grammatical subject in migrate.ts. Verified: typecheck, web typecheck, lint clean. Every edit is inside a comment or markdown prose; no source line outside a comment changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec for review — no code, contract only. Replaces HT-51's single operator password with real per-Agent identity, per that ticket's own "replace, not extend" mandate. HT-54.
The core idea
Free AGPL core = username/password, delivered through an auth-provider seam. Google SSO / magic-link / passkey are licensed marketplace modules that register against the seam — the concrete instance of the AGPL §7 module-marketplace boundary counsel is defining (HT-5). The
agent_auth_identitiestable (one Agent, many login methods) lets a premium provider attach with zero core-schema change.What it covers
Data model (
agents,agent_auth_identities,assignee→Agent FK) · the seam + identity-service · Admin/Agent roles · engine API (/auth/*,/agents/*,/setup) · session-carries-sub· invite-primary + temp-password provisioning · the six web screens (modelled on the FreeScout UX I observed black-box only — its source stays unread — rendered in our design system, Agent/Team vocabulary) · security · rollout.How it was produced (per the delegation ladder)
Fable-authored (judgment-dense, don't-delegate floor), then an independent parallel Fable-tier adversarial review against the charter,
agent-inbox-v1.md, the live schema, and HT-51's shipped auth. It surfaced 9 real findings incl. 3 blockers — all applied by me (see the changelog): a mid-session identity drop on the cookie-refresh path, the acting-Agent header as a realapi.tschange + an engine status re-check (bounds disabled/deleted Agents whose cookie is still valid), atomic/setup/last-admin/invite (no check-then-act races), uniform-401login (no status leak), temp-password activation, a scoped §7 boundary claim + identity-service seam so modules never write core tables directly, invites via theEmailSendertransport (notsendReply), and immutable email in v1.Your calls (spec §12)
Three confirmed (Admin+Agent,
/setup, both-provisioning); three open with my recommendation: per-Agent mailbox scoping (recommend defer/don't-build — avoid dead schema), profile-field scope (recommend lean + timezone), acting-Agent trust model (recommend header-assertion under the service token, per-Agent tokens later). Note plainly (§5/§8): aHELPTHREAD_API_TOKENholder still bypasses role checks — the 403s stratify humans behind the web app, not the service channel.Once you're happy with the contract, I orchestrate the build (engine + web) as a reviewed, gated wave.
🤖 Generated with Claude Code
Summary by CodeRabbit
assignee_agent_idinstead ofconversations.assignee, requiring UI/API rollout updates.