Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/programs.go
T
jschoubben 9c9ed7a1d8
mesh/delivery ready: it is merged on the Approve
mesh/delivery-group group feat/b0-checking-program checking: 3 of 3 member(s) ready
mesh/check checking 9c9ed7a1d8f1 on 2d201f781ced | novox/mesh-delivery#162 in group feat/b0-checking-program
The checker serves a checking program's input on its seat's verb input, and the build module asks it (issue 1921, B0 review round 1)
The operator's answer: mesh-build-go reading the checker's state inputs broke ADR 0596 §2. The run ask's input now names
the verb seat:mesh-delivery-checker.input; the checker's tools serve it from the state inputs (Reader.Input, checked
against its digest), and the state is the checker's own again, read by no other module.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 23:39:39 +01:00

230 lines
9.7 KiB
Go

package check
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"mesh-delivery-checker/rules"
)
// The checking program (novox/hq ADR 0644 §1, ADR 0656 §5; design 47's bootstrap delivery; issue 1921): every check has
// the program built from the group's mesh-delivery member head, else from mesh-delivery's trunk, and run in the build
// seat's sandbox through the seat node-build-go's ask `run`, with every member's head laid for it. It answers the heads'
// seat list first (`-seats`), which the manifest validator's and the composer's requests carry, and then the check's
// verdict of the facts the asking gathered (`-verdict`). The running checker asks, keeps and records; it judges nothing.
const (
// InputsStateName is the checker's own state of run inputs and InputsBucket its bucket: every input by the sha256 of
// its exact bytes, read by the checker alone. InputVerb is the seat verb a run ask's input names
// (delivery.RunInput.Verb), which the checker's tools serve from that state (Reader.Input): the Go build module asks it
// as its manifest's invokes grants it and checks the digest (delivery.RunInput.Read), never reading the state (ADR 0596
// §2; the operator's answer on milestone 47/B0).
InputsStateName = "inputs"
InputsBucket = delivery.CheckerModule + "_" + InputsStateName
InputVerb = "seat:" + catalogue.CheckerSeat + "." + delivery.CheckerInputVerb
// CheckingProgramDir and CheckingProgramPackage are where in mesh-delivery the checking program is built.
CheckingProgramDir = "modules/mesh-delivery-checker"
CheckingProgramPackage = "mesh-delivery-checker/cmd/mesh-checking-program"
// DefaultBeginBound is how long a run ask may wait for a build node to begin its step before it is put once more, and
// after that once more before the check fails naming the job: a build module that does not know the job `run` drops
// it unanswered (the lead's ruling on milestone 47/B0, 2026-11-06).
DefaultBeginBound = 5 * time.Minute
)
// Inputs keeps a run's input under its digest, exactly its bytes.
type Inputs interface {
Keep(ctx context.Context, digest string, body []byte) error
}
func (j *Examiner) beginBound() time.Duration {
if j.BeginBound > 0 {
return j.BeginBound
}
return DefaultBeginBound
}
// checkingProgram is where this check's checking program is built: the group's mesh-delivery member head, else
// mesh-delivery's trunk; false when neither is known.
func (st *state) checkingProgram() (delivery.RunProgram, string, bool) {
p := delivery.RunProgram{Repository: DeliveryRepository, Dir: CheckingProgramDir, Package: CheckingProgramPackage}
for _, pull := range st.c.Pulls {
if pull.Repository == DeliveryRepository {
p.Commit = pull.Head
return p, "the group's mesh-delivery head " + pull.Head, true
}
}
if st.j.Trunks != nil {
p.Commit = st.j.Trunks.Heads()[DeliveryRepository]
}
return p, "mesh-delivery's trunk " + p.Commit, p.Commit != ""
}
// runHeads are the members' heads, as the program reads them.
func (st *state) runHeads() []delivery.RunHead {
out := make([]delivery.RunHead, 0, len(st.c.Pulls))
for _, p := range st.c.Pulls {
out = append(out, delivery.RunHead{Repository: p.Repository, Commit: p.Head})
}
return out
}
// runProgram has the checking program built and run with args over body, and answers its output and what judged; an
// error says why it did not answer: its input over its bound, no build node beginning the job, a step that did not
// pass, or a program that exited otherwise than 0.
func (st *state) runProgram(ctx context.Context, args []string, body []byte) (json.RawMessage, *delivery.JudgedBy, error) {
prog, at, ok := st.checkingProgram()
if !ok {
return nil, nil, fmt.Errorf("it cannot be built: the trunk head of %s is not known", DeliveryRepository)
}
if st.j.Inputs == nil {
return nil, nil, fmt.Errorf("its input cannot be kept: the checker keeps no state %s", InputsStateName)
}
in, err := delivery.RunInputOf(InputVerb, body)
if err != nil {
return nil, nil, err
}
if err := st.j.Inputs.Keep(ctx, in.Digest, body); err != nil {
return nil, nil, fmt.Errorf("its input could not be kept in %s: %w", InputsStateName, err)
}
job := delivery.RunJob{Program: prog, Heads: st.runHeads(), Input: in, Args: args, Reader: "the checking program at " + at}
var ids []string
for range 2 {
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule,
At: st.j.now().UTC(), Packages: st.packagesOf(DeliveryRepository), Run: &job}
ids = append(ids, a.ID)
st.c.Progress.Set(fmt.Sprintf("the run ask %s of the checking program %s", a.ID, strings.Join(args, " ")), "")
st.c.Asked(a.ID)
if err := st.j.Asks.Put(ctx, a); err != nil {
return nil, nil, fmt.Errorf("its run ask could not be put: %w", err)
}
waiting := ctx
if watch := st.c.Watch; watch != nil {
id := a.ID
watch(id, nil) // a try names it at once (issue 1890)
waiting = buildrun.WithStepSeen(ctx, func(s delivery.Step) { watch(id, &s) })
}
step, begun, err := st.waitBegun(waiting, a)
if !begun {
continue
}
if err != nil {
return nil, nil, fmt.Errorf("its run ask %s: %w", a.ID, err)
}
if step.State != delivery.StepPassed {
return nil, nil, fmt.Errorf("its run ask %s ended %s on %s: %s", a.ID, step.State, step.Node, step.Reason)
}
var answer delivery.RunAnswer
if err := json.Unmarshal(step.Result, &answer); err != nil {
return nil, nil, fmt.Errorf("its run ask %s answered no run answer: %w", a.ID, err)
}
if answer.Exit != 0 {
return nil, nil, fmt.Errorf("it exited %d on %s: %s", answer.Exit, step.Node, answer.Said)
}
return answer.Output, &delivery.JudgedBy{Program: prog, Key: answer.Key, Built: answer.Built, Heads: job.Heads}, nil
}
return nil, nil, fmt.Errorf("no build node began the job run of the asks %s within %s each: a build module that does not know the job run drops it",
strings.Join(ids, " and "), st.j.beginBound())
}
// waitBegun waits for a's step, and stops waiting, begun false, when no build node began it within the begin bound: a
// bound on the wait's context, no timer of its own.
func (st *state) waitBegun(ctx context.Context, a delivery.StepAsk) (delivery.Step, bool, error) {
wait, cancel := context.WithCancel(ctx)
defer cancel()
begin, stop := context.WithTimeout(ctx, st.j.beginBound())
defer stop()
seen := make(chan struct{})
var once sync.Once
type ended struct {
step delivery.Step
err error
}
done := make(chan ended, 1)
go func() {
step, err := st.j.Asks.Wait(buildrun.WithStepSeen(wait, func(delivery.Step) { once.Do(func() { close(seen) }) }), a)
done <- ended{step, err}
}()
select {
case e := <-done:
return e.step, true, e.err
case <-seen:
case <-begin.Done():
if ctx.Err() != nil {
return delivery.Step{}, true, ctx.Err()
}
return delivery.Step{}, false, nil
}
e := <-done
return e.step, true, e.err
}
// seatList asks the checking program for the heads' seat list, which the validator's and the composer's requests carry
// (ADR 0636 §4); the list and what it left out are the check's notes. A list it cannot make fails the check, naming why,
// and answers false: the check ends there, with no verdict asked of a program that could not answer its list (review
// round 1 of milestone 47/B0: a build node with no job run would hold the check two begin bounds more).
func (st *state) seatList(ctx context.Context) bool {
if st.narrowTry() {
return true
}
body, err := json.Marshal(st.factsHead())
if err == nil {
var out json.RawMessage
if out, _, err = st.runProgram(ctx, []string{"-seats"}, body); err == nil {
var a rules.SeatsAnswer
if err = json.Unmarshal(out, &a); err == nil && a.V != rules.SeatsVersion {
err = fmt.Errorf("it answered a seat list of version %d, not %d", a.V, rules.SeatsVersion)
}
if err == nil {
st.c.Seats = a.Seats
st.c.note(fmt.Sprintf("the heads' seat list holds %d seat definitions", len(a.Seats)))
for _, l := range a.Left {
st.c.note("the heads' seat list leaves out " + l.Path + ": " + l.Why)
}
return true
}
}
}
st.fail(goModule, "the heads' seat list could not be made by the checking program: "+err.Error())
return false
}
// factsHead are the facts that name the check, without what the asking gathered.
func (st *state) factsHead() rules.Facts {
f := rules.Facts{V: rules.FactsVersion, Group: st.c.Group, Heads: st.c.Heads, Trunk: st.c.Trunk}
for _, p := range st.c.Pulls {
f.Members = append(f.Members, rules.Member{Repository: p.Repository, Number: p.Number, Head: p.Head, Trunk: p.Trunk})
}
return f
}
// judged is the checking program's answer to the check's facts (`-verdict`), with what judged; a program that cannot be
// asked, built or run, or does not answer a verdict, fails the check naming the checking program, never the running
// checker's rules in its place: the facts gain the failure and the verdict is worded from them as a failure.
func (st *state) judged(ctx context.Context) (rules.Answer, *delivery.JudgedBy) {
body, err := json.Marshal(st.factsOf())
var by *delivery.JudgedBy
if err == nil {
var out json.RawMessage
if out, by, err = st.runProgram(ctx, []string{"-verdict"}, body); err == nil {
var a rules.Answer
if err = json.Unmarshal(out, &a); err == nil && a.V != rules.AnswerVersion {
err = fmt.Errorf("it answered a verdict of version %d, not %d", a.V, rules.AnswerVersion)
}
if err == nil {
return a, by
}
}
}
st.fail(goModule, "the check could not be judged by the checking program: "+err.Error())
return rules.Answer{V: rules.AnswerVersion, Verdict: rules.Judge(st.factsOf())}, by
}