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 typeWhat it doesSafety posture
auto_adoptPromote a low-risk discovered identity to a governed NHITier-0 / high-risk never adopted
auto_attestRenew an active NHI’s attestation window on strong activity evidenceBounded consecutive-auto cycle cap → forced manual
auto_resolveClear a signal’s enforcement when its recovery condition is metNever resolves a superseded or manually-held signal
auto_escalateStrengthen an enforcing signal one step up the severity ladderMonotonic + 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.

FieldMeaning
id + revisionStable policy identity + monotonic revision (composite key; immutable)
typeauto_adopt | auto_attest | auto_resolve | auto_escalate
conditionsType-specific config object (see Condition Schemas)
hashSHA-256 of the canonicalized conditions (content-addressable)
change_classnormal | restrictive | emergency_disable
change_directionexpanding | restricting | neutral (the security axis)
enabledWhether this revision is the active one (created disabled)
priorityUnique 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.

POST/api/v1/tenant/governance/autonomous-policies

Create a new immutable policy revision (disabled). Requires a privileged role.

Request BodyJSON
{
  "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"
}
Response (201)JSON
{
  "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.

GET/api/v1/tenant/governance/autonomous-policies/:id

List all revisions of a policy, newest first.

GET/api/v1/tenant/governance/autonomous-policies/:id/dry-run-log

Dry-run impact log for a policy (side-effect-free evaluation history).

Response (200)JSON
{
  "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.

POST/api/v1/tenant/governance/autonomous-policies/:id/approvals

Open a dual-control approval request for a specific revision.

Request BodyJSON
{
  "revision": 1,
  "justification": "Enable contract-recovery auto-resolution for the cardiology cost centre",
  "approvalMode": "dual"
}
Response (201)JSON
{ "approvalId": "b1c2d3e4-…" }
POST/api/v1/tenant/governance/autonomous-policies/:id/approvals/:approvalId/approve

Record an approver (the caller's X-User-Identity). Rejected on self-approval or duplicate.

Response (200)JSON
{ "approved": true, "ready": false }   // ready=true once the dual-control threshold (2 distinct) is met

Activate & 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.

PATCH/api/v1/tenant/governance/autonomous-policies/:id/activate

Activate a revision. Expanding changes require X-Step-Up-Verified + a satisfied dual-control approvalId.

Request BodyJSON
{
  "revision": 1,
  "approvalId": "b1c2d3e4-…"   // required for authority-expanding activations
}
ResponsesJSON
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
PATCH/api/v1/tenant/governance/autonomous-policies/:id/disable

Retire the active revision (RETIRE, never delete — history is preserved). Requires a privileged role.

Response (200)JSON
{ "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.

ChangeEffect timingRequired controls
Authority-expanding (enable a policy, raise a ceiling, widen scope)next_batchprivileged role + step-up + dual control
restrictive (permission reduction)immediateprivileged role
neutral editnext_batchprivileged role
emergency_disable (revocation)revokeprivileged 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).

auto_resolve conditionsJSON
{
  "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)
}
auto_escalate conditionsJSON
{
  "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.