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 merged: source.main moved to novox/mesh-sdk@54aff0a3d920
mesh/delivery-group group feat/b0-checking-program merged: every member is merged
mesh/check being carried to 1b073c23c2a5 on 54aff0a3d920 | from c1b50e99a639, hq 885099eebbbd, mesh-catalog 980ade933853, mesh-delivery 2d201f781ced
mesh/delivery merged: source.main moved to novox/mesh-sdk@54aff0a3d920
mesh/delivery-group group feat/b0-checking-program merged: every member is merged
mesh/check being carried to 1b073c23c2a5 on 54aff0a3d920 | from c1b50e99a639, hq 885099eebbbd, mesh-catalog 980ade933853, mesh-delivery 2d201f781ced
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:
@@ -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"})},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user