Skip to content
PaloNexus
Request access Request

Authority Delegation

Some actions are too sensitive for a standing credential. A low-trust agent should read only the specific runbook for the incident it is actively working, for only ~5 minutes, and only after a human approved that delegation. This is the human-in-the-loop layer.

This is the central flow of the platform: an agent is denied a regulated target, opens a time-boxed delegation request, a human approves it (minting a short-lived Delegation Verifiable Credential (VC)), the retried call is allowed for exactly that task, and on TTL expiry or revocation deny-by-default reasserts. The temporary-elevation walkthrough runs this end-to-end offline on the sample organization’s seeded roles — the owner is the human the agent acts for, the sponsor the approver; here is the shape of it:

sequenceDiagram
    participant Agent as Agent (for the owner)
    participant CP as Control plane /authz
    participant IdP as agent-idp
    participant Approver as Approver (sponsor)
    Agent->>CP: check(runbooks:read, db-failover)
    CP-->>Agent: 401 needs-approval — no approved delegation
    Agent->>IdP: request_delegation(ttl=300)
    IdP-->>Agent: Delegation(status=pending)
    Approver->>IdP: Approve in /approvals console
    Note over IdP: mint Delegation VC — status=approved, notAfter
    Agent->>CP: authorize(runbooks:read, db-failover)
    CP-->>Agent: 200 allow — subject=ethan.park
    Note over Agent,CP: …TTL elapses or grant revoked…
    Agent->>CP: check(runbooks:read, db-failover)
    CP-->>Agent: 401 needs-approval — deny-by-default reasserts

Sequence: the deny → request → approve → allow → expire/revoke arc. Every hop is a real /authz decision carrying the on-behalf-of subject; the approval is a named human decision on the audit record, not a standing grant.

A delegation moves through a small, explicit lifecycle. Only the approved state (while non-expired and non-revoked) makes /authz allow; every other state is a deny:

stateDiagram-v2
    [*] --> pending: request_delegation(ttl)
    pending --> approved: approver approves (VC minted)
    pending --> denied: approver rejects
    approved --> expired: notAfter elapses
    approved --> revoked: revoke / cascade
    denied --> [*]
    expired --> [*]
    revoked --> [*]

State diagram: a delegation is pending until an authorized human decides it. approved is the only state that authorizes a call, and it is time-boxed — it leaves for expired (TTL) or revoked (live StatusList), both terminal denies.

Separation of duties is enforced by the two-gate approval rule (authority.py::decide): an approver must hold org:agents:approve and covers_scenario (at least one of the scenario’s resource scopes). The owner who requested the elevation does not self-approve. The cast below is the seeded devops-incident scenario’s — the temporary-elevation walkthrough defines each role.

ResponsibilityRole (devops-incident)Authority requiredNotes
Request elevationOwner (on-behalf-of)the agent acts on the owner’s behalf; the owner holds deployments:* but not runbooks:readthe agent never holds more than the human it represents
ApproveSponsororg:agents:approve and covers DevOps (runbooks:read)passes both gates ✅ — and is not the requester
Cannot approveAuditorholds org:agents:approve but covers Security onlyoutside_scenario_domain:devops-incident ❌ — can only audit
Cannot approveNegative personano scopes; invariant-blocked from org:agents:*hard-denied everywhere ❌
Operate the taskOperatorruns the agent within granted scopesno approval authority
AuditAuditorread the hash-chained /audit logreviews who approved what, when

A dataClass: regulated tool target makes the egress decision require a valid, approved, non-expired, non-revoked delegation for the exact (actor, task, action, resource). There is no standing grant; each grant is a single-link CapabilityCredential chaining directly to the did:web issuer — the human approval is the root authority — with a short TTL.

The control plane checks this by calling agent-idp GET /v1/delegations/check (the Go DelegationVerifier). When no valid delegation exists, /authz returns 401 with X-Palonexus-Needs-Approval: true. The middleware turns that into a LangGraph interrupt() instead of failing:

if decision["needs_human_approval"]:
interrupt({"action_requests": [request.tool_call], "reason": decision["reason"]})

A human approves in the portal; the console resumes the graph via Command(resume=...). The interrupt requires a persistent checkpointer.

The egress decision the middleware would make carries the fine-grained Action/Resource so the coarse allowlist gate and the cryptographic resource gate check one delegation:

Terminal window
# BEFORE delegation -> 401 + needs-approval=true (deny, ask a human)
curl -s -o /dev/null -w "%{http_code} needs-approval=%header{x-palonexus-needs-approval}\n" \
-XPOST localhost:9191/authz \
-H 'X-Palonexus-Actor: triage-agent' \
-H 'X-Palonexus-On-Behalf-Of: sre@corp' \
-H 'X-Palonexus-Task: INC-123' \
-H 'X-Palonexus-Service: runbooks-api' \
-H 'X-Palonexus-Target-Kind: tool' \
-H 'X-Palonexus-Action: runbook:read' \
-H 'X-Palonexus-Resource: runbooks-api:/runbooks/db-failover'
Terminal window
# A human approves a 300s time-boxed delegation at the IdP.
REQ_ID=$(curl -s -XPOST localhost:8090/v1/delegations/request -H 'content-type: application/json' -d '{
"actorName":"triage-agent","task":"INC-123","action":"runbook:read",
"resource":"runbooks-api:/runbooks/db-failover","reason":"triage INC-123 5xx spike",
"ttlSeconds":300}' | jq -r .id)
APPROVE=$(curl -s -XPOST "localhost:8090/v1/delegations/$REQ_ID/approve" \
-H 'content-type: application/json' -d '{"approver":"sre@corp"}')
VC_JTI=$(echo "$APPROVE" | jq -r .vcJti)
# AFTER approval -> re-run the egress curl above -> 200 (allow).

/v1/delegations/check returns ok=true iff an approved, non-expired (notAfter > now), non-revoked delegation exists whose actorName, task, action match and whose stored resource covers the requested one (trailing /* glob).

Holding a valid Delegation VC is necessary but not sufficient. At the resource — the runbooks-api gate — the agent additionally performs a challenge-response against its decentralized identifier (DID), proving it is the live holder in the expected execution state (this ticket, this scope), defeating stolen or replayed credentials. The SDK’s runbook_tool.py does the two-step agentdid flow once a Delegation VC is available, attaching the exec-state context (incy is the sample incident manager the ticket ids come from):

RunbookContext(
identity=identity,
delegation_vcs={resource_for(name): delegation_vc},
exec_state={"active_ticket_id": incident_id, "scope_in_use": "runbook:read",
"task": incident_id, "ticketSource": "incy"},
)

This is why runbooks-api is registered dataClass: internal, not regulated: the proxy does the coarse allowlist gate, and this fine-grained, per-resource gate runs server-side. See Credential-safe action enforcement § layering.

The portal surfaces two approval queues that share the same UI pattern (a 3s poll + React-Query invalidation):

PaloNexus Authority Delegation console showing an operator approver field above a cleared human-in-the-loop queue with zero pending delegation requests and zero active credentials

The /approvals console — the human-in-the-loop queue where an operator approves or denies delegation requests. Shown here in its empty state: the queue is clear (no pending requests), the resting state between elevations. Set the Approver field (e.g. the seeded approver identity maya.chen@northstar.example) before approving; that string lands on the delegation and the audit record.

ConsoleBacksApprovesWhen it fires
Authority Delegation (/approvals)delegation requests at agent-idpa time-boxed Delegation VC for (actor, task, action, resource)a regulated tool target needs a fine-grained, server-side-gated delegation
Credential-Safe Enforcement (/egress)the control-plane pending-egress queuea single held egress requesta regulated target with no server-side gate (e.g. scale_deployment) is held at the proxy

The Credential-Safe Enforcement path: when the proxy decision is needs-approval, it creates a pending request and holds — long-polling its status up to EGRESS_APPROVAL_TIMEOUT (default 120s). Approved → forward; denied/timeout → 403. The queue API:

Terminal window
curl -s localhost:8181/v1/egress/requests # list (newest first)
curl -s -XPOST localhost:8181/v1/egress/requests/$ID/approve -d '{"approver":"sre@corp"}'
curl -s -XPOST localhost:8181/v1/egress/requests/$ID/deny -d '{"approver":"sre@corp","reason":"…"}'

Before requesting (or approving) an elevation, dry-run the exact decision in the Policy simulator (/simulate). Its Authority-preview tab answers a design-time eligibility question — would persona P, on scenario S, be allowed authority action A? — while the Live decision tab issues a runtime-faithful dry-run against the real decision paths:

PaloNexus Policy simulator with Authority-preview and Live-decision tabs, the authority-preview tab showing persona, scenario and authority-action selectors for a design-time eligibility query

The /simulate console: what-if over the real decision paths. Use the Authority-preview tab to check whether an approver passes the two-gate rule, but trust only the Live decision tab for a real allow/deny — design-time preview is a hint, not a verdict.

A time-boxed delegation can be cut mid-flight, before it expires:

Terminal window
# Revoke the Delegation VC.
curl -s -XPOST localhost:8090/v1/revoke -H 'content-type: application/json' \
-d "{\"vcJti\":\"$VC_JTI\"}"
# The NEXT /authz for that delegation -> 401 + needs-approval=true again.

Because /authz re-checks the StatusList on every call, revocation denies the very next request regardless of remaining TTL. Prove the whole sequence — deny → approve → allow → revoke → deny — with the platform smoke script:

Terminal window
./scripts/phaseB-smoke.sh

The full multi-agent version of this, with a peer broker block-polling for the approval, is the autonomous flow.