Files
mesh-host/cmd/mesh-compose/main.go
T
jschoubben eec35a7c0c The sandbox composer composes against the seat list its request carries, and judges nothing without one (issue 1966)
The group try of 2026-11-06 10:07 failed its composition on all four
machines because mesh-compose, the node-engine's composer a check builds
at the head, loaded no seat list once the compiled one was deleted (ADR
0635 §1): the uplink's holder no longer stepped back from the resolver
file, and no holder of the registry seats was known. It now takes the
request's seats over the foundation's bootstrapped ones, as the
node-engine takes the bus's; without any it says each machine not judged
(ADR 0636 §1, ADR 0593 §9), never a refusal or pass made without them.

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

201 lines
7.4 KiB
Go

// mesh-compose is the node-engine's composer command (novox/hq ADR 0482 clause 4, ADR 0593 §7, issue 1621): it reads a
// composer.Request on standard input, reads every manifest, composes the machines named in each case and writes one
// composer.Verdict on standard output. Built at a commit of the node-engine's repository, it answers what that
// commit's composition answers, so a group that changes the node-engine's composition is judged by its own next code.
//
// mesh-compose < request.json > verdict.json
// mesh-compose -request request.json > verdict.json
// mesh-compose -catalogue <dir> [-node <name>]... [-depth resolve|compose] [-declarations] [< request.json]
//
// -catalogue reads <dir>/modules/*/module.json and a module.json at <dir>'s root as the first case's manifests (a
// checkout or `git archive` of the catalogue repository at a ref: the command fetches nothing). -node adds a machine to
// compose; -declarations answers each machine's declared paths with their owners and modes.
//
// It opens no network connection, bus, store or file beyond its standard input, the -request file and the -catalogue
// directory, and reads
// no environment. Exit codes: 0 the verdict is written (a machine that does not compose is a verdict, not an error);
// 1 the request cannot be read or run, said on standard error; 2 the flags are wrong.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"maps"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"github.com/novox/mesh-host/internal/composer"
)
func main() {
os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
}
type nodes []string
func (n *nodes) String() string { return strings.Join(*n, ",") }
func (n *nodes) Set(v string) error { *n = append(*n, v); return nil }
// run is the command: its arguments, standard input, output and error; it answers the exit code.
func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("mesh-compose", flag.ContinueOnError)
fs.SetOutput(stderr)
catalogueDir := fs.String("catalogue", "", "a catalogue checkout: <dir>/modules/*/module.json are the first case's manifests")
var machines nodes
fs.Var(&machines, "node", "a machine to compose (repeatable)")
depth := fs.String("depth", "", "resolve or compose (default: the request's, else compose)")
declarations := fs.Bool("declarations", false, "answer each composed machine's declared paths, owners and modes")
requestFile := fs.String("request", "", "read the request from this file instead of standard input (a run step that hands no standard input)")
if err := fs.Parse(args); err != nil {
return 2
}
if fs.NArg() > 0 {
fmt.Fprintf(stderr, "mesh-compose: unexpected arguments %q\n", fs.Args())
return 2
}
var raw []byte
var err error
if *requestFile != "" {
raw, err = os.ReadFile(*requestFile)
} else {
raw, err = io.ReadAll(stdin)
}
if err != nil {
fmt.Fprintln(stderr, "mesh-compose: the request cannot be read:", err)
return 1
}
var req composer.Request
if strings.TrimSpace(string(raw)) == "" {
if *catalogueDir == "" {
fmt.Fprintln(stderr, "mesh-compose: no request on standard input and no -catalogue")
return 1
}
req = composer.Request{V: composer.Version}
} else if req, err = composer.Decode(raw); err != nil {
fmt.Fprintln(stderr, "mesh-compose:", err)
return 1
}
if *catalogueDir != "" {
read, err := readCatalogue(*catalogueDir)
if err != nil {
fmt.Fprintln(stderr, "mesh-compose:", err)
return 1
}
if len(req.Cases) == 0 {
req.Cases = []composer.Case{{Name: "catalogue", Manifests: map[string]composer.Manifest{}}}
}
if req.Cases[0].Manifests == nil {
req.Cases[0].Manifests = map[string]composer.Manifest{}
}
for name, m := range read {
if _, given := req.Cases[0].Manifests[name]; !given {
req.Cases[0].Manifests[name] = m
}
}
// No list of predefined keys is read: the composer judges no manifest (novox/hq ADR 0578 §5).
}
req.Machines = append(req.Machines, machines...)
if *depth != "" {
req.Depth = *depth
}
if *declarations {
req.Declarations = true
}
v, err := runWithSeats(req)
if err != nil {
fmt.Fprintln(stderr, "mesh-compose:", err)
return 1
}
enc := json.NewEncoder(stdout)
if err := enc.Encode(v); err != nil {
fmt.Fprintln(stderr, "mesh-compose: the verdict cannot be written:", err)
return 1
}
return 0
}
// NoSeatList is what a machine reads, not judged, when the request carries no seat list (novox/hq ADR 0636 §1).
const NoSeatList = "the composer was handed no seat list, so it composes nothing (a reader of the seat list acts on nothing " +
"new until it has read the list, novox/hq ADR 0636 §1): the asker gives the seat definitions it judges against in the request's seats"
// runWithSeats composes req against the seat list the request carries (Request.Seats: the definitions the asker's seat list
// reader holds, the members' heads' laid over them in a check, ADR 0636 §4), over the foundation's bootstrapped ones
// (seatlist.Bootstrapped), as the node-engine composes against the list it read from the bus (ADR 0635 §1). The command
// compiles no seat list: without one in the request it reads every manifest and judges no machine, each said not judged
// with NoSeatList (ADR 0593 §9), never a refusal or a pass made without the seats' paths, deliveries and holders.
func runWithSeats(req composer.Request) (composer.Verdict, error) {
if len(req.Seats) == 0 {
machines := req.Machines
req.Machines = nil
v, err := composer.Run(req)
if err != nil || len(machines) == 0 {
return v, err
}
for i := range v.Cases {
v.Cases[i].Machines = map[string]composer.Machine{}
for _, node := range machines {
v.Cases[i].Machines[node] = composer.Machine{NotJudged: NoSeatList}
}
}
return v, nil
}
defs := map[string]seatdef.Definition{}
for _, d := range seatlist.Bootstrapped() {
defs[d.Seat] = d
}
for _, raw := range req.Seats {
d, err := seatdef.Parse(raw)
if err != nil {
return composer.Verdict{}, fmt.Errorf("a seat definition of the request: %w", err)
}
defs[d.Seat] = d
}
catalogue.UseSeats(catalogue.SeatsFromDefinitions(slices.Collect(maps.Values(defs))))
return composer.Run(req)
}
// readCatalogue reads every module.json of a catalogue checkout by its module's name: the root's and each of
// modules/*/. A manifest whose name cannot be read is kept under its directory's name, so the reader's refusal is said.
func readCatalogue(dir string) (map[string]composer.Manifest, error) {
paths, err := filepath.Glob(filepath.Join(dir, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
if _, err := os.Stat(filepath.Join(dir, "module.json")); err == nil {
paths = append(paths, filepath.Join(dir, "module.json"))
}
if len(paths) == 0 {
return nil, fmt.Errorf("the catalogue %s holds no module.json", dir)
}
sort.Strings(paths)
out := map[string]composer.Manifest{}
for _, p := range paths {
raw, err := os.ReadFile(p)
if err != nil {
return nil, err
}
var probe struct {
Module string `json:"module"`
}
name := filepath.Base(filepath.Dir(p))
if json.Unmarshal(raw, &probe) == nil && probe.Module != "" {
name = probe.Module
}
if _, twice := out[name]; twice {
return nil, errors.New("the catalogue " + dir + " defines " + name + " twice")
}
out[name] = composer.Manifest{Raw: raw}
}
return out, nil
}