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, executesStep-by-step
- 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_idandsnapshot_idfor later. - 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/pendingResponse:
{ "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" } ] } - 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 - Agent resubmits (if approved)
Once approved, the agent resubmits the query. For CRITICAL operations, the
snapshot_idmust 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
/pending approval:read scopeReturns all pending approvals awaiting a decision. Sorted by creation time, oldest first.
/approve/{id} approval:write scopeApproves 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.
/deny/{id} approval:write scopeDenies 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.