From 949feb7a328fb7248b4053b32731bfc4b34ca152 Mon Sep 17 00:00:00 2001 From: bigpickle Date: Mon, 5 Oct 2026 21:51:13 +0800 Subject: [PATCH] Expose checkpoint, revoke, reinstate and sweep on the CLI --- sigil.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/sigil.py b/sigil.py index 5f1c182..88b4db9 100755 --- a/sigil.py +++ b/sigil.py @@ -5,7 +5,12 @@ sigil.py keygen [pass] write an Ed25519 key (0600, encrypted if given) sigil.py enrol sigil.py challenge nonce to sign - sigil.py verify-chain check the audit log for tampering + sigil.py checkpoint [label] sign the current audit head + sigil.py verify-chain check the chain and its checkpoints + sigil.py revoke [--until UNIX] [--reason R] [--actor A] + --until omitted means permanent + sigil.py reinstate [actor] lift a revocation + sigil.py sweep prune stale nonces and rate rows sigil.py sign [pass] signature, base64 sigil.py verify agent session token sigil.py code [pass] one-time code, proving the key @@ -98,10 +103,71 @@ def main(argv) -> int: session["may_act_as_agent"] = may_act out(session) + elif cmd == "checkpoint": + # Signs the chain head with a key the database does not hold, which + # is what makes a later rewrite detectable. + if len(argv) < 4: + print("usage: sigil.py checkpoint [label] [pass]") + return 2 + private = keys.load_private(argv[2], argv[5] if len(argv) > 5 else None) + out(store.checkpoint(private, argv[3], + argv[4] if len(argv) > 4 else None)) + elif cmd == "verify-chain": - result = store.verify_chain() - out(result) - return 0 if result["ok"] else 1 + chain = store.verify_chain() + anchors = store.verify_checkpoints() + out({"chain": chain, "checkpoints": anchors}) + return 0 if chain["ok"] and anchors["ok"] else 1 + + elif cmd == "revoke": + # Positional optional args are a trap here: "revoke alice please" + # would otherwise try to parse "please" as a timestamp. Named flags + # are unambiguous, and a number only counts as a time if it parses. + allowed = {"until", "reason", "actor"} + flags, rest = {}, [] + i = 2 + while i < len(argv): + if argv[i].startswith("--"): + key = argv[i][2:] + if key not in allowed: + print(f"unknown flag --{key}; expected one of " + f"{', '.join('--' + a for a in sorted(allowed))}") + return 2 + flags[key] = argv[i + 1] if i + 1 < len(argv) else "" + i += 2 + else: + rest.append(argv[i]) + i += 1 + if len(rest) != 1: + print("usage: sigil.py revoke [--until N] [--reason R]" + " [--actor A]") + if rest: + # Refusing beats guessing which agent was meant. + print(f"expected exactly one agent name, got {rest}") + return 2 + until = None + if "until" in flags: + try: + until = int(flags["until"]) + except ValueError: + print(f"--until must be a unix timestamp, got {flags['until']!r}") + return 2 + killed = identity.revoke( + store, rest[0], until=until, + reason=flags.get("reason"), + actor=flags.get("actor", "system"), + ) + out({"agent": rest[0], "revoked": True, + "permanent": until is None, "sessions_revoked": killed}) + + + elif cmd == "reinstate": + identity.reinstate(store, argv[2], + argv[3] if len(argv) > 3 else "system") + out({"agent": argv[2], "revoked": False}) + + elif cmd == "sweep": + out(store.sweep()) elif cmd == "events": out(store.events(argv[2] if len(argv) > 2 else None,