From 7a82384e21cba336142efad35ff5ea54d473be9b Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 18:12:09 +0100 Subject: [PATCH] =?UTF-8?q?Grant=20a=20node's=20agent=20the=20seat's=20pla?= =?UTF-8?q?cement=20and=20candidates,=20so=20it=20composes=20from=20the=20?= =?UTF-8?q?seat's=20state=20(ADR=200567=20=C2=A73,=20=C2=A79,=20issue=2011?= =?UTF-8?q?26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/buscompose/buscompose_test.go | 11 +++++++++++ go/buscompose/nats.go | 4 ++-- go/buscompose/state.go | 12 ++++++------ go/compose/frombus/export.go | 11 ++++++----- go/compose/frombus/frombus.go | 12 ++++++------ 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index c5e6f90..1f23a6c 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -9,6 +9,8 @@ import ( "slices" "strings" "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" ) // Defends issue 1377 and novox/hq ADR 0547 §5: this is the controller's bus composer copied, and it composes byte for @@ -118,6 +120,15 @@ func grantsAsTheControllers(t *testing.T, got, want string) { } func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { + // ADR 0567 §3, §9: a node reads every key of the seat's placement and its candidates (it composes with the whole + // proposal laid over), and none of its open proposals; it writes neither. + reads := strings.Join(NodeComposeReads("laptop"), " ") + for bucket, granted := range map[string]bool{delivery.PlacementBucket: true, delivery.PlacementCandidatesBucket: true, + delivery.PlacementProposalsBucket: false} { + if strings.Contains(reads, "$JS.API.DIRECT.GET.KV_"+bucket+".>") != granted || strings.Contains(reads, "$KV."+bucket+".laptop") { + t.Errorf("a node is granted %s: %v, want %v", bucket, !granted, granted) + } + } raw, err := os.ReadFile(filepath.Join("testdata", "controller-user-lists.json")) if err != nil { t.Fatal(err) diff --git a/go/buscompose/nats.go b/go/buscompose/nats.go index f65cd70..0173496 100644 --- a/go/buscompose/nats.go +++ b/go/buscompose/nats.go @@ -898,8 +898,8 @@ func PermissionsFor(p Principal) (Permissions, error) { // its own modules reads the other nodes' public records, which its firewall and resolver need (ADR 0547 §2). Read // only: each node still writes its own keys alone. pub = append(pub, readGrants(delivery.HaveBucket, "*."+delivery.HaveNodeWord)...) - // And what it composes its own modules from (ADR 0547 §2, ADR 0549 §3): the indexer's placement, modules and - // manifests, the builder's builds (the manifest of a build its want names), and mesh-config's resolved values of + // And what it composes its own modules from (ADR 0547 §2, ADR 0567 §9): the seat mesh-placement's placement and + // candidates, the indexer's modules and manifests, the builder's builds (the manifest of a build its want names), and mesh-config's resolved values of // its own node alone. Read only. pub = append(pub, NodeComposeReads(p.Node)...) // And, of the other nodes' resolved values, the keys their modules serve from (issue 1126), each one key. diff --git a/go/buscompose/state.go b/go/buscompose/state.go index d795fda..9fb71f9 100644 --- a/go/buscompose/state.go +++ b/go/buscompose/state.go @@ -311,12 +311,12 @@ func RaiseBuckets(a BucketAsserter, buckets []Bucket) (undeclared []string, err // ResolvedBucket is mesh-config's state of resolved values, keyed `..`. const ResolvedBucket = "mesh-config_resolved" -// NodeComposeReads is what a node's agent reads to compose its own modules (novox/hq ADR 0547 §2, ADR 0549 §3, issue -// 1126): every key of the indexer's placement, placement candidates, modules and manifests and of the builder's builds, -// and of mesh-config's resolved values only its own node's, `.>`. Every candidate key, not only `*.`: a node -// answering its candidate composes with the whole group's placement (the seed places every node at once, and a binding -// crosses nodes), and nothing in a placement is secret. Reads only; the node is checked as every subject token is by the -// caller's principal. +// NodeComposeReads is what a node's agent reads to compose its own modules (novox/hq ADR 0547 §2, ADR 0567 §3, §9, +// issue 1126): every key of the seat mesh-placement's placement and candidates (both the assigner's states), of the +// indexer's modules and manifests and of the builder's builds, and of mesh-config's resolved values only its own node's, +// `.>`. Every candidate key, not only `*.`: a node answering its candidate composes with the whole +// proposal laid over (the seed places every node at once, and a binding crosses nodes), and nothing in a placement is +// secret. Reads only; the node is checked as every subject token is by the caller's principal. func NodeComposeReads(node string) []string { var out []string for _, bucket := range []string{delivery.PlacementBucket, delivery.PlacementCandidatesBucket, delivery.ModulesBucket, diff --git a/go/compose/frombus/export.go b/go/compose/frombus/export.go index ee55f8b..d28f95c 100644 --- a/go/compose/frombus/export.go +++ b/go/compose/frombus/export.go @@ -10,9 +10,10 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" ) -// Export is one node's placement file as the composition input the controller fills says it (novox/hq ADR 0555 §6, -// issue 1126): the inverse of Input for what a person decides, so the controller's one read-only verb -// `placement-export` answers, from its own store by its own code, exactly what the seed writes into the catalogue. +// Export is one node's placement as the composition input the controller fills says it (novox/hq ADR 0555 §6, ADR 0567 +// §7): the inverse of Input for what a person decides, so the controller's one read-only verb `placement-export` +// answers, from its own store by its own code, exactly what the seat's adopt proposes as the seed. Its answer must stay +// stable for the same store, since adopt checks its digest again at the warrant. // What the machine observes (its record) is not in it. func Export(in compose.Input, node string) (delivery.Placement, error) { var n *compose.Node @@ -24,8 +25,8 @@ func Export(in compose.Input, node string) (delivery.Placement, error) { if n == nil { return delivery.Placement{}, fmt.Errorf("frombus: the input has no node %q", node) } - // At the version the placement file is read at, with a list of modules even when none is placed: the export is the - // file the seed writes (cmd/placement-seed), and the reader refuses a value of version 0 or without its modules. + // At the version a placement key is read at, with a list of modules even when none is placed: the export is the + // seed's value, and the reader refuses a value of version 0 or without its modules. p := delivery.Placement{V: delivery.Version, Node: node, Modules: []delivery.PlacedModule{}, Decided: delivery.NodeDecided{ Endpoint: n.Place.Endpoint, Site: n.Place.Site, Hub: n.Place.Hub, Address: n.Place.Address, PublicDomain: n.PublicDomain, Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount, diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index 7632246..88cf235 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -2,13 +2,13 @@ // and the grants part compose with one composer over the same inputs (novox/hq ADR 0547 §2, ADR 0549 §3; issue 1126). // // **One composer, three callers.** compose.Compositions is the composer; Input is how each caller fills its -// compose.Input from the bus: the node agent for its own node (ADR 0547 §2), the assigner's composition check for every -// node (ADR 0539 §6's check, ADR 0547 §4), and the grants part (ADR 0547 §5). Each reads the bus with its own credential +// compose.Input from the bus: the node agent for its own node (ADR 0547 §2), the seat mesh-placement's holder judging a +// proposal for every node (ADR 0567 §4), and the grants part (ADR 0547 §5). Each reads the bus with its own credential // through a Reader, so this package holds no bus client, no store and no timer: what it is given is all it reads. // // **What it reads, and from whom.** // - which modules run on each node, with their pins, holdings, host ports, bindings and settings placement: the -// indexer's state `placement`, one key per node (ADR 0549 §2); +// seat mesh-placement's state `placement`, one key per node (ADR 0567 §1); // - what a person decided of each node (its endpoint, site, hub, address on the private network, public domain and // accounts): the same placement key, Placement.Decided; // - what each node observes of itself (its tunnel key, capabilities, keys, certificate, links and networks): the @@ -107,9 +107,9 @@ type Mesh struct { // build of at the trunk's FP2 composes from the indexer's manifest, as before. Every leaves the catalogue as the indexer // read it: the check of every node judges placement, and reads no archive. // -// Placements, when set, stands in for the indexer's placement of the nodes it names: a node's candidate placement -// (delivery.PlacementCandidate), so a node composes in shadow with a catalogue pull request's placement and compares -// the result with what it runs (issue 1126); a nil value removes the node's placement. The rest is read as always. +// Placements, when set, stands in for the placement of the nodes it names: a proposal's candidates +// (delivery.PlacementCandidate), so a node composes in shadow with a proposal laid over and compares the result with +// what it runs (ADR 0567 §3); a nil value removes the node's placement. The rest is read as always. // // InOwn is the modules the composing node runs inside its own composition (InOwn of what it runs), composed there again // (compose.Node.InOwn) rather than cut out as modules no build is given for (issue 1126).