Public: anyone can read this; the crew can change it.

From a person's profile an admin opens their account: reset a password, see sign-ins, verify, suspend, and what they own #11696

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

An admin can now open a person's account from their profile and reset their access. Not done: the other admin's own confirm, the forge count, and the browser test. The new test file passes and the gate, `tsc` and design-lint are clean. A wide run of related suites had two failures: `signins.test.ts`, which I updated, and a timeout in `map-layout-budget.test.ts`, which I take to be load but did not…

shipped production is serving it deploy d20ce1b

opened this

An admin can now open a person's account from their profile and reset their access. Not done: the other admin's own confirm, the forge count, and the browser test. The new test file passes and the gate, tsc and design-lint are clean. A wide run of related suites had two failures: signins.test.ts, which I updated, and a timeout in map-layout-budget.test.ts, which I take to be load but did not re-run. The forge's check runs the full suite on push. I did not click through a preview.

What changed

  • Manage account link: on a person's profile (src/web/pages/org.ts) an admin sees one Manage account link. It opens the existing /settings/accounts/:username, now one screen:

- role and join date; - email and whether it is verified; - passkey count and whether a password is set (never the secrets); - the last five sign-ins with their address; - repository and app counts, with crew spend; - the model allowance and ceilings; - the actions.

  • Reset access: it makes a link at /signin/access/<secret>, good for one hour and one use.

- The person sets a new password there and is signed in, then lands on /settings/passkeys to add a passkey. The admin never sees the password. - The admin copies the link from the page. It is also mailed when the forge has mail set up and the person's address is verified. - A tick box also ends all their sessions; End all sessions does that alone. A new link withdraws older ones, and only the link's hash is stored.

  • Buttons: verify email, make member or guest, suspend and unsuspend (with the existing confirm), allowance and ceilings, and remove.
  • List: /settings/accounts has a search box and the same link.
  • API: POST /api/v1/admin/accounts/{username}/reset and /end-sessions, both taking dryRun, with OpenAPI entries.
  • MCP: a new tool account_access with action: "reset" | "end_sessions". It plans unless dryRun is false, and the link is shown once. I added a tool rather than an action on account_verify because that tool is on the "no confirm" list in test/conversation-confirm.test.ts. test/mcp-size.test.ts passes.
  • Audit and notice: the events are account.access_reset, account.sessions_ended and account.access_redeemed, with the admin as actor. The person reads "An admin reset your access on …" on the first page of their next browser session.
  • Migration: one migration (pinned) adds the access_link and account_notice tables and session.address. The address is recorded once a minute from the existing "last seen" write.
  • Docs: docs/features/account-access.md.

Tests: test/account-access.test.ts covers the following:

  • a non-admin sees no link and gets a 404 on the page;
  • the link works once and is refused after an hour;
  • old sessions end;
  • the audit entry names the admin;
  • the person gets the notice once;
  • an admin who is not the forge's owner is refused on another admin;
  • the MCP dry run plans first and changes nothing.

Not done, or different from the issue

  • Another admin's confirm: I did not build it. I made it owner-only instead: the first admin still here may manage another admin; any other admin is refused. Other admins get reset and end-sessions only.
  • Forges count: left out of the "what they own" counts, because I found no per-person forge count to read.
  • Last sign-ins: these are the live browsers only, so they are empty after sessions end.
  • Notice trigger: it fires on a browser's first page, not on the sign-in itself.
  • Link path: it is under /signin/access/ because a top-level /access is a reserved name and test/reserved-names.test.ts refused it.
  • Updated test: test/signins.test.ts asserted that the owner gets a 404 on another admin's account page. That is now a 200, without the token and browser lists, so I changed the test and said why in a comment.
  • Browser test: the acceptance browser test is not written; the HTTP-level test above stands in for it.
  • Gate: it asked for a Direction: line, so the last commit is an empty one carrying it.

To try: sign in as an admin and open /<someone>. Press Manage account, then Reset access, and copy the link from the toast. Open it signed out, set a password, and check /settings/accounts/<someone>.

🤖 Generated with Claude Code

Closes #11688

commented

An admin can now open a person's account from their profile and reset their access. Not done: the other admin's own confirm, the forge count, and the browser test. The new test file passes and the gate, tsc and design-lint are clean. A wide run of related suites had two failures: signins.test.ts, which I updated, and a timeout in map-layout-budget.test.ts, which I take to be load but did not re-run. The forge's check runs the full suite on push. I did not click through a preview.

What changed

  • Manage account link: on a person's profile (src/web/pages/org.ts) an admin sees one Manage account link. It opens the existing /settings/accounts/:username, now one screen:

- role and join date; - email and whether it is verified; - passkey count and whether a password is set (never the secrets); - the last five sign-ins with their address; - repository and app counts, with crew spend; - the model allowance and ceilings; - the actions.

  • Reset access: it makes a link at /signin/access/<secret>, good for one hour and one use.

- The person sets a new password there and is signed in, then lands on /settings/passkeys to add a passkey. The admin never sees the password. - The admin copies the link from the page. It is also mailed when the forge has mail set up and the person's address is verified. - A tick box also ends all their sessions; End all sessions does that alone. A new link withdraws older ones, and only the link's hash is stored.

  • Buttons: verify email, make member or guest, suspend and unsuspend (with the existing confirm), allowance and ceilings, and remove.
  • List: /settings/accounts has a search box and the same link.
  • API: POST /api/v1/admin/accounts/{username}/reset and /end-sessions, both taking dryRun, with OpenAPI entries.
  • MCP: a new tool account_access with action: "reset" | "end_sessions". It plans unless dryRun is false, and the link is shown once. I added a tool rather than an action on account_verify because that tool is on the "no confirm" list in test/conversation-confirm.test.ts. test/mcp-size.test.ts passes.
  • Audit and notice: the events are account.access_reset, account.sessions_ended and account.access_redeemed, with the admin as actor. The person reads "An admin reset your access on …" on the first page of their next browser session.
  • Migration: one migration (pinned) adds the access_link and account_notice tables and session.address. The address is recorded once a minute from the existing "last seen" write.
  • Docs: docs/features/account-access.md.

Tests: test/account-access.test.ts covers the following:

  • a non-admin sees no link and gets a 404 on the page;
  • the link works once and is refused after an hour;
  • old sessions end;
  • the audit entry names the admin;
  • the person gets the notice once;
  • an admin who is not the forge's owner is refused on another admin;
  • the MCP dry run plans first and changes nothing.

Not done, or different from the issue

  • Another admin's confirm: I did not build it. I made it owner-only instead: the first admin still here may manage another admin; any other admin is refused. Other admins get reset and end-sessions only.
  • Forges count: left out of the "what they own" counts, because I found no per-person forge count to read.
  • Last sign-ins: these are the live browsers only, so they are empty after sessions end.
  • Notice trigger: it fires on a browser's first page, not on the sign-in itself.
  • Link path: it is under /signin/access/ because a top-level /access is a reserved name and test/reserved-names.test.ts refused it.
  • Updated test: test/signins.test.ts asserted that the owner gets a 404 on another admin's account page. That is now a 200, without the token and browser lists, so I changed the test and said why in a comment.
  • Browser test: the acceptance browser test is not written; the HTTP-level test above stands in for it.
  • Gate: it asked for a Direction: line, so the last commit is an empty one carrying it.

To try: sign in as an admin and open /<someone>. Press Manage account, then Reset access, and copy the link from the toast. Open it signed out, set a password, and check /settings/accounts/<someone>.

🤖 Generated with Claude Code

commented

The check failures came from registries my change hadn't updated. I've updated them and pushed. The failing tests now pass here, bun scripts/gate.ts and bun x tsc --noEmit print nothing, and the full suite has not been re-run.

The pull adds an admin "reset access" flow on a person's profile: a one-time link to set a new password, and an end-sessions action. The new pieces were missing from these places:

  • OpenAPI route index: regenerated with bun scripts/openapi-routes.ts for the two new admin routes.
  • MCP: docs/MCP.md is regenerated with bun scripts/mcp-table.ts for the new account_access tool. I also added the tool to the expected tool list in test/mcp.test.ts.
  • Event audiences: account.access_reset, account.sessions_ended and account.access_redeemed are now classified in src/core/event_audience.ts. This was behind the failures in shard 56.
  • Cookie sites: passkeys.ts now has one more place that sets the session cookie (the access-link sign-in), so the expected count in test/cookie-flags.test.ts goes from 4 to 5.
  • Page lists: /signin/access/:token is listed as unwalkable in test/no-polling.test.ts because it needs a link an admin minted. MAX_UNFIXTURED in test/pages-budget.test.ts goes from 145 to 146.

Not confirmed:

  • Shard 52 (mcp-system-routes): passes on its own. It fails when run in the same process as pages-budget.test.ts, because they share the global perfRecord. I couldn't tell whether that is what failed in shard 52, so it may fail again.
  • looks-owner: its 90-second failure passed in a quick local run, so I treated it as load-related.
commented

Two of the three failing tests are fixed and pushed; the over-budget routes test (mcp-system-routes) may still fail in the forge's check.

Fixed. I merged the current main into the branch. The unfilled-route lists then needed the new /signin/access/:token page in them:

  • test/pages-budget.test.ts: MAX_UNFIXTURED goes from 146 to 147.
  • test/security-floor-routes.test.ts: the new page is named, with a reason, among the routes the seed can't fill.

pages-budget and security-floor-routes pass locally, as does the merged tree's tsc and bun scripts/gate.ts. I did not re-run the full suite.

Not fixed. routes lists the over-budget route first… in test/mcp-system-routes.test.ts passes on its own but fails when run in the same process as pages-budget. The route recorder (perfRecord) stops accepting new routes after 200, and pages-budget fills it. The forge's check did not compare this test against main, so I can't say whether it is caused by this pull or is an existing problem.

I tried making the test's recorded times larger and it still failed, so I reverted that edit. If it fails again, the fix is probably a way for that test to reset the recorder, which I didn't add.

Changes requested — before the latest push

Changes needed, one round:

  1. No terminal door (fix first). An admin can reset access, end sessions, search accounts and see sign-ins only on the page, the API and MCP. There is no screen in src/tui/ and no entry where AGENTS.md says. Add one that gates reads and writes as the page does: a guest sees nothing, an admin may act, and the owner-only rule for other admins holds. A write made there must reach web viewers live.
  1. Reset access has no confirm. It mints a credential that can take over an account, and when the owner resets another admin it takes over an admin. The issue asked for a confirm on the other-admin case, and the pull drops it for an owner-only rule. Wrap Reset access and End all sessions in confirmAction, as Suspend and Remove already are. Then the other-admin confirm the issue asked for exists.
  1. Design (docs/DESIGN.md).

- Principle 1: the Actions block explains itself in a long muted paragraph. The tokens block, the access form and the docs say the same thing again. Cut each to one sentence. - Components: accessForm hand-rolls <button class="btn" data-variant="primary"> instead of btn(...). The endSessions checkbox is bare markup, so use the named field or checkbox component. - Principle 7: the admin-only note "nothing else here applies" is explanation, not a row. Drop it, or make it a row. - Run bun scripts/design-lint.ts again after these changes.

  1. Mislabelled data. "Last sign-ins" shows only live sessions, so it is empty right after End all sessions. Either keep sign-in history, or name the section "Signed in now". Mason's own note says the section does not show sign-ins.
  1. The notice can be lost. takeNotice marks it seen inside resolveAuth on any HTML GET of a fresh session. If that response is a redirect or a non-page() render, the notice is consumed and never shown. Mark it seen where it is rendered. Also, the notice only fires for a browser with a null last_seen_at, so a browser that is already signed in and was not ended never sees it. Say so in the docs, or fix it.
  1. Redeem burns the link on a hash failure. spendAccessLink runs before rehash. If rehash fails, the link is gone and the person gets "invalid". Spend it and set the password in one transaction.
  1. Tests. Mason notes mcp-system-routes fails when it shares a process with pages-budget, because perfRecord fills to 200 routes. Fix it so the check stays green, for example by resetting the recorder in pages-budget. Also add tests for the redeem refusal paths (weak or too-long password leaves the link unspent), search filtering, and the back field of roleAct rejecting non-local paths.
  1. Direction: this is a feature while the pipeline-first items are open. It is acceptable as scoped, but keep follow-ups small and single-purpose.
commented

This pushes the changes for all eight review points. bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are clean. About 215 related test files pass, including the new test/tui-account-access.test.ts. I did not run the full suite; scripts/fast.ts named 2,553 test files, so I ran a hand-picked set instead.

What changed

  1. Terminal door. src/tui/screens/account_access.ts adds an "Account access" screen, registered once in ENTRIES after people.

- It lists people with the browsers they are signed in on now. / searches, r resets access (after a confirm), and e ends all sessions (after a confirm). - It calls resetAccess and endAllSessions, so the gates are the page's. A guest sees nothing, an admin may act, and only the owner may act on another admin. - Each write appends the same bus events, so web viewers see it live. A test checks that the event reaches the bus.

  1. Confirms. Reset access and End all sessions now go through /-/confirm, with new entries in DESTRUCTIVE. The "also sign them out everywhere" tick is a field({type:"checkbox"}) and rides the confirm through carries. Both confirms apply to every account, so the other-admin confirm exists.
  2. Design.

- The Actions, tokens, access-form and docs paragraphs are now one sentence each. - The access form uses btn(). - The admin-only note is gone.

  1. Mislabelled data. The section is now "Signed in now". I chose the rename over keeping sign-in history, and the docs say history is not kept.
  2. Notice.

- page() calls takeNotice only when the notice is shown. A redirect, an API call or a HEAD no longer consumes it. - The last_seen_at null gate is gone, so a browser that is already signed in sees it too. - sessionOf now returns a notice flag from an EXISTS subquery in its existing session query. My first version added a query per page and broke the pages-budget SQL budgets, and the flag removes that cost.

  1. Redeem. The password is hashed first, then spendAccessLink and setPasswordHash run in one transaction. A refusal or a failed hash leaves the link unspent. hashPassword and setPasswordHash are split out of changePassword in src/core/users.ts for this.
  2. Tests.

- New: weak and too-long passwords leave the link unspent, search filtering, roleAct back rejecting non-local paths, the confirm flow, and the notice surviving a non-page response. - I added PerfRecord.forget() and call it in pages-budget and at the top of mcp-system-routes. security-floor-routes and no-polling also fill the recorder, so resetting only in pages-budget was not enough.

Not done

  • The three account.* events stay in READER_KINDS as in the earlier commit; I did not re-audience them.
  • No sign-in history table.

Try it

  • Open /settings/accounts/<name> as an admin.
  • Press Reset access and confirm.
  • Open the link in a private window.
  • In the terminal, open "Account access" from the palette.

🤖 Generated with Claude Code

commented

I merged main into the branch and pushed it. Typecheck is clean, and I ran only test/mcp-system-routes.test.ts and test/pages-budget.test.ts, which pass. I did not run the full suite; the forge runs it on the push. bun scripts/gate.ts ran, but I only looked at the last lines of its output and didn't check its exit status.

There were two conflicts, both from the same overlap:

  • src/perf/record.ts: my branch added forget() to the perf record to drop every pattern's samples. main had added clear() doing the same thing. I kept main's clear() and dropped my forget().
  • test/mcp-system-routes.test.ts: I took main's version, which already calls perfRecord.clear().
  • test/pages-budget.test.ts: I changed its one perfRecord.forget() call to perfRecord.clear(), since forget() no longer exists.

Behaviour is unchanged.

commented

The failing test was a site-admin person is offered the whole roster in test/palette-roster.test.ts. My branch adds an "Account access" palette entry for admins, and the test's expected list didn't include it. I added ["account-access", "Account access", "search people, reset access, end sessions"] after "People", which is where the palette puts it.

palette-roster.test.ts now passes (15 of 15) and tsc is clean. I ran no other tests; the forge's check will run the full suite on the push.

Approved

Earlier asks are met in the change as it stands: the terminal door (src/tui/screens/account_access.ts, registered in entries, gated through the same actions, events reach the bus), confirms on reset and end-sessions, the how-to in docs/features/account-access.md (README untouched), the redeem path hashing before the spend inside one transaction, the notice taken only when a page shows it, and tests for each. The latest change only adds the palette entry to the roster test, which is correct. I could not diff against 74c6ee8a0b6b because the clone lacks that commit, so I read the change as a whole. No new findings.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs