The lead's ruling on milestone 47/B0: each referenced input is bounded by the value limit the design already uses. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
163 lines
7.1 KiB
Go
163 lines
7.1 KiB
Go
package delivery
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// **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 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).
|
|
|
|
// 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.
|
|
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 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.
|
|
type RunInput struct {
|
|
State string `json:"state"`
|
|
Digest string `json:"digest"`
|
|
Size int `json:"size"`
|
|
}
|
|
|
|
// 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 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) {
|
|
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)}
|
|
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("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 nil
|
|
}
|
|
|
|
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 !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:
|
|
return fmt.Errorf("delivery: the run ask %s names an input of %d bytes, outside 1 to %d", id, i.Size, RunInputBound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r RunJob) check(id string) error {
|
|
p := r.Program
|
|
if err := checkRepository(p.Repository); err != nil {
|
|
return fmt.Errorf("the run ask %s's program: %w", id, err)
|
|
}
|
|
if !askCommitOK.MatchString(p.Commit) {
|
|
return fmt.Errorf("delivery: 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("delivery: 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("delivery: the run ask %s names no package to build (%q)", id, p.Package)
|
|
}
|
|
if len(r.Heads) == 0 {
|
|
return fmt.Errorf("delivery: the run ask %s names no head", id)
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, h := range r.Heads {
|
|
if err := checkRepository(h.Repository); err != nil {
|
|
return fmt.Errorf("the run ask %s's head: %w", id, err)
|
|
}
|
|
if !askCommitOK.MatchString(h.Commit) {
|
|
return fmt.Errorf("delivery: the run ask %s names the head of %s at no full commit", id, h.Repository)
|
|
}
|
|
if seen[h.Repository] {
|
|
return fmt.Errorf("delivery: 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("delivery: the run ask %s names the argument %q, which is no word or flag", id, a)
|
|
}
|
|
}
|
|
return r.Input.check(id)
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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-")
|
|
}
|