· MIT · Python 3.9+ · zero dependencies
Interlock gives every action an AI agent takes a receipt: it happened once (or is marked unknown when the service can’t be asked), it was authorized when it fired, and the facts it was decided on still held when it landed, even through a crash.
Live demo: interlock-demo.greenpond-c5ddc6af.westus2.azurecontainerapps.io · Landing page: az-said.github.io/Interlock/site · Offline: python3 demo.py 2
Why the obvious fix isn’t enough, in one picture. Both columns write to disk before sending and read it back after the crash; the difference is what the entry carries, because recovery can only re-check what was written down:
flowchart LR
subgraph L["The obvious fix: journal the INTENT"]
direction TB
L1["agent decides:<br/>refund $20"] --> L2["disk, fsync'd:<br/>"I am sending $20""]
L2 --> L3["send: 💥 crash"]
L3 --> L4["restart: my send never landed,<br/>my permission is still live<br/>→ resend $20"]
L4 --> L5["$40 refunded ❌<br/>its books balance: nothing alerts"]
end
MID["⏱️ mid-outage, off camera:<br/>support refunds the $20 by hand.<br/>No ID, no journal entry ,<br/>invisible to every log"]
subgraph R["Interlock: journal the REASONS"]
direction TB
R1["agent decides:<br/>refund $20"] --> R2["disk, fsync'd: "$20 BECAUSE<br/>refunded_total=0, case 4471<br/>allows it, lease live""]
R2 --> R3["send: 💥 crash"]
R3 --> R4["restart: re-read the world first ,<br/>refunded_total is now 20:<br/>premise stale"]
R4 --> R5["REFUSED, receipt names the change<br/>$20 total ✅ measured on real Stripe"]
end
MID -.-> L4
MID -.-> R4
style L5 fill:#7f1d1d,stroke:#f87171,color:#fff
style R5 fill:#14532d,stroke:#34d399,color:#fff
style MID fill:#78350f,stroke:#f5a524,color:#fff
The journal records what you did. It cannot record what the world did while you were down. So the entry has to carry the facts the decision stood on, and recovery has to re-read the world before anything is sent a second time. That one line is the difference between the columns of every results table in the proof.
pip install git+https://github.com/az-said/Interlock
1. Python, three lines. Decorate the function with the side effect and recover on startup.
from interlock import Interlock
gate = Interlock(".interlock")
@gate.effect(key=lambda order, amount: f"refund:{order}",
premises=lambda order, amount, idempotency_key: {
"refunded_by_others": refunded_total(order, excluding=idempotency_key)},
dedupes=True)
def refund(order, amount, idempotency_key):
return stripe.Refund.create(charge=charge_for(order), amount=amount, idempotency_key=idempotency_key)
gate.recover() # once, on startup
2. No code: in front of an MCP server. python3 -m interlock.mcp_proxy --config interlock.mcp.json -- <server command>
3. Let your coding agent do it. Paste docs/install-with-ai.md into Claude Code, Cursor, Codex or Copilot.
Temporal activities, OpenAI and Anthropic tool loops, LangChain and LangGraph, and Google ADK: docs/integrations.md.
Seven runs, from simulated faults to real Stripe, real Temporal and a SIGKILLed Google ADK agent, with every id listed: the proof table. Tests: tests/README.md.
| Said Azaizah | said-azaizah.vercel.app · github.com/az-said |
| Kiro Moussa | kiro.city · https://github.com/kiromoussa |
MIT licensed. Battle of the Coasts 2026, Cloud AI track, Boston.