Skip to content

Commit 497e8bb

Browse files
Sync Interlock from Intelligence Flow @ a7101e277884 (#4)
Co-authored-by: operator-stack-publisher[bot] <operator-stack-publisher[bot]@users.noreply.github.com>
1 parent c7659d2 commit 497e8bb

16 files changed

Lines changed: 496 additions & 68 deletions

File tree

README.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,26 @@ interlock decide policy.json deny-req.json
6363
}
6464
```
6565

66-
**3. The broker publishes — on truthful, correlated evidence.** The broker hashes
67-
the real staged bytes, correlates the real receipt, and atomically promotes the
68-
file to the protected path:
66+
**3. The broker publishes — on truthful, durable evidence.** The broker hashes the
67+
real staged bytes, re-reads the upstream evidence envelope from disk, and
68+
atomically promotes the file to the protected path:
6969

7070
```bash
7171
interlock publish policy.json pub.json
7272
# → repo://out/result.json now holds the byte-exact staged candidate
7373
```
7474

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

8087
**4. Every decision is a receipt you can replay.** `simulate` turns a request
8188
stream into a hash-linked receipt chain; `replay` re-derives each decision and
@@ -291,13 +298,22 @@ the exclusive-publish broker with an isolation fixture — all green under
291298
DeltaWire delegates its one protected effect — the final atomic promote — to this
292299
broker, importing Interlock as a library only (never into its compiler or
293300
evaluator); `deltawire supervise --broker` runs the whole stack through
294-
`broker.Publish`. **M3** is complete: a second, non-DeltaWire tenant (the
301+
`broker.Publish`. The broker re-reads a durable, hash-bound upstream evidence
302+
envelope written before the promote — taking status from the file, never a
303+
caller-claimed string — and gates on DeltaWire's honest pre-promote status
304+
(`release_authorized`), since the run is released only *because* the broker
305+
publishes. **M3** is complete: a second, non-DeltaWire tenant (the
295306
[`release-manifest`](interlock/examples/release-manifest) example — different
296307
artifact, actors, and receipt schema) flows through the *same* `broker.Publish`
297308
and the same conformance suite, proving the core carries no DeltaWire-specific
298-
branches. The **Pitot decision-transport leg is deferred** until Pitot grows an
299-
in-process plugin hook — it transports/observes decisions and is not the
300-
guarantee, so it is off the critical path. With M1–M3 green, extracting a public
309+
branches. The **Pitot decision-transport leg** is implemented as an ordinary
310+
Pitot subprocess Controller in a separate integration module
311+
([`integrations/pitot`](integrations/pitot)): Pitot launches the controller and
312+
streams `control.requested` events to it, and the controller runs the pure
313+
`engine.Decide` and answers allow/deny — with **no Pitot source change** and
314+
without the interlock core ever importing Pitot. This transports/observes
315+
decisions and is **not** the enforcement guarantee (the broker remains that), so
316+
it is off the critical path. With M1–M3 green, extracting a public
301317
`operatorstack/interlock` is now a reasonable next step.
302318

303319
## License

UPSTREAM.json

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
{
22
"files": {
3-
"README.md": "63493cb1b44234c5509d46c8c82201af43f56240eb85cc866316b02c3ad310c8",
3+
"README.md": "3be5bd5d06d62c659790c429a8fd97eb708e2b2dba79c23e522d385ebc19f3d8",
44
"assets/interlock-boundary.png": "9ae73098102d0589bf1bb912af6d39df4d1465eb93e8cea5b7e982e6fa79ef51",
55
"assets/interlock-boundary.svg": "0bbb5aacc90e9311aef732dc2075aa0896ec25d0789b6612a3d98922646a0f1e",
66
"assets/interlock-hero.png": "a30def06d493775458455ed891f1631200a61eafa54fa53d64474a3d340e2757",
77
"assets/interlock-hero.svg": "c8571c924fdddcf955a955be799ca0f06906abb07612ded14c7457ebd51f4f90",
88
"assets/interlock-mark.svg": "3e3a9ee57f4a7791494a84cf77406ebbea05a98c8d85c526494936bd7f79b6a0",
99
"assets/interlock-two-roles.png": "d11866f96950c2e9bd14380e6a147dfe5f153e703786695ac4bcf09741843d98",
1010
"assets/interlock-two-roles.svg": "9fd03b2654226824b733617d1069ae831739141e2ee0ec6b67d4c4749689751e",
11-
"broker/broker.go": "2fd2ad234603d39f125074fd9565da9e4f108f042f7def13e23b87da09b8bc09",
12-
"broker/broker_test.go": "f595e3230f87e4abb363ea005dc2966a01abdd8aef7e02bc2328cf1632e3bfb3",
13-
"broker/generality_test.go": "228e90b41bfe04fe743c35c58317b6f1ba9e17559f12c2257a206fc4e56c410c",
11+
"broker/broker.go": "c4250c3f2afce78e060b31fc165cc1d37fb181cf3fce07392b90ea98c2b548f0",
12+
"broker/broker_test.go": "5ccf4f6aad1ae846ce405f92fc6c75728e4adec49eaedcdd45f76e6b91dc0a6b",
13+
"broker/envelope.go": "a1f6e2333c45f5f15ce1b1d58cd29589cbc35abcdb0f93ea01c56060f7dcafcf",
14+
"broker/generality_test.go": "14b83f8dd8e25562cd5d099e7d0b9285d15d2874cddd42f3901303c0553ee596",
1415
"cmd/interlock/main.go": "6cae6aa4f32cdc942e239b96fc5e39ce87760d5d8695fc0438f55f5682daf954",
15-
"compiler/compiler.go": "3ade5b7820b19173e3500864d1e3c5385592372a9d8f82cd57cb3a3c4141e18d",
16+
"compiler/compiler.go": "50487aaa0c24b3c0d041ab5b9a410fa672de7eb77b1cd3df1ec97cd4cf2bccba",
1617
"compiler/compiler_test.go": "6f80886aa1bfdf79bca014ad47c7b48d2a6bc24c86863afa4ebeb93658a790aa",
17-
"conformance/conformance.go": "1851053b58662677c4f6638650fe0bbe2947313152518eff6ae8368bd14257d8",
18-
"conformance/conformance_test.go": "dd2afe735a62ff0b7f84fe6bfca6935cfa679a8700d836a93f1caa565281721b",
19-
"conformance/fixtures/negative.jsonl": "b5b01c61494416b47b4ec80ba876f7fd1e43ca651a1ef6263fadbd84476d0859",
20-
"conformance/fixtures/positive.jsonl": "b238faa74bd8b9adfe6a55c97ad7c77ea875e3b4fef6da5372bd570aada3716b",
18+
"conformance/conformance.go": "0e76bab423cad56f1a199fbf595ced0f5760af2be11f5f12f6896e1d298f2c80",
19+
"conformance/conformance_test.go": "270f590b4d1a90f5578c3916246e542b79a8b8690aa1b6f8af5ab5a218616413",
20+
"conformance/fixtures/hashes.jsonl": "7d33aea8dd5cc961d69f40cda0bf12353552a9e54d39a4e3c0b40e7e53dc2273",
21+
"conformance/fixtures/negative.jsonl": "0ef6dbe7df1879564ddb609f4823c440fd43514b92b5a1724babcdc8880a65cc",
22+
"conformance/fixtures/positive.jsonl": "0186ae490e1f9cb9ccfadb8f04dd88e1c6e75b1b578918c84314a6305bfc5e64",
2123
"doc.go": "ffda943422fc0104acff178f17f096df5d9d0e9065e598aa0d817c457edfb198",
22-
"engine/engine.go": "1fa05917590b49519e39a451854b4d2a208b992a31ef3c7836e8bc69465ad179",
24+
"engine/engine.go": "8ee1d012bbf9661288507056c7a015ab9d46fb16e9596d4717b38f15af748b8f",
2325
"engine/engine_test.go": "0291ca081684cd744dec98089a383e1a52093f1c5125a88623dab06d504f73a8",
2426
"examples/exclusive-publish/policy.go": "d18df2ada6684bc1815d19b4918b7ba8b7b584d7fd75e9fc7f8c0562299cd94c",
2527
"examples/generated-file-protection/policy.go": "0a9a5b33990adb770c74382bbab7e618582fd8c2ce6632deba5ca9cc652696ee",
2628
"examples/release-manifest/policy.go": "d2f005addc2842f0ec235feb64315776aa936aa2e49bb82325867b3c9bcb691c",
29+
"examples/repository-policy/policy.go": "8c7fe12c487c1369d5ce8ae24e647e050557cd2bf29c56d4010b782603debbd1",
2730
"go.mod": "a9a846b064eac2e330c18198dae19044e438bff31dcb26d928cdb2f9b5cae3e1",
28-
"interlock.go": "637e38efe0229567c82eacdfc0dceb793c512dac8defbc633d9c2d4906882aef",
31+
"interlock.go": "90243e4c4c0f056cd0b2d8981f299449231b502f2f9e6108923bcbda1aeefd06",
2932
"interlock_test.go": "06b56780f7ae90fe12b055bf426aab8b87ed4fe174698a49d729bdc6484299e2",
30-
"ir/ir.go": "b8743c39dcce5569d95433515812fbc7c8a6cbc0d6a73074ba527e487fcb8625",
33+
"ir/ir.go": "5cf039998f609e9f13f770bc518ba9127389320c9f5a71dd8ae3e5240762e718",
3134
"ir/ir_test.go": "64a962436ee5cab261093f8cb455ad92075683a7ef58997dac1e022e09d1a46a",
3235
"protocol/protocol.go": "1d9cc6ba1d59635e9c88ab9a20058491c78d0024e5e87fd25ec87628271c728e",
3336
"receipt/receipt.go": "8a21b9054b599965221e08fe158887759473d2f41d28dd3df42a1ffe1c55cb6e",
@@ -38,7 +41,7 @@
3841
"generator": "operatorstack/interlock:project-upstream",
3942
"schema_version": 1,
4043
"source": {
41-
"commit": "55e700131357f88c80f8c7fd3eedacc7b52c00ed",
44+
"commit": "a7101e277884b6356219145eb90ac9b5a0ea97e9",
4245
"path": "labs/21-interlock",
4346
"repository": "operatorstack/intelligence-flow"
4447
}

broker/broker.go

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// candidate file to a final target path, but only for a request the policy
33
// engine allows on truthful, broker-produced evidence. This is where the honest
44
// guarantee lives: the engine decides on claims, but the broker verifies the
5-
// claims against reality (the actual staged bytes, the actual upstream receipts,
6-
// the live policy hash) before it ever touches the target. Any mismatch fails
7-
// closed — the target is never modified.
5+
// claims against reality (the actual staged bytes, durable upstream evidence
6+
// re-read from disk and hash-bound to those bytes, the live policy hash) before
7+
// it ever touches the target. Any mismatch fails closed — the target is never
8+
// modified. Upstream evidence is hash-bound, not authenticated; see envelope.go.
89
package broker
910

1011
import (
@@ -19,13 +20,14 @@ import (
1920
"github.com/operatorstack/interlock/receipt"
2021
)
2122

22-
// UpstreamReceipt is an evidence receipt handed to the broker (e.g. a DeltaWire
23-
// supervision receipt). The broker correlates it by run and turns it into
24-
// truthful engine evidence; it does not trust the producer to have done so.
23+
// UpstreamReceipt points the broker at a durable evidence envelope on disk (see
24+
// envelope.go). The broker re-reads the envelope, verifies it correlates to the
25+
// run and is hash-bound to the staged bytes, and takes the receipt schema and
26+
// status FROM THE FILE — never from caller-supplied struct fields. This is why
27+
// the type carries only a Path: there is no inline status a caller could set and
28+
// have trusted.
2529
type UpstreamReceipt struct {
26-
Schema string `json:"schema"`
27-
Status string `json:"status"`
28-
RunID string `json:"run_id"`
30+
Path string `json:"path"`
2931
}
3032

3133
// PublishRequest is a request to publish a staged candidate to a target.
@@ -62,10 +64,12 @@ var ErrDenied = errors.New("interlock/broker: publish denied by policy")
6264
// promotes the staged file to the target. chain records the decision receipt.
6365
//
6466
// The broker builds a truthful EffectRequest: it hashes the real staged bytes,
65-
// stamps the live policy hash, correlates upstream receipts by run, and only
66-
// then calls the engine. It fails closed if the policy hash cannot be computed,
67-
// the staged file is unreadable, the target's prior state is not as expected, or
68-
// the engine returns anything other than allow.
67+
// stamps the live policy hash, re-reads each upstream evidence envelope from disk
68+
// (verifying run correlation and hash-binding to those bytes), and only then
69+
// calls the engine. It fails closed if the policy hash cannot be computed, the
70+
// staged file is unreadable, an upstream receipt lacks a durable envelope path,
71+
// an envelope is missing/malformed/uncorrelated/not hash-bound, the target's
72+
// prior state is not as expected, or the engine returns anything other than allow.
6973
func Publish(policy ir.Policy, req PublishRequest, chain *receipt.Chain) (Result, error) {
7074
livePolicyHash, err := policy.Hash()
7175
if err != nil {
@@ -85,20 +89,35 @@ func Publish(policy ir.Policy, req PublishRequest, chain *receipt.Chain) (Result
8589
return Result{}, err
8690
}
8791

88-
// Correlate upstream receipts by run and lower them to truthful evidence.
92+
// Re-read each upstream evidence envelope from disk and lower it to truthful
93+
// engine evidence. The schema and status come from the file, not the caller;
94+
// the envelope must correlate to this run and be hash-bound to the staged bytes
95+
// we just hashed. The envelope's own content hash is pinned as an audit-only
96+
// evidence entry: the engine ignores unknown evidence kinds and never inspects
97+
// Value, so this is committed to the receipt yet can never satisfy a rule.
8998
evidence := []protocol.Evidence{
9099
{Kind: ir.ReqStagedHashMatch, Value: stagedHash},
91100
{Kind: ir.ReqTargetHashMatch, Value: req.ExpectedTargetHash},
92101
}
93102
for _, u := range req.Upstream {
94-
if u.RunID != req.RunID {
95-
return Result{}, fmt.Errorf("interlock/broker: upstream receipt run %q does not match request run %q", u.RunID, req.RunID)
103+
if u.Path == "" {
104+
return Result{}, fmt.Errorf("interlock/broker: upstream receipt requires a durable envelope path")
96105
}
97-
evidence = append(evidence, protocol.Evidence{
98-
Kind: ir.ReqReceiptStatus,
99-
Receipt: u.Schema,
100-
Status: u.Status,
101-
})
106+
env, envelopeHash, eerr := readUpstreamEnvelope(u.Path, req.RunID, stagedHash)
107+
if eerr != nil {
108+
return Result{}, eerr
109+
}
110+
evidence = append(evidence,
111+
protocol.Evidence{
112+
Kind: ir.ReqReceiptStatus,
113+
Receipt: env.Schema,
114+
Status: env.Status,
115+
},
116+
protocol.Evidence{
117+
Kind: ir.RequirementKind("upstream_envelope"),
118+
Value: envelopeHash,
119+
},
120+
)
102121
}
103122

104123
effReq := protocol.EffectRequest{

0 commit comments

Comments
 (0)