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:
2026-11-07 02:35:31 +01:00
parent ea0b6a075b
commit d5d9f0c258
9 changed files with 393 additions and 4 deletions
@@ -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
}
+6
View File
@@ -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, ", ")
}
+5
View File
@@ -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
View File
@@ -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"`
+59 -1
View File
@@ -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")
}
+181
View File
@@ -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-")
}
+1
View File
@@ -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