Delegation tokens
See Delegation tokens for why a tool needs to know who a call was made for.
A delegation token is a short-lived JWT the Omnia operator signs and puts
on the outbound tool call as X-Omnia-Delegation. It says: this user, through
this agent, in this workspace, for this session.
Your tool verifies it against the operator’s public keys and does whatever it likes with the result: refuse the call, narrow a query, write an audit line, or nothing at all.
Opting in
Section titled “Opting in”See Delegation tokens for why opt-in is required.
apiVersion: omnia.altairalabs.ai/v1alpha1kind: ToolRegistrymetadata: name: ordersspec: handlers: - name: http type: http delegation: enabled: true # send this receiver X-Omnia-Delegation httpConfig: endpoint: http://orders.acme-ns.svc.cluster.localauth.type sets how the runtime authenticates to your tool. delegation.enabled
sets whether your tool receives a delegation token.
It is rejected on type: client handlers. It is also rejected on type: platform
handlers, which the operator serves itself and for which delegation is implied.
What is in the token
Section titled “What is in the token”| Claim | Meaning | Example |
|---|---|---|
sub | the end user, pseudonymised | a3f1… |
act.sub | the agent | agent:acme/checkout-bot |
wid | the workspace UID | 8f2c… |
sid | the session | sess-01J… |
scope | the tool being called | orders::lookup |
aud | the receiver this token was minted for | acme/orders/http |
exp | five minutes from issue, never later than the subject token it was minted from | — |
origin | how the end user came to be known | oidc |
wrole | the end user’s role in the wid workspace, when established (see below) | editor |
principals | session tokens only: the user’s IdP id and groups, when verified (see below) | ["oid-…", "grp-…"] |
sub is always there; origin says how well it is known
Section titled “sub is always there; origin says how well it is known”See Delegation tokens for what a signature does and does not certify.
| origin | means |
|---|---|
oidc | the operator verified this person against an identity provider |
client-key | an API key authenticated; no human is necessarily present |
shared-token | a shared credential authenticated |
management-plane | a dashboard-minted token, acting for someone |
edge-trust | an upstream proxy asserted the identity |
scheduled | a scheduled run; there is no end user |
Claims.UserIsVerified() reports whether origin is oidc.
Only the operator writes oidc. A mint request that asserts origin: oidc
without a subject_token is refused with 403; every other origin is the
calling pod’s account of itself and is stamped as given.
principals is on session tokens only
Section titled “principals is on session tokens only”principals lists the end user’s identity-provider principal ids: their own id
(Entra oid, else the IdP’s sub) followed by the IdP token’s groups.
memory-api matches it against the ACLs of restricted institutional memory.
- It is written only when the operator verified the user against a configured
issuer (
origin: oidc), and only into a session token (audienceomnia-subject). Re-exchanging a session token carries it forward. A token minted for a tool never carries it. - An Entra groups overage (
_claim_namesnaminggroups, orhasgroups) is never passed through: the claim then holds the user’s id alone. So does agroupsclaim that is not a list of strings, lists more than 256 groups, or holds one longer than 256 bytes. Leaving groups out narrows access; it never widens it. - A dashboard (management-plane) proof carries no groups, so it yields no
principals. - Principals are not namespaced by issuer. The claim holds the IdP’s ids
as they are, with no issuer prefix, and memory matches them against
connector-written ACLs as plain strings. With more than one trusted issuer,
an id one IdP issues to a user or group would match an ACL written for the
same string from another IdP. Entra object ids and most IdPs’
subvalues are globally unique, so this is safe in practice; configure several issuers only when their ids cannot collide.
wrole is present only when it was verified for this workspace
Section titled “wrole is present only when it was verified for this workspace”wrole is viewer, editor or owner. The mint writes it only when
origin is oidc and the role was computed for the same workspace as wid.
The dashboard computes it for a person using an agent through it, and the mint
trusts the dashboard’s management-plane token for this by default
(operator.delegationAPI.trustManagementPlane). Otherwise the claim is
absent, never guessed. Claims.User() returns an identity.User whose
AtLeast(role) is false for any user the operator did not verify.
The role travels one route, from the dashboard token to a session token to a delegation token, and each hop is bound:
- A session token carries
wroleonly when the facade’s exchange names the session (sid) it is for, and never when the subject token is itself a session token. Re-exchanging a session token keeps the user and drops the role. - A session token’s role is copied into a delegation token only for the
session in its
sidand for the agent in itsact.sub. - No token outlives the subject token it was minted from:
expis capped at the subject token’s own. - A dashboard token names the agent it was minted for (
agent); the mint refuses it from a caller that runs a different agent.
A dashboard-minted token (origin management-plane on the facade) reaches the
mint as a subject token, so tokens minted for that person carry oidc: the
operator verified the dashboard’s signature over a user the dashboard signed
in.
aud format
Section titled “aud format”aud is <namespace>/<registry>/<handler>: the receiver as Omnia names it,
not its network address. The operator’s own Sandbox API is the exception: its
audience is omnia/sandbox-api. The audience of a type: platform handler is
omnia-platform/<workspace-uid>, and the mint refuses it for any workspace but the
caller’s own. RFC 8693 typically uses a URL for aud. Your service
cannot derive its own audience value, so pass it in as configuration.
Verifying it
Section titled “Verifying it”Fetch the operator’s public keys once, cache them, and check the signature locally. There is no per-call round trip.
The delegation server speaks plain HTTP on a ClusterIP Service. Inside the cluster, fetch the JWKS document from:
http://omnia-operator.omnia-system.svc.cluster.local:8086/api/v1/.well-known/jwks.jsonwith the port from operator.delegationAPI.port.
A tool running outside the cluster needs the JWKS published. Enable one of
the chart’s two exposures, which route only GET on the exact JWKS path
and terminate TLS at the edge. The mint endpoint on the same port
(POST /api/v1/delegation/token) stays cluster-internal either way.
| Value | Exposure |
|---|---|
operator.delegationAPI.publicJWKS.externalGateway.enabled | An HTTPRoute on the chart’s external Gateway (needs gateway.enabled); externalGateway.hostname restricts it to one host |
operator.delegationAPI.publicJWKS.ingress.enabled | A Kubernetes Ingress; set ingress.host, and ingress.className, ingress.tls and ingress.annotations as for any Ingress |
The external URL is then https://<host>/api/v1/.well-known/jwks.json.
Enabling both exposures, the Gateway route without gateway.enabled, or the
Ingress without a host fails the render.
See Verify a delegation token in Go
for a worked example using pkg/delegation.
The verifier refetches the key set when it sees a key id it does not recognise. That miss is the rotation signal, so nothing polls, and a floor keeps a stream of forged key ids from turning every request into a fetch.
In another language
Section titled “In another language”Any JWT library will do. The rules are:
- Read
kidfrom the header and select that key from the JWKS. The key type isOKP/Ed25519, so the algorithm isEdDSA. - Reject any other algorithm. In particular never accept
none, and never let the token choose. - Require
issto be exactlyomnia-operator. - Require
audto contain the audience your service was configured with. - Check
exp, allowing about 30 seconds of clock skew. - Read
originbefore acting onsub. Onlyoidcmeans Omnia checked. - On an unrecognised
kid, refetch the JWKS, but not more often than every few seconds.
See Delegation tokens for what a delegation token does not guarantee.