From c8bcb2cdf5672239ab37e3b0cce1619403b83c04 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 23:43:05 +0100 Subject: [PATCH] mesh-build-go asks a run's input of the seat verb the ask names, and reads no checker state (issue 1921, B0 review round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator's answer: reading mesh-delivery-checker.inputs broke ADR 0596 §2. The manifest drops that read and invokes seat:mesh-delivery-checker.input; the run step asks the verb through the runtime (stdio.Ask) and takes only the bytes the ask names (delivery.RunInput.Read). Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../cmd/mesh-build-go/compose_test.go | 10 ++++- .../mesh-build-go/cmd/mesh-build-go/main.go | 3 +- .../mesh-build-go/cmd/mesh-build-go/module.go | 5 ++- .../mesh-build-go/cmd/mesh-build-go/run.go | 25 +++++------ .../cmd/mesh-build-go/run_integration_test.go | 42 +++++++++++++++++-- .../cmd/mesh-build-go/run_test.go | 41 ++++++++++++------ modules/mesh-build-go/module.json | 4 +- 7 files changed, 93 insertions(+), 37 deletions(-) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go b/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go index 61cdf6f30..6520a50ad 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/compose_test.go @@ -150,11 +150,17 @@ func TestAComposeAskFailsLoudly(t *testing.T) { } } -// 1622: the module reads the delivery indexer's manifests, which a compose ask names by digest. +// 1622: the module reads the delivery indexer's manifests, which a compose ask names by digest. 1921 (B0): a run's input +// is asked of the checker seat's verb input, as the manifest invokes it, and no state of the checker's inputs is read +// (ADR 0596 §2). func TestTheModuleReadsTheIndexersManifests(t *testing.T) { - if m := readManifest(t); !slices.Contains(m.Reads, ManifestsState) { + m := readManifest(t) + if !slices.Contains(m.Reads, ManifestsState) { t.Errorf("reads = %v, lacks %s", m.Reads, ManifestsState) } + if !slices.Contains(m.Invokes, runInputVerb) || slices.Contains(m.Reads, "mesh-delivery-checker.inputs") { + t.Errorf("invokes = %v, reads = %v: want %s invoked and no inputs state read", m.Invokes, m.Reads, runInputVerb) + } } // ADR 0593 §7 (C9): a compose ask naming the node-engine's repository builds its composer command in the Go module at the diff --git a/modules/mesh-build-go/cmd/mesh-build-go/main.go b/modules/mesh-build-go/cmd/mesh-build-go/main.go index b6189674b..906b8081c 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/main.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/main.go @@ -7,7 +7,6 @@ package main import ( "context" - "encoding/json" "fmt" "os" "os/user" @@ -59,7 +58,7 @@ func main() { m.Settings = m.Home + "/settings.json" m.ReadKept = readOf(stdio.State(KeptState)).Get m.ReadManifest = readOf(stdio.State(ManifestsState)).Get - m.ReadInput = func(state, key string) (json.RawMessage, error) { return readOf(stdio.State(state)).Get(key) } + m.AskInput = stdio.Ask m.Probe = ProbeGo(m.Home, m.Cache) m.Bubblewrap = ProbeBubblewrap // Outside a launch by the runtime — a build, a check — there is no node and no state to write. diff --git a/modules/mesh-build-go/cmd/mesh-build-go/module.go b/modules/mesh-build-go/cmd/mesh-build-go/module.go index b8dc764ac..d3ad804ea 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/module.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/module.go @@ -93,8 +93,9 @@ type Module struct { // ReadManifest reads a key of the delivery indexer's manifests state (ManifestsState), which a compose ask's request // names its manifests by (compose.go); GoModCache, when set, is the Go module cache in place of Cache's (a test). ReadManifest func(key string) (json.RawMessage, error) - // ReadInput reads a key of a state the module reads (module.json), where a run ask's input names its digest. - ReadInput func(state, key string) (json.RawMessage, error) + // AskInput asks a seat verb the module invokes (module.json) as the runtime calls it (stdio.Ask): the verb a run ask's + // input names, answering the input by its digest (delivery.RunInput.Read). + AskInput func(key string, body any) (json.RawMessage, error) // PlaceHead, when set, lays a run's head at a directory instead of fetching it (a test). PlaceHead func(ctx context.Context, h delivery.RunHead, dir string) error GoModCache string diff --git a/modules/mesh-build-go/cmd/mesh-build-go/run.go b/modules/mesh-build-go/cmd/mesh-build-go/run.go index 375394b67..13270f6bd 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/run.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/run.go @@ -3,8 +3,9 @@ package main // **A run ask runs a program built from a change, over heads it reads** (novox/hq ADR 0644, ADR 0656; issue 1921): the // checking program of a group's check. The module builds the program at the commit the ask names, once per // delivery.RunKey, as a compose ask builds a composer (programOf); fetches every head the ask names and lays each read-only -// under the directory MESH_RUN_HEADS names, at `/`; reads the input the ask names by its digest from the -// state the ask names and writes it to the file MESH_RUN_INPUT names; and runs the program shut off with the ask's +// under the directory MESH_RUN_HEADS names, at `/`; asks the seat verb the ask's input names for it by its +// digest (as module.json invokes it; never a read of the asker's state, ADR 0596 §2) and writes it to the file +// MESH_RUN_INPUT names; and runs the program shut off with the ask's // arguments: no network, none of the account. The answer is delivery.RunAnswer: the program's exit, the last line of its // standard output and the first line of its standard error. A program that fails is an answer, not a fault of the step. @@ -54,23 +55,17 @@ func (m *Module) run(ctx context.Context, fetch buildstep.StepFunc, a delivery.S return ans, nil } -// inputOf is the input a run names, read from the state the ask names (module.json reads it) and checked against its -// digest and size. +// inputOf is the input a run names, asked of the seat verb the ask names (module.json invokes it) and checked against +// its digest and size: bytes that are not the input named refuse the ask. func (m *Module) inputOf(in delivery.RunInput) ([]byte, error) { - if m.ReadInput == nil { - return nil, errors.New("this module reads no inputs state") + if m.AskInput == nil { + return nil, errors.New("this module asks no seat verb for a run's input") } - raw, err := m.ReadInput(in.State, in.Digest) - if err != nil { - return nil, err - } - if raw == nil { - return nil, fmt.Errorf("%s keeps no input of digest %s", in.State, in.Digest) - } - if err := in.Verify(raw); err != nil { + raw, err := in.Read(m.AskInput) + if err != nil && raw != nil { return nil, buildstep.Refuse("%v", err) } - return raw, nil + return raw, err } // placeHead lays one head's commit as a plain directory at dir, with no symlink in it; a test sets its own. diff --git a/modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go b/modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go index 77775eb1e..14d4f8fbf 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/run_integration_test.go @@ -5,6 +5,7 @@ package main import ( "context" "encoding/json" + "errors" "net" "os" "path/filepath" @@ -21,8 +22,9 @@ import ( ) // 1921/C3: a run ask put on the Go build seat's check stream is pulled by the module started whole on the real bus and -// answered on its step key .: the program, run shut off over its heads and its input (read from the -// state the ask names by digest), answers its last line and exit; a program cannot write under its heads, and one +// answered on its step key .: the program, run shut off over its heads and its input (asked by digest of the +// seat verb the ask names, which a fake checker answers on the bus as the checker's verb input does), answers its last +// line and exit; a program cannot write under its heads, and one // that dials the machine's network is shut off, and the answer says it failed. func TestARunAskIsAnsweredOnTheBusAndItsProgramIsShutOff(t *testing.T) { needTools(t, "the shut-off run", "bash", "bwrap") @@ -59,7 +61,33 @@ func TestARunAskIsAnsweredOnTheBusAndItsProgramIsShutOff(t *testing.T) { } m.PlaceHead = func(_ context.Context, h delivery.RunHead, dir string) error { return os.MkdirAll(dir, 0o700) } body := `{"v":1}` - m.ReadInput = func(string, string) (json.RawMessage, error) { return json.RawMessage(body), nil } + // The checker's verb input on the bus, and the module asking it as the runtime calls a seat verb it invokes. + asked := make(chan string, 4) + sub, err := nc.Subscribe("mesh.seat.mesh-delivery-checker.tool.input", func(msg *nats.Msg) { + var ask delivery.RunInputAsk + _ = json.Unmarshal(msg.Data, &ask) + asked <- ask.Digest + reply, _ := json.Marshal(map[string]any{"result": delivery.RunInputAnswer{Digest: ask.Digest, Input: []byte(body)}}) + _ = msg.Respond(reply) + }) + if err != nil { + t.Fatal(err) + } + defer sub.Unsubscribe() + m.AskInput = func(key string, b any) (json.RawMessage, error) { + if key != runInputVerb { + return nil, errors.New("asked " + key) + } + data, _ := json.Marshal(b) + msg, err := nc.Request("mesh.seat.mesh-delivery-checker.tool.input", data, 5*time.Second) + if err != nil { + return nil, err + } + var r struct { + Result json.RawMessage `json:"result"` + } + return r.Result, json.Unmarshal(msg.Data, &r) + } a := runAskOf(t, body) key := delivery.RunKey(a.Run.Program, delivery.ToolchainUsed{Version: "go1.26.0", Recipe: m.Recipe.Version()}, nil) @@ -102,4 +130,12 @@ func TestARunAskIsAnsweredOnTheBusAndItsProgramIsShutOff(t *testing.T) { if ans.Key != key || ans.Exit != 7 || strings.Contains(string(ans.Output), "dialed") || string(ans.Output) != `"start"` { t.Fatalf("the answer of a program that dials out = %+v output %s", ans, ans.Output) } + select { + case d := <-asked: + if d != a.Run.Input.Digest { + t.Errorf("the verb input was asked %s, want %s", d, a.Run.Input.Digest) + } + default: + t.Error("the run's input was not asked of the checker's verb input") + } } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/run_test.go b/modules/mesh-build-go/cmd/mesh-build-go/run_test.go index 961991794..31fd4c5a1 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/run_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/run_test.go @@ -2,13 +2,18 @@ package main import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" + "errors" + "fmt" "os" "path/filepath" "strings" "testing" "time" + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -17,12 +22,23 @@ import ( const runCommit = "5555555555555555555555555555555555555555" -// runInputs is the state a run ask's input is kept in, as the checker names it in the ask. -const runInputs = "mesh-delivery-checker.inputs" +// runInputVerb is the seat verb a run ask's input is served on, as the checker names it in the ask. +const runInputVerb = "seat:mesh-delivery-checker.input" + +// answersInput is a seat verb serving body by its digest, as the checker's verb input answers (delivery.RunInputAnswer). +func answersInput(body string) func(key string, b any) (json.RawMessage, error) { + return func(key string, b any) (json.RawMessage, error) { + ask, _ := b.(delivery.RunInputAsk) + if sum := sha256.Sum256([]byte(body)); key != runInputVerb || ask.Digest != hex.EncodeToString(sum[:]) { + return nil, fmt.Errorf("the checker keeps no input %s", ask.Digest) + } + return json.Marshal(delivery.RunInputAnswer{Digest: ask.Digest, Input: []byte(body)}) + } +} func runAskOf(t *testing.T, body string) delivery.StepAsk { t.Helper() - in, err := delivery.RunInputOf(runInputs, []byte(body)) + in, err := delivery.RunInputOf(runInputVerb, []byte(body)) if err != nil { t.Fatal(err) } @@ -39,12 +55,7 @@ func runAskOf(t *testing.T, body string) delivery.StepAsk { func TestAKeptProgramIsRunOverItsHeadsAndInput(t *testing.T) { m := composeModule(t) body := `{"v":1,"case":"x"}` - m.ReadInput = func(state, key string) (json.RawMessage, error) { - if state != runInputs || key != runAskOf(t, body).Run.Input.Digest { - return nil, nil - } - return json.RawMessage(body), nil - } + m.AskInput = answersInput(body) m.PlaceHead = func(_ context.Context, h delivery.RunHead, dir string) error { return os.MkdirAll(dir, 0o700) } @@ -103,11 +114,17 @@ func TestAKeptProgramIsRunOverItsHeadsAndInput(t *testing.T) { t.Errorf("an answer over the bound: %v", err) } m.Inside, m.Run = true, StepRunner(t.TempDir()) - // An input other than the one named is refused, naming it. - m.ReadInput = func(string, string) (json.RawMessage, error) { return json.RawMessage(`{"other":1}`), nil } - if _, err := m.run(context.Background(), fetch, a); err == nil || !strings.Contains(err.Error(), "not the one the ask names") { + // An input other than the one named is refused, naming it; one the verb does not answer fails the step, saying why. + m.AskInput = func(_ string, b any) (json.RawMessage, error) { + return json.Marshal(delivery.RunInputAnswer{Digest: b.(delivery.RunInputAsk).Digest, Input: []byte(`{"other":1}`)}) + } + if _, err := m.run(context.Background(), fetch, a); err == nil || !strings.Contains(err.Error(), "not the one the ask names") || !errors.As(err, new(*buildstep.Refusal)) { t.Errorf("a wrong input: %v", err) } + m.AskInput = answersInput(`{"v":2}`) + if _, err := m.run(context.Background(), fetch, a); err == nil || !strings.Contains(err.Error(), "keeps no input") { + t.Errorf("an input the verb does not answer: %v", err) + } } // 1921/C2 (F2): a compose ask's seats reach the composer: the module decodes and fills the request and writes it again, diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index 3281af8d3..3e30a5cb1 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -20,10 +20,12 @@ } ], "reads": [ - "mesh-delivery-checker.inputs", "mesh-delivery-checker.kept", "mesh-delivery-indexer.manifests" ], + "invokes": [ + "seat:mesh-delivery-checker.input" + ], "config": { "capacity": { "type": "number",