* feat: add resetApiKey core op with audit event 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. * feat: add self-service and org-admin API key reset routes 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. * feat: add auth reset-key and member reset-key CLI commands 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. * test: cover API key reset in the CLI E2E suite 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). * docs: add reset-key routes to the OpenAPI spec Keeps docs/openapi.json in sync with the two new routes, per the CI freshness check (sync-openapi.ts && git diff --exit-code). * docs: document reset-key commands in the skill and API reference 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. * fix(cli): auth reset-key writes the config key api-client reads, honors --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. * fix(cli): member reset-key persists the new key when an admin targets 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. * fix(cli): resolve member reset-key's self-target identity before rotating 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. * fix(core): wrap resetApiKeyOrgless in a transaction, document the audit-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. * test(e2e): cover the persisted-key, --json, and admin self-target reset 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. * fix(server): reread persisted config for daemon IPC auth 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/agent-fs#36 (review 5082653098, inline comment on packages/cli/src/commands/auth.ts:60). * docs: distinguish self-service key rotation from admin recovery 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/agent-fs#36 (review 5082653098, inline comment on docs/api-reference.md:48). * test(e2e): cover the daemon IPC credential regression against the real 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). --------- Co-authored-by: capchase-bot <75273842+capchase-bot@users.noreply.github.com>
6.1 KiB
API Reference
agent-fs exposes a single HTTP API. All file operations go through one dispatch endpoint.
Base URL
http://localhost:7433
Authentication
All endpoints (except /health and /auth/register) require a Bearer token:
Authorization: Bearer <api-key>
Get an API key by registering:
curl -X POST http://localhost:7433/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
Endpoints
GET /health
Health check. No auth required.
curl http://localhost:7433/health
# {"ok":true,"version":"0.1.1"}
POST /auth/register
Register a new user. Returns user ID, org ID, drive ID, and API key. The key is shown only once — it isn't stored anywhere but the user's own config.json.
curl -X POST http://localhost:7433/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "agent@example.com"}'
agent-fs auth reset-key rotates your own key while you still hold the current one — it calls /auth/reset-key, which sits behind the same auth middleware as every other endpoint, so it cannot help if the key is genuinely lost. Recovering a lost key requires an org admin to run agent-fs member reset-key <email> on the locked-out user's behalf. Either path invalidates the old key immediately.
GET /auth/me
Get current user info with default org/drive context.
curl http://localhost:7433/auth/me \
-H "Authorization: Bearer <api-key>"
# {"userId":"...","email":"...","defaultOrgId":"...","defaultDriveId":"..."}
ALL /mcp
MCP endpoint (Streamable HTTP transport). Accepts JSON-RPC requests from MCP clients. Stateless — each request creates a fresh MCP server instance.
curl -X POST http://localhost:7433/mcp \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
In practice, use agent-fs mcp (stdio proxy) rather than calling /mcp directly. The proxy handles the MCP lifecycle (initialize, tools/list, tool calls) automatically.
POST /orgs/{orgId}/ops
Dispatch any file operation. The op field determines which operation runs.
curl -X POST http://localhost:7433/orgs/<orgId>/ops \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"op": "write", "path": "/hello.md", "content": "# Hello"}'
An optional driveId field targets a specific drive. The drive must belong to {orgId} — a driveId from another org returns 404 NOT_FOUND, the same response as a nonexistent drive, so drive IDs cannot be probed across tenants. Each op requires a minimum drive role (see Access control).
See the OpenAPI spec for the full schema of each operation, or browse it interactively:
- Live endpoint:
GET /docs/openapi.json(when server is running) - Static file:
docs/openapi.json(committed to repo)
Import either into Swagger Editor or Scalar for interactive exploration.
Raw file bytes
Use the raw file route for binary-safe uploads and downloads:
curl -X PUT http://localhost:7433/orgs/<orgId>/drives/<driveId>/files/assets/logo.png/raw \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/octet-stream" \
--data-binary @logo.png
curl http://localhost:7433/orgs/<orgId>/drives/<driveId>/files/assets/logo.png/raw \
-H "Authorization: Bearer <api-key>" \
-o logo.png
The raw route preserves bytes exactly. Text indexing runs only for valid, indexable UTF-8 payloads.
PUT /raw requires the editor role (or better) on the target drive — viewers get 403 PERMISSION_DENIED, matching the JSON write op. GET /raw is viewer-accessible. As with the ops route, the driveId in the path must belong to the orgId in the path; mismatches return 404.
Access control
All routes authenticate via API key and authorize against explicit memberships:
- Strict drive membership — a drive is only visible and accessible to users with an explicit drive membership row. Creating a drive grants the creator an admin membership automatically.
- Per-op role gates — read ops (
ls,cat,search,signed-url, ...) requireviewer; write ops (write,edit,append,rm,mv,cp,revert,comment-add, ...) requireeditor;reindexrequiresadmin. - Member management is admin-only — inviting, listing, updating, and removing org members requires org
admin. Drive member routes require driveadminor admin of the owning org. - No existence oracle — requests that reference an org or drive you have no access to return
404, identical to the response for IDs that don't exist. - Scoped comment IDs — comment IDs only resolve within the org/drive context they were created in; cross-tenant IDs return
404.
Signed URLs are bearer secrets
The signed-url op is viewer-accessible and RBAC is checked only at generation time. The returned URL is a presigned S3 URL: it requires no authentication and grants download access to anyone who has it until it expires (default 24h, max 7 days). Treat signed URLs like bearer tokens — don't log them, don't post them anywhere you wouldn't post a credential, and use the shortest expiry that works (expiresIn).
Operations
All 26 operations are dispatched through POST /orgs/{orgId}/ops. Each expects {"op": "<name>", ...params}.
| Category | Operations |
|---|---|
| Content | write, cat, edit, append, tail |
| Navigation | ls, stat, tree, glob |
| File Management | rm, mv, cp |
| Version Control | log, diff, revert |
| Search | grep, fts, search |
| Maintenance | recent, reindex |
| Comments | comment-add, comment-list, comment-get, comment-update, comment-delete, comment-resolve |
For parameter details, see the OpenAPI spec.