Human-in-the-loop agent approvals
AI agents registered in Zynth Auth carry an oversight mode derived from their risk level:
AUTONOMOUS, HUMAN_IN_LOOP, or HUMAN_APPROVE. For the supervised modes, every governed
action needs a human decision at the moment the agent attempts it — a request → approve/deny
loop, not a standing grant.
Availability depends on the deployment: the approvals surface is enabled per installation. When it is off, supervised agents are simply denied governed actions (fail-closed), and
/api/v1/approvalsreturns 404.
The loop, from the agent's side
-
The agent calls a governed endpoint. If its oversight mode requires a human, the call is denied with
403and a structured detail:{ "detail": { "message": "Human approval required: command:read", "approval_request_id": "3f2c…", "approval_status": "pending" } }The pending request is registered once — retries return the same request id.
-
The agent polls its own request (no extra permission needed — an agent can always read its own asks):
curl -H "Authorization: Bearer $AGENT_TOKEN" \ https://auth.example.com/api/v1/approvals/3f2c… -
Once the status is
approved, the agent retries the original call. The approval is consumed by exactly that one execution. A later identical action starts a new request.
If the status is denied, the denial stands for a cool-off window — retrying within it will
not open a new ask. If nobody decides in time, the request expires (default 15 minutes).
Over MCP, the same loop looks different
When the agent is driving Zynth's MCP server rather than calling REST, every write tool is approval-gated and the mechanics change shape:
- The park is a tool result, not a
403. MCP treats an authorization denial as something the model can read and act on, so the agent getsisError: truewith apending human approvalmessage and theapproval_id— not a protocol failure. - Retry the identical call. The approval is fingerprint-matched to the exact tool and arguments; a retry with different arguments doesn't match and parks as a new request.
- If approvals are not enabled on the deployment, write tools are refused outright — the MCP surface has no un-gated write path.
The human side is unchanged: the same queue, the same POST /api/v1/approvals/{id}/decide, the
same rules below.
Approving a whole configuration at once
Some acts are too big to approve call-by-call. When an agent configures an organization with an IAM manifest — roles, resource permissions, invitations, an SSO scaffold — it asks once, for the whole document, and the approval binds to what applying it would actually do, not to one tool call's arguments.
In your queue these appear with resource_type: "manifest" and the permission
manifest:apply. What changes for you as the reviewer:
- The plan is in the request — you do not have to go and get it. The ask carries its
resolved plan and the console renders every step in execution order, verdicted
create/noop/conflict/invalid, together with the exact permission codes each role would carry and what each invitation's role confers. Nothing is truncated: a long plan scrolls and states its step count. (The same detail is on the API as the approval'sresourcefield.) - Your yes covers the consequence, not the document's name for it. Editing the manifest invalidates the approval as it always did — but so does the plan changing underneath it. If a role the manifest invites into gains a permission after you decided, the identical bytes would now confer more, so the apply is refused and the agent must ask again. The same is true if a built-in role's definition changes in a platform release.
- A manifest can never mint a role beyond your own authority. If the document would create a role carrying a permission you do not hold, the apply is refused naming the codes — the same rule delegations have always followed: you cannot grant what you could not grant directly. (Permissions the manifest itself creates for its own resources are excluded; nobody holds a permission that does not exist yet.) The review dialog warns you before you approve, so you are not left waiting for a refusal the agent hits minutes later.
- A refusal never burns your yes. Apply re-plans against live state first, and every one of the checks above runs before the approval is spent, so the agent can fix and retry.
- One consent, applied once. The approval is spent on the first successful apply. Re-running the same manifest is a no-op that confirms the spent decision (that is how a crashed apply resumes) — it never silently re-executes anything.
- Every step is audited under the manifest's hash as its correlation id, so "show me everything this approval authorized" is one query on the events API.
An agent that opened its own organization may arrive holding a manifest approval you already gave — on the onboarding review screen, as part of the same single consent. It is a real ledger decision recorded in your name, not a bypass; see Agent-driven onboarding.
The loop, from the human's side
Reviewers list the queue and decide:
# The pending queue (requires agents:approvals:read)
curl -H "Authorization: Bearer $TOKEN" \
"https://auth.example.com/api/v1/approvals?status=pending"
# Approve (or deny) one request (requires agents:approvals:decide)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"approve": true, "reason": "Reviewed the target resource"}' \
https://auth.example.com/api/v1/approvals/3f2c…/decide
Four rules protect the decision itself:
- You cannot approve what you could not do. The decider must hold the permission being
approved, in addition to
agents:approvals:decide. An approver can never unlock authority they don't have. (Arequire_approvalpolicy can designate a different approver permission — see below — which then replaces this rule for that request.) - You cannot decide your own request. A requester holding the decide permission is still refused on their own ask.
- Approvals are single-use and short-lived. An approval covers one execution of the exact action that was requested (same subject, same permission, same resource), and lapses unused after its window (default 10 minutes). A yes is never a standing grant.
- Only humans decide. Agent principals are rejected from the decide endpoint outright.
Break-glass
A tenant owner may pass "break_glass": true when deciding — for incident pressure, when
the normal approver set is unavailable. Every break-glass decision is audited and raises an
immediate security detection, so it always gets reviewed after the fact.
Approval-gated permissions for people
The same ledger governs human principals where the deployment enables access
governance. An ABAC policy with effect require_approval attaches an approval obligation
to a permission without granting or denying it:
{
"name": "gate command reads",
"resource": "*",
"action": "command:read",
"effect": "require_approval",
"approval": { "approver_permission": "compliance:admin" }
}
A member holding the permission is then blocked with a pending request (the 403 carries
approval_request_id), the request appears in the same approvals queue with
subject_type: "user", and an approval unlocks one execution of that exact action —
the identical single-use, fingerprint-bound, twice-time-bounded contract agents live
under. The optional approver_permission designates who may decide: when set, it
replaces the hold-the-permission rule, so a review function can approve actions it
cannot perform itself. Tenant owners are not subject to require_approval policies (the
owner bridge bypasses the policy overlay entirely).
Three more knobs on the same config:
required_approvals(1–5): the request completes only after that many distinct approvers say yes — the same person never counts twice, and one deny still ends the request. An owner's break-glass decision completes it immediately (audited and alarmed, as always).escalate_after_seconds+escalation_permission(set together): once a request has waited past the threshold, holders of the escalation permission also become eligible to decide — the original approver set is widened, never replaced.
Approval-gated permissions for people are part of access governance, which needs both
an enterprise plan (Plans and entitlements) and
the capability enabled on the deployment. Creating a require_approval policy without the
entitlement is refused with a 403 naming the upgrade; where governance is unavailable, an
existing require_approval policy is a plain deny for the actions it matches — fail-closed,
with no request registered. The agent approval loop above is never plan-gated: agent
safety is not a tier.
Why it works this way
The request → decide → execute chain is written to the tamper-evident audit log and mapped to EU AI Act Article 14 (human oversight), ISO/IEC 42001, SOC 2 CC6, and the OWASP Agentic Top 10 — the approval trail is the compliance evidence that a human supervised the agent.