# 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. This is the only way a credential is created. |
| 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 six 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. **Open the sign-in URL** in a browser the agent controls, and install the shim from
   `passkey_howto`. The shim replaces `navigator.credentials.get` so the site's credential
   request is *parked* instead of being answered by the local device, which does not hold this
   passkey.
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. **Read the parked request** — `window.__apk.pending` gives `{ rpId, origin, clientDataJSON,
   allowCredentials }`.
5. **`passkey_assert`** — send the `credentialId`, the `clientDataJSON` byte-for-byte, and the
   page origin. Get back `authData`, `signature` and `userHandle`.
6. **Hand it back to the page** with `window.__apk.deliver({...})`. The promise resolves, the site
   verifies the signature against the public key it registered at enrolment, and sets its session.

An agent connected to the **local** server skips all of that: `passkey_signin(domain)` does steps
2 to 6 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 in it, 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.
- [Failure modes](/docs/failures) — the three refusals that look alike and mean opposite things.
- [Limits](/docs/limits) — what this cannot do, stated plainly.
