I took credentials away from my agents. They still act on mail and Slack on my behalf. A developer building multiuser AI systems for production has designed an auth chain that lets external agents like Claude Code act on users' Gmail and Slack accounts without ever receiving a provider token. The system uses OAuth with dynamic client registration and scoped bearer tokens, plus a consent and account-binding layer that enforces per-account permissions and denies calls when consent is missing. The provider credential never appears in the model context, tool result, or delegated client's bearer. A common MCP setup carries auth the same way: create an API key, paste it into mcp.json or an .env file, restart the client. It works. Now the key sits in plaintext on every machine that runs the agent. It often carries one broad, fixed permission set. Every agent that reads the file gets the same set. And when one agent misbehaves, the fix is rotate the shared key everywhere. There's a second failure that arrives after you add real auth: the agent calls a tool and gets a bare 403. The user doesn't know what to approve. The agent doesn't know what to ask for. Somebody ends up reading server logs. I build multiuser AI systems for production. My agents act on users' Gmail and Slack accounts every day - external agents like Claude Code included. None of those agents receives a provider token. This is the auth chain that makes that work, including the part that took the most design: what happens when consent is missing at call time. An external agent doesn't get the Gmail or Slack credential. It gets a URL - a managed MCP endpoint my platform exposes. I call that endpoint the door, and so does the interface further down. Claude Code connects to the door as an OAuth client. Dynamic client registration DCR registers its client identity against a configured redirect allowlist. The user signs in and approves the maximum this connection may be granted. The OAuth exchange returns a scoped KDCube bearer tied to that client and grant, not a provider token. The approval screen also resolves those requested capabilities to the accounts behind them. If a required provider is not connected, it is named there with a connect link. The connect step already says what to add - the same shape as the call-time denial later in this post, moved to the front. Approve it, and the connection becomes a card. That card is the whole governance relationship. Nobody registered Claude by hand, and nobody pasted a provider token. The checklist is a ceiling: the most this app may be granted here. Its vocabulary follows the realm. Slack is a single-provider realm, so it uses Slack claims such as slack:search . Mail can span providers, so the door uses mail:read and mail:send , then the account broker resolves those to provider-specific claims such as gmail:read and gmail:send on the selected account. A ceiling is not access. That is what the ACCOUNTS section of the same card decides: which accounts, and which permissions on each. Two Google accounts, each with its own gmail:read / gmail:send ticks. The user can bind this agent read-only on one account and read+send on another. If an account can send mail but the agent isn't bound to send on it, the call is refused. When another account can satisfy it, the denial returns labeled account candidates instead of silently choosing one. And the default is closed. Untouched means nothing: an agent with no ticks on a provider has no access to any of its accounts, however capable the accounts are and whatever the connection ceiling says. On a first connect nothing is pre-checked - the picker shows the accounts, and the user decides. Everything on the card stays editable: narrow, extend, revoke. Revocation applies on the next call. There is no provider credential copied into every agent to rotate. The client's scoped KDCube bearer has its own expiry and revocation. An authorized request reaches trusted tool code with the resolved user, caller identity, and required claim - not with a provider token in its arguments. At the trusted boundary, the account broker resolves and refreshes the user's connected credential for that provider call. The provider adapter uses it for the call and does not persist it. The provider credential never appears in the model context, prompt, generated code, tool result, or delegated client's bearer. Nothing on those sides of the fence can disclose a credential it never received. And nothing rests on what was true at connect time. Every call re-crosses two gates: does this caller hold a grant for this operation, and does a connected account authorize this claim with this caller bound to use it there. The connect-time approval is only the ceiling. The per-call checks are the authority. Two recipes walk this end to end: how a tool resolves the credential https://kdcube.tech/blog/recipes/2026-07-27-resolve-a-connected-credential-your-tools-the-users-token-no-token-in-your-code at the trusted boundary, and the full chain, three ways in https://kdcube.tech/blog/recipes/2026-07-26-authenticated-mcp-in-kdcube-the-full-chain-three-ways-in . Agents grow. A tool starts needing a claim it didn't need last month. A user connects a second Slack workspace. An operation gets called for the first time mid-conversation. In a token world each of those is a mystery failure. Here a denial is not only a reason. It is a small recovery protocol, and each gate returns a different one. Gate 1: this caller lacks an operation grant. The service names the exact operation and missing claims, then points to the right grant-extension path: Gate 2: the caller is admitted, but the connected-account side cannot satisfy the operation. Once gate 1 passes, this contract names the account-side condition and the action that can resolve it: retry hint is my favorite field: the error says whether the caller can retry the operation after the user acts. When several accounts match, the platform never picks one silently. account required returns labeled candidates, and the caller retries with an account id . The KDCube chat component can render the denial as an actionable consent banner that opens the matching request in Connection Hub. An external MCP client receives the same structured recovery path in its tool response. Consent appears when the operation needs it, scoped to what it needs. In an open-ended agent turn, the needed tool and account may emerge only after reasoning starts. My in-house agents get the same card - here's one of them: An external app that OAuthed in and a hosted agent in my runtime are the same kind of citizen: a client identity, a grant, a per-account binding, the same edit and revoke. Connecting an account authorizes no agent by itself. Each agent's access is its own grant - the accounts here are the same two, the binding is not: the account the connected app may send from, this agent may only read. "What can this agent do right now" always has a visible answer. The MCP specification released on 2026-07-28 https://modelcontextprotocol.io/specification/2026-07-28 removed protocol-managed sessions. A server that needs continuity now mints an explicit handle and validates it when it comes back. Akamai's assessment https://www.akamai.com/blog/security-research/new-mcp-specification-security-teams-must-prepare is blunt: the protocol improved its foundation, but critical state and authorization boundaries now depend on how each server is built. KDCube's managed endpoint speaks MCP 2026-07-28 while continuing to serve earlier clients. On this managed path, no provider credential enters the prompt, generated code, or delegated client's bearer. The authenticated KDCube grant binds the caller and user; tenant/project comes from the deployment, not client metadata. Every call then rechecks the resource, operation, claim, account, and this caller's per-account binding. That is the answer here to provider-secret leakage, cross-user connected-account access, wrong-account use, and authority that changed after connect time. My design conclusion is simple: the agent should never hold provider credentials. A trusted layer in front should own auth, scoping, and revocation. This is that layer, built out: per-agent grants, per-account bindings, call-time fences, denials that carry their own fix. The runtime it lives in is KDCube - self-hosted, MIT licensed. The agent side stays simple: it calls tools, and when authority is missing it gets an answer it can act on. retry hint - not a 500, and not a silent success.To try any of it against your own accounts, the two walkthroughs are in the repo: connect an external client to a KDCube service https://github.com/kdcube/kdcube/blob/main/app/ai-app/docs/recipes/connections/delegate-kdcube-service-to-external-client-README.md is the path Claude took above, and authenticated MCP from zero https://github.com/kdcube/kdcube/blob/main/app/ai-app/docs/recipes/quickstart/expose-governed-service-mcp-README.md is the other side of it - declaring the door, the ceiling, and the redirect fence yourself. If you get anywhere, open an issue. I'd rather have that than a star. The design docs in the repo cover each decision in detail; the deeper write-up is Authenticated MCP in KDCube: delegated credentials, not shared https://kdcube.tech/blog/engineering/2026-06-30-authenticated-mcp-in-kdcube-delegated-credentials-not-shared . Next: someone brought me their own agent and a data API for their organization. Two days later it was live at scale on their own landing page - streaming answers you can interrupt, follow-ups while it works, files in and out, their knowledge attached, web search, memories, usage. All theirs. Third in a series on the parts of the agent stack that aren't the agent. Part 1, on dropping tool calling, is here; Part 2, on runtime economics, is here.