Private betaZynth Auth is currently in private beta testing.New organizations are created by invitation only, and no plan can be purchased yet.Request early access

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/approvals returns 404.

The loop, from the agent's side

  1. The agent calls a governed endpoint. If its oversight mode requires a human, the call is denied with 403 and 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.

  2. 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…
    
  3. 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 gets isError: true with a pending human approval message and the approval_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's resource field.)
  • 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. (A require_approval policy 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.