Skip to content
All pages

Approvals

How the moatbox gateway holds risky agent actions, how you approve, deny or undo them, and where each decision is recorded.

moatbox runs coding agents in spaces: microVMs with their own disk, on moatbox servers or on a server your team links. A gateway inside each space stands between the agent and the outside services it calls, and it can hold a risky action until a person on your team decides. This page covers which actions the gateway holds, the rules and strategies behind each hold, where you approve, deny or undo, what the agent sees while it waits, and the audit trail.

What the gateway holds

Action Held when
HTTPS calls to a connected integration (GitHub and the other services on the Integrations page) The integration classifies the request as risky, or your rule says to hold it. See Integrations for each service's list.
An integration request with a body over 1 MiB The gateway cannot inspect the body. A request that would otherwise go through, or that has no undo, waits as <integration>.uninspected with the approve-only strategy.
MCP tool calls The tool's name matches one of the hold patterns you pass when you start a run, such as "hold": ["linear.*"]. See MCP server.
Browser actions in a desktop run A navigation, click or submit whose URL, button text or form action names payment, checkout, account, password, send, delete, publish, deploy and similar words.
Deploys from a run A first public deploy of a name, a deploy of a name with no live release, or a deploy that replaces a release another run made. A new database from a run is held every time.

GitHub calls from SSH shells in the space follow the same rules. In the record, those calls end with "by the SSH workspace". SSH shells get no other integration.

Nothing else waits for a person. Connections to hosts outside the space's network allowlist are refused and logged, and you can allow the host from that log entry.

Strategies

Each held action carries a strategy. The strategy decides what approval does and whether you can undo it later. The dashboard shows the label from this table on every held action.

Strategy Dashboard label What happens
pass (recorded as "Allowed by policy") The gateway forwards the request at once and records it.
approve-only Can't be undone Waits for a decision. Nothing undoes it once it runs.
checkpoint Snapshot first, can be undone Waits for a decision. After approval the gateway saves the state the action will change, then sends it. Undo restores that state.
compensate Can be undone by a reverse action Waits for a decision. Undo runs the reverse action, such as pushing a ref back to its old commit.
hold Delayed, can be cancelled Waits for a decision. With a window, nobody has to act: the request goes out when the window ends unless someone denies it first, and Approve sends it straight away.

App, function and static-site deploys use compensate. Undo switches the app back to its previous release, and takes the app offline when it had no earlier release. You can undo only the live release, so undo newer deploys first. A new database has no undo.

Rules

A rule sets how strict the gateway is with one integration. Your team has three modes:

Dashboard label API value Effect
Hold risky actions destructive The default. The integration's own classification decides what waits.
Hold every action hold_all Requests the integration does not classify go through when they are GET, HEAD or OPTIONS. Every other request waits for a decision. A delayed send loses its window, and the summary gains "(team rule: hold every action)".
Auto-approve listed actions auto Action kinds you list go through without asking, as long as their strategy is hold or approve-only. Listed kinds with checkpoint or compensate still wait. Everything else is held as under the default.

With no rule saved, an integration uses destructive.

Edit rules in the dashboard

  1. Open Integrations and click the service.
  2. Under Default rule ("Keys without a rule of their own follow this one."), pick a mode.
  3. For Auto-approve listed actions, type kinds such as github.write under "Actions that go through without asking" and click Add. The editor suggests kinds it has seen in one of your running spaces. Click Save rule.

Each key has its own rule as well. Under the key, next to Rule:, click Change and pick Follow the default or one of the three modes. A key's own rule applies wherever that key is used and overrides the default rule.

Any team member can make a rule stricter. Only owners and admins can loosen one: the dashboard greys out the looser options for members ("Owners and admins can loosen this rule").

moatbox sends the saved rule to every running space. When a space has not taken it yet, the dashboard says "Saved. Reaches <space> once it answers." and moatbox keeps retrying every few seconds. A space that cannot read its rule file holds every action until moatbox sends the rules again.

Rules API

An API key needs the team.read action to list rules and team.admin to change them. See API keys.

Method and path What it does
GET /api/v1/teams/{team}/rules Lists the default rule for each integration, with who changed it last.
PUT /api/v1/teams/{team}/rules/{integration} Sets the default rule.
DELETE /api/v1/teams/{team}/rules/{integration} Resets the default rule to destructive.
PUT /api/v1/teams/{team}/integrations/{integration}/rule[?space={slug}] Sets the rule of the team key, or of the space's own key with ?space=.
DELETE /api/v1/teams/{team}/integrations/{integration}/rule[?space={slug}] Makes that key follow the default rule again.

The body of a PUT:

{"mode": "auto", "auto": ["github.write"]}

mode is hold_all, destructive or auto. auto takes up to 64 kinds, each starting with the integration's name and a dot. The response lists in pending the spaces that have not taken the rule yet.

Approve or deny

Only team owners and admins can approve, deny or undo. Anyone else gets 403 <email> is not an approver for this team. If the space is asleep when you decide, moatbox wakes it first.

Dashboard

Open the space and go to Approvals. The nav item shows a count of waiting actions. Each card shows "Waiting · #<id>", the strategy label, the action, the statement or request, who asked (the agent and run), the run's task and when the gateway held it. Click Approve or Deny.

Held actions also appear inline in the run's conversation view, with the same two buttons. While a run waits, the space's Agents page marks it "Waiting for you", and the Live page shows "Waiting on approval" with a Review #<id> link to the run. Below the waiting cards, "Decided recently" lists the last ten decisions, and Everything in Audit opens the full log.

Phone and browser push

Under Settings → "Approvals where you are", click Turn on push for each device. On iPhone, add the app to your Home Screen first. Owners and admins get a notification titled "moatbox: approval needed" with Approve and Deny actions. Tapping the notification itself opens the space's Approvals page.

Slack and Discord

Slack and Discord approvals need a paid plan, and only owners and admins can set them up. You bring your own Slack app or Discord application.

Slack, under Settings → Connect Slack:

  1. Set your Slack app's Interactivity Request URL to the address the form shows: https://app.moatbox.dev/api/v1/slack/interactions/<team-id>.
  2. Give the bot the chat:write scope and invite it to the channel.
  3. Enter the Bot token (xoxb-...), the Signing secret and the Channel ID, then Save.

Discord, under Settings → Connect Discord:

  1. Set the application's Interactions Endpoint URL to https://app.moatbox.dev/api/v1/discord/interactions/<team-id>.
  2. Add the bot to the channel's server.
  3. Enter the Application ID, the Public key (hex), the Bot token and the Channel ID, then Save.

For each held action, moatbox posts one message to the channel: "Held #<id> in <space>: <kind>", the statement or request (cut at 500 characters), a link to the Approvals page, and Approve and Deny buttons. A decision replaces the Slack message with a line such as my-space: #12 approved by ada@example.com via slack. In Discord, moatbox appends that line to the message and removes the buttons. When moatbox refuses a press before deciding, such as one from an unlinked account, the reason comes back as a reply only you can see.

Buttons act as the moatbox account linked to your chat account, so link it once:

  1. Press Approve or Deny. The private reply gives you your account ID and a link to https://app.moatbox.dev/api/v1/teams/<team-id>/links/<slack|discord>.
  2. Open that link while signed in to moatbox, enter the account ID and click Request a link. The request expires after 15 minutes.
  3. Press the button in chat again. The private reply now holds a confirmation link.
  4. Open it while signed in as the same user and click Confirm this chat account.
  5. Press the button in chat once more to decide.

moatbox refuses a press on a message it did not send or on a message someone edited, with "That button is not one moatbox sent."

API

curl -X POST https://app.moatbox.dev/api/v1/spaces/my-space/entries/12/approve \
  -H "Authorization: Bearer mbx_xxx"
{"message": "#12 approved"}
Method and path What it does
GET /api/v1/spaces/{space}/pending Lists actions waiting for a decision.
POST /api/v1/spaces/{space}/entries/{id}/approve Approves a held action.
POST /api/v1/spaces/{space}/entries/{id}/deny Denies a held action.
POST /api/v1/spaces/{space}/entries/{id}/undo Undoes an executed action that supports undo.
GET /api/v1/spaces/{space}/entries Lists the space's log; filters below.

An API key needs the approve permission, and only owners and admins can create such a key. See API keys. The hosted MCP server offers list_pending_approvals and no tool that decides. See MCP server.

approval.requested and approval.decided events let your own systems react to holds. See Events, webhooks and telemetry.

Timeouts

A held action waits five minutes. With no decision by then, the gateway denies it and records "(no decision within 5m0s)". The dashboard shows that outcome as Expired. A hold action with a window does the reverse: it goes out when the window ends, recorded as approved by "hold window expiry".

If the agent's connection closes while the action waits, the gateway marks it Abandoned, and it never runs. Approving it afterwards fails with "its client disconnected while it was held, so it will never run".

What the agent sees

The agent's request blocks until someone decides or the hold times out.

Action On approval On deny or timeout
Integration call The service's real response, plus a Moatbox-Hold: approved #12 by <who> header 403 with a plain-text body such as moatbox: #12 was denied (no decision within 5m0s)
MCP tool call The tool's result, with that notice appended as text A tool result marked as an error, with the same message as text

If an approved action fails when it runs, the agent gets the failure and the record shows the action as failed.

Undo

You can undo an executed action when its strategy is checkpoint or compensate, or when it is the live release of an app, function or static site. Undo refuses an approve-only action and any action whose undo state moatbox could not record.

Run undo through the API with POST /api/v1/spaces/{space}/entries/{id}/undo.

An integration's undo can fail when someone changed the same thing after the agent. GitHub's ref restore names the commit the agent pushed, so GitHub refuses the restore if anyone pushed since.

Audit trail

moatbox keeps two records.

The space's log lives in the space and records each action the gateway held or classified, plus refused connections: what it was, the strategy, the target, the decision, who decided and the reason. Open the space and go to Audit ("This space"). Filter by time frame, Who, Run, Decision (Any decision, Approved, Denied, Waiting, Allowed by policy, Undone) and Integration, then Export CSV. moatbox reads this log from the running space, so an asleep space shows only the team's record of it. The API equivalent is GET /api/v1/spaces/{space}/entries, with decision, integration, run, before and n (up to 200) as query parameters.

The team log lives in moatbox and keeps who did what across all spaces. Go to Audit → Whole team and pick Approvals or Integration rules in the event filter. It records:

  • approval.held: the gateway held an action
  • approval.approved, approval.refused, approval.undone, approval.abandoned: the outcome, with the person and the channel: web (dashboard and push), cli (API key), slack or discord
  • approval.attempt: a decision that did not go through, such as one from an account without approver rights or an unlinked chat account
  • rule.changed: a rule change, with the new mode and auto-approved kinds

Owners and admins see every event. Members see events tied to a space and their own. Through the API, use GET /api/v1/teams/{team}/audit with space, actor, kind, since, until, before and limit (up to 200), or format=csv for up to 10,000 rows. moatbox keeps the team log while the team exists, including under zero data retention. See Privacy and data retention.