LangChain Connections Gives Agents Per-Caller Identity, and Turns a Missing Permission Into a Question
What per-caller OAuth in Managed Deep Agents actually changes, why the pause-and-ask interrupt is the real design idea, and how to wire it up without inheriting a credential you cannot move
An API key sitting in .env can tell you exactly what an agent is allowed to do. It has no way at all to tell you who asked. Every ticket that agent files, every pull request it opens, every row it writes lands under one service account, and the human who set the whole thing in motion exists nowhere in the record except maybe a log line somebody has to go correlate by timestamp. We have been shipping agents into production for two years with that gap sitting wide open, and mostly the reason nobody screamed is that no auditor has asked the question yet.
LangChain shipped Connections for Managed Deep Agents on September 9, and the part worth studying is not that it fixes this. Plenty of things could fix this. It is the specific shape of the fix, and one design decision inside it that I think is the actual contribution.
The two-axis model, which is the boring part that matters
Victor Moreira's launch post lays out a model with two independent axes. A connection has an owner, which is either the agent or the caller. It also has a credential type, which is either a static secret or an OAuth grant. Those two axes do not constrain each other. An agent can hold an OAuth grant. A user can hold a static secret.
Most credential systems collapse this into one dimension by accident. You get "service credentials" and "user credentials," and the credential type comes bundled with the ownership question whether you wanted it to or not. Separating them lets you answer "shared or per-person" without also answering "secret or OAuth," and all four cells are reachable.
An agent-owned secret is the world you already live in:
uv run mda connections create tavily-agent --secret-from-env TAVILY_API_KEY
The slug tavily-agent is yours. Nothing validates it against a provider list. The value moves out of TAVILY_API_KEY and into your LangSmith workspace, and it is not part of the build. Reading it inside a tool is one line:
api_key = await connections.get("tavily-agent", {"type": "agent"})
Rotate the value at the slug and future runs pick up the new key with no redeploy. Useful, not the story.
The story is the other one:
access_token = await connections.get("github-issues", {"type": "user"})
That connection stored no value when you created it. It stored an app registration. The credential arrives per caller, at run time.
What per-caller identity does to your tools
Two consequences, and the second one is easy to skip past.
The obvious one is on writes. When create_issue runs against a user-owned GitHub connection, the issue lands in GitHub opened by the person who asked. The user.login in the API response is their handle. Not a bot's. Not "acme-agent-prod." Theirs.
The one I keep turning over is on reads. search_issues already returns different answers for different callers, before anything gets written, because private repositories one person can see and another cannot are part of the result set. Same query. Same deployment. Same code path. Different truth.
That breaks a habit most of us have. If you cache tool results, your cache key now includes the caller. If you write evals against a tool that reads a real system, that eval is only valid for the identity it was recorded under. And if a supervisor agent fans work out to subagents, you have to decide whether a subagent inherits the caller's identity or acts as the deployment.
None of that is a criticism of Connections. It is what happens when identity becomes real, and the alternative was pretending it was not.
The design idea: a missing grant is a question, not an exception
Here is the line from the post I have thought about most:
if the caller has never authorized GitHub, or their token has expired,
connections.get()pauses the run and asks for a grant instead of failing.
Sit with what the alternative looks like, because you have written it. A tool needs a credential. The credential is missing. The tool raises. The agent sees a stack trace or a 401, and now the model is doing improvisation on an auth error, which is the single worst place to let a model improvise. Best case it reports the failure cleanly. Common case it retries. Bad case it finds a different tool that "works" and does the wrong thing under the deployment's own credentials, without telling anyone.
Treating the gap as an interrupt instead changes the category of the event. The run suspends. The caller gets asked. They authorize. The run resumes where it stopped. And when a task spans two services the caller has never granted, you get one interrupt listing both, before the first model turn, rather than discovering the second missing grant four tool calls later.
There is no callback route in your project, no token store, no refresh logic, no consent screen you built. The caller never opens LangSmith.
I think this is the transferable idea, and it is worth stealing even if you never touch Managed Deep Agents. Permission gaps are not errors. They are unanswered questions, and the correct handling for an unanswered question is to ask it, not to throw. If your harness has any concept of an interrupt or a human-in-the-loop pause, that machinery is already the right place for missing credentials to land.
Put this into practice
The lowest-friction path is about fifteen minutes, and you should walk it on a throwaway deployment first, because ownership is fixed at creation time.
1. Install and look at the catalog before you decide anything.
uv tool install managed-deepagents
uv run mda connections catalog
The OAuth catalog ships inside the binary, so the version you installed decides what --oauth will accept. Check it first rather than discovering the gap mid-wiring.
2. Start with the boring cell. Create one agent-owned secret for a capability that does not differ per person, like web search, and get connections.get(slug, {"type": "agent"}) working inside a real tool. An agent-owned credential belongs to a deployment, so scaffold and deploy once before creating one.
3. Then do one user-owned OAuth connection, with your own app.
uv run mda connections create github-issues \
--oauth github \
--client-id "$GITHUB_CLIENT_ID" \
--secret-from-env GITHUB_CLIENT_SECRET \
--scope repo
Read the scope line twice. --scope repo replaces the catalog default, it does not add to it. GitHub's catalog default is read:user, which cannot open an issue. Whatever you pass becomes the whole list. This is the exact shape of bug that ships fine and fails in front of a customer.
4. Put the credential read in a helper, not in every tool. The post does this and it is the right call:
async def _github(method: str, path: str, **kwargs) -> dict:
access_token = await connections.get("github-issues", {"type": "user"})
...
search_issues and create_issue both inherit per-caller identity from _github. A third GitHub tool costs zero auth code. That is the whole payoff of putting the resolution one layer down.
5. If the service is an MCP server, check whether you need an app at all. Some MCP servers register the OAuth client themselves, and then the setup is a URL:
uv run mda connections create linear-mcp --mcp https://mcp.linear.app/mcp
No client ID, no secret, no app registration, and no scope either, because the connection negotiates its permissions from the server's own advertised metadata. Worth knowing before you go create an OAuth app you did not need.
6. Develop locally, but check where you can actually test the interrupt. Agent-owned connections resolve from MDA_DEV_<SLUG> in .env, uppercased with hyphens turned into underscores. Both sources agree on that. The two disagree on the user-owned case: the launch post says mda dev resolves the signed-in developer to a real principal so the authorization interrupt fires locally, while the documentation's "Develop locally" section says user-owned connections require an authenticated caller and Agent Auth, and instructs you to deploy the agent to exercise the authorization interrupt end to end. Plan on deploying to test it until you have confirmed otherwise on your own version. Either way, exercise that path deliberately, because it is the first thing your users will hit and the last thing you will have tried.
7. Know the fourth cell exists. --authorize stores one OAuth grant for the whole deployment, so every caller acts as one shared account, which is the right answer when you want a dedicated team account rather than per-person identity. --allowed-scope caps what later authorizations may request, and --authorize-url with --token-url covers providers outside the catalog.
Honest limitations
Ownership is fixed at creation, not at read time. The post is explicit: ownership is set by mda connections create, and connections.get() only selects among credentials that already exist. If you create a connection agent-owned and later decide it should be per-caller, you are recreating it, which means re-authorizing everyone. Decide this before you have users, not after.
This is a managed product, not open-source deepagents. Connections ship in Managed Deep Agents v0.7.0 and later. If your reason for running open-source deepagents is that you did not want a hosted control plane holding your credentials, Connections is not for you, and nothing here changes that calculation. The two-axis model and the interrupt pattern are still worth copying by hand.
The catalog is versioned inside the binary. Twenty-three services ship in it. Which providers --oauth accepts depends on the version installed, which makes "does this work" a question with a per-machine answer during a team rollout. Pin the tool version the way you pin anything else.
The scope-replacement behavior is a footgun and the post knows it. It gets called out in an indented note, which tells you people hit it. A default of read:user replaced without warning by whatever you passed produces a working authorization flow and a failing write.
Per-caller identity moves the risk, it does not delete it. You have swapped one over-privileged shared token for many correctly-scoped personal tokens, and the second arrangement is better, but it is also a larger surface with more grants to audit and more people whose revocation you now depend on. And an agent acting under a real person's identity is an agent whose mistakes are attributable to that person. That is the point, and it is also a thing you should tell your users in plain language before they click authorize.
Nothing here is verified beyond the vendor's own post and docs, which is also where the local-development conflict above came from. Treat every behavior claim as documented rather than field-tested, and go break the interrupt path yourself.
What to do with this
Open whatever agent you have in production right now and ask one question: which human does its last write action belong to? If the honest answer is a service account, you have a gap, and it will not stay theoretical. The first time a customer asks why a bot filed a ticket in their name, or an auditor asks who approved a change, the answer "we can correlate it from logs" is going to sound exactly as thin as it is.
You do not need LangChain's product to close it. You need three things from this design: ownership and credential type as separate decisions, resolution at call time rather than at deploy time, and a missing grant that suspends the run and asks rather than throwing an error at a model that will try to be helpful about it. The third one is the one nobody builds on their own, and it is the one that makes the other two survive contact with real users.
Go look at the interrupt path in your own harness. It is probably already there, built for something else.
Sources: Connections: managed credentials and per-caller identity for Managed Deep Agents, LangChain, Victor Moreira, September 9, 2026; Managed Deep Agents Connections documentation.
Medium metadata
- Title: LangChain Connections Gives Agents Per-Caller Identity, and Turns a Missing Permission Into a Question
- Subtitle: What per-caller OAuth in Managed Deep Agents actually changes, why the pause-and-ask interrupt is the real design idea, and how to wire it up without inheriting a credential you cannot move
- Tags: AI Agents, LangChain, OAuth, Developer Tools, Agent Security
- Recommended topic: Artificial Intelligence
- Canonical: import from the fervorai.dev URL