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
This commit is contained in:
2026-11-06 12:01:00 +01:00
parent 3b700886f2
commit eec35a7c0c
2 changed files with 90 additions and 4 deletions
+47 -1
View File
@@ -23,11 +23,17 @@ import (
"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"
)
@@ -105,7 +111,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if *declarations {
req.Declarations = true
}
v, err := composer.Run(req)
v, err := runWithSeats(req)
if err != nil {
fmt.Fprintln(stderr, "mesh-compose:", err)
return 1
@@ -118,6 +124,46 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
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) {
+43 -3
View File
@@ -17,6 +17,7 @@ import (
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
"github.com/novox/mesh-host/internal/beside"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/composer"
@@ -49,11 +50,25 @@ func meshOf() compose.Input {
func request(t *testing.T, depth string) composer.Request {
t.Helper()
r := composer.Request{V: composer.Version, Depth: depth, HoldingsFromClaims: true,
Machines: []string{"anchor", "laptop", "ace"},
Cases: []composer.Case{{Name: "trunk", Manifests: captured(t)}}}
Machines: []string{"anchor", "laptop", "ace"}, Seats: seats(t),
Cases: []composer.Case{{Name: "trunk", Manifests: captured(t)}}}
return withInput(t, r, meshOf())
}
// seats is the seat list a request carries, as the checker hands the one its reader holds (ADR 0635 §1, ADR 0636 §4).
func seats(t *testing.T) []json.RawMessage {
t.Helper()
var out []json.RawMessage
for _, d := range seatlisttest.Definitions() {
raw, err := json.Marshal(d)
if err != nil {
t.Fatal(err)
}
out = append(out, raw)
}
return out
}
// withInput is r with in as its input, as a caller puts it on the wire: the request's shape carries the input as JSON.
func withInput(t *testing.T, r composer.Request, in compose.Input) composer.Request {
t.Helper()
@@ -124,6 +139,31 @@ func TestTheCommandWritesOneVerdictPerManifestAndMachine(t *testing.T) {
t.Errorf("%s does not compose and says no reason", name)
}
}
// Issue 1966, the group try of 2026-11-06 10:07: the composer judges against the seat list its request carries, the
// node-engine's from the bus (ADR 0635 §1). The uplink's holder steps back from the resolver file for the resolver's
// holder by the seat's definition; without the list nothing is judged (ADR 0636 §1), never "both declare the path".
resolverMachine := func(withSeats bool) composer.Machine {
r := composer.Request{V: composer.Version, Depth: composer.DepthCompose, HoldingsFromClaims: true,
Machines: []string{"laptop"}, Cases: []composer.Case{{Name: "trunk", Manifests: captured(t)}}}
if withSeats {
r.Seats = seats(t)
}
r = withInput(t, r, compose.Input{Nodes: []compose.Node{{Name: "laptop",
Assigned: []string{"networkmanager", "systemd-resolved", "pacman", "systemd", "dnsmasq"},
Capabilities: map[string]bool{"file-references": true, "privileged": true, "service-manager": true, "package-manager": true, "uplink-networkmanager": true}}}})
code, v, stderr := runJSON(t, nil, encode(t, r))
if code != 0 || len(v.Cases) != 1 {
t.Fatalf("exit %d: %s", code, stderr)
}
return v.Cases[0].Machines["laptop"]
}
if m := resolverMachine(true); strings.Contains(m.Reason+strings.Join(m.Problems, " "), "both declare") || m.NotJudged != "" {
t.Errorf("with the seat list, the uplink's and the resolver's holders on one machine: %+v, want the uplink's stepping back", m)
}
if m := resolverMachine(false); m.Composes || m.NotJudged != NoSeatList || m.Reason != "" {
t.Errorf("without a seat list: %+v, want not judged, saying why", m)
}
}
// 1621/AC3: over the captured catalogue and a mesh of three machines, the command's verdict equals, machine for
@@ -196,7 +236,7 @@ func TestACatalogueCheckoutIsComposedForANode(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, "modules", "demo", "module.json"), []byte(manifest), 0o644); err != nil {
t.Fatal(err)
}
in := encode(t, withInput(t, composer.Request{V: composer.Version}, compose.Input{Nodes: []compose.Node{{Name: "laptop", Assigned: []string{"demo"}}}}))
in := encode(t, withInput(t, composer.Request{V: composer.Version, Seats: seats(t)}, compose.Input{Nodes: []compose.Node{{Name: "laptop", Assigned: []string{"demo"}}}}))
code, v, stderr := runJSON(t, []string{"-catalogue", dir, "-node", "laptop", "-declarations"}, in)
if code != 0 {
t.Fatalf("exit %d: %s", code, stderr)