diff --git a/docs/architecture.md b/docs/architecture.md index 558360fb..10977ed5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -226,33 +226,37 @@ path. ## Signing in to a third-party app -An agent authorizes against somebody else's app the way a person does, driving -a headless browser, because the app is a website with a login box and a popup. -The agent server is the authorization server, so the page in that popup is -ours, and it has to decide without a human present. +An agent signs in to somebody else's app without a browser and without ever +holding a key. The app is an ordinary OAuth client: it binds a loopback +listener and pushes an authorization request naming the agent's account. The +agent server is the authorization server, so what that request becomes is +ours — a decision record, saying which client asked, what it asked for, and +what the owner's policy made of it. -The problem that shape creates is knowing which agent is in front of the page. -A cookie is only as good as a browser profile on a machine that runs agents, -and handing the agent a key puts a credential inside a model's context. -Neither is necessary, because the identity mechanism for this already exists. -The agent confirms through a tool call, and the PreToolUse hook stamps the -acting DID onto it exactly as it does for a status update. The agent holds -nothing. +The problem that shape creates is knowing which agent is answering. A cookie +is only as good as a browser profile on a machine that runs agents, and +handing the agent a key puts a credential inside a model's context. Neither is +necessary, because the daemon on that host already holds one credential per +context. It follows the records for the accounts it issued, hands each one to +the context it belongs to, and carries the answer back presenting that +account's own token. The agent holds nothing. -Confirming after the page rather than declaring before it is what makes the -binding exact. Three things have to line up, and each covers a different lie: -the hook's stamp says which agent is confirming, the one-time reference says -which request and that whoever is confirming reached the page, and the account -named in the authorization request has to be the agent the hook stamped. -Relaying a reference to another agent fails that last check. +Answering a record rather than a URL is what makes the binding exact. The +agent is shown what was asked and what policy would grant, and replies with a +one-time token — not an account, not a page, not an address. Which account +signs in is read off the record the daemon is holding that token in, so there +is no identity field on the wire for a caller to fill in with another agent's, +and the server checks the credential presented rather than a name in a body. +A token relayed to another agent's daemon matches no record it holds. -The honest limit is the same one. The hook runs inside the harness -process, so the agent holds no credential at the model level and anything with -code execution in that process can still make the same call claiming to be -another agent. That is model-level custody, which is real, and not -process-level custody, which no arrangement here provides. +The honest limit has moved but not gone. The daemon's socket is guarded by the +directory, the file mode and the peer's uid, so anything running as this user +can connect to it and approve what is waiting. That is user-level custody, +which is real, and not per-agent custody, which no arrangement on a shared +machine provides. What it no longer is, is a claim: there is nothing to assert +and therefore nothing to assert falsely. -
+
@@ -261,57 +265,57 @@ process-level custody, which no arrangement here provides. - agent - the model - tool + hook - mcp server, or a cli - browser - headless - the app - third party - agent pds - authorization server - - - - - - - open the app, type its own handle - - sign in - - PAR: client id, scopes - - request_uri - - redirect - - GET /authorize?request_uri=… - - the page: what is asked, what policy does to it, - and a one-time reference for this request - - the agent reads it - - confirm(reference) - the agent holds no key - - hook stamps the acting DID - - confirm + the stamped DID - - is the reference live and unused? - does the account match the stamped DID? - what does the owner's policy allow? - - the diff: requested, granted, removed, and why - - redirect with the code - - code - - token, and the granted scopes + agent + the model + hook · didbot-oauth + short-lived, one per event or command + didbot-agentd + holds the account tokens + the app + third party, on this host + agent pds + authorization server + + + + + + + start the client, name its own handle + + PAR: client id, scopes, the agent's account + a decision record, pending + no browser is ever opened + + listPendingAuthorizations, held open + + who asked, what for, what policy did to it + outbound only: nothing here is reachable + + report + + and what is waiting for this context + + the agent reads it + + didbot-oauth approve <token> + a token, not an account + + approve(token) + + the record names the account + + approveAuthorization, as that account + + is the token live and unused? + does the credential authenticate as the account? + what does the owner's policy allow? + + the granted scopes, and the client's redirect + + the code, fetched on loopback + + the code, for a token and the granted scopes
## Why a stop is local