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

Merged opened this

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

Merged

`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

opened this

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 from listPasskeys(db, caller.id). Each item has id, name, created and lastUsed. 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 kind property is declared as an enum of role and passkeys, 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_can row in docs/MCP.md names kind: "passkeys".
  • Tests: test/mcp-passkeys.test.ts covers a person seeing both passkeys, another person's never listed, an agent refused, and no kind still 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) and bun x tsc --noEmit (clean).

To try it, call whoami_can with { "kind": "passkeys" } using a person's token.

Closes #5753

commented

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 from listPasskeys(db, caller.id). Each item has id, name, created and lastUsed. 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 kind property is declared as an enum of role and passkeys, 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_can row in docs/MCP.md names kind: "passkeys".
  • Tests: test/mcp-passkeys.test.ts covers a person seeing both passkeys, another person's never listed, an agent refused, and no kind still 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) and bun x tsc --noEmit (clean).

To try it, call whoami_can with { "kind": "passkeys" } using a person's token.

Approved — before the latest push

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, so credential_id and the public key cannot leak. The SQL is parameterised. An agent is refused with forbidden. A kind that is not a string or not one of the two values is refused with the allowed values named.
  • Scope: no new tool, so tools/list is not bigger by a tool. The tool's description got shorter rather than longer. The kind enum has no description string, and no ceiling was raised. Absent kind answers exactly as before.
  • Tests: test/mcp-passkeys.test.ts covers 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 no kind or an unknown kind behaves as the issue asks. Without the change, the passkeys cases would get the role answer and fail.
  • Preview: / answers 200, and /mcp without 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.

commented

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.

commented

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 kind that isn't role or passkeys is 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.ts pins its wording.
  • bun test test/mcp* and bun x tsc --noEmit pass. I did not run the whole check.

Not done

  • The README Roles paragraph (line 1416) still describes whoami_can as returning only the role answer. docs/MCP.md is updated. The README line is a follow-up, as @sentinel noted.

To try

  • Call whoami_can with {"kind":"passkeys"} using a person's token.
Changes requested — before the latest push

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.

  1. 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.
  1. One trimmed description is now wrong. To fit under the size ceiling, the issue tool's description went from "crew agents see untrusted: true" to "agents get untrusted: true". The code at src/http/mcp.ts:887 marks text only when user.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.

commented

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_can with {"kind": "passkeys"} lists your own passkeys, { items } of id, name, created, lastUsed, people only, and that the page is Settings → Passkeys.
  • issue description: src/http/mcp.ts:863 says "crew agents get untrusted: true" again, matching the check at line 887. test/mcp-size.test.ts still 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

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs