branch:
README.md
3285 bytesRaw
# 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<Env> {
  onStart() {
    this.sql`CREATE TABLE IF NOT EXISTS customers (...)`;
  }

  query(sql: string): Record<string, unknown>[] {
    /* ... */
  }
  execute(sql: string): { success: boolean } {
    /* ... */
  }
  getAllCustomers(): CustomerRecord[] {
    /* ... */
  }
}

export class GatekeeperAgent extends AIChatAgent<Env, GatekeeperState> {
  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