Give each agent its own token, and stop trusting the body - #34
Open
davidmckayv wants to merge 3 commits into
Open
Give each agent its own token, and stop trusting the body#34davidmckayv wants to merge 3 commits into
davidmckayv wants to merge 3 commits into
Conversation
Calling a tool back used to need one deployment-wide secret, with the Bot and the person read out of the request body. Anything holding that secret could spend any Bot's grants and write any name into the audit trail. The trail is the product, and a forgeable trail is worse than none because it is believed. Two credentials now, because they answer different questions and one cannot answer both. The token says which agent is calling. Issued per agent, held by whoever runs it, and stored here only as a hash, so a database dump is not a set of working credentials. Issuing again rotates; revoking leaves the agent able to talk and unable to reach anything outside a conversation. Null is the default, which is what a URL somebody pasted should get. The run assertion says which Bot and which person. Signed by this deployment, ten minutes long, sent in forwardedProps and handed straight back. An agent is in no position to assert who it acts for, so it carries our note instead of making a claim. They are checked against each other: an agent may only act as the Bot its token was issued for, so an assertion seen once cannot be replayed by another credentialled agent. The deployment-wide token still authenticates the Bots that ship in the box, but it no longer asserts anything. Driven end to end, not just unit tested. A registered outside agent with its own token called a granted tool and the audit row named the visitor rather than "agent". The same assertion presented with a second agent's token was refused 403, no assertion at all 401, an unknown token 401. Also fixes a latent migration bug this work uncovered. drizzle.config.ts still named computer.ts, deleted when computer use went, and did not name the policy.ts that replaced it, so action_policy looked absent and the next generated migration opened with DROP TABLE "action_policy" CASCADE. It destroys the deployment's boundary. The config now names the right file, the new columns are folded into the single baseline this repo keeps rather than a second migration, and the baseline was verified by rebuilding the database from nothing. The demo seed also refuses with an explanation when the roster has not been written yet, instead of a foreign-key error naming a constraint.
Two things the merge dropped, both found by driving it rather than by any gate. The set naming which tools this deployment runs was not built, so the filter that reads it threw `ours is not defined` and the run died before it said anything. Green typecheck, green suite, dead Bot: the variable is only reached once a model actually asks for a tool. With it back, the reason it exists matters more than the crash. A run is offered two kinds of tool that no naming rule separates: the Bot's grants, which execute through the policy and the audit trail, and the components the surface draws. A call the surface owns now ends the run, which is how a browser tool is supposed to work, the surface draws it or puts the question to a person and starts the next run with the answer. Running the loop through it instead invents a result: the Bot apologises for a chart the person is looking at, and an approval card answered on its behalf waits for a click that can never land.
davidmckayv
requested review from
MikeRyanDev,
guidovizoso and
tylerslaton
as code owners
August 20, 2026 04:21
guidovizoso
approved these changes
Aug 20, 2026
The token this deployment issues an agent was minted as `okai_agt_...`, which is the other product's prefix. The prefix exists so a leaked credential can be recognised on sight, in a log or by a secret scanner, and one naming a product this is not defeats the only job it has. It is `obot_agt_` now. The panel that issues it, and the code around it, still called a Bot a teammate. That rename happened everywhere else already. Found by opening the screen. The security work these sit on was proven through the API, which is the right place to prove a refusal, and it meant nobody had looked at the thing a person actually uses to mint one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Closes the weakness #33 disclosed.
/api/agent-tools/callauthenticated with one deployment-wide secret and then read the Bot and the actor out of the request body. Anything holding that secret could spend any Bot's grants and write any person's name into the audit trail.The trail is the product. A forgeable trail is worse than no trail, because it is believed.
Two credentials now, answering two different questions:
They are checked against each other. An assertion names the Bot it was issued for, and a call is refused unless that Bot is the one the presented token belongs to, so an agent cannot replay an assertion it happened to see and act as somebody else's Bot.
Taken from
CopilotKit/openkai(a9b3722), renamed, with the demo and visitor pieces dropped.Notes on the port
0000is already applied everywhere, so editing it means the columns never appear. They arrive as0001_swift_morph.sql, and a test asserts that rather than asserting they are in the base.sign/verifylived in OpenKai's visitor module, which is demo-only here. They are lifted intoserver/src/auth/signed-value.ts, keyed under a label derived from the deployment's encryption key so a signature here can never be confused with a credential ciphertext there.Where it runs
agent_profiles, in Postgres.Boundary and audit
A defect this also fixes
A run is offered two kinds of tool and no naming rule separates them: the Bot's grants, which execute through the policy, and the components the surface draws. The agent was running both. That invented results, so the Bot apologised for a chart the person was looking at, and an approval card answered on its behalf waited for a click that could never land. A call the surface owns now ends the run, which is how a browser tool is meant to work.
Proof
Driven in Chrome.
POST /api/agent-tools/callholding the deployment-wide token, naming a tool, with no run assertion:401 Not authorised. Before this PR that call succeeded.mcp.call_succeeded,notes/search_notes, botrisk-analyst, actordev-local-user. On Make LangChain the default harness, and let a framework Bot call tools back #33 that same row read actoragent.17 unit tests on the token and the assertion, including that another agent presenting a valid assertion it did not earn is refused, that an edited Bot fails verification, and that an expired assertion is refused.
Migration verified on a clean database through the full chain, and
db:generatereports no drift.format:check,lint,typecheckclean. 683 pass, 5 skip, 0 fail across 78 files.