diff --git a/CHANGELOG.md b/CHANGELOG.md index aa1faf9..df3954e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ the current version is `interlock.__version__` (mirrored into `VERSION` by ### Added - `interlock.LeaseNotIssued`, a subclass of `InterlockError`, plus `LeaseManager.issued()` and `LeaseManager.is_revoked()`. +- 90-second demonstrator (`scripts/demo_90s.py`, transcript in `demo/TRANSCRIPT.md`): a narrated + end-to-end run of grant, keepalive, permit, expiry denial, resource-scoped fault gate, + global e-stop, provenance-stamped claim, and ledger audit. Exits non-zero on any unexpected + behavior. ## [0.1.0] — 2026-09-27 ### Added diff --git a/README.md b/README.md index e366cf5..ec9bc55 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ claim = ilk.claim( The fastest path to a first lease is [`docs/guides/quickstart.md`](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`](demo/TRANSCRIPT.md). + ## How it fails | Condition | Behavior | diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..94e43ec --- /dev/null +++ b/demo/README.md @@ -0,0 +1,23 @@ +# The 90-second demonstrator + +`scripts/demo_90s.py` runs Interlock end-to-end in one terminal, narrated: + +1. **GRANT** — authority comes into existence only here, and it expires. +2. **KEEPALIVE** — renewal moves the expiry; it cannot resurrect a dead lease. +3. **PERMIT** — the gate passes: e-stop clear, lease live, no gating fault. +4. **EXPIRED** — a lapsed lease is denied, and the denial is on the record. +5. **FAULT** — a gating fault refuses permits on its resource only (`web:read` + gated, `docs:write` unaffected). +6. **E-STOP** — one engagement halts every permit; release is explicit. +7. **CLAIM** — the run is stamped as a provenance-bearing claim. +8. **AUDIT** — the ledger holds every permit and every denial. + +The expected output is captured in [TRANSCRIPT.md](TRANSCRIPT.md); the script +exits non-zero if any step behaves unexpectedly, so the transcript doubles as +a smoke test. + +Run it: + +```bash +python3 scripts/demo_90s.py +``` diff --git a/demo/TRANSCRIPT.md b/demo/TRANSCRIPT.md new file mode 100644 index 0000000..761d9cc --- /dev/null +++ b/demo/TRANSCRIPT.md @@ -0,0 +1,12 @@ +[01] GRANT — authority comes into existence only here, and it expires. +[02] lease 8da9a55b030c4424af6f25f633e05157 → docs:write · holder research-agent · ttl 30s +[03] KEEPALIVE — renewal moved the expiry (moved=True). It cannot resurrect a dead lease. +[04] PERMIT — gate passed: e-stop clear, lease live, no gating fault. brief.md written. +[05] EXPIRED — the lapsed lease is denied. The denial is on the record. +[06] FAULT — TOOL_TIMEOUT gates web:read. Faults are resource-scoped: docs:write is unaffected. +[07] PERMIT — docs:write still flows while web:read is gated. +[08] E-STOP — one engagement halts every permit, everywhere. +[09] RELEASE — explicit release only. Permits flow again. +[10] CLAIM — provenance-stamped [inferred]: Demo run: 3 permits granted, 3 denied (expiry, fault, e-stop). +[11] AUDIT — ledger holds 3 permits, 3 denials, 1 claim. Nothing happened off the record. +[12] denial reasons on record: estop, fault, lease_expired diff --git a/scripts/demo_90s.py b/scripts/demo_90s.py new file mode 100644 index 0000000..603385a --- /dev/null +++ b/scripts/demo_90s.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Interlock 90-second demonstrator. + +A narrated end-to-end run of the substrate, in one terminal: + + GRANT authority comes into existence only here, and it expires + KEEPALIVE renewal moves the expiry; it cannot resurrect a dead lease + PERMIT the gate passes: e-stop clear, lease live, no gating fault + EXPIRED a lapsed lease is denied + FAULT a gating fault refuses permits on its resource only + E-STOP one engagement halts every permit; release is explicit + CLAIM the run is stamped as a provenance-bearing claim + AUDIT the ledger holds every permit and every denial + +Run: python3 scripts/demo_90s.py +""" + +from __future__ import annotations + +import sys +import time + +sys.path.insert(0, "src") + +from interlock import EStopEngaged, FaultActive, Interlock, LeaseExpired # noqa: E402 + +_step = 0 + + +def say(text: str) -> None: + global _step + _step += 1 + print(f"[{_step:02d}] {text}", flush=True) + + +def main() -> int: + ilk = Interlock() + + say("GRANT — authority comes into existence only here, and it expires.") + lease = ilk.grant(resource="docs:write", holder="research-agent", ttl_seconds=30) + say(f" lease {lease.id} → docs:write · holder research-agent · ttl 30s") + + old_expiry = lease.expires_at + ilk.leases.keepalive(lease) + moved = lease.expires_at > old_expiry + say(f"KEEPALIVE — renewal moved the expiry ({moved=}). It cannot resurrect a dead lease.") + + with ilk.permit(lease, action="write", target="brief.md"): + say("PERMIT — gate passed: e-stop clear, lease live, no gating fault. brief.md written.") + + short = ilk.grant(resource="docs:write", holder="research-agent", ttl_seconds=1) + time.sleep(1.2) + try: + with ilk.permit(short, action="write", target="late.md"): + pass + say("EXPIRED — UNEXPECTED: lapsed lease was permitted") + return 1 + except LeaseExpired: + say("EXPIRED — the lapsed lease is denied. The denial is on the record.") + + web = ilk.grant(resource="web:read", holder="research-agent", ttl_seconds=300) + ilk.faults.report(code="TOOL_TIMEOUT", severity="major", resource="web:read") + try: + with ilk.permit(web, action="fetch", target="https://example.com/feed"): + pass + say("FAULT — UNEXPECTED: gated resource was permitted") + return 1 + except FaultActive: + say("FAULT — TOOL_TIMEOUT gates web:read. Faults are resource-scoped: docs:write is unaffected.") + with ilk.permit(lease, action="write", target="brief-v2.md"): + say("PERMIT — docs:write still flows while web:read is gated.") + + ilk.estop.engage(reason="operator review requested") + try: + with ilk.permit(lease, action="write", target="brief-v3.md"): + pass + say("E-STOP — UNEXPECTED: permit passed under e-stop") + return 1 + except EStopEngaged: + say("E-STOP — one engagement halts every permit, everywhere.") + ilk.estop.release() + with ilk.permit(lease, action="write", target="brief-v3.md"): + say("RELEASE — explicit release only. Permits flow again.") + + claim = ilk.claim( + text="Demo run: 3 permits granted, 3 denied (expiry, fault, e-stop).", + provenance="demo:scripts/demo_90s.py", + authority=lease.id, + review_after_days=7, + falsifier="re-run scripts/demo_90s.py and compare the transcript", + ) + say(f"CLAIM — provenance-stamped [{claim.status}]: {claim.text}") + + permits = list(ilk.ledger.read_series("permits")) + denials = list(ilk.ledger.read_series("denials")) + claims = list(ilk.ledger.read_series("claims")) + say( + f"AUDIT — ledger holds {len(permits)} permits, {len(denials)} denials, " + f"{len(claims)} claim. Nothing happened off the record." + ) + reasons = sorted({d.get("reason") for d in denials}) + say(f" denial reasons on record: {', '.join(reasons)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())