Interlock, explained
A receipt for AI agent actions: proof an action happened once, was authorized when it fired, and its assumptions still held. Finance teams use it to stop hand-approving every agent action.
This page walks the whole project from the challenge to the code to what's next. Drag the crash around in the next section first. Everything after it is explaining what you just saw.
The crash, live
A customer paid $100. Support approved one $20 refund. The agent sends it. Drag the crash to any point in the sequence and pick what the payment service can do. Watch what a plain retry does versus what Interlock does.
Plain retry (most agents today)
Interlock
The five stops are the five moments a real process can die. Stop 3 is the one the track brief describes: the service did it, the response never came back.
The root, in one sentence
An agent decides on premises that are true when it decides; its action lands later; and nothing carries the premises along with the action to be re-checked when it lands.
At the wire, a timeout has four possible meanings and one observation. That's the Two Generals problem (1975): provably impossible to close from the client side alone. You design around it and say what's left.
What a timeout could mean
The request never arrived.
The service did it; the response was lost.
The service is still working.
The service crashed halfway.
What AI adds
Re-running the program doesn't reproduce the decision.
Data the agent reads can change what the program does.
Permissions move while the agent runs.
The four facts the brief asks for
Proposed
The model said "refund $20."
Authorized
The agent held a live refund grant at that instant.
Executed
The service actually moved $20.
Recorded
Our record says refunded, and can prove it.
Every framework today collapses these into one log line. A crash can separate any two. Each separation wants a different repair, so the journal keeps all four.
The machine
Five rules. Write the decision to disk before acting. Fix the effect's identity when the request is approved, never by the model. Carry the premises and re-check at commit. Treat authority as a lease checked at dispatch. On recovery, never guess.
The output of every effect is a receipt: four booleans and a final state. An auditor reads receipts instead of reconciling logs.
Three kinds of service
Whether "exactly once" is even possible depends on the other side, not on you. So the gate declares which kind of service it's talking to and promises only what that kind allows.
Tier 1: dedupes on a key
Stripe with an idempotency key. Retry is safe; the service returns the earlier result.
Guarantee: exactly once.
Tier 2: can be looked up
Most APIs with a GET, a database, a filesystem. On recovery, ask what it has, then commit without re-sending.
Guarantee: exactly once, one extra read.
Tier 3: neither
Email, webhooks, many internal services. "Sent and lost" and "never sent" look identical.
Guarantee: at most once, ambiguity surfaced to a person. A refund that never happened may stay blocked. That's the stated cost.
Tier 3 is an impossibility result, not a bug. The brief lists "an impossibility result paired with a useful weaker guarantee" as a valid format. That's Finding 1.
What we measured
Every cell regenerates from one command. Two baselines: today's plain retry, and the conventional "just use idempotency keys" durable operation. Then the gate at each tier.
Refund agent: $100 paid, one $20 refund approved
Parallel coding agents: same gate, different world
The findings
1. Exactly-once belongs to the target
Achievable at tiers 1 and 2. Undecidable at tier 3; the honest guarantee is at-most-once plus a surfaced ambiguity, and the cost is on the table.
2. Keys are necessary and insufficient
Idempotency handles crash, duplicates, and the $30 re-decision at the service. It still refunds under a revoked lease and on an ineligible order, because the service can't see the agent.
3. Premises are a dial with a floor
File hashes never land broken code but refuse benign edits. Symbols land benign edits, catch renames and duplicated work, and can't see a same-signature meaning change.
4. Recovery is when the world moves
Our own tests found the gate re-sent without re-checking premises. Support refunded by hand during the outage; the gate paid twice. Fixed: a resend is a new dispatch.
Since then the team ran the key rows against real Stripe (test mode) and a real Temporal server, with the same outcomes. Those results live in results/stripe_live.md and results/temporal_live.md.
Under the code: one refund, traced
- The agent builds a proposal: the approved case id, its lease, the premises it saw (order eligible, $100 paid), and the effect (refund $20).
effect_id_for()hashes the case id, not the model's words. Same case, same id, forever. A model that says $30 on retry produces the same id with a different payload, and that's refused.journal.append("PROPOSED")writes the decision and premises to disk with an fsync.- Checks, in order: already committed? lease live right now? premises still true against the world? Any no →
REFUSED:*. journal.append("DISPATCHED"). Only now does the call go out. If the process dies after this line, the journal knows something was started.- On restart,
recover()finds every DISPATCHED without COMMITTED and acts by tier: retry, look up, or write AMBIGUOUS. Before any resend it re-checks lease and premises, because the outage is exactly when the world moves. receipt()reads the journal back into four facts and a final state.
The protocol is about 250 lines across journal.py, leases.py, and gate.py. Everything else in the repo is evidence (experiments, tests, live runs) or adapters (Stripe, Temporal, MCP, the decorator). If you strip it to what's judged, it's those 250 lines and the two tables.
The field
| Layer | Question it answers | Who |
|---|---|---|
| Artifacts in | Is this package safe to install? | Sandboxing tools |
| Policy and inventory | What may agents do? | Governance platforms |
| Durable execution | Resume after failure | Workflow engines; agent SDKs with first-party support |
| Injection containment | Can data change control flow? | Capability-based research systems |
| Dedup at the receiver | Don't do it twice | Payment APIs with idempotency keys |
| Agent-native payment rails | Let agents hold and move money | New rails, funded this year |
| Effects | Did it happen, once, under live authority, on premises that still hold? | Nobody |
The empty row is empty for a structural reason. Model providers sit on one side of the effect, API providers on the other, workflow engines upstream. Nobody owns the seam. Seams are where payment processors, telephony APIs, and bank-data aggregators came from.
How we got here
What we ruled out
Log more
A log records what the logger saw. The fact you need is on a machine you don't control, and it can only tell you by sending a message, which can also be lost.
A proxy that records every request
A third party in the same problem. It has to terminate TLS to see payment traffic, which nobody allows. The storage half is right; the recovery protocol is the actual work.
Idempotency keys alone
Handles crash and duplicates at the service. Can't see authority or premises. Measured: the second column.
Durable execution alone
Checkpoints the step's result; a step that dies after the effect re-runs the tool and replays the old decision. Measured: the third column in the repo's table.
A detector for bad actions
Model-quality claim, needs a benchmark, and the brief says keep it separate from protocol guarantees.
A sandbox
Admission control on artifacts, not correctness of effects. Already built, and by people we know.
Agents messaging each other
Every message arrives too late or becomes a check at the commit point. So we built the check at the commit point.
Hash every file the agent read
Safe but refuses benign edits. Measured: the file-hash column. Over-fires.
What's next
Before Monday noon
Verify the live runs and citations by hand. Fix the stale demo text in the README. Point the old repo URL at the new one.
Record the 90-second video: face, cookie, terminal, one table.
One more real row if there's time: a GitHub merge target, or the premise-coverage table on a real repo.
The next 90 days
Ten interviews with people who approve agent actions. The approval-inbox mix is an assumption; the first real number replaces it.
One design partner running refunds. Measure approvals and wrong payouts before and after.
The notary: a shared log both agent and service write to, lifting tier-3 services to tier 2 without changing their API. That's the hosted product.