diff --git a/go/catalogue/checker_seat.go b/go/catalogue/checker_seat.go index a553dd5..ff589f5 100644 --- a/go/catalogue/checker_seat.go +++ b/go/catalogue/checker_seat.go @@ -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-", "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"})}, } } diff --git a/go/delivery/checker.go b/go/delivery/checker.go index e060e70..e2c43fc 100644 --- a/go/delivery/checker.go +++ b/go/delivery/checker.go @@ -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" diff --git a/go/delivery/stepasks_compose_test.go b/go/delivery/stepasks_compose_test.go index 36f28e1..3e179c8 100644 --- a/go/delivery/stepasks_compose_test.go +++ b/go/delivery/stepasks_compose_test.go @@ -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 diff --git a/go/delivery/stepasks_run.go b/go/delivery/stepasks_run.go index d828738..8eb1775 100644 --- a/go/delivery/stepasks_run.go +++ b/go/delivery/stepasks_run.go @@ -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 -// (`.`), 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:.`, 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:.`), 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 .", id, i.State) + case !runVerbOK(i.Verb): + return fmt.Errorf("delivery: the run ask %s serves its input on %q, which is no seat:. 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 {