Skip to content

operatorstack/interlock

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Interlock — The agent proposes. The broker publishes.

Give one protected effect to code the agent cannot influence.

Code-defined effect policy and brokered authority for coding-agent tooling.

A coding agent runs shell commands, spawns child processes, and edits files. You cannot reliably observe every filesystem write a python generate.py performs — the command string tells you nothing about what the process does. Interlock takes the honest position: the guarantee you can keep is who holds the authority to perform an effect, not whether you can see every attempt to perform it.

Interlock lets a separately compiled policy grant exclusive authority over one protected effect — "publish this staged file to this final path" — to a verified broker. The agent stays free to reason and to modify its own workspace; the one effect that matters is landed only by code the agent cannot influence at decision time.

Interlock decides. The broker performs. The agent never holds the authority.

Start here (no toolchain)

The fastest first run needs no Go toolchain. interlock init asks what you're protecting and writes a declarative policy you can test immediately:

interlock init                       # interactive: "What are you protecting?"
# → .interlock/policy.json, .interlock/tests.jsonl, .interlock/README.md
interlock test                       # re-decides every checked-in vector
PASS agent may edit src/**
PASS agent may not force-push main
PASS push to main requires release-main approval
PASS push to main with approval is allowed

RESULT: PASS  (4/4)

The PASS lines are live engine decisions against policy.json, not a static table — edit the policy and re-run to watch a line turn red. Pick a template non-interactively with interlock init --authoring json --template main-branch (templates: generated, main-branch, release-artifact, custom, empty).

When you outgrow the declarative form, interlock init --authoring go <dir> scaffolds a programmable Go policy — same canonical IR, same hash, same engine (see Policy as code below).

See it work

The fastest way to understand Interlock is to watch an agent's publish get denied and a verified publisher's succeed. Every command below is exactly what scripts/validate.sh exercises on a clean build, so the behavior is verified, not aspirational.

1. Compile a code-defined policy to canonical IR. A policy is a tiny Go module that emits its IR on stdout; compile builds and runs it:

interlock compile ./examples/exclusive-publish -o policy.json
interlock check policy.json      # validates the IR and prints its SHA-256 hash

The exclusive-publish policy says three things: the agent may work freely in repo://work/**, the agent may not write or publish repo://out/result.json, and a verified publisher may publish it — but only with a matching policy hash, a staged-candidate hash, and a released DeltaWire supervision receipt.

2. The agent's own publish is denied. No rule grants the agent publish authority over the artifact, and an explicit deny rule covers it:

echo '{"protocol":"interlock.effect.v1","request_id":"cli-1","run_id":"cli-run","actor":"agent","operation":"artifact.publish","resource":{"kind":"file","uri":"repo://out/result.json"}}' > deny-req.json
interlock decide policy.json deny-req.json
{
  "protocol": "interlock.decision.v1",
  "request_id": "cli-1",
  "policy_hash": "sha256:…",
  "outcome": "deny",
  "rule_id": "deny-agent-artifact",
  "reason": "the producing agent may not touch the protected artifact"
}

3. The broker publishes — on truthful, durable evidence. The broker hashes the real staged bytes, re-reads the upstream evidence envelope from disk, and atomically promotes the file to the protected path:

interlock publish policy.json pub.json
# → repo://out/result.json now holds the byte-exact staged candidate

Upstream evidence is a durable envelope ({schema, run_id, status, artifact_sha256}) the broker re-reads — it takes the receipt schema and status from the file, never from a caller-claimed struct field, and requires the envelope's artifact_sha256 to match the exact staged bytes it just hashed. This is hash-binding, not authenticity: it proves the evidence refers to these bytes for this run — defeating stale, copied, cross-run, or typo'd claims — but does not by itself prove who produced them (whoever can write the staged file can write the envelope). If the staged hash, policy hash, envelope status, run correlation, hash-binding, or expected target state is wrong, the publish fails closed and the target is left untouched. The engine compares claims; the broker is what makes claims truthful.

4. Every decision is a receipt you can replay. simulate turns a request stream into a hash-linked receipt chain; replay re-derives each decision and verifies the chain end to end:

interlock simulate policy.json reqs.jsonl <run_id> -o receipts.jsonl
interlock replay   policy.json reqs.jsonl receipts.jsonl

Replay fails closed on a changed policy, a missing or reordered link, mismatched evidence, a duplicate or out-of-order sequence, a cross-run receipt, or any committed field edited after the fact.

Does it work?

Every claim below is checked by interlock verify, which runs the release proof against the shipped runtime — the real engine, broker, and receipt chain, not a prose description of them. This section is generated by scripts/gen-proof.sh and diff-gated in CI: a hand-edited table fails the build, so a green row can never drift into marketing fiction. Run it yourself:

interlock verify              # PASS/FAIL per guarantee, RESULT, commit, protocol
interlock verify --format json
Claim Result Evidence
Canonical IR is deterministic PASS 4 frozen policy hashes
Engine decisions conform PASS positive + negative vectors
Broker publishes exact bytes PASS broker lifecycle suite
Evidence mismatch fails closed PASS negative broker vectors
Receipt chains detect mutation PASS replay suite
Pitot transports decisions PASS Controller round-trip

Verified at version dev, commit d35e1b8-dirty.

The interlock verify binary carries these checks with no toolchain required. A compatibility-lock corpus (conformance/compat/v0.1.0/) freezes today's policy hashes and decisions: future releases may add vocabulary, but a changed old hash or old decision is a breaking change and fails CI.

Why Interlock, and not Pitot?

Pitot is deliberately generic transport: it has no file-write action kind, and its hook event carries only a bare command string. Teaching Pitot to gate file writes by parsing shell text would be dishonest — it cannot see child-process writes — and would pollute a transport layer with a much larger policy concern. The responsibility split stays clean:

Component Owns
Interlock code-defined policy + brokered protected effects
Pitot transport of observable actions/decisions (policy-neutral)
DeltaWire producing and independently verifying one candidate artifact

An isolated agent proposes a staged candidate; Interlock validates evidence and its broker is the sole authority that lands the protected artifact

Two actors, one authority

An agent may write its workspace but is denied the protected artifact, while a broker holds exclusive publish authority and returns a receipt

Actor May do May not do
Agent reason, edit, write staging, propose a publish write or publish the protected artifact
Broker publish the artifact on correlated evidence anything the compiled policy does not allow

The separation is structural, not advisory: in strict mode the agent has no write authority over the protected path, so a denied agent cannot fall back to touching the file itself.

Policy as code: Go authors, canonical IR decides

Go is the authoring language. Canonical IR is the execution language.

Arbitrary Go may construct a policy — loops, helpers, tables, whole packages. Only framework predicates may decide a request. There are no arbitrary Go callbacks at decision time, because a callback would reintroduce hidden I/O, nondeterminism, and replay failure into the one component that must stay pure.

il.Policy("exclusive-publish.v1").
    Actor("agent").
    Actor("publisher").
    File("artifact", "repo://out/result.json").
    Tree("workspace", "repo://work/**").
    Allow("agent-workspace").By("agent").To(il.Write, il.Delete).On("workspace").
        Because("the agent may work freely in its own workspace").Add().
    Deny("deny-agent-artifact").By("agent").To(il.Write, il.Publish).On("artifact").
        Because("the producing agent may not touch the protected artifact").Add().
    Allow("allow-publisher").By("publisher").To(il.Publish).On("artifact").
        Requiring(
            il.PolicyHashMatch(),
            il.StagedHashMatch(),
            il.ReceiptStatus("deltawire.supervision.receipt.v1", "released"),
        ).
        Because("the verified publisher may publish a staged candidate").Add()

interlock compile builds and runs the module's entrypoint, which prints Build().Emit() — the canonical IR. Equivalent source produces identical IR and therefore an identical policy hash (proved by check-determinism.sh). The compiler rejects structurally invalid policies before any request is evaluated: duplicate ids, unknown actors/resources/operations, conflicting allow/deny on the same matcher, unreachable (shadowed) rules, and requirements on deny rules.

Arbitrary construction, canonical result — a helper can loop over generated-output globs and emit one deny + one evidence-gated allow per tree (examples/generated-file-protection); the Go is gone by decision time, leaving a plain, hashable decision table.

Effect model (fixed V1 vocabulary)

Deliberately small — it can grow later without an IR redesign.

Operations Resource kinds Outcomes
filesystem.read · write · delete file — exact URI allow
filesystem.rename_from · rename_to tree — prefix/glob (repo://…/**) deny (default)
process.execute · artifact.publish process — executable class require · fault

Allow rules may carry evidence predicates: policy_hash_match, staged_hash_match, target_hash_match, and receipt_status. Excluded from V1: databases, HTTP, Git, browsers, payments, secrets, deployments, messaging, and any custom decision-time predicate.

The enforcement boundary (what is and isn't guaranteed)

Interlock is explicit about its coverage. There are three modes; only one is the V1 guarantee.

  • Observed — a host-visible, truthful file action is intercepted before execution and routed for an allow/deny. Enforceable only where a native action faithfully describes the effect. Pitot emits no file-write action kind today, so this is a later "if available" fixture, not the guarantee.

  • Opaque processpython generate.py is observable only as process.execute. Interlock must not infer filesystem effects from the command string. It records the process event and nothing more.

  • Strict (the V1 guarantee) — the agent runs in an isolated workspace with no write authority over the protected artifact:

    agent → staged candidate (workspace-writable)
          → DeltaWire verifies the candidate
          → Interlock validates receipts + policy hash
          → BROKER atomically publishes to the protected path
    

The guarantee is not "Interlock sees every write." It is: the agent cannot modify the protected artifact, and only the broker can — and only for a request the compiled policy allows on truthful evidence. Remove isolation or the broker, and Interlock makes no claim about child-process writes. This honesty is enforced by the broker tests, not just asserted here.

See Enforcement model — transport is not authority for how this guarantee holds across local, agent, and cloud-sandbox environments.

Install

Prebuilt binary (no Go toolchain). The installer detects your platform, downloads a pinned release, verifies its SHA-256 checksum, installs the binary, and runs interlock doctor + the repository-policy demo:

# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/operatorstack/interlock/main/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/operatorstack/interlock/main/install.ps1 | iex

Prebuilt binaries are published for macOS (arm64/amd64), Linux (amd64/arm64), and Windows (amd64). Every release attaches checksums.txt, a per-archive SBOM, and signed build provenance you can verify with the GitHub CLI:

gh attestation verify interlock_<version>_<os>_<arch>.tar.gz --repo operatorstack/interlock

Prefer to pin a version or install elsewhere: set INTERLOCK_VERSION=v0.1.0 or INTERLOCK_INSTALL_DIR=$HOME/.local/bin before running the installer.

Homebrew (coming soon). brew install operatorstack/tap/interlock ships in a follow-up once the tap is published.

With a Go toolchain. Install the latest tagged release, or build from a checkout and inspect environment readiness:

go install github.com/operatorstack/interlock/cmd/interlock@latest
go build -o interlock ./cmd/interlock
interlock doctor
interlock doctor
  version         : dev
  policy protocol : interlock.policy.v1
  effect protocol : interlock.effect.v1
  receipt schema  : interlock.receipt.v1
  go toolchain    : available
  note            : init --authoring json and test need no toolchain

interlock version prints the release version, embedded commit, build date, and the three protocol/schema versions.

interlock init (bare) sets up the no-toolchain JSON policy described in Start here; interlock init --authoring go <dir> scaffolds a programmable Go module. The positional interlock init <dir> is a deprecated alias for --authoring go <dir> (it prints a migration note) and will be removed before 1.0 — a positional path should never silently pick an authoring mode.

Validation

scripts/validate.sh is the single entrypoint. It runs gofmt, go vet, the test suites (repeated, and under -race), the purity boundary check, IR determinism, embed-FS conformance, a static CGO_ENABLED=0 build, and a full CLI lifecycle (init [go|json] → test → compile → check → explain → decide → publish → simulate → replay) — including a negative case proving replay rejects a mismatched policy and that interlock test fails closed on a tampered policy.

bash scripts/validate.sh

Project layout

interlock/
├── spec/        typed constructors → validated in-memory policy
├── compiler/    spec → canonical IR, with structural diagnostics
├── ir/          canonical Effect Policy IR: stable bytes + stable SHA-256
├── engine/      pure Decide(policy, request) → decision (no I/O, deterministic)
├── protocol/    effect-request / decision wire types
├── receipt/     hash-linked decision receipts + replay verification
├── broker/      exclusive-publish: verify → stage → atomic promote → receipt
├── workspace/   isolation helpers for the strict-mode experiment
├── conformance/ embedded positive/negative fixtures + Go tests
├── examples/    exclusive-publish · generated-file-protection
└── cmd/interlock/  the reference Go executable

Design principles

  1. Authority, not interception. Guarantee who may perform an effect, not that every attempt is observed.
  2. Go authors; IR decides. Arbitrary construction, framework-only decisions — deterministic, hashable, replayable.
  3. Fail closed. Any mismatch in policy, evidence, hash, or correlation denies the effect and leaves the target untouched.
  4. The engine trusts claims; the broker makes them truthful. Purity lives in the engine; real hashing and receipt correlation live in the broker.
  5. Be honest about coverage. Never claim a guarantee the architecture cannot keep — command strings do not secure opaque process effects.

What Interlock does not decide

Interlock does not define whether work is correct, whether an artifact is safe to ship, or what "released" means. It enforces that if your compiled policy says a publish is allowed on given evidence, then only the broker performs it, and only when that evidence is truthful. Producing and verifying the candidate is DeltaWire's job; transporting observable actions is Pitot's.

The agent proposes. The broker publishes.

Status

This is a research lab. M1 (the standalone core) is complete: spec + compiler

  • canonical IR/hash, a pure decision engine, hash-linked receipts + replay, and the exclusive-publish broker with an isolation fixture — all green under scripts/validate.sh with zero external dependencies. M2 is complete: DeltaWire delegates its one protected effect — the final atomic promote — to this broker, importing Interlock as a library only (never into its compiler or evaluator); deltawire supervise --broker runs the whole stack through broker.Publish. The broker re-reads a durable, hash-bound upstream evidence envelope written before the promote — taking status from the file, never a caller-claimed string — and gates on DeltaWire's honest pre-promote status (release_authorized), since the run is released only because the broker publishes. M3 is complete: a second, non-DeltaWire tenant (the release-manifest example — different artifact, actors, and receipt schema) flows through the same broker.Publish and the same conformance suite, proving the core carries no DeltaWire-specific branches. The Pitot decision-transport leg is implemented as an ordinary Pitot subprocess Controller in a separate integration module (integrations/pitot): Pitot launches the controller and streams control.requested events to it, and the controller runs the pure engine.Decide and answers allow/deny — with no Pitot source change and without the interlock core ever importing Pitot. This transports/observes decisions and is not the enforcement guarantee (the broker remains that), so it is off the critical path. With M1–M3 green, extracting a public operatorstack/interlock is now a reasonable next step.

License

Apache-2.0

About

Code-defined effect policy and brokered authority for coding-agent tooling. The agent proposes; the broker publishes.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages