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.
89package broker
910
1011import (
@@ -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.
2529type 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.
6973func 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