AI agents should draft. Code should validate. Humans should approve. Systems should dispatch.
A portable contract pattern for adding approval gates to AI automation workflows. Use it when an agent wants to send a message, update a record, create a ticket, call an API, or trigger another workflow.
This repo is opinion + versioned JSON Schemas + conformance examples. It is not a framework. Drop the contracts into your own stack and keep the side-effect credential away from the agent.
AI Agent -> ProposedAction -> ValidationRecord -> ReviewSnapshot
|
v
ApprovalRequest -> ApprovalRecord
|
ResolutionRecord
|
dispatch-time authority check
|
v
deterministic Dispatcher
|
v
DispatchRecord + complete audit snapshot
Nine boundaries make the gate real:
- ProposedAction serializes the complete action. Its RFC 8785 digest binds the action type, call identity, arguments, tenant, expiry, risk, and idempotency key.
- Delegation provenance preserves stable agent and nested call identities from the root invocation through the terminal action.
- ValidationRecord attests that the exact action passed structural, payload, semantic, policy-input, and authority-input checks before notification.
- ReviewSnapshot binds the complete arguments and other decision material shown to the approver to the action and payload hashes.
- ApprovalRequest is durable, addressable state. A callback resolves one known, pending request and the approved request is consumed exactly once.
- ApprovalRecord names the actor, enforces separation of duties, and binds the decision to the immutable policy revision and reviewed content.
- ResolutionRecord durably returns the terminal decision without reopening an approval when delivery or agent resumption fails.
- Dispatcher revalidates current authority immediately before invocation, uses the stable idempotency key, and preserves explicitly unknown outcomes.
- Audit stream is append-only, redacted, and hash-linked; its export manifest proves the expected count, sequence bounds, event-set digest, and terminal hash.
If the agent still has the side-effect credential, the approval path remains optional from the agent's point of view.
| Contract | Purpose |
|---|---|
proposed-action.schema.json |
Exact action draft, expiry, risk, payload schema, and idempotency identity |
validation-record.schema.json |
Immutable pre-notification validation result for an exact action |
review-snapshot.schema.json |
Exact decision material shown to the approver, bound by canonical hashes |
approval-policy.schema.json |
Immutable policy revision, material inputs, quorum, and channel requirements |
approval-request.schema.json |
Durable pending identity, notifications, terminal state, and single-use consumption |
approval-record.schema.json |
Human or policy decision, action and payload hashes, signatures, and settlement evidence |
resolution-record.schema.json |
Durable terminal receipt with independent delivery and acknowledgement state |
authority-snapshot.schema.json |
Dispatch-time authority status for approver, policy, workspace, and dispatcher |
dispatch-record.schema.json |
Attempt ceiling, provider outcome, unknown-outcome handling, and reconciliation |
audit-event.schema.json |
Redacted lifecycle evidence with actor, event time, ingestion time, and hash chain |
audit-snapshot.schema.json |
Completeness manifest for one exported audit-event set |
approval-envelope.schema.json |
Portable bundle plus cross-contract profile for a completed lifecycle |
actions/*.schema.json |
Strict payload references for email.send, crm.update_record, ticket.create, db.update_row, api.call, and n8n.trigger_workflow, each naming the fields the approver must see as the target |
Schema IDs are pinned to the v3.1.0 release. Pin a released ID in production; do not
resolve schemas from the mutable default branch.
- Validate the proposal and payload, record every required check in a passed
ValidationRecord, and refuse admission on any failed or missing check. - Canonicalize the complete
ProposedActionwith RFC 8785 and store its SHA-256 asaction_hashon the approval request. - Persist a
ReviewSnapshotand request before notifying any channel. Resolve callbacks only by the pending request revision, tenant, nonce, action hash, and exact reviewed-content hash. - Recompute the effective action after any JSON Patch modifications. Enforce the
policy's approver eligibility, exclusion, requester/agent separation, and signing
key rules before writing an append-only
ApprovalRecord. - Emit a durable
ResolutionRecord; retry only its delivery when agent resumption fails. Never repeat the decision or create a fresh execution grant. - Atomically consume the approved request, revalidate current authority, then
dispatch with the stable
idempotency_key. An unknown provider outcome goes to reconciliation, not retry. - Append the lifecycle events and export them with an
AuditSnapshot. Never put raw payloads, credentials, callback tokens, or provider secrets in failure or audit details.
The complete synthetic lifecycle is in
examples/approval-envelope.json. The validator
checks IDs, hashes, quorum, expiry, sequence linkage, and attempt ceilings that JSON
Schema alone cannot compare across documents.
Version 3 also supplies complete edited,
rejected, expired,
and recovered lifecycles. Refusal is a normal
terminal result; it requires neither an approving decision nor a provider invocation.
Recovery retains the unknown attempt, a linked reconciliation record, and a bounded
retry with fresh authority. The portable vectors in
tests/hardening.json cover these boundaries.
Version 3.1 adds a fail-closed policy outcome (require_approval, auto_approve, or
deny; anything else is recorded as deny), revocation
of an approved request until the moment it is consumed, structured agent_feedback on
refusals so the agent can revise instead of re-asking, an mcp_elicitation channel
(profile), and review-target binding for every action type.
tests/canonicalization.json is a language-neutral
RFC 8785 corpus with exact canonical UTF-8 bytes and SHA-256 results. Use it to prove
that implementations in different languages bind the same document to the same hash.
tests/adversarial.json exercises deceptive identities,
display controls, metadata injection, redaction boundaries, and review tampering.
The reference validator checks your envelopes, standalone records, and audit exports with the same rules it applies to this repository:
python3 scripts/validate_contracts.py --envelope out/envelope.json
python3 scripts/validate_contracts.py --envelope a.json --envelope b.json
python3 scripts/validate_contracts.py --record approval-record out/approval.json --json
python3 scripts/validate_contracts.py --audit-events out/audit.jsonl --audit-snapshot out/snapshot.jsonSeveral envelopes are also checked as a set: one approval request per call, and one action per idempotency key. Audit exports verify offline, including a range that continues from a retained checkpoint hash. In CI, use the bundled action:
- uses: renezander030/agent-approval-gate@v3.1.0
with:
envelopes: out/envelope.jsonSee docs/contracts.md for every option
and exit status.
Authenticated web approvals may carry a webauthn-es256 signature. Its challenge is
the SHA-256 of the RFC 8785 canonical ApprovalRecord with signatures omitted, so
the passkey proof covers the exact action, payload, review snapshot, actor, decision,
and policy evidence. The conformance validator checks the challenge, origin, RP ID,
user-presence and user-verification flags, signature counter, P-256 key, and ES256
signature. See
examples/webauthn-approval-record.json.
The profile includes digest-addressed registration evidence, but independent trust
still depends on retaining that credential registration outside the platform being
audited. WebAuthn signs a challenge, not readable action text; the approval UI must
render the bound ReviewSnapshot immediately before invoking the passkey. The proof
does not independently establish that a platform-controlled browser rendered that
snapshot faithfully, so trusted client presentation remains a deployment boundary.
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/python scripts/generate_examples.py
.venv/bin/python -m unittest discover -s tests -vexamples/n8n-approval-workflow.json is an
importable wait-and-resume reference. Unlike a notification-only workflow, it calls a
real schema validator, persists validation and review evidence before notification,
accepts a signed callback, durably delivers the resolution, revalidates authority,
atomically consumes the approval, sends an idempotency key to the dispatcher, and
exports a complete audit snapshot on both success and refusal paths.
Configure these deployment-owned endpoints after import:
APPROVAL_VALIDATOR_URL— validates the versioned schemas.APPROVAL_GATE_URL— stores requests and resolves/consumes callbacks atomically.APPROVAL_AUDIT_URL— appends and hash-links audit events.APPROVAL_DISPATCH_URL— deterministic side-effect endpoint.APPROVAL_POLICY_IDandTG_APPROVER_CHAT_ID— policy and approver routing.
The workflow deliberately does not embed a queue, database, validator, or dispatcher.
Those are trust-boundary components, not Code-node snippets. See
docs/n8n-reference.md.
- Not a framework, SDK, CLI, daemon, hosted queue, or dispatcher.
- Not coupled to one LLM, orchestrator, approval channel, or protocol.
- Not a prompt-injection filter, rate limiter, budget engine, or ACL synchronization service.
- Not third-party-verifiable proof when the platform itself holds the signing key.
- Not a WebAuthn UI, credential-registration service, or trust-anchor store.
See docs/architecture.md for the threat model and
docs/contracts.md for canonicalization, lifecycle, and
compatibility rules.
- skillgate applies the deterministic-gate idea to the development finish line.
-
Action-bound approval audit in SQLite (PAAN #15) — persistence errors block release; action/payload/target-version binding and revocation.
-
Tested automation reference examples — offline fixtures for malformed hook input, uncertain queue dispatch, approval consumption and permission revocation.
MIT licensed. Maintained by René Zander.