AI agents are the new plague ships.
In 1423 Venice stopped letting ships sail straight into the city. Their cargo waited on an island, the lazaretto, until inspectors cleared it. Contaminated goods were burned. Clean goods came ashore.
Lazaret does the same for your API. Every write waits in a ledger at the edge, an independent Claude reviewer inspects it, and only what passes reaches your database. The reviewer can only judge; it has no way to act.
Fire an attack at a real helpdesk
Tidewater Support is a small helpdesk with its own AI support agent, running behind a real Lazaret deployment. Send it something below and follow the write through the ledger: held, sealed into a batch, reviewed, then committed or dropped.
Attack console
Each button sends a case from the evaluation corpus as a signed-in Tidewater customer. Sending needs a quick bot check, because every write is reviewed by a paid model.
Responses from the gateway appear here.
The ledger
No writes yet this hour. Send one from the console, or open Tidewater and write a ticket yourself.
Decided at the edge
Logins, API keys and credits can't wait for a batch, so a fast model reviews them inline, under a 3 second deadline. Blocked routes never reach a model at all.
Waiting for a human
Writes the reviewer wouldn't clear, or declined to judge, are held here. An operator releases or drops each one, and that decision becomes a training label for the Memory.
What Tidewater's AI agent would have done
A simulation: a deliberately naive agent reads two copies of the ticket queue. One copy has the attack ticket in it, as if writes went straight to the database. The other is the real queue behind Lazaret.
Without Lazaret
With Lazaret
How a write gets through
Five stages between a client and your database. Select one to see what it does.
The gateway normalises the path, matches a route, and scores the request: attack signatures, mass-assignment fields, per-session and per-route behaviour (bursts, failed logins), and rules the Memory has learned. Reads pass through. Blocked routes stop here. The score travels with the write as evidence.
A Durable Object with an append-only event log in SQLite. Its state is a replay of that log, so a crash loses nothing that was acknowledged. Writes collect in a batch that seals on a 30 second timer, when full, or at once on a risk spike. The author sees their own pending writes in reads; nobody else does.
Claude Sonnet 5.5 reviews each sealed batch. It has no tools and no network, and it only returns JSON verdicts. Payloads are encoded as data, so text that addresses the reviewer is just more evidence. A risky allow goes to Claude Opus 5.5 for a second look. Only the configured model can decide: a decline or a fallback model's answer holds the write for a human.
Approved writes are sent to origin exactly as the client sent them, HMAC-signed with an idempotency key, inside a checkpointed Cloudflare Workflow. A retry never commits twice. Drops spread to every write that depended on them (taint), so a harmless-looking reply to a malicious ticket falls with it.
Every verdict is archived. After a batch with drops, a model proposes deterministic rules. Each one is backtested against past traffic, shadowed on live traffic, and promoted only while it stays clean. Human decisions from the review queue are labels too, so the Memory also learns from what the reviewer missed.
Five rules it is built on
- The harness owns the loop
The Ledger runs the state machine. The reviewer is called by it, sandboxed, and can only return a verdict.
Watch a batch step through its states in the ledger above. The reviewer never starts or ends anything.
- The brain is separate from the hands
The reviewer judges and a separate committer acts. A prompt-injected reviewer has nothing to act with.
All 8 prompt injections aimed at the reviewer were stopped in the last live eval.
- The log is the source of truth
State is rebuilt by replaying the event log, so an eviction or crash loses nothing that was acknowledged.
The ledger view is itself a replay: reload this page and it rebuilds from the same event log.
- Retry instead of restarting
Review resumes from checkpoints, and commits carry an idempotency key, so a retry never commits twice.
A failed review attempt shows as Retrying on its batch, and resumes from the last finished step.
- Learn from the logs
Patterns the reviewer confirms become deterministic gate rules, so the system gets cheaper and faster over time.
The Memory section below shows the rules learned so far and what they caught.
Measured against real attacks
120 labelled requests, 63 attacks and 57 honest ones, sent through the real gateway, Ledger and reviewer with live Claude models. Run of 29 October 2026, from evals/results/live-latest.json.
- Attacks stopped
- 63/63 recall 100%
- Honest writes stopped
- 0/57 false positive rate 0%
- Stopped by the gate alone
- 5/63 with no model call and 0 false positives
- Cost
- $0.23 per run, $2.12 per 1,000 reviewed writes
Where the 63 stopped writes were caught
63 attacks and 0 honest writes, by the stage that stopped them. Most never needed the stronger model.
| Stage | Writes stopped |
|---|---|
| Gate, before any model call | 5 |
| Inline review | 10 |
| Inline review unavailable (failed closed) | 0 |
| Batch review | 45 |
| Escalation to the stronger model | 0 |
| Taint | 3 |
| Its first write was stopped at the gate | 0 |
Attacks stopped, by kind
| Kind | Stopped |
|---|---|
| Prompt injection aimed at the support agent | 10 of 10 |
| Stored XSS | 8 of 8 |
| Prompt injection aimed at the reviewer | 8 of 8 |
| SQL injection | 8 of 8 |
| Refund fraud | 7 of 7 |
| Mass assignment | 5 of 5 |
| Credential stuffing and spraying | 4 of 4 |
| Taint (follow-ups to an attack) | 3 of 3 |
| Privilege escalation | 3 of 3 |
| Spam | 2 of 2 |
| Path traversal | 1 of 1 |
| Template injection | 1 of 1 |
| Server-side request forgery | 1 of 1 |
| NoSQL injection | 1 of 1 |
| Command injection | 1 of 1 |
Inline review is fast, not instant
Logins, keys and credits wait on a fast model before they reach Tidewater. Its first token alone takes about 0.8 s, so no inline verdict can be quicker than that. A call past the 3.0 s deadline fails closed; in this run that happened 0 times.
- First token, p50 0.8 s
- Inline verdict, p50 1.6 s
- Inline verdict, p95 2.3 s
- Deadline 3.0 s
Batch review isn't on anyone's critical path: a batch of writes took 4.2 s at the median and 7.2 s at p95, after the 30 second cycle.
How often the model declined to judge
A model can decline a request it reads as cyber-offensive. Lazaret never lets anything else decide in its place: a declined write is held for a human, never allowed.
| Reviewer | Calls | Declined | Why |
|---|---|---|---|
| Batch (Sonnet 5.5) | 20 | 0 (0%) | none |
| Escalation (Opus 5.5) | 4 | 0 (0%) | none |
| Inline (Haiku 5.5) | 82 | 0 (0%) | none |
It learns from what it catches
Every verdict is archived. After a batch with drops, a model proposes a deterministic rule. The rule is tested against past traffic, watched on live traffic, and only then allowed to flag or block at the gate, where it costs nothing and takes microseconds. Where a rule and the reviewer disagree, a person decides.
Under a sustained attack, the gate takes over
6 windows of about 50 writes, 30% of them attack variants. 3 learned rules were promoted to block. Model calls per 1,000 writes went from 327 to 283, and the gate made 33% of the stops in the last window, up from 0%. Cost per 1,000 writes did not fall: $2.32 in the first window, $2.48 in the last, because honest writes still all need a model and they are most of the traffic. The learned rules did not stop any attack family held out of training: each rule fits the payloads it was learned from.
Show the numbers, window by window
| Window | Writes | Attacks stopped | Honest writes stopped | Model calls per 1,000 | Cost per 1,000 | Stops at the gate | Unseen families at the gate | Rule changes |
|---|---|---|---|---|---|---|---|---|
| 1 | 52 | 15 of 15 | 0 of 35 | 327 | $2.32 | 0 of 15 | 0% | 1 to shadow |
| 2 | 52 | 15 of 15 | 0 of 35 | 423 | $2.80 | 0 of 15 | 0% | 2 to shadow |
| 3 | 51 | 15 of 15 | 1 of 35 | 333 | $2.56 | 0 of 16 | 0% | 2 to flag, 2 to shadow |
| 4 | 53 | 15 of 15 | 0 of 35 | 358 | $2.57 | 0 of 15 | 0% | 2 to block, 2 to flag, 1 to shadow, 1 rejected |
| 5 | 50 | 15 of 15 | 1 of 35 | 320 | $2.66 | 1 of 16 | 0% | 1 to block, 1 to flag, 4 to shadow, 1 rejected |
| 6 | 53 | 15 of 15 | 0 of 35 | 283 | $2.48 | 5 of 15 | 0% | 1 to flag, 3 to shadow |
- The human queue and disputed writes were worked by a simulated operator, labelling from the eval corpus 1 window later. A real deployment gets those labels only from a person.
- The traffic is generated: each corpus attack is re-sent as encoded, re-cased, split and moved variants (no model writes them), mixed with benign variants. It is not captured production traffic.
- 16 attack families were held out of training entirely; the third chart measures only them, with the gate alone.
Live campaign run of 29 October 2026: Claude reviewing, Claude proposing the rules, $0.80 in all (evals/results/campaign-latest.json).
Rules the demo has learned
Loading the live Memory…
Put it in front of your own API
Lazaret is a TypeScript library for Cloudflare Workers. The deployment that protects Tidewater is one config file: which routes are held, which are reviewed inline, which never reach origin, and what each write touches.
export const config = defineLazaret({
origin: "https://api.example.com",
routes: [
{ match: "POST /api/login", policy: "INLINE_REVIEW" },
{ match: "POST /api/tickets", policy: "ESCROW",
resources: ["ticket:{receipt}"] },
{ match: "* /admin/*", policy: "BLOCK" },
{ match: "GET /*", policy: "PASS" },
],
review: { dailyBudgetUsd: 10 },
});- The repository, with the library, Tidewater, the evals and this site
- The guide to adding Lazaret to an existing API
- The eval corpus and runner, to check the numbers above yourself