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:

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 act claim: an agent acting for a user

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:

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.

Out-of-band approval: an agent's request raises a pending action, the agent's channel reports input required, the user approves in the console, and the agent's next message on the task lets Orrery retry it with the same arguments

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:

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:

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:


The Checklist

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.

Read on Tirraflow