feat: connect a person's own account for assistant turns that act for them
Merged
DA
7d ago+1659+1659+0123456789012345678901234567890123456789-99-99-01234567890123456789
AIM-598 (2 of 2)
Stacked on #8043.
Summary
- Whose connection. A turn credential that acts for a person (an agent principal with
AuthorizerUserIDU) now resolves personal auth clients against U's own connection. Before, it used the agent subject, which owns no connection.connectionSubjectfeeds both the mandatorymcp:authcheck and the token lookup, for direct servers and gateway members.- Admission already binds U as the represented user with U's own policy, so the existing
connection_owner = $represented_userrule now matches U. The agent's, owner's and U's policies all still apply, and U's owner blocks keep working. - Turns that act for nobody (workload principals) are unchanged: no person's connection resolves.
- Admission already binds U as the represented user with U's own policy, so the existing
- Recovery link. On a direct server, when U's connection for the chosen personal client is missing or expired, the gate answers
Connection requiredorReconnect required(a 412 with a readable message) instead of the 401 that sent the runtime into the owner-only OAuth flow. The message carries a link and tells the assistant to deliver it privately or not at all.- The link is
<site>/mcp/connection_recovery/{token}, always on the canonical dashboard origin. It is an opaque, single-use cache entry bound to U, the organization and the auth client (with its registration and server), expiring after 15 minutes. The message is not logged, because it carries the link. - Start:
- The browser's Speakeasy session must be U. A signed-out browser goes to login and comes back.
- Support and impersonated sessions are refused.
- U must still be an active member who passes the
mcp:authcheck for the client. - Only then is the link used up and U's upstream login started, through the existing remote-login flow.
- Completion: before the connection is stored, the callback rechecks U's membership and
mcp:auth. If the browser presents a session, it must be U. If the callback lands on a host without the session cookie, the server-held, single-use state bound to U stands in. The login is consumed only after these checks, so a stray callback cannot spend it, and a recovery login whose entry is missing fails closed. - Someone else sees "This link is for someone else" and learns nothing more, and the link stays usable for U. A denied U sees "No access" with no way to connect. Neither page reveals the upstream URL.
- Recovery never changes the assistant's choice or picks another client.
- The link is
- Delivery. The assistant instructions name the exact link prefix, so a link from any other origin (for example, one an MCP server's own error text makes up) is never relayed. The Slack and dashboard adapters route genuine links to the person the turn acts for (Slack: ephemeral, else DM; never the channel or the owner). Without a private route, the assistant gives non-sensitive "open Speakeasy and connect" instructions. The owner-only routing of
assistant_mcp_auth_requiredis unchanged. - Scope
- Links are issued for direct servers. A gateway member keeps pointing to the gateway's sign-in page, where the person signs in as themselves and connects their own account, which turns acting for them then use.
- Links are not tied to a thread. The person asks the assistant to retry after connecting.
Motivation
RFC sections 7.4, 7.5 and 7.7: an agent uses only its authorizing user's connection and never borrows the owner's. When that connection is missing, the person gets a connect link bound to them, which completion verifies. A denial stays "No access".