Skip to main content

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/issue signs 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/verify checks, in order: structure and canonical form, algorithm, known non revoked key, signature, issuer, audience (among EAI_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.verify deny) in its own transaction.
  • Keys: the private key comes from OpenBao (EAI_CONTEXT_SIGNING_KEY_REF, production) or from EAI_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 plus EAI_CONTEXT_MAX_TTL_SECONDS; POST /v1/context/keys/{kid}/revoke (platform_admin) stops it at once. GET /v1/context/jwks publishes 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. With EAI_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 and redelegation_allowed to agent:, app:, service: or subagent:. 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 within max_depth, only when the parent allows a redelegation.
  • POST /v1/delegations/{id}/revoke revokes every descendant (chain @> [id]) and their signed contexts; POST /v1/delegations/revoke-principal revokes 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) stores root_principal_id, on_behalf_of, delegation_id, workload_identity and mandate_id (V119). Without delegation_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 (403 delegation_revoked, session_revoked, mandate_revoked, policy_denied, audited).
  • POST /v1/delegations/authorize returns the decision layer by layer (chain, session, user, agent, delegation, classification, audience, resource, relationship, policy); allow only 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 most EAI_BREAK_GLASS_MAX_SECONDS). The approval needs two distinct approvers (security_officer or tenant_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}/context signs a context carrying authorization.break_glass.
  • Approvals: the requester, the user an agent acts for (on_behalf_of) and the excluded_approvers of the request can never approve (403 separation_of_duties); a quorum counts distinct approvers only (409 already_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_id comes from EAI_WORKLOAD_SPIFFE_ID and caller_spiffe_id from the authenticated SPIFFE caller; test_workload_spiffe_identity_reaches_run_context_and_audit proves 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_user runs it against a local OPA HTTP endpoint: OPA receives on_behalf_of and delegation_id, a policy change refuses the next action, an unreachable OPA refuses (fail closed).