Two pizzas arrive, but you tried to order one: the classic double-order bug
You phone a pizza place. They say "got it", and the line drops before you hear it. Did the order go in? You call back and order again. Two pizzas arrive, but you tried to order one. ππ
Software does this to robots. This note follows exactly that failure through a small example I built: how to catch it without trusting the code that caused it, and what the sandbox it ran in did that an ordinary test could not.
Who's who
I run code that AI agents write inside sandboxes, through a small open-source service I built called plimsoll. One kind of code platforms now let AI write is the adapter: the piece that translates between the platform's own system and a vendor's API (the list of requests the vendor's computers accept). Here the vendor runs a fleet of warehouse robots.
| who | what it is | in the pizza story |
|---|---|---|
| π€ robot r2 | one of the vendor's three robots | the oven |
| π’ the vendor | a pretend robot company, fleetsim: a simulator I wrote | the pizza place |
| π the adapter | AI-written JavaScript that places and tracks orders for the platform | you, on the phone |
| π¦ the sandbox | a locked box the adapter runs in, with no network and no way into my machine | a phone booth with one line out |
| π the broker | plimsoll's gatekeeper: it forwards only six kinds of request and adds a key that works for this one run | the switchboard operator |
| π the ledger | the vendor's own list of every order it accepted | the tickets on the kitchen rail |
| βοΈ the grader | eight rules checked after the run, against the ledger and the robots | the manager who reads the rail, not your story |
| π§Ύ the receipt | a signed record of exactly what went into the sandbox and what came out | a tamper-evident envelope |
The idea the whole example rests on: the adapter's account of itself counts for nothing. A grader that asks the adapter how it went is asking the waiter whether the waiter did a good job.
The setup
The vendor publishes an OpenAPI document (a machine-readable menu) of eight operations. Its "create an order" takes an optional command_id, which is your order number, and the menu says what a repeat does: "a create repeated with a command_id already accepted answers with that original mission instead of a new one". Call back with the order number and they say "yep, that one's in the oven".
The adapter runs in the sandbox and reaches the vendor only through the broker. The broker allows six of the eight operations and adds a credential (the key the vendor checks on every request) minted for that one run, so the adapter never holds the vendor's real key. A harness, the test driver, hands the adapter the same short list of commands in eight scenarios: a normal day, a robot that breaks, a robot broken from the start, an overloaded vendor, and four ways for an answer to go missing. It writes down whatever the adapter reports, and then the grader decides. One of the eight rules is one_order_per_command: each command becomes exactly one order on the vendor's ledger.
The lost answer
In the lost_ack scenario ("ack" is short for acknowledgement, the "got it"), the vendor accepts the second command's order, starts the robot, and then answers with HTTP status 503 (a server's "something went wrong, try later") and the body "acknowledgement lost". The order exists; the adapter is told it failed. The line dropped right after "got it".
The example's first adapter, adapter-v1.js, does what most of us would do: it calls back and orders again. It sends no command_id, so the vendor cannot tell a retry from a new order, and places a second one. Robot r2 drives to (10,4), finishes, and then drives to (10,4) again, like someone who walked into the kitchen and forgot why. The adapter reports the second order, m3, as done. From where it sits, everything went fine. π
The vendor's event log for that command, as the example page replays it (the number is the event's place in the log):
| # | first adapter | second adapter |
|---|---|---|
| 2 | order m2 for r2 | order m2 for r2, command run13-c2 |
| 3 | r2 starts m2 | r2 starts m2 |
| 4 | answer lost (503) | answer lost (503) |
| 5 | order m3 for r2 | repeat of run13-c2, answered with m2 |
| 14 | m2 done at (10,4) | m2 done at (10,4) |
| 15, 16 | r2 starts m3, m3 done at (10,4) | nothing more for r2 |
The grader reads the rail, not the waiter. The first adapter's run fails one rule of eight, one_order_per_command: "run04-c2 became 2 orders (m2, m3)". The second adapter, adapter-v2.js, calls back with the same order number; the vendor says "that's m2", the ledger holds one order for the command, and the run passes all eight rules. You can replay the first adapter's run and the second's on the example page: press Play and watch r2 do its extra lap.
Where that bug came from matters. adapter-v1.js was written in the same session that built the example, so it is not an independent trial. When three models (Claude Haiku, Sonnet and Opus) each wrote an adapter from the contract (the vendor's menu plus the rules) in a clean session with no tools, all three used the order number. The point is not that AI gets this wrong. It is that a platform can check it without taking the adapter's word for it.
When nobody can know
The harder case is the silent scenario. The vendor accepts the order, and from then on its gateway (the front desk in front of its servers) answers 504, "the server behind me did not answer", to everything about that robot. The phone just rings. Nothing the adapter can do will tell it whether the pizza is coming.
The second adapter's patience is 40 calls per operation, retries included, a quarter of a second apart. It uses all of them over about ten seconds, hears 40 rings and no answer, and returns an error coded unresolved: "no answer after 40 attempts; the vendor may or may not hold the order". That is the honest answer, and the contract asks for exactly that. (The vendor does hold it, as m2, still running.)
The example also carries ten adapters broken on purpose, each in one way. One of them gives up after three tries and reports refused, which a platform reads as "nothing happened, safe to order again". The grader fails it on report_is_true: "the adapter reported an error coded "refused" ... and the vendor holds the order as m2". Believing it would put a second order on the robot, which is where we came in. ππ
What plimsoll did here, and what it didn't
Fair question by now: the double order is a bug in the adapter, so what did plimsoll do about it? It did not prevent it. No sandbox stops code from ordering twice when ordering is its job. What plimsoll changes is everything around the bug: whether you can run an AI's code against the vendor at all, what that code can reach while it runs, and whether anyone can check the verdict afterwards.
Without something like plimsoll, the usual way to find out whether an AI's adapter works is to run it: on a laptop, in CI (the servers that run a project's tests), or in production behind a switch. Then the adapter is just another program and gets what programs get: your files, your network, and the vendor's real key in its environment, where any line of it can read it or send it somewhere. Its "all done" is believed, because nothing else is keeping count, and the two pizzas show up in production.
In this example the right-hand column was tested, not promised. The boundary probe, a step that is not an adapter and tries what hostile code would, ran under the same grant. A request straight to the vendor failed with ECONNREFUSED, since the sandbox has no network. A raw DELETE /robots/r1 that skipped the injected client got 403 from the broker. A search of its environment and its client found no credential, because the broker adds the key on the host after the route is checked. Over the 51 runs the vendor saw 50 different keys, one per run that reached it, each living 95 seconds. And when the vendor was overloaded, the broker's breaker opened on each 503 and held the adapter's next calls back for a second, so retries could not pile onto a vendor that had just said it was struggling.
One row is the example, not plimsoll: the grader is ordinary code that ships with the example, and you could write a ledger check without plimsoll. What you cannot easily do without something like it is run that check on code you did not write and do not trust, with the vendor's real key nowhere in reach, and hand someone else the evidence.
I can vouch for the first row personally. In both trial runs, on 14 and 15 October, the helper that gave each AI agent its practice result (it ran the agent's draft adapter on one scenario and sent back what happened) ran that adapter with my machine's own Node, outside any sandbox. Nobody noticed for two days, until a security review flagged it. Nothing bad happened, which is luck, not design. The helper now runs adapters in the sandbox through the real broker, and a test plants a secret variable and checks that the adapter can neither read it nor list my home directory.
The receipt
Every run is sealed into a signed record, the tamper-evident envelope: a hash (a fingerprint that changes if any byte changes) of exactly what went into the sandbox and what came out, signed with a key the test run made for itself. Each verdict is sealed too, and tied to its run. One line of the published file can be checked on its own. Here is the second adapter's lost-answer run, from a clone of the repository:
$ go build -o plimsoll-attest ./cmd/plimsoll-attest
$ cd docs/examples/adapter
$ ../../../plimsoll-attest verify-entry -pub harness.pub -line 13 bundle.jsonl
entry verified: line 13, signed by key 02c7b7eed354d6297e60f1f69eb7579acc1fcdc5a35cd1c398f05ddf5fd94fc7: record 3c54bdfe1ccb0b753d9e44238afc250fc7e7212f5aa444242f5fe59319ac674e, a single run; the signed record states provider docker and isolation "kernel". It authenticates the recorded messages, not that what the guest printed is true. Not established by this check: the bundle around the line, and for a session call that its session's chain is whole; plimsoll-attest verify checks a whole bundle
Now change one byte. In run 13's stored answer the adapter checked on its order 4 times; make it 5 in a copy and check again:
$ ../../../plimsoll-attest verify-entry -pub harness.pub -line 13 bundle-altered.jsonl
plimsoll-attest: attest: broken bundle chain: the line's link signs other content
Seal broken. π§ΎβοΈ
And go run ./examples/adapter -verify docs/examples/adapter rebuilds every verdict from its evidence: "verified: 51 run records and 51 qualification statements, every verdict rebuilt from its evidence".
What this does not prove
A seal proves who sealed the envelope, not that the letter inside is true. Concretely: the vendor is my simulator, and the ledger is its own account, read by the signer after the run; you can rebuild a verdict from it, but not check it against a real robot company. A signature proves these bytes were seen by whoever holds the key, and nothing on the page vouches for the key. The record states what plimsoll's server says about the sandbox (docker under gVisor, a layer that gives the sandbox a kernel of its own, which plimsoll calls the "kernel" tier); that is configuration and startup checks, not hardware attestation (a chip vouching for the software it runs). The event log in the table above is drawn for reading; the signed parts are the ledger, the robots and the reports.
And this exam is not hard for current models. Opus's adapter passed all eight scenarios, and Haiku's and Sonnet's failed only the overloaded vendor, by hanging up before it recovered and saying so. What the example shows is the grading, not a leaderboard.
If you build integrations like this: give every command an order number the vendor honours, let "I don't know" be an answer the adapter is allowed to give, judge the adapter by the vendor's state, not its story, and run it where it cannot reach your keys.
Try it
go run ./examples/adapter -verify docs/examples/adapter checks the published evidence with nothing but Go. The full run, go run ./examples/adapter, needs docker; it builds plimsoll's server from source and does all 51 runs (SANDBOX_DOCKER_RUNTIME=runsc for gVisor, as the published evidence was). The contract, the vendor, the grader and the broken adapters are in examples/adapter ↗. plimsoll is Apache-2.0: https://github.com/plimsollmark/plimsoll ↗