Interlock replaces ambient agent authority with leases that expire on their own clock. A global e-stop and a fault bus gate every action, and the gate fails closed.
Agents act with ambient, unexpiring authority. Filesystem, credentials, and network access get granted once for the whole session and revoked only by killing the process. The 2026 agent stack has an authority problem, not a capability problem.
Mobile robots solved this a decade ago. Boston Dynamics' Spot admits intent through a narrow gate of command plus lease plus clock, with keepalives, a global e-stop, and a fault taxonomy built into the structure rather than written as policy. Interlock transfers that architecture to software agents as a zero-dependency Python substrate.
Inside the substrate, no action executes without a live lease, a clear e-stop, and no gating fault. Interlock.permit() checks in this order and raises on the first failure:
- e-stop clear, else
EStopEngaged - lease issued by this
Interlockand unaltered, elseLeaseNotIssued; then unrevoked and unexpired, elseLeaseRevokedorLeaseExpired - no
majororcriticalfault on the lease's resource, elseFaultActive
- Authority expires. A lease exists only from
grant()and dies atexpires_atunless a keepalive renews it. - A keepalive cannot resurrect a dead lease. Renewal after expiry or revocation raises.
- Only issued leases pass. The lease manager keeps its own record of every lease it grants, and that record decides expiry and revocation. Editing a
Leaseobject grants nothing. - Revocation is permanent.
- E-stop release is an operator decision, never a timeout. One engagement refuses every permit until an explicit
release(). - Every permit and every denial is recorded in the ledger's
permitsordenialsseries, with the reason. - Claims carry provenance: source, authority (the lease id), review date, and a falsifier.
Interlock is not published on PyPI, so pip install interlock does not install this package. Install it from a clone:
git clone https://github.com/FetchDoge/interlock.git && cd interlock
pip install -e ".[test]" && pytest -qfrom interlock import Interlock
ilk = Interlock()
# GRANT: authority comes into existence only here, and it expires.
lease = ilk.grant(resource="docs:write", holder="research-agent", ttl=1800)
# EXERCISE: every action passes the gate.
with ilk.permit(lease, action="write", target="brief.md"):
... # raises EStopEngaged, LeaseNotIssued, LeaseRevoked, LeaseExpired, or FaultActive otherwise
# A major or critical fault refuses further permits on the affected resource.
ilk.faults.report(code="TOOL_TIMEOUT", severity="major", resource="web:read")
# AUDIT: claims carry provenance, authority, review date, falsifier.
claim = ilk.claim(
text="14 pages fetched; 2 endpoints timed out.",
provenance="tool:web-fetch",
authority=lease.id,
review_after_days=7,
falsifier="re-fetch returns different content",
) # auto-appended to the ledger's "claims" seriesThe fastest path to a first lease is docs/guides/quickstart.md.
90-second demonstrator: python3 scripts/demo_90s.py runs the substrate end to end, narrated: grant, keepalive, permit, expiry denial, resource-scoped fault gate, global e-stop, provenance-stamped claim, and ledger audit. It exits non-zero if any step misbehaves. Expected output: demo/TRANSCRIPT.md.
| Condition | Behavior |
|---|---|
| E-stop engaged | Every permit() raises EStopEngaged and logs a denials record |
| Lease expired (no keepalive in time) | LeaseExpired. A late keepalive also raises |
Lease not issued by this Interlock (hand-built, from another instance, or edited after issue) |
LeaseNotIssued and a denials record with reason lease_not_issued |
| Lease revoked | LeaseRevoked, permanently. Resetting lease.revoked on the object does not restore it |
major/critical fault on the resource |
FaultActive until the fault is cleared. minor faults are recorded but do not gate |
Known limits:
- The gate runs at
permit()entry. An e-stop engaged during awithblock does not interrupt code already running inside it; it refuses the next permit. - State is in-process and in-memory. Interlock gates actions routed through it and cannot stop an agent that bypasses the substrate. It is one layer of defense in depth.
- The ledger is append-only and self-describing JSONL (format descriptor, per-series schemas, closing index). Reopening a path appends a new segment and never truncates unless you pass
mode="w". Every record is flushed as it is written, so it survives a crash of the writing process. Onlyclose()fsyncs, so a power loss can still drop the last records. It is not hash-chained or signed, so treat it as an audit log, not tamper evidence.
CHARTER.md has the full design rationale and safety caveats.
- 68 tests pass:
pytest -q, re-run 2026-10-15 onmainat68f2d6e. CI runs the same suite on Python 3.11, 3.12, and 3.13, plusrufflint and format checks and a version-consistency check. - The quickstart above was executed on 2026-10-07: the gate refused a permit with
FaultActiveafter amajorfault. INTAKE_LEDGER.mdlists every source behind the transfer, with reading depth and an explicit unverified list.
src/interlock/:leases,estop,faults,claims,interfaces,ledger. Zero runtime dependencies.docs/concepts/: concept documentation.docs/guides/quickstart.md: first lease.CHARTER.md: design rationale and safety caveats.
v0.1.0 (release, 2026-09-27): core substrate with tests. See CHANGELOG.md.
Built by Marcus Richards, directing AI coding agents.
Copyright 2026 Marcus Richards. Licensed under the Apache License 2.0. See LICENSE.