Approvals

Put a human between an agent and anything irreversible.

The approval loop

A push matching an approval rule returns HTTP 403:

{
  "code": "REQUIRE_APPROVAL",
  "requestId": "7f3a9c21-4b0e-4c55-9d1a-2e6f0b8c7a13",
  "requiredApprovals": 2,
  "message": "Review 7f3a9c21-… with the host approval CLI, then retry the same ref updates"
}

Git clients may only show the status code. Inspect and decide on the trusted host:

npm run approvals
npm run approve -- <request-id-or-8-char-prefix>
npm run deny -- <request-id-or-8-char-prefix>

Then retry the exact push from the agent.

What a grant covers

A grant applies to the exact ref updates (old and new object IDs), runtime context, and configuration digest. It:

  • expires 15 minutes after the request,
  • is consumed durably before forwarding — including when upstream fails,
  • can't be shared by concurrent retries.

A new commit or a configuration change needs a new approval. Denial rejects that request only; it doesn't create a permanent rule. Pending approvals are capped (maxPending, default 50) — beyond that, 429 TOO_MANY_PENDING.

Reviewer identity

Every review carries a reviewer and a source decided by the broker, never the client:

SourceComes fromVerified?
local:<name>Host CLI (AGENTGATE_REVIEWER or $(whoami))No — anyone with admin socket access can type any name
oidc:<email>Approval web UI via your IdPYes — HMAC-signed by approval-web, ±60 s

Require verified reviewers on sensitive rules:

{ "id": "review-main", "action": "git.push", "repository": "acme/payments",
  "ref": "refs/heads/main", "effect": "approval", "approvals": 2, "reviewerSources": ["oidc"] }

Security

Without reviewerSources: ["oidc"], anyone with socket access can supply both approvals of a two-person rule as two made-up local: names.

Self-approval and duplicates

  • A review from the push's delegating runtime.human is rejected as SELF_APPROVAL. Identities are compared without scheme, case-insensitively, ignoring +tag; local:developer counts as [email protected].
  • A second review from the same identity is rejected as DUPLICATE_REVIEWER.
  • Any single deny closes the request as denied.

Approval web UI

approval-web lists pending approvals and lets reviewers decide from a browser. It has no login of its own and must be reached only through oauth2-proxy.

  • Trusts X-Forwarded-Email only with a matching proxy secret (constant-time compare).
  • AGENTGATE_APPROVAL_EMAIL_DOMAINS restricts reviewer domains — set it and oauth2-proxy's email_domains.
  • CSRF tokens are bound to reviewer, request, and action; POSTs require an exact Origin.
  • Content-Security-Policy: default-src 'none', X-Frame-Options: DENY, and HTML-escaped agent text.
  • Mounts broker state read-only; it only talks to admin.sock.

Warning

Anyone holding the approval-web admin secret can approve as any oidc: reviewer. Treat approval-web and its host as part of the approval trust boundary.

Slack notifications

Set AGENTGATE_APPROVAL_WEBHOOK_URL_FILE (preferred) to post a Slack-compatible message once per new request. It includes the request ID, repository, action, identity, refs with short object IDs, count, expiry, and — with AGENTGATE_APPROVAL_PUBLIC_URL — a review link.

It never includes PR bodies, and it escapes &, <, > in agent-controlled fields so a branch name can't forge @channel. Notifier failures are logged and never delay the push.