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)
mesh/delivery ready: it is merged on the Approve
mesh/check checking c8bcb2cdf567 on 980ade933853 | novox/mesh-catalog#622 in group feat/b0-checking-program
mesh/delivery-group group feat/b0-checking-program checking: 3 of 3 member(s) ready

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
This commit is contained in:
2026-11-06 23:43:05 +01:00
parent 608c4d4be6
commit c8bcb2cdf5
7 changed files with 93 additions and 37 deletions
@@ -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
@@ -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.
@@ -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
+10 -15
View File
@@ -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 `<owner>/<name>`; 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 `<owner>/<name>`; 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.
@@ -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 <node>.<ask id>: 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 <node>.<ask id>: 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")
}
}
@@ -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,
+3 -1
View File
@@ -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",