Skip to content

Delegation tokens

When an agent calls a tool, the tool has no idea who it is acting for. It knows a request arrived. It does not know which agent instructed it, or which person that agent was working on behalf of.

A delegation token closes that gap. The operator signs a short-lived JWT naming the user, the agent, the workspace and the session. The agent’s runtime requests it from the operator and sets it on the outbound tool call as X-Omnia-Delegation. The tool decides what to do with that: refuse the call, narrow a query, write an audit line, or nothing at all.

The runtime asks the operator for a token when it calls a handler that opted in. The operator checks who is asking with the Kubernetes API, signs the token, and the tool checks the signature against the operator’s published keys.

sequenceDiagram
participant R as Runtime container
participant O as Operator
participant K as Kubernetes API server
participant T as Tool (receiver)
R->>O: POST /api/v1/delegation/token (ServiceAccount token)
O->>K: TokenReview
K-->>O: ServiceAccount identity
O-->>R: signed JWT
R->>T: tool call with X-Omnia-Delegation
T->>O: GET /api/v1/.well-known/jwks.json
O-->>T: public keys

The operator works out the workspace and the agent from the cluster, using the authenticated ServiceAccount. The session and the end-user pseudonym come from the request, so the calling pod supplies those two.

No token is sent unless the handler asks for one. Most tools are third parties that have never heard of Omnia, and sending them a signed statement about a real user would be a credential-shaped artifact handed out for nothing. A handler declares delegation.enabled: true to receive one; see Opting in for the field.

sub is always there; origin says how well it is known

Section titled “sub is always there; origin says how well it is known”

A signature means the operator minted the token. It does not mean every claim inside it was independently checked, and which is which is the first thing you need.

Every caller gets a sub, a stable pseudonym, regardless of how it authenticated: through an identity provider, an API key, or a schedule with no human involved. The origin claim says how that sub came to be known. Only origin: oidc means the operator itself verified an identity provider’s signature and read the subject off the result. Every other value is the calling agent pod’s own account of how it authenticated, not something the operator attested. A token with no origin at all falls in the same group. Check origin before you act on sub.

wid (the workspace) and act.sub (the agent) need no such check. The operator derives both from the calling pod’s Kubernetes ServiceAccount on every path and never takes them from the request. wid is the UID of the Workspace that owns the ServiceAccount’s namespace, and is absent when the namespace belongs to no Workspace. act.sub is an agent that runs as that ServiceAccount. When one ServiceAccount runs several agents, the pod chooses which of them to name, but only from that set.

The token says who is asking. What they may do is your decision, from your own source of truth: authorization is a separate concern.

aud names one receiver, in the form <namespace>/<registry>/<handler>, and exp is five minutes after issue at most. When the token was derived from the user’s session token, it is capped at that token’s own expiry if that is sooner. The receiver does the refusing: a verifier built with the receiver’s audience accepts only tokens whose aud is that audience, so a token minted for one handler fails at a receiver configured with a different audience. A receiver that skips the audience check accepts any.

Jira and Microsoft Graph have never heard of Omnia and ignore this header. Giving those services a per-user identity means giving them a credential they understand, through a separate on-behalf-of mechanism.