feat(web): per-Agent login, session identity & team management — web layer (HT-54) - #71
Conversation
Session payload becomes {v:2, iat, sub} — sub is the signed-in Agent's
id, threaded from mintSessionCookie's now-required first parameter so
the compiler rejects any identity-less re-mint. middleware.ts's
sliding-refresh re-stamp threads session.payload.sub through instead
of silently dropping it. PUBLIC_PATHS gains /setup; a new
PUBLIC_PREFIXES rule covers /invite/{token}. uiAuthConfig() drops
HELPTHREAD_UI_PASSWORD entirely — the engine is the verification
authority now (specs/auth/agents-and-auth.md §8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Types (Agent, SelfAgent, AuthProviderDescriptor) and typed functions
(getAuthProviders, postSetup, postVerify, getMe, listAgents,
createAgent, getAgent, patchAgent, deleteAgent, setAgentPassword,
resendInvite, acceptInvite) typed 1:1 against the engine branch's
src/api/agents.ts handlers, including the {agent}/{agents}/
{providers,needsSetup}/inviteSent wrapper shapes.
request() gains an internal actingAgent option: verifies the session
cookie via next/headers and attaches X-Helpthread-Agent-Id, throwing
a session-expired ApiError (no engine call) when there is no valid
session to assert. Per spec §8's pinned rule, the header rides on
every /agents/* call, /auth/me, and putAssignee — never on the
pre-session bootstrap endpoints or the other existing conversation
calls. A 401 that carried the header now gets a NEW SESSION_ERROR_DIGEST
(auth-error.ts) instead of AUTH_ERROR_DIGEST, so the two failure modes
("your session is stale" vs "the deployment's service token is bad")
route to different recoveries.
putAssignee's body becomes {assigneeAgentId} (was {assignee: 'me'|null})
— the breaking change specs/auth/agents-and-auth.md §10 calls out.
ConversationSummary.assigneeAgentId replaces the old assignee field.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T-54) loginAction/setupAction/acceptInviteAction call postVerify/postSetup/ acceptInvite and mint the session cookie from the returned Agent's id — the engine is the sole verification authority now (scrypt hash-at-rest, constant-time compare, no account enumeration, spec §4/§9). The scryptSync/timingSafeEqual plaintext-env compare from HT-51 is deleted along with the ~500ms failure delay: that delay blunted a scripted guesser against a web-side plaintext compare that no longer exists: a web-side sleep on top of the engine's own timing posture adds nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four new/extended public and shell surfaces (specs/auth/agents-and-auth.md
§7), each carrying LoginScreen's existing "NEW designed surface — requires
TJ fidelity sign-off" flag and reusing its two documented ds/ workarounds
(native password input, Button.requestSubmit()) rather than inventing new
ones:
- /setup (SetupScreen, public): name/email/password+confirm, zero-Agents-
guarded — the page redirects to /login once an Agent already exists.
- /login (LoginScreen, extended): renders one email+password form per
kind:'credentials' provider from GET /auth/providers; redirects to /setup
when needsSetup.
- /invite/{token} (InviteAcceptScreen, public): set password + confirm;
the engine's 401 (invalid/expired token) renders inline as a calm error
— there's no separate token-validation endpoint to pre-check against.
- AppError.tsx gains a SessionExpired branch for SESSION_ERROR_DIGEST: signs
out via the existing logoutAction (clears the cookie, redirects to
/login) instead of showing AuthFailure's deployment-token copy, which
would be the wrong story for a stale per-Agent session.
TopBar's avatar menu now shows the signed-in Agent's name/email/initials
(root layout.tsx fetches getMe(), swallowing a 401 the same way it already
swallows the notifications fetch — this layout has no error boundary above
it) and "Your profile" routes to /settings/team/{selfId}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/settings/team (admin-only UI; roster cards with initials, role chip,
invited/disabled badges, client-side search), /settings/team/new (role,
first/last name, email, invite-or-admin-password provisioning — "Send
an invite email" defaults on with "An invite can be sent later," an
admin-set password field appears only when it's off), and
/settings/team/{id} (name/timezone editable by self-or-admin, role and
Disabled toggle admin-only — Disabled hidden on self so an admin can't
lock themselves out here, change/reset password, resend invite while
invited, and Delete for admin-on-another using ConversationScreen's
two-step arm pattern, never confirm()).
A non-admin visiting /settings/team or /settings/team/new — or someone
else's /settings/team/{id} — is redirected to their own profile instead
of ever reaching the engine's 403: the simpler of the brief's two
options, and it keeps an engine 403 from ever rendering as a crash.
agent-actions.ts is the write path: unlike actions.ts's conversation
mutations (bearer-only, so each action re-verifies the session cookie
itself), every call here goes through api.ts's actingAgent option,
which already verifies the session and refuses to call the engine
without one — no redundant hasValidSession() gate needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ConversationScreen's assignee control now lists the Agent roster (listAgents(), ACTIVE Agents only) instead of the old single-operator Anyone/Me toggle: "Anyone" (null), "Assign to me" (resolves selfId from getMe()), then every other active Agent by name. putAssigneeAction's signature follows the api.ts body-shape change (assigneeAgentId). Unassigned/Mine/Assigned (InboxScreen, folder-counts.ts) now split on assigneeAgentId against the viewing Agent's own id, not the retired 'me' sentinel — every route that renders these folders (the shell layout's rail counts, /dashboard, /inbox/[folder], /conversations/[id]) now fetches getMe() to supply that id. Next's fetch request memoization collapses the repeated getMe() calls within one render pass to a single network round trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The env-var docs now describe the /setup-first-run + per-Agent-login story (specs/auth/agents-and-auth.md §8) instead of HT-51's single shared operator password, and the "Shipped so far" section lists the Agents & Authentication surfaces this branch adds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two-lens adversarial review + live browser drive of the full stack:
- invited-Agent Save dead-end (MAJOR): an admin Save always bundled
`status`, which the engine's closed lifecycle 409s on invited Agents —
every edit of a pending invite failed; status is now omitted for invited
- timezone select gains 'UTC': Intl.supportedValuesOf('timeZone') does not
include it (verified live), so the engine-default 'UTC' fell back to the
first option and a Save silently rewrote it to Africa/Abidjan
- role segmented controls: broken label htmlFor (no matching control)
replaced with semantic <fieldset aria-label> per biome a11y
- setupAction: non-409 failures now show generic copy, never raw internals
- invite-accept: 8-char helper text (consistency with /setup)
- LoginScreen doc: the email field's TextInput limitation added to the
HT-52 tracking note
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe web application shifts from shared operator authentication to per-Agent accounts, signed Agent sessions, setup and invite flows, Agent administration, mailbox permissions, Agent-based assignment, and identity-aware inbox navigation. ChangesPer-Agent authentication and onboarding
Team management
Agent-aware inbox
Authenticated shell
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 `@web/src/components/AgentProfileScreen.tsx`:
- Around line 93-103: Reset all local profile state whenever the Agent changes,
including name, timezone, role, disabled status, password fields, validation
errors, and delete-arming state, keyed by agent.id in the AgentProfileScreen
component; alternatively key the component by agent.id from the route page so it
remounts for each agent. Ensure navigation between agents cannot retain the
previous agent’s values when patchAgentAction submits updates.
In `@web/src/components/ConversationScreen.tsx`:
- Around line 1022-1029: Handle rejected server-action promises consistently: in
web/src/components/ConversationScreen.tsx lines 1022-1029, update updateAssignee
to catch request rejection and restore the previous assignee; in
web/src/components/NewAgentScreen.tsx lines 114-139, catch the relevant action
rejection and populate the form error; in
web/src/components/AgentProfileScreen.tsx lines 109-180, add rejection handling
to the save, password, invite, and deletion flows while preserving each flow’s
existing error behavior.
- Around line 673-675: Update the conversation-ID reset effect in
ConversationScreen to also reset assigneeAgentId from the newly selected
conversation’s assigneeAgentId, alongside the existing state resets. Ensure
navigating between conversations cannot retain or mutate the previous
conversation’s assignee.
In `@web/src/components/InviteAcceptScreen.tsx`:
- Around line 93-106: Update the error branch in InviteAcceptScreen so
recoverable acceptInviteAction failures, including “Could not reach the server,”
keep the invite form and retry controls visible while displaying the error above
them. Only hide the form when the action provides an explicit terminal-error
discriminator for an expired or otherwise invalid invite.
- Around line 43-57: Add overflowY: 'auto' to the fixed container style in
InviteAcceptScreen’s returned root div, matching SetupScreen’s behavior so
invite controls remain accessible and the form scrolls on short viewports.
🪄 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: a7249b8a-9633-4e76-af50-3c69f3d361b8
📒 Files selected for processing (32)
web/README.mdweb/src/app/(shell)/conversations/[id]/page.tsxweb/src/app/(shell)/inbox/[folder]/page.tsxweb/src/app/(shell)/layout.tsxweb/src/app/dashboard/page.tsxweb/src/app/invite/[token]/page.tsxweb/src/app/layout.tsxweb/src/app/login/page.tsxweb/src/app/settings/team/[id]/page.tsxweb/src/app/settings/team/new/page.tsxweb/src/app/settings/team/page.tsxweb/src/app/setup/page.tsxweb/src/components/AgentProfileScreen.tsxweb/src/components/AppError.tsxweb/src/components/ConversationScreen.tsxweb/src/components/InboxScreen.tsxweb/src/components/InviteAcceptScreen.tsxweb/src/components/LoginScreen.tsxweb/src/components/NewAgentScreen.tsxweb/src/components/SetupScreen.tsxweb/src/components/TeamListScreen.tsxweb/src/components/TopBar.tsxweb/src/lib/actions.tsweb/src/lib/agent-actions.tsweb/src/lib/api-types.tsweb/src/lib/api.tsweb/src/lib/auth-actions.tsweb/src/lib/auth-error.tsweb/src/lib/folder-counts.tsweb/src/lib/format.tsweb/src/lib/session.tsweb/src/middleware.ts
- AgentProfileScreen keyed by agent.id (state re-seeds on A→B navigation) - ConversationScreen's per-conversation reset re-seeds assigneeAgentId (deps stay conversation.id-only — re-seeding on same-conversation field refresh would wipe an open composer; documented ignore) - rejected server-action invocations normalized everywhere (assignee rollback; NewAgent form error; profile flows via an invokeAction helper; invite accept) - invite-accept: container scrolls on short viewports; transient failures keep the form (retry) while a dead token shows the terminal panel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Manage ▾ is the global-admin doorway (Settings, Team) — admins only, whole trigger; avatar menu is personal-only (Your Profile, Log out) - routes: /settings/team* → /manage/agents* ; per-Agent area gains a left sidebar (Profile / Permissions) via AgentDetailShell - new Permissions screen: All/None quick links + per-mailbox checkboxes (admin targets get the implicit-access note), wired to the as-built engine mailbox-access endpoints - keyboard shortcuts moved under Settings; ? key unchanged Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Amended per the maintainer's admin-IA fidelity review (77c7dd2): |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/components/TeamListScreen.tsx (1)
79-80: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse
Agentfor human support staff.
web/src/components/TeamListScreen.tsx#L79-L80: change “Invite your first teammate” to “Invite your first Agent.”web/README.md#L32-L37: replace “single shared operator password” with wording that does not label staff as operators.As per coding guidelines, use
Agentsexclusively for human support staff.🤖 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 `@web/src/components/TeamListScreen.tsx` around lines 79 - 80, Update the empty-state text in TeamListScreen.tsx to say “Invite your first Agent.” Also revise the wording in web/README.md at lines 32-37 to replace “single shared operator password” with terminology that does not call human support staff operators, using “Agent” or “Agents” consistently.Source: Coding guidelines
🤖 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 `@web/src/components/AgentPermissionsScreen.tsx`:
- Around line 61-70: Update save() to catch promise rejections from
putAgentMailboxesAction, ensuring invocation failures display the same failure
toast instead of escaping the transition. Follow the existing error-handling
pattern used by NewAgentScreen while preserving the current result-based
!result.ok handling and success toast.
---
Outside diff comments:
In `@web/src/components/TeamListScreen.tsx`:
- Around line 79-80: Update the empty-state text in TeamListScreen.tsx to say
“Invite your first Agent.” Also revise the wording in web/README.md at lines
32-37 to replace “single shared operator password” with terminology that does
not call human support staff operators, using “Agent” or “Agents” consistently.
🪄 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: 8e24e371-aedd-4e5f-89fa-99db7047802d
📒 Files selected for processing (16)
web/README.mdweb/src/app/manage/agents/[id]/page.tsxweb/src/app/manage/agents/[id]/permissions/page.tsxweb/src/app/manage/agents/new/page.tsxweb/src/app/manage/agents/page.tsxweb/src/components/AgentDetailShell.tsxweb/src/components/AgentPermissionsScreen.tsxweb/src/components/AgentProfileScreen.tsxweb/src/components/NewAgentScreen.tsxweb/src/components/SettingsScreen.tsxweb/src/components/ShortcutsProvider.tsxweb/src/components/TeamListScreen.tsxweb/src/components/TopBar.tsxweb/src/lib/agent-actions.tsweb/src/lib/api-types.tsweb/src/lib/api.ts
…creen (HT-54) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
General / Appearance / Keyboard shortcuts down a left rail (the same FolderItem rail the Agent detail area uses) — Settings sections are the injection points HT-56 and modules extend; client-side section state within the one route until sections gain depth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A theme is a personal preference — the three-scope rule puts it in the personal scope, not Manage → Settings (TJ). Device-local persistence for now; account-synced preference rides HT-61. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The web half of specs/auth/agents-and-auth.md (HT-54), on top of #70's engine. Deploys coordinated with the engine (spec §10): both breaking changes (assignee shape, session v2) land in one synchronized deploy; nothing auto-deploys.
What's here
sub(the Agent id);mintSessionCookie(sub)requires it at the type level, and the middleware's sliding refresh threads the verifiedsub(the spec's called-out silent identity-drop trap). v1 cookies fail closed → one re-login.request()reads+verifies the session cookie and attachesX-Helpthread-Agent-Idexactly per the spec's per-endpoint rule; a 401 on a header-carrying call routes to a new SESSION digest (log in again), bearer 401s keep the AuthFailure path.HELPTHREAD_UI_PASSWORDare retired./setup, per-Agent/login(driven byGET /auth/providers),/settings/team(list+search),/settings/team/new(invite-primary + admin-set password),/settings/team/[id](profile: role, disable, password, timezone, two-step delete, resend invite),/invite/[token]. Avatar menu wired to the real Agent./auth/me, Mine/Unassigned/Assigned computed onassigneeAgentId.Five net-new designed surfaces beyond the frozen prototype, each flagged in its module doc: SetupScreen, TeamListScreen, NewAgentScreen, AgentProfileScreen, InviteAcceptScreen (LoginScreen extended). All composed from
ds/**primitives —git diff -- web/src/components/ds/is empty. Known sign-off-eye items: role chip vs Disabled pill render similarly on the team list; dsTextInputlimits the login email field's autocomplete semantics (HT-52 note extended).Review & verification
UTC(not inIntl.supportedValuesOf) and would have silently rewritten UTC Agents' timezones.hti.token (set password → signed in) and replay rejected with the calm invite-invalid copy; invited-Agent edit Save persists post-fix; timezone shows UTC.next build✅ (all routes dynamic).src/**only) — session/middleware behavior is covered by the engine-side suite + the browser drive; noted, not silently skipped.Charter: no new dependencies; no
ds/**edits; Agents/Team vocabulary throughout; FreeScout used as black-box UX reference only.🤖 Generated with Claude Code
Summary by CodeRabbit
/setup, per-agent email/password login, and public invite acceptance via/invite/{token}(with invalid/expired handling)./manage/agents, including agent profile management and mailbox permissions.