Skip to content

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.

See Delegation tokens for why opt-in is required.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ToolRegistry
metadata:
name: orders
spec:
handlers:
- name: http
type: http
delegation:
enabled: true # send this receiver X-Omnia-Delegation
httpConfig:
endpoint: http://orders.acme-ns.svc.cluster.local

auth.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.

ClaimMeaningExample
subthe end user, pseudonymiseda3f1…
act.subthe agentagent:acme/checkout-bot
widthe workspace UID8f2c…
sidthe sessionsess-01J…
scopethe tool being calledorders::lookup
audthe receiver this token was minted foracme/orders/http
expfive minutes from issue, never later than the subject token it was minted from—
originhow the end user came to be knownoidc
wrolethe end user’s role in the wid workspace, when established (see below)editor
principalssession 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.

originmeans
oidcthe operator verified this person against an identity provider
client-keyan API key authenticated; no human is necessarily present
shared-tokena shared credential authenticated
management-planea dashboard-minted token, acting for someone
edge-trustan upstream proxy asserted the identity
scheduleda 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 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 (audience omnia-subject). Re-exchanging a session token carries it forward. A token minted for a tool never carries it.
  • An Entra groups overage (_claim_names naming groups, or hasgroups) is never passed through: the claim then holds the user’s id alone. So does a groups claim 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’ sub values 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 wrole only 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 sid and for the agent in its act.sub.
  • No token outlives the subject token it was minted from: exp is 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 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.

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.json

with 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.

ValueExposure
operator.delegationAPI.publicJWKS.externalGateway.enabledAn HTTPRoute on the chart’s external Gateway (needs gateway.enabled); externalGateway.hostname restricts it to one host
operator.delegationAPI.publicJWKS.ingress.enabledA 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.

Any JWT library will do. The rules are:

  1. Read kid from the header and select that key from the JWKS. The key type is OKP / Ed25519, so the algorithm is EdDSA.
  2. Reject any other algorithm. In particular never accept none, and never let the token choose.
  3. Require iss to be exactly omnia-operator.
  4. Require aud to contain the audience your service was configured with.
  5. Check exp, allowing about 30 seconds of clock skew.
  6. Read origin before acting on sub. Only oidc means Omnia checked.
  7. 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.