Agents Acting for People Part 3: Security Principles for Any Agent Platform
Part 2 gave each tool call a token for the right user and the right target. That settles what a call may do. It leaves three questions open: who is really asking, who may approve a dangerous action,...
Status, November 2026: each principle below is enforced in Orrery 0.20.0. The examples come from there, but none of them depends on Google ADK.
Part 2 gave each tool call a token for the right user and the right target. That settles what a call may do. It leaves three questions open: who is really asking, who may approve a dangerous action, and what a caller may see. This part covers eight principles for agent platform security that answer them. They hold whatever framework you use.
1. Identity Comes From a Verified Token, Never From Text
An agent runtime is full of places that look like they know who the user is: session state, message metadata, a user field in a request body, the model's own summary of the conversation. Each one can be written by something other than your authentication layer, and tool output is attacker-reachable text. A pod annotation can say "the user is an admin".
So identity has exactly one source: the token your server verified on this request. Copy it into turn-scoped memory, and treat every other copy as display only. If session state holds a role, it needs a server-only marker next to it, and anything without that marker gets the lowest role. That way a tool writing role=admin earns nothing.
2. Bind Tokens to Your Audience and Type
A valid signature isn't enough. Check that the token was issued for you:
Issuer and audience. Orrery refuses to start with authentication on but no
JWT_AUDIENCEorJWT_ISSUER, unless you opt out explicitly withORRERY_ALLOW_UNBOUND_TOKENS. Otherwise a token naming no audience, or one from another issuer the same key verifies, would be accepted.Token type. When your console's client id is also your audience, its ID token and its access token share an
aud. SettingJWT_REQUIRED_TYP=at+jwtis what refuses the ID token.A separate audience per surface. Agents calling you over A2A get their own required audience. As long as the identity provider issues the two separately, a console token is never accepted as an agent's, and the reverse.
The same rule runs the other way, which is why Part 1 said never to forward the user's token: a target that accepted it would be skipping this exact check.
3. Classify Every Caller: Human, Delegated or Service
Once agents call agents, "a verified token" no longer means "a person". Orrery sorts every verified token into one of three kinds:
Kind | Recognised by | Role |
|---|---|---|
Human | Its OAuth client is on a list of human-facing clients (the console). Never over A2A | From the token |
Delegated | A verified RFC 8693 | The lower of the user's role and the agent's ceiling (viewer by default) |
Service | Anything else | The lower of the token's role and a per-client ceiling (viewer by default), and always read-only autonomy |
The act claim is read only from the verified token. A delegated agent never gets more than the user who delegated it, and it can get less.
One caveat from Orrery's own defaults: the human list (ORRERY_HUMAN_CLIENTS) is opt-in. Left unset, every console token counts as human, and startup logs a warning. Set it in production.
4. Only a Human Decides
Dangerous tools wait for approval. That gate assumes the approver is a person, and a requester check alone doesn't verify it. A machine token with the operator role can trigger scale_deployment and then send "approve" itself. The same subject is on both sides, so the check passes. In Orrery's console that is exactly what happens while the human list is unset.
So a decision counts only when:
it comes from a human principal. A delegated or service caller's "approve" is recorded as no decision at all, whatever the text says;
it comes from the person who raised the action. Anyone may deny, but only the requester may approve;
it is newer than the action. An "approve" typed before the action existed can never authorize it;
the whole message is a deliberate word ("approve", "proceed", "go ahead"), not a casual "ok", which people type for many reasons.
An action an agent raises on a user's behalf still needs a path to approval. That path is out of band: the user approves it in the console, over a separate authenticated request, and the agent's task resumes afterwards. The agent's own channel can never carry the decision.

5. Fail Closed When the Person Is Missing
Every "act as the user" feature has an easy fallback: if no user can be found, use the platform's credentials. That fallback is where the privilege escalation lives. Each of these refuses instead:
impersonation with no verified caller answers the 403 the target would give;
token exchange with no subject token offers the user no tools from that server;
a session role without the server's marker becomes a viewer;
a malformed boolean in config stops startup instead of reading as
false. For a default-on control like authentication, reading a typo asfalsewould fail open.
Where an unattended job really needs to act as the platform, it says so explicitly with a named service scope, which code reviewers can see. Orrery also has a deployment-wide switch, ORRERY_IMPERSONATION_FALLBACK=service, for a rollout. It is a visible setting someone chose, not a default the platform falls into.
6. Isolate Conversations by Principal, Not Just by User
Session stores key conversations by a user id. A delegated agent's user id is its user's subject, so subject scoping alone lets any agent acting for Alice read Alice's conversations, and every other agent's.
Orrery gives each machine principal its own session prefix, a hash of its kind, client, acting agent and subject. A machine caller can list and continue only sessions under its prefix. A person still sees everything under their subject, including what their agents did. Memory recall is switched off for machine sessions, because memory is keyed by that same shared subject.
One detail: "someone else's conversation" and "no such conversation" return the same 404, byte for byte. Anything else lets a caller probe which ids exist.
7. Least Privilege Per Call, Not Per Deployment
Three independent limits stack where they are configured, and each one can only lower access:
Who: role-based access from the principal (viewer, operator, admin).
Where: protected namespaces, opt-in, where only admins may change things.
How much: the deployment's autonomy level, when one is set: read-only, mutating, or destructive with confirmation. Service callers are always held to read-only.
On top of those, the per-user token from Part 2, or impersonation, lets the target apply its own permissions. A request has to pass every limit that is configured.
8. Record What a Credential Did
Audit the attempt, not only the success. Orrery's audit hook runs before the permission gates, so a refused call is in the record, along with every token exchange and every approval and refusal of a pending action. Two rules keep the record safe to keep:
record the call's (scrubbed) arguments, its outcome and its size, not the raw result, which holds infrastructure data that names people;
scrub credentials before anything is written, including the ones a tool echoed back.
The Checklist
Identity comes from the verified token on this request, and nowhere else.
Tokens are checked for your issuer, your audience and, where audiences overlap, the access-token type.
Callers are human, delegated or service, and machine callers have ceilings.
Only the human who asked can approve, with a fresh, deliberate decision.
No user means no access, never the platform's access.
Conversations are scoped by principal, and a refusal looks like a miss.
Role, scope and autonomy each narrow access, then the target narrows it again.
Attempts are audited, and secrets never reach the record.
To see these principles from the attacker's side, the Agents in the Open series runs an Orrery sandbox on the public internet, where invited readers can try to get past them.