The domain SDK carries the run ask's rules and the checker's client asks a program's input (issue 1921, milestone 47/G D2)
B0's mesh-sdk go/delivery run lines move to their registry homes: the run job, the one-job rule, the Go check stream alone, the input's bound, digest and key in sdk/buildstep; AskWhat's run case in sdk/buildrun; the ask of the verb input and the check of a verdict's judged-by in meshdeliverycheckerclient. B0's tests come in as rows of the existing tables. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -77,6 +77,32 @@ func checkComposition(c *checker.VerdictsByGroupHeadsValueComposition) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkJudgedBy refuses a checking program named at no repository, full commit and package, kept under no program key,
|
||||
// or over no head or a head at no full commit (novox/hq ADR 0644, ADR 0656; issue 1921).
|
||||
func checkJudgedBy(j *checker.VerdictsByGroupHeadsValueJudgedBy) error {
|
||||
const who = "meshdeliverycheckerclient: "
|
||||
if j == nil {
|
||||
return nil
|
||||
}
|
||||
if CheckRepository(j.Program.Repository) != nil || !isCommit(j.Program.Commit) || j.Program.Package == "" {
|
||||
return fmt.Errorf(who+"the verdict is judged by a checking program built at no repository, full commit and package (%+v)", j.Program)
|
||||
}
|
||||
if !strings.HasPrefix(j.Key, "program-") {
|
||||
return fmt.Errorf(who+"the verdict's checking program is kept under %q, which is no program key", j.Key)
|
||||
}
|
||||
if len(j.Heads) == 0 {
|
||||
return fmt.Errorf(who + "the verdict's checking program read no head")
|
||||
}
|
||||
for _, h := range j.Heads {
|
||||
if CheckRepository(h.Repository) != nil || !isCommit(h.Commit) {
|
||||
return fmt.Errorf(who+"the verdict's checking program read %s at %q, which is no full commit", h.Repository, h.Commit)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCommit(s string) bool { return len(s) == 40 && strings.Trim(s, "0123456789abcdef") == "" }
|
||||
|
||||
// CheckVerdict refuses a verdict a reader could not place: no group, heads that are not 12 lowercase hex digits, a repository
|
||||
// both merged and read beside, a carried-from that carries nothing, a series change from a series to itself or named twice, a
|
||||
// level that is neither unit nor integration or counts a negative number, a time below zero, a pass that says nothing of a
|
||||
@@ -145,6 +171,9 @@ func CheckVerdict(v checker.VerdictsByGroupHeadsValue) error {
|
||||
if err := checkComposition(v.Composition); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkJudgedBy(v.JudgedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, level := range v.OverBound {
|
||||
if err := checkLevel(level); err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package meshdeliverycheckerclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -44,6 +45,11 @@ func TestAVerdictIsRefusedForWhatAReaderCouldNotPlace(t *testing.T) {
|
||||
v.Composition = &checker.VerdictsByGroupHeadsValueComposition{State: CompositionPass}
|
||||
}, "mesh-sdk commit"},
|
||||
"the same level over bound twice": {func(v *verdict) { v.OverBound = []string{LevelUnit, LevelUnit} }, "twice"},
|
||||
// 1921 (B0, G): the checking program that judged it.
|
||||
"judged at no full commit": {func(v *verdict) { j := judgedBy(); j.Program.Commit = "0123456"; v.JudgedBy = &j }, "checking program built at no repository, full commit"},
|
||||
"judged under no key": {func(v *verdict) { j := judgedBy(); j.Key = "composer-1"; v.JudgedBy = &j }, "composer-1"},
|
||||
"judged over no head": {func(v *verdict) { j := judgedBy(); j.Heads = nil; v.JudgedBy = &j }, "no head"},
|
||||
"judged over a branch": {func(v *verdict) { j := judgedBy(); j.Heads[0].Commit = "main"; v.JudgedBy = &j }, "novox/mesh-delivery"},
|
||||
} {
|
||||
v := passing()
|
||||
c.edit(&v)
|
||||
@@ -51,6 +57,12 @@ func TestAVerdictIsRefusedForWhatAReaderCouldNotPlace(t *testing.T) {
|
||||
t.Errorf("%s: %v, want a refusal saying %q", name, err, c.says)
|
||||
}
|
||||
}
|
||||
judged := passing()
|
||||
j := judgedBy()
|
||||
judged.JudgedBy = &j
|
||||
if err := CheckVerdict(judged); err != nil {
|
||||
t.Errorf("a pass judged by a checking program is refused: %v", err)
|
||||
}
|
||||
failed := passing()
|
||||
failed.State, failed.Failed = VerdictFail, &checker.VerdictsByGroupHeadsValueFailed{Machine: "ace"}
|
||||
if err := CheckVerdict(failed); err != nil {
|
||||
@@ -191,6 +203,33 @@ func TestTheCodecsWriteAtTheVersionAndReadBackThroughTheRules(t *testing.T) {
|
||||
if _, err := EncodePreview(p); err == nil {
|
||||
t.Error("a preview with no merge commit and no conflict is written")
|
||||
}
|
||||
// 1921 (B0, G): a checking program's input is asked of the seat's verb input by its digest, and an answer for
|
||||
// another digest, or bytes that are not the digest, is refused.
|
||||
body := []byte(`{"v":1}`)
|
||||
digest := DigestOf(body)
|
||||
for name, c := range map[string]struct {
|
||||
answer any
|
||||
says string
|
||||
}{
|
||||
"its own bytes": {InputAnswerOf(body), ""},
|
||||
"another digest": {checker.InputAnswer{Digest: DigestOf([]byte("x")), Input: InputAnswerOf(body).Input}, "answered the input"},
|
||||
"bytes that are not it": {checker.InputAnswer{Digest: digest, Input: InputAnswerOf([]byte(`{"v":2}`)).Input}, "whose digest is"},
|
||||
"bytes in no base64": {checker.InputAnswer{Digest: digest, Input: "%%"}, "no base64"},
|
||||
"no answer of the layout": {"nothing", "answered no input"},
|
||||
} {
|
||||
got, err := ReadInput(func(key string, b any) (json.RawMessage, error) {
|
||||
if key != "seat:mesh-delivery-checker.input" || b != (checker.InputInput{Digest: digest}) {
|
||||
t.Errorf("%s: asked %s %+v", name, key, b)
|
||||
}
|
||||
return json.Marshal(c.answer)
|
||||
}, digest)
|
||||
switch {
|
||||
case c.says == "" && (err != nil || string(got) != string(body)):
|
||||
t.Errorf("%s: %q, %v", name, got, err)
|
||||
case c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)):
|
||||
t.Errorf("%s: %v, want a refusal saying %q", name, err, c.says)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The kept and preview keys: a key reads back to its parts, and a part that does not key is refused.
|
||||
@@ -221,3 +260,10 @@ func TestKeptAndPreviewKeysReadBack(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func judgedBy() checker.VerdictsByGroupHeadsValueJudgedBy {
|
||||
const c = "0123456789abcdef0123456789abcdef01234567"
|
||||
return checker.VerdictsByGroupHeadsValueJudgedBy{
|
||||
Program: checker.VerdictsByGroupHeadsValueJudgedByProgram{Repository: "novox/mesh-delivery", Commit: c, Dir: "modules/mesh-delivery-checker", Package: "p"},
|
||||
Key: "program-0123456789abcdef", Built: true, Heads: []checker.VerdictsByGroupHeadsValueJudgedByHeadsItem{{Repository: "novox/mesh-delivery", Commit: c}}}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package meshdeliverycheckerclient
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
||||
)
|
||||
|
||||
// InputVerb is the seat's verb input as a manifest's `invokes` names it and a run ask's input names it: asked of the
|
||||
// seat, on no one machine (novox/hq ADR 0644, ADR 0656; issue 1921). A checking program's input is kept by the checker
|
||||
// by the sha256 of its exact bytes, and asked of it by the build module that runs the program (ADR 0596 §2).
|
||||
const InputVerb = "seat:" + checker.Seat + "." + checker.VerbInput
|
||||
|
||||
// InputAnswerOf is the verb input's answer for body: its digest and its exact bytes, base64.
|
||||
func InputAnswerOf(body []byte) checker.InputAnswer {
|
||||
return checker.InputAnswer{Digest: DigestOf(body), Input: base64.StdEncoding.EncodeToString(body)}
|
||||
}
|
||||
|
||||
// DigestOf is the digest a checking program's input is kept and asked by: the sha256 of its bytes, 64 lowercase hex
|
||||
// digits.
|
||||
func DigestOf(body []byte) string {
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ReadInput asks the verb input, with ask (a tools bundle's stdio.Ask, a process's Conn.Call), for the input kept by
|
||||
// digest and answers its bytes, refusing an answer for another digest and bytes whose sha256 is not the digest.
|
||||
func ReadInput(ask func(key string, body any) (json.RawMessage, error), digest string) ([]byte, error) {
|
||||
const who = "meshdeliverycheckerclient: "
|
||||
raw, err := ask(InputVerb, checker.InputInput{Digest: digest})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(who+"asking %s for the input %s: %w", InputVerb, digest, err)
|
||||
}
|
||||
var a checker.InputAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf(who+"%s answered no input for %s: %w", InputVerb, digest, err)
|
||||
}
|
||||
if a.Digest != digest {
|
||||
return nil, fmt.Errorf(who+"%s answered the input %q, asked %s", InputVerb, a.Digest, digest)
|
||||
}
|
||||
body, err := base64.StdEncoding.DecodeString(a.Input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(who+"%s answered the input %s in no base64: %w", InputVerb, digest, err)
|
||||
}
|
||||
if got := DigestOf(body); got != digest {
|
||||
return nil, fmt.Errorf(who+"the input %s answered %d bytes whose digest is %s", digest, len(body), got)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -118,6 +118,12 @@ func AskWhat(a buildstep.Ask) string {
|
||||
}
|
||||
case a.Compose != nil:
|
||||
parts = append(parts, "compose", str(a.Compose.Fetch, "repository")+"@"+short(str(a.Compose.Fetch, "commit")))
|
||||
case a.Run != nil:
|
||||
parts = append(parts, "run "+a.Run.Program.Package, a.Run.Program.Repository+"@"+short(a.Run.Program.Commit))
|
||||
if len(a.Run.Args) > 0 {
|
||||
parts = append(parts, strings.Join(a.Run.Args, " "))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%d heads", len(a.Run.Heads)))
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
@@ -165,4 +165,9 @@ func TestAskWhat(t *testing.T) {
|
||||
if got, want := AskWhat(build), "delivery ask, module mesh-x, fp abcdef012345"; got != want {
|
||||
t.Errorf("build = %q", got)
|
||||
}
|
||||
// 1921: a run ask's step names the program, where it is built, its arguments and how many heads it reads.
|
||||
run := buildstep.Ask{Kind: "check", Run: &buildstep.RunJob{Program: buildstep.RunProgram{Repository: "novox/mesh-delivery", Commit: "fedcba9876543210", Package: "./cmd/mesh-checking-program"}, Heads: []buildstep.RunHead{{}, {}}, Args: []string{"-verdict"}}}
|
||||
if got, want := AskWhat(run), "check ask, run ./cmd/mesh-checking-program, novox/mesh-delivery@fedcba987654, -verdict, 2 heads"; got != want {
|
||||
t.Errorf("run = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -197,6 +197,9 @@ type StepAsk struct {
|
||||
Build *BuildJob `json:"build,omitempty"`
|
||||
Check *CheckJob `json:"check,omitempty"`
|
||||
Compose *ComposeJob `json:"compose,omitempty"`
|
||||
// Run is a run ask's job (run.go): a program built from a change and run over the heads, on the Go seat's check
|
||||
// stream alone, as a compose.
|
||||
Run *RunJob `json:"run,omitempty"`
|
||||
// Needs are what a check's tests need of the holder's node (CheckNeed); a holder lacking one declines the ask.
|
||||
Needs []string `json:"needs,omitempty"`
|
||||
// Packages pin the mesh packages a build or a check builds against.
|
||||
@@ -323,16 +326,18 @@ func (a StepAsk) check() error {
|
||||
return fmt.Errorf("buildstep: the ask %s names no time", a.ID)
|
||||
}
|
||||
jobs := 0
|
||||
for _, set := range []bool{a.Build != nil, a.Check != nil, a.Compose != nil} {
|
||||
for _, set := range []bool{a.Build != nil, a.Check != nil, a.Compose != nil, a.Run != nil} {
|
||||
if set {
|
||||
jobs++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case jobs != 1:
|
||||
return fmt.Errorf("buildstep: the ask %s holds a build, a check or a compose, exactly one", a.ID)
|
||||
return fmt.Errorf("buildstep: the ask %s holds a build, a check, a compose or a run, exactly one", a.ID)
|
||||
case a.Compose != nil && (a.Kind != AskCheck || a.Language != AskGo):
|
||||
return fmt.Errorf("buildstep: the ask %s is a compose, which the Go seat's check stream alone carries", a.ID)
|
||||
case a.Run != nil && (a.Kind != AskCheck || a.Language != AskGo):
|
||||
return fmt.Errorf("buildstep: the ask %s is a run, which the Go seat's check stream alone carries", a.ID)
|
||||
case a.Build != nil && a.Kind == AskRecheck:
|
||||
return fmt.Errorf("buildstep: the ask %s is a build on the recheck stream, which carries checks alone", a.ID)
|
||||
case a.Check != nil && a.Kind == AskDelivery:
|
||||
@@ -357,6 +362,8 @@ func (a StepAsk) check() error {
|
||||
return a.Build.check(a.ID)
|
||||
case a.Compose != nil:
|
||||
return a.Compose.check(a.ID)
|
||||
case a.Run != nil:
|
||||
return a.Run.check(a.ID)
|
||||
}
|
||||
return a.Check.check(a.ID)
|
||||
}
|
||||
@@ -597,7 +604,7 @@ type FetchRefusal struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// StepResult is what any step answered: one layout for the four kinds, as the seats' `StepResult` (the fields of the
|
||||
// StepResult is what any step answered: one layout for the five kinds, as the seats' `StepResult` (the fields of the
|
||||
// kinds do not meet). A step's result bytes read into it strictly.
|
||||
type StepResult struct {
|
||||
Artifacts []AskBuilt `json:"artifacts,omitempty"`
|
||||
@@ -609,6 +616,9 @@ type StepResult struct {
|
||||
Built bool `json:"built,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Verdict json.RawMessage `json:"verdict,omitempty"`
|
||||
Exit int `json:"exit,omitempty"`
|
||||
Output json.RawMessage `json:"output,omitempty"`
|
||||
Said string `json:"said,omitempty"`
|
||||
Declined bool `json:"declined,omitempty"`
|
||||
Needs []string `json:"needs,omitempty"`
|
||||
Lacks []string `json:"lacks,omitempty"`
|
||||
|
||||
@@ -29,10 +29,24 @@ func goodCheck() StepAsk {
|
||||
Check: &CheckJob{Fetch: Fetch{Repository: "novox/mesh-catalog", Commit: testCommit}, Levels: []string{LevelUnit}}}
|
||||
}
|
||||
|
||||
// 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("seat:mesh-delivery-checker.input", []byte(`{"v":1}`))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return StepAsk{ID: testAskID, Kind: AskCheck, Language: AskGo, Asker: "mesh-delivery-checker",
|
||||
At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC),
|
||||
Run: &RunJob{Program: RunProgram{Repository: "novox/mesh-delivery", Commit: testCommit, Dir: "modules/mesh-delivery-checker",
|
||||
Package: "./cmd/mesh-checking-program"},
|
||||
Heads: []RunHead{{Repository: "novox/mesh-delivery", Commit: testCommit}, {Repository: "novox/mesh-sdk", Commit: testCommit}},
|
||||
Input: in, Args: []string{"-verdict"}}}
|
||||
}
|
||||
|
||||
// The ask the three build seats share is one neutral form: it round-trips, and every ill-formed ask is refused,
|
||||
// naming what is wrong (the rules of the old delivery.StepAsk, now the shared runner's).
|
||||
func TestAskCheckAndRoundTrip(t *testing.T) {
|
||||
for _, a := range []StepAsk{goodBuild(), goodCheck()} {
|
||||
for _, a := range []StepAsk{goodBuild(), goodCheck(), runAsk()} {
|
||||
raw, err := EncodeStepAsk(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -66,6 +80,44 @@ func TestAskCheckAndRoundTrip(t *testing.T) {
|
||||
t.Errorf("%s: %v, want %q", c.name, err, c.want)
|
||||
}
|
||||
}
|
||||
// 1921 (B0, G): a run ask goes only on the Go seat's check stream, alone, builds a clean package at a full commit
|
||||
// over heads named once at full commits, and names its input on a seat's verb by a digest within its bound.
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
edit func(*StepAsk)
|
||||
want string
|
||||
}{
|
||||
{"run beside a check", func(a *StepAsk) { a.Check = goodCheck().Check }, "or a run, exactly one"},
|
||||
{"run on the delivery stream", func(a *StepAsk) { a.Kind = AskDelivery }, "is a run"},
|
||||
{"run on the Node seat", func(a *StepAsk) { a.Language = AskNode }, "is a run"},
|
||||
{"run at a short commit", func(a *StepAsk) { a.Run.Program.Commit = "0123456" }, "no full commit"},
|
||||
{"run outside the repository", func(a *StepAsk) { a.Run.Program.Dir = "../x" }, "no clean directory"},
|
||||
{"run of no package", func(a *StepAsk) { a.Run.Program.Package = "" }, "no package"},
|
||||
{"run over no head", func(a *StepAsk) { a.Run.Heads = nil }, "no head"},
|
||||
{"run over a head twice", func(a *StepAsk) { a.Run.Heads = append(a.Run.Heads, a.Run.Heads[0]) }, "twice"},
|
||||
{"run over a branch", func(a *StepAsk) { a.Run.Heads[1].Commit = "main" }, "novox/mesh-sdk at no full commit"},
|
||||
{"input on a state", func(a *StepAsk) { a.Run.Input.Verb = "mesh-delivery-checker.inputs" }, "no seat:<seat>.<verb>"},
|
||||
{"input on one machine", func(a *StepAsk) { a.Run.Input.Verb = "seat:mesh-delivery-checker.input@ace" }, "no seat:<seat>.<verb>"},
|
||||
{"input by no digest", func(a *StepAsk) { a.Run.Input.Digest = "d1" }, "no sha256 digest"},
|
||||
{"input over its bound", func(a *StepAsk) { a.Run.Input.Size = RunInputBound + 1 }, "outside 1 to"},
|
||||
{"argument of the shell", func(a *StepAsk) { a.Run.Args = []string{"-x; rm"} }, "no word or flag"},
|
||||
} {
|
||||
a := runAsk()
|
||||
c.edit(&a)
|
||||
if _, err := EncodeStepAsk(a); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: %v, want %q", c.name, err, c.want)
|
||||
}
|
||||
}
|
||||
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
|
||||
if err := in.Verify([]byte(`{"v":1}`)); err != nil {
|
||||
t.Errorf("the input's own bytes: %v", err)
|
||||
}
|
||||
if err := in.Verify([]byte(`{"v":2}`)); err == nil || !strings.Contains(err.Error(), "not the one the ask names") {
|
||||
t.Errorf("bytes that are not the digest: %v", err)
|
||||
}
|
||||
chk := goodCheck()
|
||||
chk.Check.Levels = []string{"nightly"}
|
||||
if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "nightly") {
|
||||
@@ -230,6 +282,12 @@ func TestPauseCapacityAndComposerKey(t *testing.T) {
|
||||
if k == ComposerKey("novox/r", testCommit, "d", "p", tc, PackageVersions{"m/x": b, "m/y": a}) || k == ComposerKey("novox/r", testCommit, "d", "p", tc, nil) {
|
||||
t.Error("other versions, or none, share the key")
|
||||
}
|
||||
// 1921 (B0): a run's program is kept the same way, under a key of its own kind.
|
||||
p := RunProgram{Repository: "novox/mesh-delivery", Commit: testCommit, Dir: "modules/mesh-delivery-checker", Package: "p"}
|
||||
if rk := RunKey(p, tc, nil); !strings.HasPrefix(rk, "program-") || len(rk) != len("program-")+16 || rk != RunKey(p, tc, nil) ||
|
||||
RunKey(p, tc, PackageVersions{"m/x": a}) == rk {
|
||||
t.Errorf("run key = %q", rk)
|
||||
}
|
||||
if ComposerKey("novox/r", testCommit, "d", "p", tc, nil) != ComposerKey("novox/r", testCommit, "d", "p", tc, PackageVersions{}) {
|
||||
t.Error("an empty ask changed the key")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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;
|
||||
// design 47, *What ADR 0656 leaves to this design*: the build seat's ask `run`; issue 1921). It goes on the Go seat's
|
||||
// check stream, a whole job pulled where there is capacity, like a compose: the build module fetches Program at its
|
||||
// commit and builds it once per RunKey, in a step that runs nothing but the build; fetches every head of Heads and lays
|
||||
// each read-only under the directory RunHeadsEnv names, at its repository's path (`<owner>/<name>`); reads the input
|
||||
// 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 by its digest, measured
|
||||
// against RunInputBound before it asks, and serves it on a verb of its seat (RunInput.Verb); the build module asks that
|
||||
// verb as its manifest's `invokes` grants it, never reading the asker's state (ADR 0596 §2), so the ask on the stream
|
||||
// never carries it (issue 1930). The layouts are the part build-step's `RunJob`, `RunProgram`, `RunHead`, `RunInput`
|
||||
// and `RunAnswer`.
|
||||
|
||||
// RunJob is a run ask's job.
|
||||
type RunJob struct {
|
||||
// Program is where the program is built: the repository and full commit fetched, the Go module's directory in it and
|
||||
// the program's import path. The ask's Packages are the mesh package versions it builds against (ADR 0573 §2, §4).
|
||||
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 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"`
|
||||
// Reader names, for a reader of the ask, what the run is for ("checking program of mesh-delivery head").
|
||||
Reader string `json:"reader,omitempty"`
|
||||
}
|
||||
|
||||
// RunProgram is where a run's program is built from.
|
||||
type RunProgram struct {
|
||||
Repository string `json:"repository"`
|
||||
Commit string `json:"commit"`
|
||||
Dir string `json:"dir"`
|
||||
Package string `json:"package"`
|
||||
}
|
||||
|
||||
// RunHead is one commit a run's program reads.
|
||||
type RunHead struct {
|
||||
Repository string `json:"repository"`
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type RunInput struct {
|
||||
Verb string `json:"verb"`
|
||||
Digest string `json:"digest"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// RunAnswer is a run ask's step result: whether this run built the program or took the one kept under its key, the
|
||||
// key, the program's exit, the last line of its standard output (Output, raw when it is JSON, else as a JSON string),
|
||||
// and Said: the first line of its standard error, or why the step could not run it.
|
||||
type RunAnswer struct {
|
||||
Built bool `json:"built"`
|
||||
Key string `json:"key"`
|
||||
Exit int `json:"exit"`
|
||||
Output json.RawMessage `json:"output,omitempty"`
|
||||
Said string `json:"said,omitempty"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// RunHeadsEnv names, for the program, the directory its heads are laid in; RunInputEnv the file its input is written to.
|
||||
const (
|
||||
RunHeadsEnv = "MESH_RUN_HEADS"
|
||||
RunInputEnv = "MESH_RUN_INPUT"
|
||||
)
|
||||
|
||||
// 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("buildstep: a run's input of %d bytes is over its bound of %d bytes", len(body), RunInputBound)
|
||||
}
|
||||
sum := sha256.Sum256(body)
|
||||
in := RunInput{Verb: verb, Digest: hex.EncodeToString(sum[:]), Size: len(body)}
|
||||
return in, in.check("")
|
||||
}
|
||||
|
||||
// Verify refuses body when it is not the input i names: another size or another digest.
|
||||
func (i RunInput) Verify(body []byte) error {
|
||||
sum := sha256.Sum256(body)
|
||||
if len(body) != i.Size || hex.EncodeToString(sum[:]) != i.Digest {
|
||||
return fmt.Errorf("buildstep: 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
|
||||
}
|
||||
|
||||
var (
|
||||
runDigestOK = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||
runArgOK = regexp.MustCompile(`^[-=_./a-zA-Z0-9]+$`)
|
||||
)
|
||||
|
||||
func (i RunInput) check(id string) error {
|
||||
switch {
|
||||
case !runVerbOK(i.Verb):
|
||||
return fmt.Errorf("buildstep: 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("buildstep: the run ask %s names its input by %q, which is no sha256 digest", id, i.Digest)
|
||||
case i.Size <= 0 || i.Size > RunInputBound:
|
||||
return fmt.Errorf("buildstep: the run ask %s names an input of %d bytes, outside 1 to %d", id, i.Size, RunInputBound)
|
||||
}
|
||||
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 checkRunRepository(id, what, repository string) error {
|
||||
owner, name, ok := strings.Cut(repository, "/")
|
||||
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
|
||||
return fmt.Errorf("buildstep: the run ask %s names %s %q, which is no repository <owner>/<name>", id, what, repository)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RunJob) check(id string) error {
|
||||
p := r.Program
|
||||
if err := checkRunRepository(id, "its program's repository", p.Repository); err != nil {
|
||||
return err
|
||||
}
|
||||
if !askCommitOK.MatchString(p.Commit) {
|
||||
return fmt.Errorf("buildstep: the run ask %s builds its program at no full commit", id)
|
||||
}
|
||||
if p.Dir == "" || p.Dir != path.Clean(p.Dir) || strings.HasPrefix(p.Dir, "/") || p.Dir == ".." || strings.HasPrefix(p.Dir, "../") {
|
||||
return fmt.Errorf("buildstep: the run ask %s names no clean directory inside the repository (%q)", id, p.Dir)
|
||||
}
|
||||
if p.Package == "" || strings.ContainsAny(p.Package, " \t\n") {
|
||||
return fmt.Errorf("buildstep: the run ask %s names no package to build (%q)", id, p.Package)
|
||||
}
|
||||
if len(r.Heads) == 0 {
|
||||
return fmt.Errorf("buildstep: the run ask %s names no head", id)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, h := range r.Heads {
|
||||
if err := checkRunRepository(id, "a head of", h.Repository); err != nil {
|
||||
return err
|
||||
}
|
||||
if !askCommitOK.MatchString(h.Commit) {
|
||||
return fmt.Errorf("buildstep: the run ask %s names the head of %s at no full commit", id, h.Repository)
|
||||
}
|
||||
if seen[h.Repository] {
|
||||
return fmt.Errorf("buildstep: the run ask %s names %s twice", id, h.Repository)
|
||||
}
|
||||
seen[h.Repository] = true
|
||||
}
|
||||
for _, a := range r.Args {
|
||||
if !runArgOK.MatchString(a) {
|
||||
return fmt.Errorf("buildstep: the run ask %s names the argument %q, which is no word or flag", id, a)
|
||||
}
|
||||
}
|
||||
return r.Input.check(id)
|
||||
}
|
||||
|
||||
// RunKey is the key a build module keeps a built program under, as ComposerKey keeps a composer: what it was built
|
||||
// from, the toolchain and the asked package versions, hashed (`program-` and 16 hex digits).
|
||||
func RunKey(p RunProgram, toolchain ToolchainUsed, versions PackageVersions) string {
|
||||
return "program-" + strings.TrimPrefix(ComposerKey(p.Repository, p.Commit, p.Dir, p.Package, toolchain, versions), "composer-")
|
||||
}
|
||||
@@ -186,6 +186,7 @@ type Ask struct {
|
||||
Compose *struct {
|
||||
Fetch map[string]any `json:"fetch"`
|
||||
} `json:"compose"`
|
||||
Run *RunJob `json:"run"`
|
||||
}
|
||||
|
||||
// CheckAskID refuses an id that is no ask's: base-36 milliseconds and six hex digits, as NewID gives. Its form keeps it apart from
|
||||
|
||||
Reference in New Issue
Block a user