Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,91 @@ by the broker tests, not just asserted here.
See [Enforcement model — transport is not authority](docs/concepts/enforcement-model.md)
for how this guarantee holds across local, agent, and cloud-sandbox environments.

## Build on Interlock

Two supported ways to build against Interlock, both honest about the boundary
above: they **transport and shape** decisions; they never decide or enforce.

### Language clients (TypeScript, Python)

The `clients/` directory ships generated protocol types plus a hand-written,
parity-gated canonical encoder for **TypeScript** and **Python**. They carry
exactly three things — the wire **data types**, the **canonical encoder** that
produces the same bytes (and therefore the same SHA-256) as the Go source, and the
ergonomics to **shape** a request. They carry **no `decide`, no `publish`, no
broker**: enforcement stays the trusted Go executable, and a build-time guardrail
([`scripts/check-clients.sh`](https://github.com/operatorstack/intelligence-flow/blob/main/labs/21-interlock/scripts/check-clients.sh))
fails the build if a client ever grows an executable decision surface.

The two packages ship as **one version-locked release** — the same CI gate fails
the build if the TypeScript and Python package versions drift, so an external
builder can never pin mismatched clients against one controller.

```bash
# TypeScript (Node >= 22 runs .ts natively; zero runtime deps)
cd clients/typescript && npm install
node examples/decision-request.ts # resolve a resource by id → typed, canonically-encoded EffectRequest

# Python (>= 3.11; zero runtime deps — stdlib hashlib/json only)
cd clients/python && pip install -e .
python examples/decision-request.py
```

The types are **generated from the Go wire structs** (`go run ./clients/gen/main.go`)
and diff-gated in CI, so a DTO can never silently drift from the protocol. Each
client's canonical encoder is proven byte-for-byte against a **frozen Go corpus**
(`conformance/compat/v0.1.0/`): the parity example re-canonicalizes every frozen
policy and asserts the hash matches. That frozen-corpus parity is the real bar —
"a client compiles" is not enough to ship one.

What a client is *for*: hashing a policy identically across languages, and shaping
an `EffectRequest` to send to a decision controller. What it is **not** for:
deciding the request (that is the Go engine) or performing the effect (that is the
Go broker). Porting either into another language is an explicit non-goal.

### Run a decision over Pitot

To route a decision through a running host, Interlock ships an ordinary
[Pitot](https://github.com/operatorstack/pitot) subprocess Controller in a separate
module ([`integrations/pitot`](integrations/pitot)) — **no Pitot source change**,
and the interlock core never imports Pitot. Pitot launches the controller and
streams `control.requested` events; the controller runs the pure `engine.Decide`
and answers allow/deny, with Pitot's deadline and fail-closed
`on_timeout`/`on_unavailable` defaults layered on top.

```bash
# the integration is its own Go module (keeps the interlock core Pitot-free)
cd integrations/pitot && go build -o interlock-pitot-controller ./cmd/interlock-pitot-controller
interlock compile ./examples/exclusive-publish -o policy.json
```

Register the controller under the canonical request kind `interlock.effect`, then
issue a normal Pitot request whose `data` is an Interlock `EffectRequest`:

```yaml
controllers:
interlock.effect: # the request kind Pitot routes on
id: interlock-effect
command: [./interlock-pitot-controller, --id, interlock-effect, --policy, policy.json]
deadline_ms: 5000
on_timeout: deny
on_unavailable: deny
```

```bash
pitot run --config config.yaml --runtime rt.json &
pitot request interlock.effect --runtime rt.json --data '{"protocol":"interlock.effect.v1", ...}'
```

**One request kind, or fail closed.** Pitot routes a request to a controller by
matching the request kind against the config key. If the client's kind and the
config key disagree, Pitot finds no controller and denies **every** request
silently — an engine is never consulted. Interlock keeps a single canonical value
(`client.DefaultRequestKind = "interlock.effect"`) and a build-time drift guard
that fails CI the moment the constant, the README, or `scripts/validate.sh` move
apart, so a mismatch is diagnosed, never silent. This is transport plumbing, not
the enforcement guarantee — the broker remains that.

## Install

**Prebuilt binary (no Go toolchain).** The installer detects your platform,
Expand Down
4 changes: 2 additions & 2 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
".gitattributes": "3d38cf98dcc69b2af4941b76ed3f04e51273fdbd4fc54032e88cf3223b4b9198",
".goreleaser.yml": "ad08b869ad32652522cef430ba556a86d09a43ea53b5a3935425204dcedb8b33",
"LICENSE": "bd17d47aefaa7406616179288058001b6532881a2517254fe668d7e9c4965cfb",
"README.md": "39b4cc851a57c654132a73b84d594e61c8a568fd3aed26d86e503133dcdb84c8",
"README.md": "88a42ebb31047e70eddf7236ceb67c2cebfd8f8fe0a89740d4eff0a63d174d1a",
"assets/interlock-boundary.png": "9ae73098102d0589bf1bb912af6d39df4d1465eb93e8cea5b7e982e6fa79ef51",
"assets/interlock-boundary.svg": "0bbb5aacc90e9311aef732dc2075aa0896ec25d0789b6612a3d98922646a0f1e",
"assets/interlock-hero.png": "a30def06d493775458455ed891f1631200a61eafa54fa53d64474a3d340e2757",
Expand Down Expand Up @@ -125,7 +125,7 @@
"generator": "operatorstack/interlock:project-upstream",
"schema_version": 1,
"source": {
"commit": "44cdd76953435a74daac53faa5917bbb19deb014",
"commit": "dcf7cad047a1dfe1b210ee303c5cf96013cee204",
"path": "labs/21-interlock",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
Loading