Mumega

How to Add an Approval Gate to an Agent Stack That Doesn't Have One

An approval gate is a checkpoint between an agent deciding to do something and that action actually happening: propose, then a distinct identity approves, then a separate step applies it. OpenClaw doesn’t ship one — which is exactly why the framework’s operators keep searching for how to sandbox it and add approvals after the fact. You can build a working version yourself, using the same pattern we shipped, broke, and re-fixed in our own production code.

This is not theory. Below is the exact shape of the fix we shipped after finding that our own first attempt at an approval gate had a hole big enough for one API key to request, approve, and apply its own production push.

What is an approval gate, and why doesn’t OpenClaw ship one?

An approval gate is three things working together: a proposal (a stored record of an intended action, not the action itself), a gate (a distinct actor — usually human, sometimes a different service — that reviews and approves or rejects it), and an apply step (a separate execution that only runs after approval, and re-checks its own preconditions before running).

OpenClaw doesn’t have this because it wasn’t built as a governance layer — it’s an agent runtime, and its creator ran the project at a $10-20k/month loss before OpenAI hired him away in February 2026. A project in that position ships the runtime, not a control plane. At GTC 2026, Mistral’s CEO named the exact shape of what’s missing: tools like OpenClaw lack “identity management for agents, audit trails for agent actions, role-based access controls at the agent level, and centralized control planes”. An approval gate is the concrete, buildable piece of that list — the part you can actually add this week without waiting for the framework to grow one.

The minimum pattern: propose → gate → apply

The smallest version of a working gate is a state machine with four states: pending → approved → applied, with rejected and rolled_back as exits. Every gated action becomes a record, not a direct call:

{
  id: "req_8f2a1c",
  status: "pending",
  action: "publish_to_production",
  proposed_by: "agent-key-9d3e",
  diff: { before: <snapshot>, after: <intended change> },
  rollback: { note: "revert to before snapshot" },
  created_at, approved_at, applied_at, rejected_at, rolled_back_at
}

Three rules make this actually function as a gate instead of a formality:

  1. The apply step only runs against approved records. Trying to apply a pending record fails outright — no auto-promote, no implicit approval.
  2. The diff is computed once, at proposal time, and stored. The human reviews the stored diff, not a live re-render of “whatever the current state would produce” — otherwise you’ve built a review of nothing.
  3. Only the apply step performs the real side effect. Nothing else in the pipeline is allowed to write, delete, or publish directly. If there’s a second code path that skips the record, the gate is decorative.

This is the shape our own approvals system has used since May 2026 for content edits inside mumcp, our WordPress-facing MCP plugin — and it’s the shape that later turned out to have a real hole, described next.

Closing the self-approval hole

Here is the fix we actually shipped, and why it existed.

We extended the same approve/apply state machine to gate staging→production pushes — a genuinely destructive, hard-to-undo action. The design looked complete: propose, human approves, apply executes. What we missed: nothing stopped the same API key that created the push request from also being the key that approved and applied it. One credential could request a production push, approve its own request, and execute it — a fully “gated” flow that a single compromised or careless key could walk through end to end. The review step existed. It reviewed nothing, because the same actor was on both sides of it.

The fix, shipped as commit 094a25e in June 2026: store the identity of the requester (requested_by_key_id) directly on the approval record when it’s created, then add an explicit check on both the approve and apply endpoints that compares the acting identity to that stored value and returns a hard 403 if they match. Non-conflicting approvals (routine edits that don’t carry a requester field) pass through unaffected — the check only fires where separation of duties actually matters.

Two more checks came out of the same fix pass, both worth generalizing:

  • Scope, not just identity. Alongside the self-approval block, we moved every staging/production-push tool and endpoint from write-level access to admin-level access — closing the gap where a key with routine write permission could reach an action that should have required a stronger credential. We had to check this at both the tool-call layer and the REST layer, because they were two separate doors into the same destructive action, and only one of them had been locked.
  • Re-verify at apply, not just at approval. Time passes between “approved” and “applied.” We added a fresh check, at apply time, that the destination environment still isn’t classified as production by the live environment list — guarding against the target being reclassified in the gap between approval and execution. A gate that only checks preconditions once, at proposal time, is trusting that nothing moved. Don’t trust that.

Generalized, the rule is: if the same actor can both request and clear a review, you don’t have a review — you have a delay. Store the requester’s identity on the record, and check it on every transition that matters.

What to log so the gate is auditable

An audit trail is not a log line that says “agent published post 412.” It’s a record that answers five questions for any action, months later, without needing to ask anyone:

QuestionField
Who proposed it?proposed_by (identity, not a display name)
What exactly was proposed?diff (the stored before/after, not a re-derived one)
Who approved or rejected it, and when?approved_at / rejected_at + acting identity
Who applied it, and did the target still match?applied_at + apply-time re-verification result
How would you undo it?rollback (a snapshot, a reverse action, or an explicit manual-recovery note)

If any of these five is missing, you don’t have an audit trail — you have a log that happens to mention agents. This is the specific gap Mensch named at GTC 2026, and it’s also the thing enterprise buyers are now paying $28 billion in security acquisitions to get from someone, because most agent stacks — OpenClaw included — don’t produce it by default.

Where this plugs into an existing agent stack

You don’t need to fork OpenClaw, or any other framework, to add this. The gate sits between the agent’s decision and the action’s execution — a small service or table that owns the propose/approve/apply state machine — and every write-capable tool call gets rerouted through it instead of executing directly:

  1. Every tool that currently performs a write (publish, delete, deploy, push) calls propose() instead, and returns the proposal id.
  2. A human-facing surface (dashboard, Slack approval, CLI) lists pending proposals and calls approve() or reject().
  3. A separate apply() step — never triggered by the same call that requested it — re-verifies and performs the actual write.

Two upgrades are worth making once this minimum version works, both things we learned the hard way:

  • Rebind execution to the stored payload, not the caller’s input. Once you have a gate, the next hole is the apply step trusting whatever the caller sends at execute time instead of the content that was actually approved — we call this approve-A/execute-B substitution, and wrote up the fix in detail separately.
  • Have a different reviewer try to break the gate’s own code before you trust it. Our self-approval hole shipped past a correctness review that confirmed the happy path worked. It’s exactly the kind of bug adversarial review is built to catch — a second pass whose job is to defeat the code, not confirm it.

If building and maintaining this yourself isn’t the point — you want the pattern shipped, not a project — mupot ships propose/approve/apply/audit as a built-in layer on top of your agent fleet rather than something you assemble from scratch.

Sources

Share