Files
mesh-delivery/sdk/buildstep/run.go
T
jschoubben d5d9f0c258 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
2026-11-07 02:35:31 +01:00

182 lines
7.9 KiB
Go

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-")
}