Repository navigation
feat(server): first-class API key reset (self-service + org-admin) - #3
Open
capchase-bot wants to merge 14 commits into
Open
capchase-bot wants to merge 14 commits into
capchase-bot wants to merge 14 commits into
Conversation
Lost/leaked API keys have no recovery path today — reissuing one required a direct DB write. resetApiKey rotates the hash and records an api_key_reset event (self vs admin method) so the action is auditable.
POST /auth/reset-key lets a user rotate their own key. POST /orgs/:orgId/members/:userId/reset-key lets an org admin rotate a member's key when they've lost it, using the same resetApiKey op so both paths get the audit trail. A user with zero orgs (should not happen given personal orgs are auto-created) falls back to a raw rotation with no event, since there's no orgId to attach it to.
Gives users and org admins a way to rotate a lost/leaked key from the CLI instead of needing direct DB access. Self-reset saves the new key to config like registration does; the admin path never writes the member's key to the admin's own config.
Self-reset and admin-reset scenarios against a real daemon: old key 401s, new key works. Also updates the stale comment on the FUSE auth-expired test, which still fakes revocation by corrupting the local config — now that resetApiKey exists, the comment explains why that test doesn't just call it (it's exercising the mount's read path for an already-invalid key, not the reset op).
Keeps docs/openapi.json in sync with the two new routes, per the CI freshness check (sync-openapi.ts && git diff --exit-code).
Adds both CLI commands to the SKILL.md command tables (plus trigger phrases so the skill fires on "lost my api key" style prompts), and tells registration-flow readers in docs/api-reference.md how a lost key is recovered.
…rs --json api-client.ts resolves config.apiKey before config.auth.apiKey, but reset-key only ever wrote auth.apiKey. A user with a persisted top-level apiKey (e.g. from the dashboard onboarding path) got a stale, now-invalid key after a successful reset. Write both fields so whichever one the client reads holds the fresh key. Also honor the root --json flag like sibling commands (member reset-key, org list, ...) do.
… their own email An org admin resetting their own email via 'member reset-key' had their server-side credential invalidated with no local persistence, locking them out of their own session. Detect the self-target case (compare against client.getMe()) and save the fresh key to config the same way auth reset-key does.
…ting the key client.getMe() was called after the reset POST, using the caller's own key — but a self-target reset just invalidated that key server-side, so the identity check itself 401'd. Move the getMe() call before the reset so it still has a valid credential. Caught by the new e2e admin-self-target-reset test.
…it-event gap Match resetApiKey's atomicity guarantee for the orgless fallback path. Recording an api_key_reset event like the org-scoped path does isn't possible here: events.orgId is a NOT NULL FK to orgs, and this path by definition has no orgId to attach one to (blocked — see PR discussion, no schema change made unilaterally). Adds direct core-layer coverage: no CLI/HTTP path can reach this state (every registration auto-creates a personal org whose last admin can never be removed), so the test simulates it by stripping org membership after creation, same precondition the /auth/reset-key route checks for.
…et paths Adds three regression cases that would have caught the config-field mismatch (auth.ts) and self-lockout (member.ts) bugs fixed in prior commits on this branch: - auth reset-key --json returns structured output - reset-key updates whichever config field api-client.ts actually reads (simulates the dashboard-onboarding shape where only the top-level apiKey field is populated) - an admin resetting their own email via member reset-key persists the fresh key instead of locking themselves out Also clears AGENT_FS_API_KEY/AGENT_FS_API_URL in testEnv()'s base env, extending the existing stray-host-env defense (previously only covered AGENT_FS_DEFAULT_ORG_ID and friends): an agent-swarm worker's own real agent-fs credentials were leaking into runRaw() calls, pointing config-only-auth tests at production instead of the ephemeral test daemon.
capchase-bot
commented
Sep 1, 2026
capchase-bot
left a comment
Author
There was a problem hiding this comment.
Verdict: APPROVE-equivalent
Reviewed at head 10579771. CI is green (run); the added coverage addresses the original five review requests.
- Verified fixed — stale persisted credential.
auth reset-keynow writes bothapiKeyandauth.apiKey, matching the precedence inApiClient. The persisted-config regression, including a follow-upauth whoami, is inscripts/e2e.ts. - Verified fixed — admin self-target reset.
member reset-keyresolves the caller before rotation and persists the returned key only for self-targets. The end-to-end self-target test verifies a subsequent authenticated command inscripts/e2e.ts. - Partially fixed — acceptable — orgless reset.
events.orgIdis a non-null FK, so this state cannot produce the normal event without a schema/design change. The fallback is now transactional and documented inusers.ts. Supported flows make the state unreachable: creation makes a personal org (users.ts), and removal from one is rejected (orgs.ts). The direct DB-level regression test is therefore the appropriate way to cover the defensive fallback (identity.test.ts). - Verified fixed —
--json.auth reset-keyreads the root option and returns only JSON; its E2E assertion is inscripts/e2e.ts. - Verified fixed — test gaps. The persisted-key and self-target E2Es above cover the black-box paths. The only orgless test requires deliberately breaking the database invariant, which is covered at the core boundary rather than fabricated through a CLI/HTTP flow.
Local targeted test/typecheck could not run because this checkout lacks installed dependencies (drizzle-orm/bun-sqlite and bun-types); this is an environment issue, not a PR failure. GitHub CI ran successfully at this exact SHA.
The IPC resolveApiKey closure read the `config` object captured once at process startup, so a self-reset (which rewrites config.json in place) left every FUSE mount authenticating with the revoked key until the daemon was restarted. getConfig() already does a fresh disk read with no cache, so rereading it per call is enough. Adds a unit regression test that runs both the frozen-at-startup and the reread-per-call resolver against one running IPC server to prove the fix needs no daemon restart. Addresses desplega-ai#36 (review 5082653098, inline comment on packages/cli/src/commands/auth.ts:60).
docs/api-reference.md claimed a lost key is recoverable via `auth reset-key`. That endpoint sits behind the same auth middleware as every other route, so an owner who genuinely lost their key gets 401 and can't call it. Only `member reset-key` (org admin) recovers a locked-out user; `auth reset-key` is rotation while the current key is still available. Checked skills/agent-fs/SKILL.md and the CLI --help text for both reset-key commands — both already describe rotation accurately, no change needed there. Addresses desplega-ai#36 (review 5082653098, inline comment on docs/api-reference.md:48).
…l socket The unit test in packages/server/src/ipc/__tests__/server.test.ts reproduces the resolver pattern in isolation. This adds the same regression at the e2e level, against the real daemon process's Unix socket (the one the Rust FUSE helper speaks to): register, connect over IPC, reset via the real CLI path, then reconnect on the same running daemon and confirm the rotated key authenticates — no restart in between. Needs msgpackr to speak the daemon's wire protocol from scripts/e2e.ts; it's already a direct dependency of packages/server and packages/cli, just not resolvable from the repo root until now. Per CLAUDE.md's release checklist (IPC contract change).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds a first-class way to reset an agent-fs API key, replacing the current "re-register" workaround — keys are shown exactly once at registration today and there was no way to rotate a leaked key, or recover a member's lost key, without creating a brand-new user.
auth reset-keyis self-service rotation (you still hold the current key); recovering a genuinely lost key is the org-admin path (member reset-key), since/auth/reset-keysits behind the same auth middleware as every other endpoint.Two new endpoints:
POST /auth/reset-keylets an authenticated user rotate their own key, andPOST /orgs/{orgId}/members/{userId}/reset-keylets an org admin rotate a member's key (403 for non-admins acting on the route, 404 when the target is not a member of that org).Two new CLI commands:
agent-fs auth reset-keyandagent-fs member reset-key <email>, both with--jsonsupport; the admin command never writes the target member's new key into the admin's own local config.Every reset inserts an
api_key_resetaudit event (method: "self"or"admin") in the same transaction as the key-hash update, via the newresetApiKeycore op inpackages/core/src/identity/users.ts(plus a smallresetApiKeyOrglessfallback for the defensive case of a self-reset by a user with zero orgs).Test plan
Run from the repo root on this branch:
Expected:
tsc --buildexits 0, no errors.bun run build bun run testExpected:
524 pass / 0 fail / 57 skip.bun run scripts/e2e.ts "bun run packages/cli/src/index.ts --" --local-onlyExpected:
128/128passing (15 skipped, all Docker/MinIO-gated) — this is the exact CIe2ejob.Expected: clean, no diff (OpenAPI spec already committed up to date).
Expected: no output (stale-
.jsguard).Redeploy surfaces
agent-fs.sandbox.capchase.com— needs a redeploy to pick up the two new routes (/auth/reset-key,/orgs/{orgId}/members/{userId}/reset-key) and the newresetApiKeycore op. See DEPLOYMENT.md.live/andlanding/(Vercel) — no functional change in this PR (CLI/server-only), so no redeploy is required for these, but they auto-deploy previews on this PR regardless per the standard Vercel Git integration.Plan
Implements the plan Daniel approved in
thoughts/6557a439-15b8-4c55-a639-638626f4983e/research/2026-09-01-agent-fs-api-key-reset.md.Note
This PR replaces desplega-ai#35, which was opened by mistake against the upstream
desplega-ai/agent-fsinstead ofCapchase/agent-fs. Same branch, same commits, correct base repo.