Skip to content
PaloNexus
Request access Request

Enterprise IAM API

The enterprise identity-and-access-management (IAM) API syncs directories, resolves employees from login tokens, governs agent ownership, exchanges delegations for short-lived tokens, and issues or verifies governance credentials. This reference defines the request and response contracts for the core IAM features (F1–F6) and the compliance and provenance credential dimensions (F20, F24, F25). These PaloNexus feature identifiers also appear in the section headings below.

These features live in the agent-idp service, alongside the agent onboarding / delegation / revocation APIs documented in HTTP API §4. They turn PaloNexus from an agent-only control plane into one that knows the workforce behind each agent: who an employee is, who owns an agent, who may delegate authority, and what short-lived token an agent may exchange that delegation for. For the why, see Connect agents to enterprise authority.

Base URL — same service as the rest of agent-idp: :8090 (env PORT). All these endpoints are unauthenticated management-plane calls in the MVP; the cryptographic edge is the STS token they mint (F6) and the verifiable-presentation (VP) verification under onboarding.

Conventions

  • Request bodies are camelCase (tenantId, ownerRef, agentProof) — they are Pydantic models. Responses are snake_case (employee_subject, owner_active, authority_basis) — they are stored records, returned as-is minus internal _seq cursors.
  • Fail-closed. Unknown owner, inactive approver, cross-tenant reference, unusable delegation, disallowed audience — every ambiguous case denies. A stale login token can never reactivate an inactive employee, and a suspended agent can never receive a delegation or mint a token.
  • Errors use the envelope {"error": {"code", "message"}}.
  • Tenant-scoped. Every record carries a tenant_id; a call for tenant A never reads or writes tenant-B state.
  • Live machine-readable contract: OpenAPI at /openapi.json, Swagger UI at /docs.

Ingest a per-tenant SCIM (System for Cross-domain Identity Management) 2.0 snapshot (the full desired state of Users + Groups) and reconcile it into the directory. Snapshot diffing gives joiner / mover / leaver / rehire handling and idempotency for free: re-posting the same snapshot reports everything unchanged.

The stable enterprise subject is <idp>:<tenant_id>:<external_id> — derived from the identity provider’s (IdP’s) durable id (Entra ID oid / Okta id, surfaced as SCIM externalId), never from email, so an email change can never fork a person.

MethodPathPurpose
POST/v1/directory/syncreconcile a SCIM snapshot (auto-runs the F4 cascade after)
GET/v1/directory/employees?tenant=&status=list employees (filter by tenant + status)
GET/v1/directory/employees/{subject}one employee by stable subject (404 if absent)
GET/v1/directory/groups?tenant=list groups
GET/v1/directory/groups/{group_key}one group (404 if absent)
GET/v1/directory/syncs?tenant=sync-run history, newest first
GET/v1/directory/conflicts?tenant=on-demand dangling/inactive-manager conflicts (tenant required)

Request — tenantId and idp are control fields; users/groups are raw SCIM 2.0 resources (camelCase, including the enterprise extension):

{
"tenantId": "acme-corp",
"idp": "entra",
"mode": "snapshot",
"users": [
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"id": "8a1f-acme-1001",
"externalId": "oid-1001",
"userName": "alice.chen@acme-corp.example",
"name": { "givenName": "Alice", "familyName": "Chen" },
"displayName": "Alice Chen",
"title": "Engineering Manager",
"emails": [{ "value": "alice.chen@acme-corp.example", "primary": true }],
"active": true,
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"department": "Engineering",
"manager": { "value": "oid-1000" }
}
}
],
"groups": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": "eng-all",
"displayName": "Engineering - All",
"members": [{ "value": "oid-1001" }]
}
]
}

idp defaults to entra; mode defaults to snapshot. App assignments ride on the SCIM entitlements attribute; groups/roles on groups/roles.

Response — the sync report (a sync-run record) with a cascade block appended (the F4 revocation cascade that auto-runs after every sync):

{
"id": "uuid",
"tenant_id": "acme-corp",
"idp": "entra",
"started_at": "ISO8601",
"finished_at": "ISO8601",
"status": "ok",
"counts": {
"users_created": 1, "users_updated": 0, "users_deactivated": 0,
"users_reactivated": 0, "users_unchanged": 0,
"groups_created": 1, "groups_updated": 0, "groups_unchanged": 0,
"errors": 0
},
"errors": [],
"conflicts": [],
"cascade": {
"agents_suspended": 0, "agents_quarantined": 0,
"delegations_revoked": 0, "delegations_invalidated": 0,
"delegations_expired": 0, "by_reason": {}
}
}

status is "ok", or "partial" if any record failed to parse (those land in errors[] and are skipped — one bad row never corrupts the rest). Lifecycle outcomes:

Snapshot changeEffect
new stable subject appearscreate (users_created)
dept / manager / group / app / email change (same external_id)update, same subject
active:false, or subject absent from snapshotdeactivate (users_deactivated)
previously-inactive subject reappears activereactivate (users_reactivated)
Terminal window
curl -s -XPOST localhost:8090/v1/directory/sync -H 'content-type: application/json' \
-d @snapshot.json | jq '.status, .counts'

Keyed by employee_subject. source is always scim; session is set only by F2 identity resolution (token-sourced, never authoritative).

FieldTypeMeaning
employee_subjectstringstable subject <idp>:<tenant_id>:<external_id> (the key)
idpstringentra / okta
tenant_idstringPaloNexus tenant
external_idstringIdP stable id (Entra oid / Okta id, via SCIM externalId)
external_idp_idstring|nullthe SCIM id if distinct from externalId
emailstring|nullprimary email
display_namestring|null
given_name, family_name, title, departmentstring|null
manager_subjectstring|nullthe manager’s stable subject
groups[]stringdirectory group external ids (sorted, deduped)
roles[]stringdirectory roles
app_assignments[]stringfrom SCIM entitlements
statusstringactive / inactive
activeboolstatus == "active"
source"scim"provenance
created_at, updated_at, deactivated_at, last_synced_atRFC3339|null
sessionobject|nullF2 token session (see §2); never set by sync

Group record: group_key (<tenant_id>:<external_id>, the key), tenant_id, external_id, display_name, members (list of employee_subject), and created_at / updated_at / last_synced_at.

Sync-run record: id, tenant_id, idp, started_at, finished_at, status (ok / partial), counts (the table above), errors ([{resource_type, external_id, error}]), conflicts.

Conflicts (/v1/directory/conflicts and the conflicts array on a sync) surface manager references that dangle. Each is {type, subject, detail} with type one of manager_missing (manager not in the tenant) or manager_inactive (manager deactivated).


2. Employee identity / token precedence (F2)

Section titled “2. Employee identity / token precedence (F2)”

Resolve a decoded login-token claim set against the SCIM directory under explicit source precedence. SCIM is authoritative; the token contributes session context and surfaces conflicts — it never mutates an authoritative field.

MVP: claims are trusted edge input — no JSON Web Token (JWT) signature verification (deferred to BACKLOG).

MethodPathPurpose
POST/v1/identity/resolveresolve token claims → effective identity + session + conflicts
GET/v1/identity/sessions?tenant=employees with a recent sign-in, newest first

Request — idp/tenant are optional hints; otherwise the token iss (or Entra tid) is matched through the issuer map:

{
"claims": {
"iss": "https://login.microsoftonline.com/1111…/v2.0",
"oid": "oid-1001",
"email": "alice.chen@acme-corp.example",
"groups": ["eng-all"],
"roles": []
},
"idp": "entra",
"tenant": "acme-corp"
}

Subject derivation is per-IdP and never from email:

IdPsubject claimstable subject
entraoid (falls back to sub/oid)entra:<tenant>:<oid>
oktasubokta:<tenant>:<sub>

Without idp+tenant, the token iss is looked up in the issuer map; an Entra token can also be matched by its tid GUID embedded in a mapped issuer.

Response:

{
"resolved": true,
"reason": "ok",
"employee_subject": "entra:acme-corp:oid-1001",
"idp": "entra",
"tenant_id": "acme-corp",
"effective": {
"employee_subject": "entra:acme-corp:oid-1001",
"email": "alice.chen@acme-corp.example",
"display_name": "Alice Chen",
"department": "Engineering",
"manager_subject": "entra:acme-corp:oid-1000",
"groups": ["eng-all"],
"roles": [],
"status": "active",
"active": true
},
"session": {
"last_seen_at": "ISO8601",
"idp_issuer": "https://login.microsoftonline.com/1111…/v2.0",
"token_email": "alice.chen@acme-corp.example",
"token_groups": ["eng-all"],
"token_roles": [],
"preferred_username": "alice.chen@acme-corp.example",
"conflicts": []
},
"conflicts": [],
"precedence": { "...": "..." }
}

resolved=false (with reason and an unresolved conflict) when the issuer is unknown, a subject claim is missing, or the subject has no SCIM employee (not auto-provisioned in MVP). effective.active is SCIM status, so a stale token over an inactive employee yields active:false — access denied.

Precedence map (returned as precedence):

FieldSource of truth
subjectissuer + tenant + IdP subject (never email)
statusscim — a token can never make an inactive employee active
managerscim
departmentscim
groupsscim for durable membership; token groups kept as session claims
emailscim (token updates session only)
display_namescim (token updates session only)
rolesscim / explicit (raw token roles are not auto-privileged)

Conflict types (in conflicts[], each {type, detail}):

TypeWhen
email_conflicttoken email ≠ directory email (directory kept)
group_conflicttoken claims groups not in the directory (kept as session claims only)
stale_token_inactivetoken presented but the employee is inactive — access denied
unresolvedclaims could not be mapped to a stable subject / no matching employee

GET /v1/identity/sessions returns, per employee with a session, {employee_subject, display_name, tenant_id, status, email, department, session}.

Terminal window
curl -s -XPOST localhost:8090/v1/identity/resolve -H 'content-type: application/json' \
-d '{"claims":{"iss":"https://globex.okta.com","sub":"00u-7","email":"sam@globex"},"tenant":"globex","idp":"okta"}' \
| jq '.resolved, .effective.active, .conflicts'

Every agent must have accountable, tenant-scoped ownership. No agent is orphaned, and none reaches active without a valid active owner + business sponsor + risk tier + approved runtime. Owner health is re-derived from the live directory, so an F1 sync that deactivates an employee instantly shows owner_inactive on every agent they own.

MethodPathPurpose
POST/v1/governance/agentsregister a governed agent (201)
GET/v1/governance/agents?tenant=&status=list governed agents
GET/v1/governance/agents/{name}one governed agent (404 if absent)
POST/v1/governance/agents/{name}/transitionmove lifecycle status
POST/v1/governance/agents/{name}/transfer-ownershipreassign owner
GET/v1/governance/issues?tenant=orphan / inactive-owner / incomplete-active surface
{
"agentName": "triage-agent",
"tenantId": "acme-corp",
"ownerType": "employee",
"ownerRef": "entra:acme-corp:oid-1001",
"teamRef": "acme-corp:grp-sre",
"businessSponsor": "entra:acme-corp:oid-1000",
"riskTier": "high",
"approvedRuntime": "doks_prod",
"createdBy": "entra:acme-corp:oid-1001"
}

ownerType=employeeownerRef is an F2 subject; ownerType=team / service_account_owner_groupownerRef is a group key <tenant>:<external_id>. An unknown or cross-tenant owner is rejected 400. An inactive owner may register but the agent stays draft (it cannot activate). transition to active rejects 409 activation_blocked with the blocker list if the gate is not met.

GET/POST responses are enriched with live owner health (owner_*, sponsor_*, activation_blockers):

FieldTypeMeaning
agent_namestringthe key
tenant_idstring
owner_typestringemployee / team / service_account_owner_group
owner_refstringF2 subject or group key
team_refstring|nulloptional owning team
business_sponsorstring|nullsponsoring employee subject
risk_tierstring|nulllow / medium / high / critical
approved_runtimestring|nullone of the approved runtimes (below)
statusstringlifecycle status (below)
created_by, created_at, updated_at, last_reviewed_at
history[]objectappend-only {at, action, by, detail} audit trail
owner_displaystringresolved owner display name (enriched)
owner_ok / owner_activeboollive owner health (enriched)
sponsor_display / sponsor_activeif a sponsor is set (enriched)
activation_blockers[]stringwhat stops active — empty = ready (enriched)

Taxonomy

Owner typesRisk tiers
employee, team, service_account_owner_grouplow, medium, high, critical
Approved runtimes
local_dev, doks_dev, doks_stage, doks_prod, github_actions, kubernetes_job, external_mcp_client

These are example runtime labels describing where an agent runs; the doks_* values are illustrative (substitute e.g. k8s_prod, eks_prod) and imply no DigitalOcean/DOKS dependency — PaloNexus runs on any Kubernetes or via Docker Compose.

Statuses: draft, pending_approval, approved, active, suspended, quarantined, retired. The F4 cascade sets suspended/quarantined directly; the endpoints below govern manual transitions.

Status-transition map (POST …/transition {status, by}):

FromAllowed targets
draftpending_approval, retired
pending_approvalapproved, draft, retired
approvedactive, suspended, retired
activesuspended, quarantined, retired
suspendedactive, quarantined, retired
quarantinedactive, retired
retired(terminal)

A disallowed move returns 409 bad_transition; → active re-checks the activation gate, so a recovered-but-orphaned agent still cannot re-activate.

POST …/transfer-ownership takes {ownerType, ownerRef, by}; the new owner must resolve and be active (400 otherwise).

GET /v1/governance/issues returns [{agent_name, type, detail, status}] with type one of owner_missing, owner_inactive, incomplete_active.

Terminal window
curl -s -XPOST localhost:8090/v1/governance/agents -H 'content-type: application/json' -d '{
"agentName":"triage-agent","tenantId":"acme-corp","ownerType":"employee",
"ownerRef":"entra:acme-corp:oid-1001","businessSponsor":"entra:acme-corp:oid-1000",
"riskTier":"high","approvedRuntime":"doks_prod"}'
curl -s -XPOST localhost:8090/v1/governance/agents/triage-agent/transition -d '{"status":"approved","by":"admin"}'

4. Governance delegations + revocation cascade (F4)

Section titled “4. Governance delegations + revocation cascade (F4)”

A governance delegation grants a governed agent authority to act, by an accountable human. Revocation is durable persistent state, not a transient denial: the cascade suspends/quarantines agents and revokes/invalidates delegations whenever the underlying human, owner, sponsor, group, or agent state goes bad — and writes a reason-coded log row.

POST /v1/authority/delegations is the grant endpoint. It is also an authorization decision (the F5 human-authority gate) — see §5 for the authority fields and bases.

MethodPathPurpose
POST/v1/authority/delegationsgrant a delegation (201; authorized — see §5)
GET/v1/authority/delegations?tenant=&agent=&status=list, newest first
GET/v1/authority/delegations/{id}one delegation (404 if absent)
GET/v1/authority/delegations/{id}/usableis it usable right now?
POST/v1/authority/delegations/{id}/revokerevoke {reason?, by?}
POST/v1/revocation/cascade?tenant=run the cascade on demand; returns a report
GET/v1/revocation/log?tenant=durable revocation-event log, newest first
FieldTypeMeaning
delegation_idstringthe key
tenant_id, agent_idstring
granted_bystringapproving/granting employee subject
requester_refstring|nullthe human who requested it
task_id, task_descriptionstring|null
actionstringe.g. runbook:read
resourcestringe.g. runbooks-api:/runbooks/* (trailing /* glob)
resource_typestring|null
required_groupstring|nulla group the granter must remain in
statusstringpending / approved / active / revoked / expired / superseded / invalidated
vc_statusstringvalid / revoked / expired / superseded
authority_basis, authority_evidence, policy_decisionF5 — see §5
expires_atint|nullunix seconds
created_at, approved_at, revoked_atRFC3339|null
revocation_reasonstring|nullreason code if terminated
history[]objectappend-only {at, action, by, detail}

GET …/usable returns {usable, status, reason}usable=true only for an active, non-expired delegation; this is exactly what the F6 STS consumes.

POST /v1/revocation/cascade returns the report {agents_suspended, agents_quarantined, delegations_revoked, delegations_invalidated, delegations_expired, by_reason}. Each consequence also appends a durable log row (GET /v1/revocation/log):

FieldTypeMeaning
idstring
tenant_idstring
kindstringagent_suspend / agent_quarantine / delegation_revoke / delegation_invalidate / delegation_expire / vc_revoke
reason_codestringone of the reason codes below
agent_id, delegation_idstring|nullthe affected subject
sourcestringauto (lifecycle) / admin (manual)
bystringactor
detailstring|null
atRFC3339

The cascade emits the marked codes automatically; the rest are part of the charter set and may be supplied to manual revoke.

Reason codeEngine-emittedTrigger
owner_inactiveowning employee deactivated → agent suspended, its live delegations revoked
owner_missingowner unresolvable/cross-tenant → agent quarantined, delegations revoked
sponsor_inactivebusiness sponsor deactivated → agent suspended
delegation_expiredexpires_at reached → delegation expired
delegation_grantor_lost_authoritythe granting human went inactive → delegation invalidated
group_removedgranter no longer in the delegation’s required_groupinvalidated
agent_inactiveagent suspended/quarantined/retired → its live delegations invalidated
manual_admin_revocation✅ (default)the default reason for …/revoke
owner_transferredcharter reason code
sponsor_changedcharter reason code
team_inactivecharter reason code
role_removedcharter reason code
manager_changedcharter reason code
employee_disabledcharter reason code
tenant_disabledcharter reason code
policy_changedcharter reason code
Terminal window
curl -s -XPOST 'localhost:8090/v1/authority/delegations/'$ID'/revoke' \
-d '{"reason":"manual_admin_revocation","by":"admin"}'
curl -s -XPOST 'localhost:8090/v1/revocation/cascade?tenant=acme-corp' | jq '.by_reason'

F4 records who granted a delegation; F5 makes the grant an authorization decision. The human requester and approver must be authenticated, active employees in the agent’s tenant, and the approver must actually hold authority over the resource/task. The proven basis and its evidence are written onto the delegation (authority_basis, authority_evidence, policy_decision) and audited, so every grant is explainable.

The decision uses the F1/F2 directory + F3 governance — no separate policy engine.

POST /v1/authority/delegations (the authorized grant)

Section titled “POST /v1/authority/delegations (the authorized grant)”

The §4 grant body, plus the F5 authority fields:

{
"tenantId": "acme-corp",
"agentId": "triage-agent",
"approverRef": "entra:acme-corp:oid-1000",
"requesterRef": "entra:acme-corp:oid-1001",
"action": "runbook:read",
"resource": "runbooks-api:/runbooks/db-failover",
"taskId": "INC-42",
"requiredGroup": "acme-corp:grp-sre",
"expiresInSeconds": 3600,
"breakGlass": false
}
  • approverRef is the approving human (an active employee subject). grantedBy is accepted as an alias; one of the two is required (400 missing_approver otherwise).
  • requesterRef defaults to the approver if omitted.
  • breakGlass:true short-circuits to the manual_break_glass basis (explicit, audited).

On success → 201 with the full gov-delegation record (status:"active") carrying the authority_basis / authority_evidence / policy_decision.

#BasisHow it is evidenced
1manual_break_glassbreakGlass:true explicitly invoked
2palo_nexus_adminapprover holds the palonexus_admin role or is in an admin group (grp-security)
3business_sponsorapprover is the agent’s business_sponsor
4service_ownerapprover is the agent’s employee owner (owner_ref)
5team_ownerapprover is in the agent’s owning team/group
6resource_ownerapprover owns the resource (or is in the team that owns it) per the resource-ownership map
7manager_chainapprover is in the requester’s manager chain
8group_membershipapprover is a member of the delegation’s requiredGroup
(none)no basis matched → 403 authority_denied
CodeHTTPWhen
inactive_requester403requester is an inactive employee
inactive_approver403approver is an inactive employee
cross_tenant403agent / approver / requester belongs to a different tenant
authority_denied403no authority basis matched
agent_not_active409the agent is suspended / quarantined / retired

(Also: invalid_approver / invalid_requester 400 for an unknown employee, agent_not_governed 404.)

The MVP resource-ownership map used by the resource_owner basis (tenant required):

[
{ "resource": "runbooks-api:/runbooks/*", "owner_type": "team", "owner_ref": "acme-corp:grp-sre" },
{ "resource": "k8s:ml-namespace", "owner_type": "employee", "owner_ref": "entra:acme-corp:oid-1013" }
]
Terminal window
curl -s -XPOST localhost:8090/v1/authority/delegations -H 'content-type: application/json' -d '{
"tenantId":"acme-corp","agentId":"triage-agent","approverRef":"entra:acme-corp:oid-1000",
"requesterRef":"entra:acme-corp:oid-1001","action":"runbook:read",
"resource":"runbooks-api:/runbooks/db-failover","taskId":"INC-42","expiresInSeconds":3600}' \
| jq '.authority_basis, .status'

Exchange delegation evidence + an agent proof-of-possession into a short-lived, audience-bound EdDSA JWT that an ordinary resource server can verify. The token separates the agent subject (sub) from the human actor (act) and carries the delegation_id / task_id it was minted from. The F4 cascade composes for free: a revoked / invalidated / expired delegation is not usable, so the STS refuses to mint.

MethodPathPurpose
POST/v1/sts/tokenexchange → short-lived agent access token
GET/v1/sts/tokens?tenant=STS audit log (issued + denied; metadata only)
{
"tenantId": "acme-corp",
"agentId": "triage-agent",
"delegationId": "uuid",
"taskId": "INC-42",
"action": "runbook:read",
"resource": "runbooks-api:/runbooks/db-failover",
"audience": "https://runbooks.acme.internal",
"agentProof": { "type": "mock_pop", "value": "pop:triage-agent" },
"requestedTtl": 600
}

Response envelope:

{
"access_token": "<eddsa-jwt>",
"token_type": "Bearer",
"expires_in": 600,
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"delegation_id": "uuid",
"task_id": "INC-42",
"audience": "https://runbooks.acme.internal",
"jti": "uuid",
"claims": { "...": "the decoded claim set (below)" }
}

TTL cap: requestedTtl is clamped to 900s (MAX_TTL); default 600s. An excessive request is reduced, not denied.

Audience allowlist — the audience must be one of:

https://api.acme.internal/k8s
https://runbooks.acme.internal
https://pagerduty.acme.internal

Proof-of-possession (MVP): agentProof must be {"type":"mock_pop","value":"pop:<agentId>"}. Its SHA-256 binds into the cnf claim. DPoP / mutual-TLS (mTLS) / DID-bound proofs are deferred.

Signing key: the token reuses the existing issuer Ed25519 key (ISSUER_PRIVATE_KEY_B64) — no new signing key; header alg=EdDSA, typ=at+jwt, kid=<issuer DID>. JWKS / rotation are deferred.

ClaimValue
issthe issuer DID
subagent:<tenant_id>:<agent_id>
sub_typeagent
actthe human actor (requester, else granter)
act_typeemployee
audthe requested (allowlisted) audience
iat / expissued-at / expiry (iat + ttl)
jtiunique token id
tenant_id, agent_id
delegation_idthe delegation it was minted from
task_idrequest taskId, else the delegation’s
action, resource, resource_typefrom request, else the delegation
cnf{ "mock_pop_sha256": "<first 32 hex of proof hash>" }
scopethe effective action

Every outcome (issued or denied) is written to the durable token log (metadata only, never token contents). Denials return 403 with one of:

CodeWhen
agent_not_foundagent is not governed
tenant_mismatchagent or delegation belongs to a different tenant
agent_inactiveagent is suspended / quarantined / retired
owner_invalidagent owner is not valid/active
delegation_not_foundno such delegation
delegation_agent_mismatchdelegation is for a different agent
delegation_expireddelegation reached its expiry
delegation_revokeddelegation is revoked / invalidated / otherwise unusable
task_mismatchrequest taskId ≠ the delegation’s
action_mismatchrequest action ≠ the delegation’s
resource_mismatchrequest resource not covered by the delegation’s
actor_inactivethe human actor is inactive
audience_not_allowedaudience not in the allowlist
missing_proof / invalid_proofagentProof absent / wrong type or value

GET /v1/sts/tokens returns the log rows: {id, tenant_id, decision (issued/denied), reason, agent_id, delegation_id, jti, sub, act, aud, task_id, iat, exp, at}.

Terminal window
curl -s -XPOST localhost:8090/v1/sts/token -H 'content-type: application/json' -d '{
"tenantId":"acme-corp","agentId":"triage-agent","delegationId":"'$ID'",
"audience":"https://runbooks.acme.internal",
"agentProof":{"type":"mock_pop","value":"pop:triage-agent"}}' \
| jq '.expires_in, .claims.sub, .claims.act'

A named-standard attestation about an agent (GDPR, HIPAA, SOC2-TypeII, EU-AI-Act-Art50, …), issued by an accountable human holding the compliance_auditor role. Query is public; issuance requires the role. Revocation does not currently check the role — a known, tracked gap (unlike provenance credentials in §9, where revoke does check it). See Governance credentials for the concept.

MethodPathPurpose
POST/v1/compliance/credentialsissue a compliance credential (role-gated)
GET/v1/compliance/credentials?agent=&standard=&status=query credentials (public)
POST/v1/compliance/credentials/{credential_id}/revokerevoke a credential (not role-gated — see note above)
GET/v1/agents/{agent_id}/compliancethe agent’s currently-valid compliance credentials
GET/v1/agents/{agent_id}/disclosurecombined compliance + provenance disclosure artifact
{
"agentId": "securityops-agent",
"standard": "GDPR",
"scope": "PII access during containment actions",
"issuerRef": "entra:acme-corp:oid-1042",
"evidenceRef": "https://audit-evidence.acme-corp.internal/gdpr-2026",
"expiresAt": "2027-07-03T00:00:00Z"
}

Response (201) — the stored record plus the signed Verifiable Credential (VC) (Feature 24):

{
"credential_id": "uuid",
"agent_id": "securityops-agent",
"standard": "GDPR",
"scope": "PII access during containment actions",
"issuer_ref": "entra:acme-corp:oid-1042",
"evidence_ref": "https://audit-evidence.acme-corp.internal/gdpr-2026",
"issued_at": "2026-07-03T18:00:00Z",
"expires_at": "2027-07-03T00:00:00Z",
"status": "valid",
"revoked_at": null,
"revocation_reason": null,
"vc_jwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6Li4uIn0...",
"vc_jti": "uuid"
}

expiresAt omitted means “no expiry” — modeled internally as a long TTL on the signed VC (the JWT-VC schema has no “never expires” claim shape), not a separate code path.

CodeWhen
invalid_issuerissuerRef is not a known employee
inactive_issuerissuer employee is inactive
compliance_issuer_not_authorizedissuer does not hold compliance_auditor
missing_standardstandard is empty
not_found(revoke) no such credential_id

GET /v1/agents/{agent_id}/disclosure response shape:

{
"agent_id": "securityops-agent",
"provenance": { "...": "see F25 below, or null if none current" },
"compliance": [
{ "standard": "GDPR", "scope": "PII access during containment actions", "status": "valid",
"issued_at": "2026-07-03T18:00:00Z", "expires_at": "2027-07-03T00:00:00Z",
"issuer_ref": "entra:acme-corp:oid-1042" }
],
"generated_at": "2026-07-04T12:05:00Z"
}

8. Cryptographic verifiability & issuer key history (F24)

Section titled “8. Cryptographic verifiability & issuer key history (F24)”

The infrastructure both compliance and provenance credentials sign against — not a separate credential type, but what makes vc_jwt on either one independently verifiable.

MethodPathPurpose
GET/v1/issuer/key-historythe issuer’s current + superseded signing keys
GET/status/{list_id}StatusList2021 revocation list (same endpoint delegation VCs use)
GET/.well-known/did.jsonthe issuer’s did:web DID document
{
"keys": [
{ "kid": "did:web:agent-idp.agent-idp.svc#key-1", "pub_multibase": "z6Mk...",
"valid_from": "2026-01-01T00:00:00Z", "valid_until": "2026-06-01T00:00:00Z",
"superseded_by": "did:web:agent-idp.agent-idp.svc#key-2", "current": false },
{ "kid": "did:web:agent-idp.agent-idp.svc#key-2", "pub_multibase": "z6Mk...",
"valid_from": null, "valid_until": null, "superseded_by": null, "current": true }
]
}

A credential signed under a since-rotated key still verifies: the Decentralized Identifier (DID) document lists every historical key alongside the current one, and agentdid’s resolver matches a JWT’s kid header against any verificationMethod entry, not just the current key.

Offline verification (no PaloNexus API call)

Section titled “Offline verification (no PaloNexus API call)”

agentdid.verify_bundle(vc_jwt, did_document, status_snapshot) verifies a credential fully offline from three fetched artifacts — no dependency on agent-idp being reachable:

from agentdid import verify_bundle
claims = verify_bundle(
vc_jwt, # the credential's "vc_jwt" field
did_document, # GET /.well-known/did.json
status_snapshot=status_doc, # GET /status/default
)

This same function verifies both compliance and provenance credentials with no code changes between them — the underlying mechanism (agentdid.issue_vc’s extra_subject param) is credential-type-agnostic, not hardcoded to one type.


A self-declared attestation of what produced an agent’s outputs — base model, training-data lineage, declared model owner — issued by a distinct provenance_attestor role. Query is public; issuance and revocation require the role. See Governance credentials for the supersession model.

MethodPathPurpose
POST/v1/provenance/credentialsissue a provenance credential (role-gated); auto-supersedes the agent’s prior valid one
GET/v1/provenance/credentials?agent=&status=query credentials (public)
POST/v1/provenance/credentials/{credential_id}/revokerevoke a credential (role-gated)
GET/v1/agents/{agent_id}/provenancethe agent’s current (valid) provenance credential, or null
{
"agentId": "securityops-agent",
"baseModel": "claude-sonnet-5",
"modelVersion": "2026-05-snapshot",
"trainingDataSources": ["public-web-corpus", "anthropic-hh-rlhf"],
"watermarkingScheme": "none declared",
"declaredOwner": "Anthropic",
"issuerRef": "entra:acme-corp:oid-2091",
"evidenceRef": "https://model-registry.acme-corp.internal/models/claude-sonnet-5"
}

Response (201):

{
"credential_id": "uuid",
"agent_id": "securityops-agent",
"base_model": "claude-sonnet-5",
"model_version": "2026-05-snapshot",
"training_data_sources": ["public-web-corpus", "anthropic-hh-rlhf"],
"watermarking_scheme": "none declared",
"declared_owner": "Anthropic",
"issuer_ref": "entra:acme-corp:oid-2091",
"evidence_ref": "https://model-registry.acme-corp.internal/models/claude-sonnet-5",
"issued_at": "2026-07-04T12:00:00Z",
"status": "valid",
"superseded_at": null,
"superseded_by": null,
"revoked_at": null,
"revocation_reason": null,
"vc_jwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6Li4uIn0...",
"vc_jti": "uuid"
}

Issuing a second credential for the same agent marks the prior one superseded in the same response cycle — no separate call needed. Revoking requires issuerRef in the request body, same role check as issuance:

{ "issuerRef": "entra:acme-corp:oid-2091", "reason": "false base-model declaration" }
CodeWhen
invalid_issuerissuerRef is not a known employee
inactive_issuerissuer employee is inactive
provenance_issuer_not_authorizedissuer does not hold provenance_attestor
missing_base_model / missing_declared_ownerrequired field is empty
not_found(revoke) no such credential_id

Supersession vs. revocation: issuing a new credential for an agent that already has a valid one flips the prior record to superseded — a plain store update, never a revocation-log entry, never a cascade suspend. Only an explicit revoke of a required credential (a governed agent with require_provenance_credential: true) feeds the F4 revocation cascade, with reason code provenance_credential_revoked.


  • Connect agents to enterprise authority — the concept and the F1–F6 story
  • Governance credentials — the compliance/provenance concept and the cryptographic-verifiability story (F20, F24, F25)
  • HTTP API — the control-plane, egress, and agent-idp onboarding APIs
  • Requirements docs in the platform repo (docs/requirements/): 01-directory-sync.md (F1), 02-employee-identity.md (F2), 03-agent-ownership-governance.md (F3), 04-revocation-cascade.md (F4), 05-human-authority-delegation.md (F5), 06-agent-sts-token-exchange.md (F6), 20-compliance-credential-vc.md (F20), 24-cryptographically-verifiable-credentials.md (F24), 25-provenance-credential-vc.md (F25)