Identity inherited end to end: Context Authority, delegations, mandates, break glass
Version française : ../identity-context-delegation.md
Kit sections 7, 8.1 and 8.2. Requirements IAM_-007, IAM_-008, IAM_-009, IAM_-010, IAM_-011, IAM_-013, IAM_-014, IAM_-017, AGEN-014, RELE-005, AUDI-001, AUDI-002, AUDI-003, AUDI-005. API: control-plane-v1.md, section "Context Authority, delegations, mandates and break glass". Settings: configuration.md.
Absolute rule
Every agent, sub-agent and governed application carries, verifiably, the original user (root_principal_id, on_behalf_of), its own technical identity (workload_identity) and the delegation chain (delegation_chain). Each action applies the intersection of the rights of the user, the agent, the delegation, the resource (OpenFGA or the builtin tuples) and the policy (OPA or the builtin engine). No path grants more.
Context Authority (eai.domains.context)
POST /v1/context/issuesigns an Execution Context v1 (contracts/v1/execution-context.schema.json, every field group of kit 8.1). Format: JWS compact, header{alg, kid, typ: eai-ctx+jws}, header and payload in RFC 8785 canonical JSON (eai.domains.context.jcs), algorithm ES256 (ECDSA P-256) or EdDSA (Ed25519),kid= RFC 7638 thumbprint. Claims:iss,aud(list),iat,exp,jti,sub,tid,ctx.- The identity block carries
token_reference(method and credential id, or a subject digest), never the raw token: the contract refuses a value that looks like a JWT. POST /v1/context/verifychecks, in order: structure and canonical form, algorithm, known non revoked key, signature, issuer, audience (amongEAI_CONTEXT_AUDIENCES), lifetime (EAI_CONTEXT_LEEWAY_SECONDS), tenant, issuance record (eai.execution_contexts: unknown or revoked jti refused), document digest, delegation chain. Every refusal is audited (context.verifydeny) in its own transaction.- Keys: the private key comes from OpenBao (
EAI_CONTEXT_SIGNING_KEY_REF, production) or fromEAI_CONTEXT_SIGNING_KEY(dev and test, refused in prod). Only the public JWK is stored (eai.context_signing_keys). Changing the key is a rotation: the previous key is retired and still verifies until its last signature plusEAI_CONTEXT_MAX_TTL_SECONDS;POST /v1/context/keys/{kid}/revoke(platform_admin) stops it at once.GET /v1/context/jwkspublishes the keys that may verify. - Fail closed: no key, unreadable key, OpenBao unreachable, revoked key or key registry unavailable gives 503
context_authority_unavailable, nothing is signed or accepted. WithEAI_CONTEXT_REQUIRED=1(mandatory in prod) a run without a signed context is refused.
Delegations (eai.domains.delegation)
POST /v1/delegations: a user or an application delegates scopes, audiences, resource restrictions, a classification ceiling,max_depth, an expiry andredelegation_allowedtoagent:,app:,service:orsubagent:. The scopes must be held by the caller and the classification must not exceed its clearance. An agent cannot create a root delegation.POST /v1/delegations/{id}/restrict: a child is always covered by its parent (scopes, audience, resources with*as the only wildcard, a sound subset test), classification not higher, expiry not later, depth = parent + 1 withinmax_depth, only when the parent allows a redelegation.POST /v1/delegations/{id}/revokerevokes every descendant (chain @> [id]) and their signed contexts;POST /v1/delegations/revoke-principalrevokes everything rooted at a user.- Every tenant table (
delegations,execution_contexts,agent_mandates,break_glass_grants) has RLS enabled and forced (V115 to V118).
Runtime (eai.domains.agents, eai.domains.delegation.runtime)
- A run (
POST /v1/agents/{id}/runs) storesroot_principal_id,on_behalf_of,delegation_id,workload_identityandmandate_id(V119). Withoutdelegation_id, an implicit delegation is created: caller scopes intersected with the agent declared scopes, lower classification. - A sub-agent (
POST /v1/runs/{id}/subagents) receives a delegation restricted from the parent one, intersected with the sub-agent scopes: never wider. - Every sensitive step (
tool_call,model_call,subagent,act,approval) re-validates the delegation chain, the session, the mandate and a policy decision before it is recorded: a revocation or a policy change is effective at the next action (403delegation_revoked,session_revoked,mandate_revoked,policy_denied, audited). POST /v1/delegations/authorizereturns the decision layer by layer (chain, session, user, agent, delegation, classification, audience, resource, relationship, policy);allowonly when every layer allows; a relationship engine error is a deny.
Scheduled agents: mandates (IAM_-011)
A scheduled run acts under an approved mandate bound to an active service identity (POST /v1/mandates, approved by another person through /v1/approvals/{id}/decide). Its delegation is rooted at service:<name> on behalf of mandate:<id>: no human identity is invented. Without a mandate the scheduler skips the run (skipped: mandate_required, audited schedule.run deny). Revoking the mandate revokes its delegations.
Break glass (IAM_-013) and four eyes (IAM_-014)
POST /v1/break-glass: roles and scopes, a justification, a duration (at mostEAI_BREAK_GLASS_MAX_SECONDS). The approval needs two distinct approvers (security_officerortenant_admin), never the requester nor the beneficiary. Once approved the grant is active, expires on its own (break_glass.expire), can be revoked, and every delegation created under it is bound to it and never outlives it.POST /v1/break-glass/{id}/contextsigns a context carryingauthorization.break_glass.- Approvals: the requester, the user an agent acts for (
on_behalf_of) and theexcluded_approversof the request can never approve (403separation_of_duties); a quorum counts distinct approvers only (409already_decided).
Audit (AUDI-001 to AUDI-005)
The Context Authority and the runtime bind an identity context (eai.audit context variable, eai.domains.context.state). Every audit event of the same tenant written while it is bound is an Audit Event 1.1 with on_behalf_of, root_principal_id, delegation (id, parent, chain, depth, mandate, break glass), agent (run, parent run, depth, sub-agent flag), workload, session, causal (request, execution, action, trace, span, parent event) and authorization.decision_ids, approvals.approval_ids. These fields are part of the hash chain (audit_events.context, V119); events written outside a verified context stay Audit Event 1.0.
Tests
services/control-plane/tests/test_identity_context_delegation.py (signature, issuer, audience, expiry, tenant, revocation, rotation, fail closed, narrower delegations, pattern algebra, sub-agent rights, revocation during a run, session closed, user revoked, policy change, mandate, break glass, four eyes, audit fields, RLS), contracts/tests/test_contracts.py (contracts), tests/test_agents_execution.py (scheduler with mandate).
Limits
- SPIFFE:
workload_identity.spiffe_idcomes fromEAI_WORKLOAD_SPIFFE_IDandcaller_spiffe_idfrom the authenticated SPIFFE caller;test_workload_spiffe_identity_reaches_run_context_and_auditproves it reaches the run, the signed context and the audit event. The SVID itself is issued by SPIRE outside the Control Plane. - OpenBao reference: the KV read is tested against a simulated OpenBao backend (
test_signing_key_is_read_from_the_openbao_reference_and_never_exposed: key read, never stored nor returned, missing secret and malformed reference refused); no live OpenBao server is contacted by the tests. - OPA: the policy layer uses
eai.policy.evaluate(OPA when configured, builtin otherwise).test_opa_decision_is_taken_before_each_action_with_the_original_userruns it against a local OPA HTTP endpoint: OPA receiveson_behalf_ofanddelegation_id, a policy change refuses the next action, an unreachable OPA refuses (fail closed).