# Gatekeeper — Approval Queue with Sub-Agent Isolation An AI agent that manages a customer database, where **reads are free but writes require human approval**. The database lives in a **sub-agent** with its own isolated SQLite, so the agent structurally cannot bypass the approval queue. ## How It Works ``` GatekeeperAgent (extends AIChatAgent) │ │ LLM ──▶ Tools ──▶ Approval Queue (action_queue table, parent SQLite) │ │ │ ┌──────────────────────▼──────────────────────────────────────┐ │ │ CustomerDatabase (Agent — own isolated SQLite) │ │ │ query() / execute() / getAllCustomers() │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ customers table (parent CANNOT access directly) │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────┘ ``` The parent has no path to customer data except through the sub-agent's typed RPC methods. This makes the approval queue structurally enforceable — not just a convention. ## Key Pattern ```typescript import { Agent } from "agents"; import { AIChatAgent } from "@cloudflare/ai-chat"; export class CustomerDatabase extends Agent { onStart() { this.sql`CREATE TABLE IF NOT EXISTS customers (...)`; } query(sql: string): Record[] { /* ... */ } execute(sql: string): { success: boolean } { /* ... */ } getAllCustomers(): CustomerRecord[] { /* ... */ } } export class GatekeeperAgent extends AIChatAgent { private _getDb() { return this.subAgent(CustomerDatabase, "database"); } // The ONLY path to mutate customer data async approveAction(id: number) { const db = await this._getDb(); db.execute(action.sql); } } ``` The LLM's `mutateDatabase` tool queues actions for approval. The `queryDatabase` tool reads freely via `db.query()`. Only `approveAction()` calls `db.execute()`. ## Quick Start ```bash npm start ``` ## Try It 1. "Show me all customers" — reads via `db.query()`, logged as observation 2. "Upgrade all East customers to Gold" — queued for approval 3. Click **Approve** — executes via `db.execute()`, table updates 4. Click **Revert** — undone via `db.execute(revertSql)` 5. Click **Reject** — nothing happens ## Related - [gadgets-subagents](../gadgets-subagents) — fan-out/fan-in with parallel sub-agents - [gadgets-chat](../gadgets-chat) — multi-room chat via sub-agents - [gadgets-sandbox](../gadgets-sandbox) — isolated database sub-agent with dynamic Worker isolates - [design/rfc-sub-agents.md](../../design/rfc-sub-agents.md) — RFC for the sub-agent API