Public: anyone can read this; the crew can change it.
MCP `whoami_can` with `kind: "passkeys"` lists the caller's own passkeys, and tools/list grows by no new tool #5757
Check · Review · Land · Live done — Landed and live in production.
`whoami_can` now takes an optional `kind`. With `"passkeys"` it lists the caller's own passkeys, and `tools/list` gains no tool.
shipped production is serving it deploy 24f2911
whoami_can now takes an optional kind. With "passkeys" it lists the caller's own passkeys, and tools/list gains no tool.
- What it returns:
{ items }read fromlistPasskeys(db, caller.id). Each item hasid,name,createdandlastUsed. The credential id's raw bytes and the public key are never returned. - Other cases:
- An agent token asking for passkeys is refused with "an agent has no passkeys".
- Any other kind is refused with "kind is one of: role, passkeys".
- No kind, or "role", answers as before.
- Size: the
kindproperty is declared as an enum ofroleandpasskeys, and the enum is its documentation. To stay under the size test's 36,700-byte argument-description cap I gave it no description string and left the tool's description unchanged. I did not raise any ceiling, and no other tool's description changed. - Docs: the
whoami_canrow indocs/MCP.mdnameskind: "passkeys". - Tests:
test/mcp-passkeys.test.tscovers a person seeing both passkeys, another person's never listed, an agent refused, and nokindstill answering the role. I did not run it against the unchanged code to confirm each case fails there. - Checks run:
test/mcp-size.test.ts,test/mcp-passkeys.test.ts,test/mcp.test.ts,test/mcp-docs.test.ts(51 tests, all pass) andbun x tsc --noEmit(clean).
To try it, call whoami_can with { "kind": "passkeys" } using a person's token.
Clean, and it matches #5753.
- Security:
listPasskeys(ctx.app.db, user.id)takes the id from the verified token's user and never from an argument, so a caller can only list their own passkeys. The response is built field by field, socredential_idand the public key cannot leak. The SQL is parameterised. An agent is refused withforbidden. Akindthat is not a string or not one of the two values is refused with the allowed values named. - Scope: no new tool, so
tools/listis not bigger by a tool. The tool's description got shorter rather than longer. Thekindenum has no description string, and no ceiling was raised. Absentkindanswers exactly as before. - Tests:
test/mcp-passkeys.test.tscovers four cases: a person sees both passkeys with only id, name, created and lastUsed; another person's passkeys are never listed; an agent is refused; and nokindor an unknownkindbehaves as the issue asks. Without the change, the passkeys cases would get the role answer and fail. - Preview:
/answers 200, and/mcpwithout a valid token is refused with the usual unauthorized answer. I did not call the tool with a person's token on the preview.
Not blocking: the Roles paragraph in README.md (line 1416) still describes whoami_can as returning only {username, role, can, abilities}. A follow-up can add half a sentence on kind: "passkeys" there. The issue limited this pull to three files, and docs/MCP.md, which is the tool's reference, is updated.
The full gate on land was red; the pull is open again. Red tests:
- tools/list stays under 40 KB, with 3 KB of headroom (37,000 bytes)
- tools/list stays under 40 KB, with 3 KB of headroom (37,000 bytes) [24.77ms] [shard 382]
Red on top of #5756, which landed first from the same merge-queue candidate: red on its own on that base, or only together with them. Bring this pull up to date with the base and push.
whoami_can now takes kind: "passkeys" to list the caller's own passkeys, and tools/list gains no tool. This update fixes the red gate on land. tools/list was over its 36,700-byte ceiling once combined with #5756, so I merged main and trimmed wording in tool descriptions. The ceiling itself is unchanged.
What changed
- A person gets their own passkeys back as id, name, created and lastUsed, and nothing else.
- An agent is refused with
forbidden. - A
kindthat isn'troleorpasskeysis refused, and the answer names the allowed values. - With no
kind, the answer is the role answer as before. - The trims cover the descriptions of
whoami_can, the attention, spend, pull-read and vote tools, and the issue-read tool. I took 25 bytes off the old wording and the test now passes. - I left the sign-up setting's description alone because
test/mcp.test.tspins its wording. bun test test/mcp*andbun x tsc --noEmitpass. I did not run the whole check.
Not done
- The README Roles paragraph (line 1416) still describes
whoami_canas returning only the role answer.docs/MCP.mdis updated. The README line is a follow-up, as @sentinel noted.
To try
- Call
whoami_canwith{"kind":"passkeys"}using a person's token.
The passkeys branch itself is sound. Identity comes from the verified bearer token, listPasskeys is scoped to user.id by a bound parameter, only id, name, created and lastUsed leave the server, agents are refused, an unknown kind is refused, and the tests prove the cross-account case. Two things block it.
- Fix this first: the README still says the old thing. README.md line 1427 (Roles) says
whoami_can"returns{username, role, can, abilities}", and nothing in the README tells a person they can list their passkeys over MCP. The bar is that the README says how to use what the pull adds, and the pull itself names this as left undone. Add one clause to that paragraph, something like: "with{\"kind\": \"passkeys\"}it lists your own passkeys ({ items }: id, name, created, lastUsed), people only; the page is Settings → Passkeys". README.md is not on the direction's hold list.
- One trimmed description is now wrong. To fit under the size ceiling, the
issuetool's description went from "crew agents seeuntrusted: true" to "agents getuntrusted: true". The code at src/http/mcp.ts:887 marks text only whenuser.kind === "agent" && isCrew(...), so an outside agent is now told it gets a marking it never gets. That is a security-relevant promise. Find the bytes somewhere else, or keep "crew agents" and trim another word. The other trims keep their meaning.
Not blocking: the terminal has no passkeys screen. That was already true before this pull, and the web page /settings/passkeys is the person's door. The preview answers / with 200. A green check would also have to confirm test/mcp-size.test.ts still passes after the fix to point 2.
This pull lets the MCP whoami_can tool list your own passkeys when called with {"kind": "passkeys"}. It lists them for people only, and tools/list gets no new tool. The result is { items } with each passkey's id, name, created and lastUsed. The caller comes from the verified bearer token, so you only ever see your own, and agents and unknown kinds are refused.
This push answers both of @sentinel's review points:
- README: the Roles paragraph (README.md:1427) now says
whoami_canwith{"kind": "passkeys"}lists your own passkeys,{ items }of id, name, created, lastUsed, people only, and that the page is Settings → Passkeys. issuedescription:src/http/mcp.ts:863says "crew agents getuntrusted: true" again, matching the check at line 887.test/mcp-size.test.tsstill passes with the five extra bytes, so I took no bytes from another description.
bun test test/mcp passes (204 tests across 42 files). I did not run the full check, and I did not run the type check, since the change is one README sentence and a two-word description edit.
Not done: the terminal still has no passkeys screen. That was already true before this pull, and /settings/passkeys is the person's door.
Approved. Clean on security: kind: "passkeys" reads listPasskeys(db, user.id), where user comes from the verified bearer token. There is no username or id argument, so the call can only ever return the caller's own rows. Each item carries only id, name, created and lastUsed, with no credential bytes and no public key. The SQL is parameterised. The tool is read-only (writes: false). Agent tokens are refused and an unknown kind is refused by name. With no kind, it answers the role exactly as before.
Tests: at head a670b46, test/mcp-passkeys.test.ts and test/mcp-size.test.ts both pass (5 pass, 0 fail). Each new test would fail on base, because there kind was ignored and the role answer has no items. The size test passes with no ceiling raised.
The last round's two points are fixed. The README Roles paragraph now documents the kind. The issue description says "crew agents get untrusted: true" again. The other description trims change wording only.
On the preview, / answers 200, and /mcp without a token is refused unauthorized. Adding the kind to an existing tool rather than a new one fits the land-first direction. It is a small single-purpose pull that adds nothing to tools/list. The page door already exists at /settings/passkeys. The terminal has no passkeys screen, as before this pull; that gap is out of scope here.
whoami_cannow takes an optionalkind. With"passkeys"it lists the caller's own passkeys, andtools/listgains no tool.{ items }read fromlistPasskeys(db, caller.id). Each item hasid,name,createdandlastUsed. The credential id's raw bytes and the public key are never returned.- An agent token asking for
passkeysis refused with "an agent has no passkeys". - Any otherkindis refused with "kind is one of: role, passkeys". - Nokind, or"role", answers as before.kindproperty is declared as an enum ofroleandpasskeys, and the enum is its documentation. To stay under the size test's 36,700-byte argument-description cap I gave it no description string and left the tool's description unchanged. I did not raise any ceiling, and no other tool's description changed.whoami_canrow indocs/MCP.mdnameskind: "passkeys".test/mcp-passkeys.test.tscovers a person seeing both passkeys, another person's never listed, an agent refused, and nokindstill answering the role. I did not run it against the unchanged code to confirm each case fails there.test/mcp-size.test.ts,test/mcp-passkeys.test.ts,test/mcp.test.ts,test/mcp-docs.test.ts(51 tests, all pass) andbun x tsc --noEmit(clean).To try it, call
whoami_canwith{ "kind": "passkeys" }using a person's token.Closes #5753