Approval workflow

How Backstop holds destructive queries for operator review, the approval lifecycle, and how agents and operators communicate through the approval API.

When a query is classified at a risk level that requires approval (configurable, default: HIGH, IMPACT_CRITICAL, CRITICAL), Backstop holds execution and waits for an operator decision. The agent is notified via the query response, and the operator approves or denies via the API.

The approval lifecycle

Agent submits query


Gateway classifies: CRITICAL


Recovery point verified (if table-recoverable)


Gateway returns approval_required
  { approval_id: "appr_xxx", snapshot_id: "snap_xxx" }


Operator reviews via /pending

   ┌───┴───┐
   ▼       ▼
Approve  Deny
   │       │
   ▼       ▼
Agent  Agent receives
polls  denied response


Agent resubmits with snapshot_id


Gateway verifies snapshot, executes

Step-by-step

  1. Agent submits the query

    The agent sends a query through the SDK or JSON-RPC API. The gateway classifies it and, if approval is required, responds immediately with:

    {
      "status": "approval_required",
      "risk_level": "CRITICAL",
      "approval_id": "appr_4f9e2c1a",
      "snapshot_id": "snap_a3f9",
      "message": "Query requires operator approval and verified recovery readiness."
    }

    The agent should store the approval_id and snapshot_id for later.

  2. Operator reviews pending approvals

    The operator (a human or automation with approval scope) polls the pending approvals endpoint:

    curl -H "Authorization: Bearer operator-token" \
      http://localhost:8080/pending

    Response:

    {
      "pending": [
        {
          "id": "appr_4f9e2c1a",
          "agent_id": "cursor-local",
          "sql": "DROP TABLE users",
          "risk_level": "CRITICAL",
          "snapshot_id": "snap_a3f9",
          "snapshot_age_seconds": 45,
          "created_at": "2026-05-06T10:30:00Z"
        }
      ]
    }
  3. Operator approves or denies
    # Approve
    curl -X POST -H "Authorization: Bearer operator-token" \
      http://localhost:8080/approve/appr_4f9e2c1a
    
    # Deny
    curl -X POST -H "Authorization: Bearer operator-token" \
      http://localhost:8080/deny/appr_4f9e2c1a
  4. Agent resubmits (if approved)

    Once approved, the agent resubmits the query. For CRITICAL operations, the snapshot_id must be included so the gateway can verify recovery readiness:

    Python SDK:

    # The SDK handles resubmission automatically after approval
    # You can also pass snapshot_id explicitly:
    db.execute("DROP TABLE users", snapshot_id="snap_a3f9")

    Node.js SDK:

    await backstop.executeQuery("DROP TABLE users", {
      snapshotId: "snap_a3f9",
    });

    JSON-RPC:

    {
      "jsonrpc": "2.0",
      "method": "tools/call",
      "params": {
        "name": "execute_query",
        "arguments": {
          "query": "DROP TABLE users",
          "snapshot_id": "snap_a3f9",
          "agent_id": "cursor-local",
          "db_url": "postgresql://..."
        }
      }
    }

Approval API reference

GET/pending approval:read scope

Returns all pending approvals awaiting a decision. Sorted by creation time, oldest first.

POST/approve/{id} approval:write scope

Approves a pending query. The agent can now resubmit with the associated snapshot_id. Returns 200 on success, 404 if the approval ID is not found.

POST/deny/{id} approval:write scope

Denies a pending query. The query is permanently rejected. Returns 200 on success.

Quarantine

Backstop tracks how many times an agent submits queries that are blocked or denied within a configurable time window. If the count exceeds max_blocked_attempts_per_window, the agent is quarantined:

  • All future queries from that agent are rejected until the quarantine expires
  • A quarantine alert is generated
  • The quarantine duration is configurable (quarantine_duration_seconds)
{
  "max_blocked_attempts_per_window": 3,
  "quarantine_duration_seconds": 1800,
  "dangerous_retry_window_seconds": 600
}

This prevents an agent from repeatedly probing for approval on the same destructive query, or from trying to find a path through policy by slight SQL variations.