# Agent Passkey

Agent Passkey holds a person's passkeys and signs WebAuthn assertions with them, so an agent can
sign in **as that person** without ever holding a secret.

The private key is generated inside the service and never leaves it. An agent does not get the
key, a cookie, a password or a recovery code. It gets one signature, over bytes it hands in, for
one ceremony, on a site the person ticked — and every one of those signatures is scoped, logged
and revocable.

## The shape of it

| Piece | What it does |
|---|---|
| The service | Holds the passkeys. Signs. Enforces the origin, the policy, the approval and the rate limit. |
| The extension | Enrols a passkey, in a human's own browser, on a site they are already signed in to. The default way a credential is created; the [`enrol` scope](/docs/enrol) is the other. |
| The remote MCP server | `https://app.solospace.dev/mcp` — OAuth 2.1, no API key. Signs bytes for an agent that drives its own browser. |
| The local MCP server | A stdio server that drives its own Chromium and does the whole ceremony in one call. |
| The dashboard | Connectors, agents, approvals, and an append-only audit log. |

## Signing someone in, end to end

The remote server has no browser. It signs bytes; the agent drives the page. That split is the
whole design, and it is why the procedure has several steps rather than one.

1. **`passkey_list`** — which sites are connected, the sign-in URL for each, and the operating
   notes for that site. The notes say where the passkey option hides.
2. **`passkey_begin_signin`** mints a single-use ceremony ticket and returns `armUrl`. In a
   browser with the Agent Passkey extension, the agent just **navigates** the tab to `armUrl`: the
   extension binds the ticket to that tab and sends it back to the page. No page script at all.
   For a browser without the extension, the result also carries `shim` — a page script with the
   ticket baked in that stands in for `navigator.credentials.get` — which works where the site's
   CSP lets the page reach this service (`shimProcedure` says which).
3. **Drive the page to the passkey prompt.** This is the step that actually fails. Most sites will
   not offer a passkey until they know who is signing in, and many hide it behind "Try another
   way".
4. **Click the site's own passkey button, then watch.** When the site asks for a credential, the
   extension's background worker (or, on the shim path, THE PAGE) posts the ceremony bytes to this
   service itself and gets the signature back — the agent carries nothing. Poll
   `window.__apk.status` (`waiting`, `signing`, `signed`, `failed: <reason>`), or call
   `passkey_signin_status({ ticket })` for the server's view of the same ticket.
5. **The site sets its session.** Watch for the navigation.

If the page's own CSP blocks it from reaching this service (`github.com` and `login.gov.il` do),
the extension path is unaffected — its worker is not subject to the page's CSP — and it is what
`next` names. With no extension there, `ifNotArmed` gives the agent one sentence to ask the
person. For an agent whose browser tool can evaluate scripts and return long strings whole, the shim
also degrades on its own: the request is parked in `window.__apk.pending` instead, and the agent
ferries `clientDataJSON` to `passkey_complete_signin`, gets back `authData`/`signature`/
`userHandle`, and hands them to `window.__apk.deliver({...})`. That fallback is the one case
where the agent carries ceremony bytes at all.

An agent connected to the **local** server skips all of that: `passkey_signin(domain)` does the
whole thing in one call, in its own Chromium. An agent can tell which situation it is in by
looking at its own tool list — if `passkey_signin` is usable there, the local server is
connected.

## The rule that shapes everything else

The `UP` and `UV` flags in a WebAuthn assertion are a signed factual claim to the relying party:
*a human was present, and a human was verified*. This service will not lie about them.

A connector in **approval** mode wakes a real person before every signature, and only then are the
flags set. A connector in **silent** mode signs `UP=0/UV=0` — honestly — and most sites reject
that. There is no third setting where the flags are true because it would be convenient.

## Where to go next

- [Quickstart](/docs/quickstart) — connect an agent, enrol a passkey.
- [MCP reference](/docs/mcp) — every tool, every argument, every status.
- [REST API reference](/docs/rest) — the same tools as plain HTTP under `/v1/*`.
- [Adding passkeys via the API](/docs/enrol) — the `enrol` scope, and the step it cannot do.
- [Integrating a multi-customer product](/docs/integrators) — subjects, session policies, rotation.
- [Failure modes](/docs/failures) — the three refusals that look alike and mean opposite things.
- [Limits](/docs/limits) — what this cannot do, stated plainly.
