Interlock coordinates AI agents that change production.
An agent asks Interlock before it changes anything. Interlock finds every service the change can affect. If another change already affects them, the new change waits. If the change is risky, a human must approve it. Interlock records each decision.
The problem
Companies run many agents at the same time. Each agent knows only its own task. Two tasks can look unrelated and still break the same service.
Diagram: checkout depends on orders and payments. Orders depends on orders-db. Payments depends on payments-db.
"An AI agent deleted our production database."
Interlock: a delete in prod needs a human. An agent scoped to staging cannot reach prod.
"Who approved the command, how do you prove what it did."
Interlock: each decision is stored before the action runs, with the chain from the agent to the human who authorised it.
"Merge conflicts, duplicated functions, agents working at cross-purposes."
Interlock: two changes that reach the same service cannot run together. The second change waits.
Scenarios you can run
These buttons change the live system. Select a scenario, run it, then watch the map and the log.
Benchmark: the same fleet with and without Interlock
24 agents, 40 production resources, 180 simulated seconds, the same random seed. Without Interlock the fleet makes more changes, but most of them collide.
- Collisions on customer-facing services
- 116 without, 0 with
- Changes that landed cleanly
- 58 without, 107 with
- Average wait before a change starts
- 0s without, 29.5s with
The wait comes from a strict rule: two changes that reach the same customer-facing service run one after the other. To get these numbers again, run scripts/bench.py. You can also turn Enforcement off in the console.
How a request is decided
Services, databases, topics, flags, teams, people and agents are nodes in one FalkorDB graph. Interlock answers each request with graph queries.
# the agent asks first POST /api/request { "agent": "deployer-03", "action": "deploy", "target": "payments@prod", "reason": "v2.4 rollout" } # the answer says what to do and why { "status": "waiting", "reason": "Waits for migrator-11 (migrate orders-db). Both changes reach checkout: orders-db → orders → checkout, and payments → checkout." }
// everything that depends on the target, up to 2 hops away MATCH (t:Resource {id: $target}) OPTIONAL MATCH p = (d:Resource)-[:DEPENDS_ON*1..2]->(t) WITH t, d, min(length(p)) AS hops RETURN t.id AS target, collect({id: d.id, hops: hops, tier: d.tier}) AS zone // deploy and migrate: 2 hops. restart: 1. delete: 3. // the zone is stored as IMPACTS edges on the Intent node.
// check and grant in one write: two agents cannot both win MATCH (n:Intent {id: $id}) OPTIONAL MATCH (n)-[a:IMPACTS]->(r:Resource)<-[b:IMPACTS]-(g:Intent {status: 'granted'}) WHERE g <> n AND ( (a.role = 'target' AND n.mode = 'exclusive') OR (b.role = 'target' AND g.mode = 'exclusive') OR (r.tier = 1 AND n.mode = 'exclusive' AND g.mode = 'exclusive')) WITH n, collect(DISTINCT g) AS blockers SET n.status = CASE WHEN size(blockers) = 0 THEN 'granted' ELSE 'waiting' END FOREACH (g IN blockers | MERGE (n)-[:WAITS_FOR]->(g))
// is a human behind this agent, through any sub-agents? MATCH (a:Agent {id: $agent}), (r:Resource {id: $target})-[:OWNED_BY]->(team:Team) OPTIONAL MATCH (h:Human)-[:AUTHORIZED]->(top:Agent)-[d:DELEGATED_TO*0..3]->(a) WHERE (h)-[:CAN_CHANGE]->(team) RETURN h.name, [x IN nodes(d) | x.name] AS chain // agents that wait on each other in a circle MATCH p = (a:Agent)-[:WAITS_ON*2..8]->(a) RETURN [x IN nodes(p) | x.id] AS cycle LIMIT 1 // the record, written before the action runs MATCH (n:Intent {id: $iid}) CREATE (:Decision {verdict: $verdict, reason: $reason, path: $path, chain: $chain})-[:ABOUT]->(n)
Graph model
Systems, teams and people are loaded once. Intents, decisions and waits are written while agents work. The lock table and the audit trail are the same graph.
Resource is a service, datastore, topic or feature flag. DEPENDS_ON points from the dependent to the dependency, so an impact zone is a reverse walk.
Intent is one requested change. Its IMPACTS edges are the lock. WAITS_FOR shows its queue. Deadlocks are cycles in WAITS_ON.
Decision is the audit record: the verdict, the reason, the dependency path and the authority chain.
Policies
Interlock enforces these outside the agent. A prompt cannot change them.
Connecting your agents
Any agent that can call a tool can use Interlock: Claude Code, Cursor or your own workers, over MCP or HTTP.
# Claude Code: add Interlock as an MCP server claude mcp add interlock \ -e INTERLOCK_URL=https://your-interlock \ -e INTERLOCK_AGENT=claude-code \ -- python -m interlock.mcp_server # tools the agent gets request_change heartbeat release why_blocked status list_resources
# plain HTTP, from any worker curl -X POST $INTERLOCK/api/request \ -H 'Content-Type: application/json' \ -d '{"agent": "deploy-bot", "action": "deploy", "target": "payments@prod", "reason": "v2.4"}' # renew the lease while working, release when done curl -X POST $INTERLOCK/api/heartbeat -d '{"intent": "int-..."}' curl -X POST $INTERLOCK/api/release -d '{"intent": "int-..."}'
Open the console
See every agent, change, approval and decision, with the Cypher behind each one.