Autonomous NHI Governance API
Autonomous governance lets AuthHub adopt, attest, resolve, and escalate non-human identity (NHI) governance decisions at machine speed — always inside a fail-safe envelope. This page documents the policy administration API that controls those workers: immutable, versioned policies with RBAC, step-up, and two-person dual control for high-impact changes.
Feature-flagged — opt-in per deployment
These endpoints are gated behind AUTONOMOUS_NHI_POLICY_ADMIN_ENABLED and return 404 when the flag is off. Each feature worker is separately flagged (off by default) AND requires an enabled per-tenant policy — a deliberate double gate, so no autonomous action can occur without both explicit switches on.
Base Path
/api/v1/tenant/governance/autonomous-policies
All endpoints require X-Tenant-ID, Authorization: Bearer, and X-User-Role. Authority-expanding activations additionally require X-Step-Up-Verified: true (set by the console after a re-authentication challenge). Mutations require a privileged role (tenant_admin or caldicott_guardian).
Feature Workers
Each worker is a policy-driven transition through a single, versioned write path. All are off by default and governed by a per-tenant policy of the matching type.
| Policy type | What it does | Safety posture |
|---|---|---|
| auto_adopt | Promote a low-risk discovered identity to a governed NHI | Tier-0 / high-risk never adopted |
| auto_attest | Renew an active NHI’s attestation window on strong activity evidence | Bounded consecutive-auto cycle cap → forced manual |
| auto_resolve | Clear a signal’s enforcement when its recovery condition is met | Never resolves a superseded or manually-held signal |
| auto_escalate | Strengthen an enforcing signal one step up the severity ladder | Monotonic + capped; never past the policy ceiling |
A human restrictive action (suspend / hold / block) always beats an autonomous action; a human permissive action never bypasses a hard safety invariant.
Policy Model
Policies are immutable revisions. Editing a policy never mutates a stored revision — it appends a new one. Every autonomous decision references its policy_id + revision (+ hash), so any past decision stays reproducible. Policies are RETIRED (disabled), never deleted. Exactly one revision of a policy is active at a time, and activation is atomic. Every mutation is written to the tamper-evident audit hash chain.
| Field | Meaning |
|---|---|
| id + revision | Stable policy identity + monotonic revision (composite key; immutable) |
| type | auto_adopt | auto_attest | auto_resolve | auto_escalate |
| conditions | Type-specific config object (see Condition Schemas) |
| hash | SHA-256 of the canonicalized conditions (content-addressable) |
| change_class | normal | restrictive | emergency_disable |
| change_direction | expanding | restricting | neutral (the security axis) |
| enabled | Whether this revision is the active one (created disabled) |
| priority | Unique within (tenant, type) among active revisions |
Create a Revision
Create a new immutable revision. If policyId is supplied, a new revision is appended to that policy; otherwise a fresh policy is minted at revision 1. Revisions are always created disabled — activation is a separate, guarded step.
/api/v1/tenant/governance/autonomous-policiesCreate a new immutable policy revision (disabled). Requires a privileged role.
{
"type": "auto_resolve",
"name": "contract-recovery",
"conditions": {
"allowedSignalTypes": ["contract_expiring"],
"recoverySignalType": "contract_renewed",
"minStabilitySeconds": 300,
"maxEvidenceStalenessSeconds": 900
},
"changeClass": "normal",
"changeDirection": "expanding",
"priority": 100,
"policyId": "optional-existing-policy-uuid"
}{
"id": "fe12a0da-4067-4320-86e9-758478ce4527",
"revision": 1,
"hash": "a55bf0381c43…"
}List Revisions
All revisions of a policy remain addressable forever — old revisions are the audit record.
/api/v1/tenant/governance/autonomous-policies/:idList all revisions of a policy, newest first.
/api/v1/tenant/governance/autonomous-policies/:id/dry-run-logDry-run impact log for a policy (side-effect-free evaluation history).
{
"policyId": "fe12a0da-4067-4320-86e9-758478ce4527",
"revisions": [
{ "id": "fe12…", "revision": 2, "type": "auto_resolve", "enabled": true, "hash": "9c1f…", "changeClass": "normal", "changeDirection": "expanding", "priority": 100, "createdBy": "alice@nhs.uk" },
{ "id": "fe12…", "revision": 1, "type": "auto_resolve", "enabled": false, "hash": "a55b…", "changeClass": "normal", "changeDirection": "expanding", "priority": 100, "createdBy": "alice@nhs.uk" }
]
}Dual-Control Approvals
Authority-expanding activations require two-person dual control. Open an approval request, then have two distinct approvers (neither the requester) approve it. A requester can never self-approve.
/api/v1/tenant/governance/autonomous-policies/:id/approvalsOpen a dual-control approval request for a specific revision.
{
"revision": 1,
"justification": "Enable contract-recovery auto-resolution for the cardiology cost centre",
"approvalMode": "dual"
}{ "approvalId": "b1c2d3e4-…" }/api/v1/tenant/governance/autonomous-policies/:id/approvals/:approvalId/approveRecord an approver (the caller's X-User-Identity). Rejected on self-approval or duplicate.
{ "approved": true, "ready": false } // ready=true once the dual-control threshold (2 distinct) is metActivate & Disable
Activation is the guarded operation. The change class + direction, the caller’s role and step-up state, and (for expanding changes) the approval row are all checked before the revision goes live. Activating a revision atomically disables any previously-active revision of the same policy.
/api/v1/tenant/governance/autonomous-policies/:id/activateActivate a revision. Expanding changes require X-Step-Up-Verified + a satisfied dual-control approvalId.
{
"revision": 1,
"approvalId": "b1c2d3e4-…" // required for authority-expanding activations
}200 { "activated": true, "revision": 1 }
403 { "error": "step_up_required" } // expanding change without X-Step-Up-Verified: true
403 { "error": "dual_control_required" } // fewer than 2 distinct approvers
403 { "error": "requester_self_approval" } // the requester tried to approve their own change
403 { "error": "role_not_privileged" } // caller lacks tenant_admin / caldicott_guardian
404 { "error": "not_found" } // no such revision/api/v1/tenant/governance/autonomous-policies/:id/disableRetire the active revision (RETIRE, never delete — history is preserved). Requires a privileged role.
{ "ok": true }Change Classes & Required Controls
The controls required for a change scale with how much it expands autonomous authority. You can always stop autonomy with only a privileged role; expanding it is the hard case.
| Change | Effect timing | Required controls |
|---|---|---|
| Authority-expanding (enable a policy, raise a ceiling, widen scope) | next_batch | privileged role + step-up + dual control |
| restrictive (permission reduction) | immediate | privileged role |
| neutral edit | next_batch | privileged role |
| emergency_disable (revocation) | revoke | privileged role |
next_batch = takes effect on the next evaluation batch (batch-snapshot semantics). immediate = invalidates already-read authorizations at once. revoke = kill-switch semantics; halts in-flight authority immediately.
Condition Schemas
The conditions object is type-specific. Unknown or wrong-typed fields fall back to safe defaults (a malformed revision degrades to defaults, it never crashes a worker).
{
"allowedSignalTypes": ["contract_expiring"], // empty = any
"recoverySignalType": "contract_renewed", // the signal that clears enforcement
"minStabilitySeconds": 300, // anti-flap dwell (default 300)
"maxEvidenceStalenessSeconds": 900 // recovery evidence freshness (default 900)
}{
"allowedSignalTypes": ["contract_expiring"], // empty = any
"minDwellSeconds": 300, // dwell before a step (default 300)
"maxAction": "suspend" // ceiling on the severity ladder (default suspend)
}auto_adopt and auto_attest conditions mirror their per-type policy columns (risk-band / tier / observation thresholds; activity window / renew window / cycle cap).
Safety Substrate
Autonomous workers run inside a fail-safe substrate. These are operator controls, not customer API surface, but they bound everything the workers can do:
- •Kill-switch (two-plane, fail-closed): a per-tenant pause halts all autonomous commits — both by a fast admission flag and by a transactional authority-epoch fence, so a stale in-flight worker cannot commit past the pause. If the substrate is unreachable, admission is treated as prohibited (never optimistically allowed).
- •Rate limiting & budget hierarchy: a non-bypassable tenant-global cap is checked before any per-source bucket; on breach only the offending source is throttled — there is no automatic global pause.
- •OCC-conflict circuit breaker: a single identity generating excessive write conflicts is quarantined on its own — the rest of the source keeps flowing.
- •Controlled rehydration: after an outage, catch-up is rate-bounded (DEGRADED mode) to avoid a thundering-herd burst; missed work is re-evaluated against current state, never blindly replayed.
Every autonomous decision persists a reason-provenance record (policy id + revision + hash, evidence sequence, server-computed risk band, state versions, correlation ids) sufficient to reproduce it, appended to the tamper-evident audit hash chain.
