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
182 lines
7.9 KiB
Go
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-")
|
|
}
|