A run's input is asked of a verb of the asker's seat, never read from its state (issue 1921, B0 review round 1)
mesh/delivery ready: it is merged on the Approve
mesh/delivery-group group feat/b0-checking-program checking: 4 of 4 member(s) ready
mesh/check checking 1b073c23c2a5 on c1b50e99a639 | novox/mesh-sdk#170 in group feat/b0-checking-program

The operator's answer: a build module reading the checker's state broke ADR 0596 §2. RunInput names the seat verb it is
served on (seat:<seat>.<verb>), RunInput.Read asks it and takes only the bytes named, and the seat
mesh-delivery-checker defines the verb input (RunInputAsk, RunInputAnswer).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-06 23:33:13 +01:00
parent bdf44cd8e0
commit 1b073c23c2
4 changed files with 100 additions and 19 deletions
+9 -2
View File
@@ -3,8 +3,8 @@ package catalogue
// CheckerSeat is the seat mesh-delivery-checker holds.
const CheckerSeat = "mesh-delivery-checker"
// checkerVerbs are the checker seat's verbs: ask a try, read one, end one by hand. Optional until the checker serves them
// everywhere.
// checkerVerbs are the checker seat's verbs: ask a try, read one, end one by hand, and answer a checking program's input.
// Optional until the checker serves them everywhere.
func checkerVerbs() []Verb {
return []Verb{
{Name: "try", Optional: true, Description: "Try a pushed head before its pull request is opened: the checker " +
@@ -28,5 +28,12 @@ func checkerVerbs() []Verb {
"id. A try that ended already is refused.",
Input: schema(map[string]string{"id": "the try's id, try-<digits>", "why": "why it is ended, in words"},
[]string{"id", "why"})},
{Name: "input", Optional: true, Description: "A checking program's input the checker keeps, by the sha256 of its " +
"exact bytes: the facts of a check its verdict is asked of, or the heads its seat list is asked of, as a run " +
"ask names it (its input's verb and digest). Answers the digest and the bytes, base64; a digest it keeps no " +
"input of is an error. Asked by the build module that runs the program, as its manifest's invokes grants it. " +
"Read-only.",
Input: schema(map[string]string{"digest": "the input's sha256, 64 lowercase hex digits, as the run ask names it"},
[]string{"digest"})},
}
}
+4
View File
@@ -34,6 +34,10 @@ const (
CheckerKeptHistory = 1
)
// CheckerInputVerb is the verb of the seat mesh-delivery-checker that serves a checking program's input by its digest
// (RunInput.Verb; asked RunInputAsk, answering RunInputAnswer; issue 1921, milestone 47/B0).
const CheckerInputVerb = "input"
// The test levels (ADR 0445 clause 2) and the states of a verdict.
const (
LevelUnit = "unit"
+34 -3
View File
@@ -2,6 +2,7 @@ package delivery
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
@@ -18,7 +19,7 @@ func composeAsk() StepAsk {
// runAsk is the checker's run of a checking program built at a mesh-delivery head, over two heads (issue 1921, B0).
func runAsk() StepAsk {
in, err := RunInputOf("mesh-delivery-checker.inputs", []byte(`{"v":1}`))
in, err := RunInputOf("seat:mesh-delivery-checker."+CheckerInputVerb, []byte(`{"v":1}`))
if err != nil {
panic(err)
}
@@ -82,7 +83,8 @@ func TestAComposeAskIsRefusedWhereItDoesNotBelong(t *testing.T) {
"with no head": func(a *StepAsk) { a.Run.Heads = nil },
"with a head twice": func(a *StepAsk) { a.Run.Heads = append(a.Run.Heads, a.Run.Heads[0]) },
"with a head at a branch": func(a *StepAsk) { a.Run.Heads[1].Commit = "main" },
"with an input in no state": func(a *StepAsk) { a.Run.Input.State = "inputs" },
"with an input on a state": func(a *StepAsk) { a.Run.Input.Verb = "mesh-delivery-checker.inputs" },
"with an input on one machine": func(a *StepAsk) { a.Run.Input.Verb = "seat:mesh-delivery-checker.input@ace" },
"with an input by no digest": func(a *StepAsk) { a.Run.Input.Digest = "d1" },
"with an input over its bound": func(a *StepAsk) { a.Run.Input.Size = RunInputBound + 1 },
"with an argument of the shell": func(a *StepAsk) { a.Run.Args = []string{"-x; rm"} },
@@ -95,7 +97,7 @@ func TestAComposeAskIsRefusedWhereItDoesNotBelong(t *testing.T) {
}
// The asker refuses an input over its bound before it asks, naming its size; the build module refuses bytes that
// are not the input named.
if _, err := RunInputOf("mesh-delivery-checker.inputs", make([]byte, RunInputBound+1)); err == nil || !strings.Contains(err.Error(), "262145 bytes") {
if _, err := RunInputOf("seat:mesh-delivery-checker.input", make([]byte, RunInputBound+1)); err == nil || !strings.Contains(err.Error(), "262145 bytes") {
t.Errorf("an input over its bound: %v", err)
}
in := runAsk().Run.Input
@@ -105,6 +107,35 @@ func TestAComposeAskIsRefusedWhereItDoesNotBelong(t *testing.T) {
if err := in.Verify([]byte(`{"v":2}`)); err == nil {
t.Error("another input read as the one named")
}
// The build module asks the verb the input names for its digest and takes only the bytes named (the operator's answer
// on B0: the input travels by a verb of the asker's seat, never a read of its state).
for name, c := range map[string]struct {
answer string
err error
want string
}{
"the bytes named": {answer: `{"digest":"` + in.Digest + `","input":"eyJ2IjoxfQ=="}`},
"other bytes": {answer: `{"digest":"` + in.Digest + `","input":"eyJ2IjoyfQ=="}`, want: "is not the one the ask names"},
"another digest": {answer: `{"digest":"` + strings.Repeat("0", 64) + `","input":"eyJ2IjoxfQ=="}`, want: "answered the input"},
"no input": {answer: `[1]`, want: "answered no input"},
"an error of the callee": {err: errors.New("keeps no input of that digest"), want: "keeps no input of that digest"},
} {
var asked string
var body any
got, err := in.Read(func(key string, b any) (json.RawMessage, error) {
asked, body = key, b
return json.RawMessage(c.answer), c.err
})
if asked != "seat:mesh-delivery-checker.input" || body != (RunInputAsk{Digest: in.Digest}) {
t.Errorf("%s: asked %s with %+v", name, asked, body)
}
switch {
case c.want == "" && (err != nil || string(got) != `{"v":1}`):
t.Errorf("%s: %q, %v", name, got, err)
case c.want != "" && (err == nil || !strings.Contains(err.Error(), c.want)):
t.Errorf("%s: %v, want %q", name, err, c.want)
}
}
}
// 1622/AC3, 1921: a composer is kept under one key per repository, commit, directory, package, toolchain and asked versions, so two asks of
+53 -14
View File
@@ -8,6 +8,8 @@ import (
"path"
"regexp"
"strings"
"git.novox.be/novox/mesh-sdk/go/calladdress"
)
// **A run ask has a program built from a change and run in the build seat's sandbox** (novox/hq ADR 0644, ADR 0656;
@@ -18,8 +20,10 @@ import (
// Input names, checks its digest and writes it to the file RunInputEnv names; then runs the program with Args, no
// network and no bus account (ADR 0448 §2, §4), and answers RunAnswer on its step key.
//
// The input is carried by reference, as a compose carries its manifests: the asker keeps it in a state of its own by its
// digest (RunInput), measured against RunInputBound before it asks, so a message on the bus never carries it (issue 1930).
// The input is carried by reference, as a compose carries its manifests: the asker keeps it by its digest, measured
// against RunInputBound before it asks, and serves it on a verb of its seat (RunInput.Verb, answered RunInputAnswer); the
// build module asks that verb as its manifest's `invokes` grants it, never reading the asker's state (ADR 0596 §2; the
// operator's answer on milestone 47/B0), so the ask on the stream never carries it (issue 1930).
// RunJob is a run ask's job.
type RunJob struct {
@@ -28,7 +32,7 @@ type RunJob struct {
Program RunProgram `json:"program"`
// Heads are the commits the program reads, each laid read-only at its repository's path; a repository at most once.
Heads []RunHead `json:"heads"`
// Input names the program's input, kept by the asker by its digest.
// Input names the program's input, kept by the asker by its digest and served on a verb of its seat.
Input RunInput `json:"input"`
// Args are the program's arguments (a checking program's `-seats` or `-verdict`), each a word or a flag.
Args []string `json:"args,omitempty"`
@@ -50,14 +54,27 @@ type RunHead struct {
Commit string `json:"commit"`
}
// RunInput is a run's input by reference: the state it is kept in, named as a manifest's `reads` names a state
// (`<module>.<state>`), its key (the sha256 of its bytes, 64 lowercase hex digits) and its size in bytes.
// RunInput is a run's input by reference: the seat verb the asker serves it on, named as a manifest's `invokes` names it
// (`seat:<seat>.<verb>`, asked of no one machine), its digest (the sha256 of its bytes, 64 lowercase hex digits) and its
// size in bytes. The verb is asked RunInputAsk and answers RunInputAnswer.
type RunInput struct {
State string `json:"state"`
Verb string `json:"verb"`
Digest string `json:"digest"`
Size int `json:"size"`
}
// RunInputAsk is what a verb a RunInput names is asked: the input's digest.
type RunInputAsk struct {
Digest string `json:"digest"`
}
// RunInputAnswer is that verb's answer: the digest asked and the input's exact bytes (base64 in JSON, so no encoding on
// the way changes a byte the digest covers).
type RunInputAnswer struct {
Digest string `json:"digest"`
Input []byte `json:"input"`
}
// RunInputBound is the most bytes a run's input may hold: the bus's value limit the design already uses (256 KiB), so
// the asker keeps it as one value (the lead's ruling on milestone 47/B0, 2026-11-06).
const RunInputBound = 256 << 10
@@ -68,14 +85,14 @@ const (
RunInputEnv = "MESH_RUN_INPUT"
)
// RunInputOf is the reference to body kept in state, refusing a body over RunInputBound, naming its size, so an asker
// refuses it before asking.
func RunInputOf(state string, body []byte) (RunInput, error) {
// RunInputOf is the reference to body served on verb (`seat:<seat>.<verb>`), refusing a body over RunInputBound, naming
// its size, so an asker refuses it before asking.
func RunInputOf(verb string, body []byte) (RunInput, error) {
if len(body) > RunInputBound {
return RunInput{}, fmt.Errorf("delivery: a run's input of %d bytes is over its bound of %d bytes", len(body), RunInputBound)
}
sum := sha256.Sum256(body)
in := RunInput{State: state, Digest: hex.EncodeToString(sum[:]), Size: len(body)}
in := RunInput{Verb: verb, Digest: hex.EncodeToString(sum[:]), Size: len(body)}
return in, in.check("")
}
@@ -83,21 +100,37 @@ func RunInputOf(state string, body []byte) (RunInput, error) {
func (i RunInput) Verify(body []byte) error {
sum := sha256.Sum256(body)
if len(body) != i.Size || hex.EncodeToString(sum[:]) != i.Digest {
return fmt.Errorf("delivery: the input read from %s under %s is not the one the ask names (%d bytes, %d named)", i.State, i.Digest, len(body), i.Size)
return fmt.Errorf("delivery: the input %s answered for %s is not the one the ask names (%d bytes, %d named)", i.Verb, i.Digest, len(body), i.Size)
}
return nil
}
// Read asks the verb i names for its input with ask (a tools bundle's stdio.Ask, a process's Conn.Call) and answers its
// bytes, refusing an answer for another digest or bytes that are not the input named.
func (i RunInput) Read(ask func(key string, body any) (json.RawMessage, error)) ([]byte, error) {
raw, err := ask(i.Verb, RunInputAsk{Digest: i.Digest})
if err != nil {
return nil, fmt.Errorf("delivery: asking %s for the input %s: %w", i.Verb, i.Digest, err)
}
var a RunInputAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("delivery: %s answered no input for %s: %w", i.Verb, i.Digest, err)
}
if a.Digest != i.Digest {
return nil, fmt.Errorf("delivery: %s answered the input %q, asked %s", i.Verb, a.Digest, i.Digest)
}
return a.Input, i.Verify(a.Input)
}
var (
runStateOK = regexp.MustCompile(`^[a-z0-9][-a-z0-9]*\.[a-z0-9][-a-z0-9]*$`)
runDigestOK = regexp.MustCompile(`^[0-9a-f]{64}$`)
runArgOK = regexp.MustCompile(`^[-=_./a-zA-Z0-9]+$`)
)
func (i RunInput) check(id string) error {
switch {
case !runStateOK.MatchString(i.State):
return fmt.Errorf("delivery: the run ask %s keeps its input in %q, which is no <module>.<state>", id, i.State)
case !runVerbOK(i.Verb):
return fmt.Errorf("delivery: the run ask %s serves its input on %q, which is no seat:<seat>.<verb> of no one machine", id, i.Verb)
case !runDigestOK.MatchString(i.Digest):
return fmt.Errorf("delivery: the run ask %s names its input by %q, which is no sha256 digest", id, i.Digest)
case i.Size <= 0 || i.Size > RunInputBound:
@@ -106,6 +139,12 @@ func (i RunInput) check(id string) error {
return nil
}
// runVerbOK says key names a seat's verb, asked of no one machine.
func runVerbOK(key string) bool {
a, err := calladdress.Parse(key)
return err == nil && a.Seat != "" && a.Node == ""
}
func (r RunJob) check(id string) error {
p := r.Program
if err := checkRepository(p.Repository); err != nil {