From aee971c1987acad70dc8f9e49e2a5380a0906753 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:21:02 +0200 Subject: [PATCH 01/13] Name a node's composition bucket, its keys, and the digest with the move's judgement beside it (issue 1943, S1) The controller writes one module's composition per node and fingerprint, and the assigner reads its digest and judgement before it writes a want (ADR 0539 clauses 1, 2 and 14). --- go/delivery/composed.go | 196 ++++++++++++++++++++++++++++++ go/delivery/composed_keys_test.go | 117 ++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 go/delivery/composed.go create mode 100644 go/delivery/composed_keys_test.go diff --git a/go/delivery/composed.go b/go/delivery/composed.go new file mode 100644 index 0000000..c5d30c7 --- /dev/null +++ b/go/delivery/composed.go @@ -0,0 +1,196 @@ +package delivery + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" +) + +// The controller's compositions (novox/hq ADR 0539 clauses 1 to 4). A **composition** is one module's resources +// composed for one node at one fingerprint, written by the controller, which keeps the inputs. It lives in a bucket of +// each node, `composed-`, which only that node's node-engine reads (a composition can carry a sealed value), under +// `..`, and the node's own resources (adoption, firewall openings, accounts, the +// resolver, identities and groups) under `.node`. Beside it, under the same key in `mesh-controller_composed`, +// which the assigner reads, the controller writes the composition's digest and its judgement of the move from the +// composition the node runs now (ComposedDigest). The controller writes both by compare-and-set while it holds the +// lease; the operator's credential is a second writer on the hand path alone (clause 10, mesh-compose). +// +// A composition is derived state (clause 4): its key and its digest are deleted by the controller when its fingerprint +// is no longer among the module's five newest and no want names it, and each bucket keeps five revisions of a key. +const ( + // ComposedBucketPrefix starts the name of a node's bucket of compositions, ComposedBucketFor(node). + ComposedBucketPrefix = "composed-" + // ComposedHistory is the revisions a node's composition bucket keeps per key (ADR 0539 clause 4). + ComposedHistory = 5 + // ControllerModule is the module of the controller, the writer of the compositions. + ControllerModule = "mesh-controller" + // ControllerComposedBucket is the controller's bucket of each composition's digest and judgement, `_`, read by the assigner (ADR 0539 clauses 2 and 6). + ControllerComposedBucket = ControllerModule + "_composed" + // ControllerComposedHistory is the revisions that bucket keeps per key. + ControllerComposedHistory = 5 + // ComposedKeep is how many of a module's newest fingerprints keep their compositions though no want names them + // (ADR 0539 clause 4). + ComposedKeep = 5 +) + +// nodeWord is the last part of a node's own key, in the have bucket and in a node's composition bucket alike. +const nodeWord = "node" + +var bucketNode = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +// ComposedBucketFor is the name of a node's bucket of compositions, "composed-", as declaration.FileBucketFor +// names a node's object bucket (ADR 0488 §4). The node name must be one a bucket and a subject token accept: letters, +// digits, '_' and '-'. +func ComposedBucketFor(node string) (string, error) { + if node == "" { + return "", fmt.Errorf("delivery: the node name is empty; a node's composition bucket is named for its node") + } + if !bucketNode.MatchString(node) { + return "", fmt.Errorf("delivery: the node name %q is not one a bucket accepts: letters, digits, '_' and '-' only", node) + } + return ComposedBucketPrefix + node, nil +} + +// ComposedKey is the key, in ComposedBucketFor(node) and in ControllerComposedBucket, of a module's composition for a +// node at a fingerprint: `..`, the hex being the fingerprint's key part (FingerprintKeyPart). A +// module named `node` is refused, since `.node` is the node's own key. +func ComposedKey(node, module, fingerprint string) (string, error) { + have, err := HaveKey(node, module) + if err != nil { + return "", err + } + part, err := FingerprintKeyPart(fingerprint) + if err != nil { + return "", err + } + return have + "." + part, nil +} + +// ComposedNodeKey is the key of a node's own composition: `.node`. +func ComposedNodeKey(node string) (string, error) { + return HaveNodeKey(node) +} + +// ParseComposedKey is the inverse of ComposedKey and ComposedNodeKey: isNode is true for `.node`, with module +// and fingerprint empty. It refuses any other shape, naming the key. +func ParseComposedKey(key string) (node, module, fingerprint string, isNode bool, err error) { + refused := func(why error) (string, string, string, bool, error) { + return "", "", "", false, fmt.Errorf("delivery: %q is not a composition key, .. or .node: %w", key, why) + } + parts := strings.Split(key, ".") + switch len(parts) { + case 2: + if parts[1] != nodeWord { + return refused(fmt.Errorf("a module's composition names its fingerprint")) + } + if err := checkName("node", parts[0]); err != nil { + return refused(err) + } + return parts[0], "", "", true, nil + case 3: + node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1]) + if err != nil { + return refused(err) + } + if isNode { + return refused(fmt.Errorf("the node's own key names no fingerprint")) + } + fp, err := ParseFingerprintKeyPart(parts[2]) + if err != nil { + return refused(err) + } + return node, module, fp, false, nil + } + return refused(fmt.Errorf("it has %d parts", len(parts))) +} + +// Judgement is the controller's judgement of a move from the composition a node runs, as its have names it, to a new +// one (ADR 0539 clauses 2 and 14, by ADR 0377 §3's tests). +type Judgement string + +// The judgements of a move. +const ( + // JudgedRecreates is a move that recreates a container. + JudgedRecreates Judgement = "recreates" + // JudgedMovesData is a move that gives the module's data another place. + JudgedMovesData Judgement = "moves-data" + // JudgedNeither is a move that does neither, and the judgement of a composition the node never ran. + JudgedNeither Judgement = "neither" + // JudgedCannot is a move that cannot be judged, because the composition the node runs is gone or cannot be read. + JudgedCannot Judgement = "cannot-judge" +) + +var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) + +func checkDigest(digest, of string) error { + if !digestForm.MatchString(digest) { + return fmt.Errorf("delivery: %s %q is not sha256:<64 lowercase hex digits>", of, digest) + } + return nil +} + +// ComposedDigest is the value, in ControllerComposedBucket, beside a composition: its Digest (ComposedDigestOf), and +// the Judgement of the move to it from the composition the node runs now, whose digest is Against (empty when the node +// runs no composition of the module). Words say why, and are required for a move that cannot be judged. Rewritten when +// the node's have changes. +type ComposedDigest struct { + V int `json:"v"` + Digest string `json:"digest"` + Judgement Judgement `json:"judgement"` + Against string `json:"against,omitempty"` + Words string `json:"words,omitempty"` +} + +func (c ComposedDigest) check() error { + if err := checkDigest(c.Digest, "the composition's digest"); err != nil { + return err + } + if c.Against != "" { + if err := checkDigest(c.Against, "the digest a move is judged against"); err != nil { + return err + } + } + switch c.Judgement { + case JudgedRecreates, JudgedMovesData: + if c.Against == "" { + return fmt.Errorf("delivery: a move judged %s names the composition it is judged against", c.Judgement) + } + case JudgedNeither: + case JudgedCannot: + if c.Words == "" { + return fmt.Errorf("delivery: a move that cannot be judged says why") + } + default: + return fmt.Errorf("delivery: the judgement %q is not %s, %s, %s or %s", c.Judgement, JudgedRecreates, + JudgedMovesData, JudgedNeither, JudgedCannot) + } + return nil +} + +// EncodeComposedDigest is c as JSON at Version; it refuses an unknown judgement, a digest not of the sha256 form, a +// move judged to recreate or move data against nothing, and a move that cannot be judged without its reason. +func EncodeComposedDigest(c ComposedDigest) ([]byte, error) { + if err := c.check(); err != nil { + return nil, err + } + c.V = Version + return json.Marshal(c) +} + +// DecodeComposedDigest reads the value beside a composition; it refuses an unknown version (ErrUnknownVersion) and +// what EncodeComposedDigest refuses. +func DecodeComposedDigest(data []byte) (ComposedDigest, error) { + var c ComposedDigest + if err := decode(data, &c); err != nil { + return ComposedDigest{}, err + } + if err := c.check(); err != nil { + return ComposedDigest{}, err + } + return c, nil +} + +// SameComposedDigest says whether two values are the same, so the controller writes only when they differ. +func SameComposedDigest(a, b ComposedDigest) bool { return a == b } diff --git a/go/delivery/composed_keys_test.go b/go/delivery/composed_keys_test.go new file mode 100644 index 0000000..dec6987 --- /dev/null +++ b/go/delivery/composed_keys_test.go @@ -0,0 +1,117 @@ +package delivery + +import ( + "errors" + "strings" + "testing" +) + +// 1943/AC1: a node's composition bucket is named for the node, and a node with no name, or one a bucket refuses, +// is refused, naming why. +func TestComposedBucketFor(t *testing.T) { + b, err := ComposedBucketFor("ace") + if err != nil || b != "composed-ace" { + t.Fatalf("ComposedBucketFor(ace) = %q, %v", b, err) + } + for _, bad := range []string{"", "a.b", "a*", "a b", "a>"} { + if _, err := ComposedBucketFor(bad); err == nil || !strings.Contains(err.Error(), "node") { + t.Errorf("ComposedBucketFor(%q) = %v, want a refusal naming the node", bad, err) + } + } +} + +// 1943/AC1: a module's composition key and the node's own key round-trip through one parser; an empty node, a +// module named node, a bad fingerprint and a key of another shape are refused, naming why. +func TestComposedKeys(t *testing.T) { + fp := fp2of('c') + key, err := ComposedKey("ace", "mesh-mail", fp) + if err != nil || key != "ace.mesh-mail."+strings.Repeat("c", 64) { + t.Fatalf("ComposedKey = %q, %v", key, err) + } + node, module, back, isNode, err := ParseComposedKey(key) + if err != nil || node != "ace" || module != "mesh-mail" || back != fp || isNode { + t.Errorf("ParseComposedKey(%q) = %q, %q, %q, %v, %v", key, node, module, back, isNode, err) + } + own, err := ComposedNodeKey("ace") + if err != nil || own != "ace.node" { + t.Fatalf("ComposedNodeKey = %q, %v", own, err) + } + node, module, back, isNode, err = ParseComposedKey(own) + if err != nil || node != "ace" || module != "" || back != "" || !isNode { + t.Errorf("ParseComposedKey(%q) = %q, %q, %q, %v, %v", own, node, module, back, isNode, err) + } + + refusals := []struct { + node, module, fp, why string + }{ + {"", "mesh-mail", fp, "node name is empty"}, + {"ace", "node", fp, `"node" is refused`}, + {"ace", "", fp, "module name is empty"}, + {"ace", "mesh-mail", "src2:abc", "not a fingerprint of the new form"}, + {"ace", "mesh-mail", "src1:" + strings.Repeat("c", 64), "not a fingerprint of the new form"}, + {"a.b", "mesh-mail", fp, "node name"}, + } + for _, r := range refusals { + if _, err := ComposedKey(r.node, r.module, r.fp); err == nil || !strings.Contains(err.Error(), r.why) { + t.Errorf("ComposedKey(%q, %q, %q) = %v, want a refusal saying %q", r.node, r.module, r.fp, err, r.why) + } + } + if _, err := ComposedNodeKey(""); err == nil { + t.Errorf("ComposedNodeKey(\"\") accepted") + } + for _, bad := range []string{"", "ace", "ace.mesh-mail", "ace.node.x", "ace.mesh-mail.abc", ".mesh-mail." + strings.Repeat("c", 64), + "ace.node." + strings.Repeat("c", 64), "ace.mesh-mail." + strings.Repeat("c", 64) + ".x", "want.ace.mesh-mail"} { + if _, _, _, _, err := ParseComposedKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("ParseComposedKey(%q) = %v, want a refusal naming it", bad, err) + } + } +} + +// 1943/AC1: the controller's value beside a composition, its digest and the judgement of the move, round-trips; an +// unknown judgement, a digest not of the sha256 form, a move judged against nothing, and a cannot-judge without its +// reason are refused, naming why; an unknown version is refused as one. +func TestComposedDigestValue(t *testing.T) { + digest := "sha256:" + strings.Repeat("d", 64) + against := "sha256:" + strings.Repeat("e", 64) + for _, v := range []ComposedDigest{ + {Digest: digest, Judgement: JudgedNeither}, + {Digest: digest, Judgement: JudgedNeither, Against: against}, + {Digest: digest, Judgement: JudgedRecreates, Against: against, Words: "the container's image changes"}, + {Digest: digest, Judgement: JudgedMovesData, Against: against}, + {Digest: digest, Judgement: JudgedCannot, Words: "the composition the node runs is gone"}, + } { + raw, err := EncodeComposedDigest(v) + if err != nil { + t.Fatalf("EncodeComposedDigest(%+v): %v", v, err) + } + back, err := DecodeComposedDigest(raw) + v.V = Version + if err != nil || !SameComposedDigest(back, v) { + t.Errorf("round trip of %+v = %+v, %v", v, back, err) + } + } + refusals := []struct { + v ComposedDigest + why string + }{ + {ComposedDigest{Digest: digest, Judgement: "maybe"}, `judgement "maybe"`}, + {ComposedDigest{Digest: digest}, `judgement ""`}, + {ComposedDigest{Digest: "sha256:abc", Judgement: JudgedNeither}, "not sha256"}, + {ComposedDigest{Judgement: JudgedNeither}, "not sha256"}, + {ComposedDigest{Digest: digest, Judgement: JudgedRecreates}, "against"}, + {ComposedDigest{Digest: digest, Judgement: JudgedMovesData}, "against"}, + {ComposedDigest{Digest: digest, Judgement: JudgedCannot}, "why"}, + {ComposedDigest{Digest: digest, Judgement: JudgedNeither, Against: "sha256:zz"}, "not sha256"}, + } + for _, r := range refusals { + if _, err := EncodeComposedDigest(r.v); err == nil || !strings.Contains(err.Error(), r.why) { + t.Errorf("EncodeComposedDigest(%+v) = %v, want a refusal saying %q", r.v, err, r.why) + } + } + if _, err := DecodeComposedDigest([]byte(`{"v":2,"digest":"` + digest + `","judgement":"neither"}`)); !errors.Is(err, ErrUnknownVersion) { + t.Errorf("version 2 = %v, want ErrUnknownVersion", err) + } + if _, err := DecodeComposedDigest([]byte(`{"v":1,"digest":"` + digest + `","judgement":"perhaps"}`)); err == nil { + t.Errorf("an unknown judgement was read") + } +} From 826fd73cf2ffa1c1393f6f44871a1766152be2fb Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:26:32 +0200 Subject: [PATCH 02/13] Give a composition its value, its canonical digest and its files by digest; key a dotted module escaped (issue 1943, S2; issue 1150) A node applies one module's composition at a time, so each carries only its own module's resources, checked resource by resource as the node-engine reads them, and its digest follows its content alone (ADR 0539 clauses 1 and 4). The fixtures are the controller's plan answers for ace and g14, their sealed resources taken out. --- go/declaration/resource.go | 47 +++ go/delivery/composed_value.go | 298 +++++++++++++++++ go/delivery/composed_value_test.go | 317 +++++++++++++++++++ go/delivery/delivery.go | 48 ++- go/delivery/testdata/declaration-ace.json.gz | Bin 0 -> 74424 bytes go/delivery/testdata/declaration-g14.json.gz | Bin 0 -> 45025 bytes 6 files changed, 703 insertions(+), 7 deletions(-) create mode 100644 go/declaration/resource.go create mode 100644 go/delivery/composed_value.go create mode 100644 go/delivery/composed_value_test.go create mode 100644 go/delivery/testdata/declaration-ace.json.gz create mode 100644 go/delivery/testdata/declaration-g14.json.gz diff --git a/go/declaration/resource.go b/go/declaration/resource.go new file mode 100644 index 0000000..8733768 --- /dev/null +++ b/go/declaration/resource.go @@ -0,0 +1,47 @@ +package declaration + +import ( + "encoding/json" + "fmt" +) + +// ParseResource reads one resource as Parse reads each of a declaration's resources: by its type, strictly, refusing +// a field its kind does not use and anything its kind refuses, and never an action (the link may not carry one, ADR +// 0005). It judges the resource alone: what Parse judges across resources (an opening on a node not adopted, the +// files the provisioner fills, what replaces what) is for the whole a node applies (novox/hq ADR 0539 clause 1: a +// composition is one module's resources, and a node applies its compositions together with its own key). +func ParseResource(raw json.RawMessage) (Resource, error) { + var head struct { + ID string `json:"id"` + Type Type `json:"type"` + } + if err := json.Unmarshal(raw, &head); err != nil { + return nil, &RefusalError{Problems: []string{"not a resource: " + err.Error()}} + } + where := "resource" + if head.ID == "" { + return nil, &RefusalError{Problems: []string{where + ": no id. Identity is what lets the host know this is " + + "the same resource it applied last time"}} + } + where = fmt.Sprintf("resource %q", head.ID) + resource := newOf(head.Type) + if resource == nil { + return nil, &RefusalError{Problems: []string{fmt.Sprintf("%s: unknown type %q. This host understands %s", + where, head.Type, vocabulary())}} + } + if unknown := unknownFields(raw, resource); len(unknown) > 0 { + var problems []string + for _, field := range unknown { + problems = append(problems, fmt.Sprintf("%s: a %s does not use %q, and it is set. Refused rather "+ + "than ignored", where, head.Type, field)) + } + return nil, &RefusalError{Problems: problems} + } + if err := json.Unmarshal(raw, resource); err != nil { + return nil, &RefusalError{Problems: []string{fmt.Sprintf("%s: %s", where, err)}} + } + if problems := resource.validate(where, false); len(problems) > 0 { + return nil, &RefusalError{Problems: problems} + } + return resource, nil +} diff --git a/go/delivery/composed_value.go b/go/delivery/composed_value.go new file mode 100644 index 0000000..8ec92bc --- /dev/null +++ b/go/delivery/composed_value.go @@ -0,0 +1,298 @@ +package delivery + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + + "git.novox.be/novox/mesh-sdk/go/declaration" +) + +// NodeOwn is what a node's own composition (`.node`) carries beside its resources: what the declaration says of +// the node as a whole (novox/hq ADR 0539 clause 2: adoption, accounts, identities and groups). A module's composition +// carries none of it. +type NodeOwn struct { + // Adoption says the node is adopted and which of its modules are taken (ADR 0100, ADR 0103); nil for a converged node. + Adoption *declaration.Adoption `json:"adoption,omitempty"` + // Identities is the identity of every module assigned to the node (ADR 0328 §4), Groups and Libraries every library + // it is reached by (ADR 0328 §3). + Identities []declaration.Identity `json:"identities,omitempty"` + Groups []declaration.Group `json:"groups,omitempty"` + Libraries []declaration.Library `json:"libraries,omitempty"` + // Provisioner is the account the provisioner's client runs as on the node (ADR 0349 §4). + Provisioner string `json:"provisioner_account,omitempty"` +} + +func (o NodeOwn) empty() bool { + return o.Adoption == nil && len(o.Identities) == 0 && len(o.Groups) == 0 && len(o.Libraries) == 0 && o.Provisioner == "" +} + +// Composed is a composition (ADR 0539 clause 1): one module's resources composed for one node at one fingerprint, or, +// with Module and FP empty, the node's own resources and parts (Own). Each resource is a declaration resource as the +// node-engine reads it, saying its module; it is kept as JSON and written canonically (its keys sorted, its spacing +// dropped), so its digest (ComposedDigestOf) is a function of its content alone. Data says the module declares data, +// so the node agent backs it up before it moves (ADR 0429 §14). Filled names the files of the module the provisioner +// fills (ADR 0349 §5). A file over 64 KiB travels by digest and size (ByDigest), its content in the node's object +// bucket (ADR 0539 clause 4). +type Composed struct { + V int + Node string + Module string + FP string + Data bool + Resources []json.RawMessage + Filled []string + Own NodeOwn +} + +// composedWire is a composition as it is written. +type composedWire struct { + V int `json:"v"` + Node string `json:"node"` + Module string `json:"module,omitempty"` + FP string `json:"fp,omitempty"` + Data bool `json:"data,omitempty"` + Resources []json.RawMessage `json:"resources"` + Filled []string `json:"filled_files,omitempty"` + Own *NodeOwn `json:"own,omitempty"` +} + +// canonicalJSON is raw with its object keys sorted and its spacing dropped, its numbers kept as written. +func canonicalJSON(raw json.RawMessage) (json.RawMessage, error) { + var v any + d := json.NewDecoder(bytes.NewReader(raw)) + d.UseNumber() + if err := d.Decode(&v); err != nil { + return nil, err + } + if _, err := d.Token(); err != io.EOF { + return nil, fmt.Errorf("more after the value") + } + return json.Marshal(v) +} + +func (c Composed) check() error { + if _, err := HaveNodeKey(c.Node); err != nil { + return err + } + if c.Module == "" { + if c.FP != "" { + return fmt.Errorf("delivery: the node's own composition names no fingerprint, and this one names %s", c.FP) + } + if c.Data || len(c.Filled) > 0 { + return fmt.Errorf("delivery: the node's own composition holds no module's data or filled files") + } + } else { + if _, err := ComposedKey(c.Node, c.Module, c.FP); err != nil { + return err + } + if !c.Own.empty() { + return fmt.Errorf("delivery: the composition of %s carries the node's own parts, which only .node carries", c.Module) + } + } + seen := map[string]bool{} + files := map[string][]*declaration.File{} + for _, raw := range c.Resources { + r, err := declaration.ParseResource(raw) + if err != nil { + return fmt.Errorf("delivery: the composition of %q on %s: %w", c.Module, c.Node, err) + } + if seen[r.Identity()] { + return fmt.Errorf("delivery: the composition of %q on %s holds the resource %q twice", c.Module, c.Node, r.Identity()) + } + seen[r.Identity()] = true + if r.DeclaredModule() != c.Module { + return fmt.Errorf("delivery: the resource %q is of %q, and this is the composition of %q on %s", + r.Identity(), r.DeclaredModule(), c.Module, c.Node) + } + if f, ok := r.(*declaration.File); ok { + if f.TravelsByDigest() { + return fmt.Errorf("delivery: the file %q is over %d bytes and travels by digest (ByDigest), not whole", + f.ID, declaration.FileReferenceThreshold) + } + files[f.Path] = append(files[f.Path], f) + } + } + for _, path := range c.Filled { + if len(files[path]) == 0 { + return fmt.Errorf("delivery: the composition of %q fills %q, and declares no file there", c.Module, path) + } + for _, f := range files[path] { + if f.OwnerAtRuntime == "" { + return fmt.Errorf("delivery: the file %q is filled by the provisioner and says no owner-at-runtime", f.ID) + } + } + } + return nil +} + +// wire is c checked and made canonical, ready to marshal. +func (c Composed) wire() (composedWire, error) { + if err := c.check(); err != nil { + return composedWire{}, err + } + w := composedWire{V: Version, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, Filled: c.Filled, + Resources: make([]json.RawMessage, len(c.Resources))} + for i, r := range c.Resources { + canon, err := canonicalJSON(r) + if err != nil { + return composedWire{}, fmt.Errorf("delivery: a resource of %q is not JSON: %w", c.Module, err) + } + w.Resources[i] = canon + } + if !c.Own.empty() { + own := c.Own + w.Own = &own + } + return w, nil +} + +// EncodeComposed is c as JSON at Version, each resource canonical. It refuses a composition whose node or fingerprint +// a key refuses, a resource its kind refuses (an action among them), a resource of another module, a module's +// composition carrying the node's own parts, a file over 64 KiB carried whole, and a filled path no file declares. +func EncodeComposed(c Composed) ([]byte, error) { + w, err := c.wire() + if err != nil { + return nil, err + } + return json.Marshal(w) +} + +// DecodeComposed reads a composition; it refuses an unknown version (ErrUnknownVersion), a field it does not know, and +// what EncodeComposed refuses. +func DecodeComposed(data []byte) (Composed, error) { + if err := checkVersion(data); err != nil { + return Composed{}, err + } + var w composedWire + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + if err := d.Decode(&w); err != nil { + return Composed{}, fmt.Errorf("delivery: a composition does not fit its shape: %w", err) + } + c := Composed{V: w.V, Node: w.Node, Module: w.Module, FP: w.FP, Data: w.Data, Resources: w.Resources, + Filled: w.Filled} + if w.Own != nil { + c.Own = *w.Own + } + if err := c.check(); err != nil { + return Composed{}, err + } + return c, nil +} + +// ComposedDigestOf is a composition's digest, "sha256:", of its encoding. A want names it (ADR 0539 clause 1), +// and the controller writes it beside the composition. +func ComposedDigestOf(c Composed) (string, error) { + raw, err := EncodeComposed(c) + if err != nil { + return "", err + } + sum := sha256.Sum256(raw) + return "sha256:" + hex.EncodeToString(sum[:]), nil +} + +// SameComposed says whether two compositions are the same value, so the controller writes +// only when they differ. +func SameComposed(a, b Composed) bool { + ra, errA := EncodeComposed(a) + rb, errB := EncodeComposed(b) + return errA == nil && errB == nil && bytes.Equal(ra, rb) +} + +// ByDigest is c with every file over 64 KiB given by its digest and size, and the content of each by digest, for the +// writer to put in the node's object bucket (declaration.FileBucketFor) under declaration.ObjectName before it writes +// the composition (ADR 0539 clause 4, ADR 0488 §3). A file that is sealed, filled with secrets or written into another +// stays as it is (declaration.File.TravelsByDigest). +func (c Composed) ByDigest() (Composed, map[string][]byte, error) { + out := c + out.Resources = make([]json.RawMessage, len(c.Resources)) + objects := map[string][]byte{} + for i, raw := range c.Resources { + out.Resources[i] = raw + r, err := declaration.ParseResource(raw) + if err != nil { + return Composed{}, nil, err + } + f, ok := r.(*declaration.File) + if !ok || !f.TravelsByDigest() { + continue + } + ref, body, err := f.AsReference() + if err != nil { + return Composed{}, nil, err + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(raw, &fields); err != nil { + return Composed{}, nil, err + } + delete(fields, "content") + delete(fields, "bytes") + fields["digest"], _ = json.Marshal(ref.Digest) + fields["size"], _ = json.Marshal(ref.Size) + rewritten, err := json.Marshal(fields) + if err != nil { + return Composed{}, nil, err + } + out.Resources[i] = rewritten + objects[ref.Digest] = body + } + return out, objects, nil +} + +// SplitComposed splits a node's composed resources into its compositions, by key (ComposedKey, ComposedNodeKey): each +// resource into the composition of the module it says, under that module's fingerprint in fps, and a resource that +// says no module into the node's own, with own. Each filled path goes with the module whose file declares it. The order +// of the resources is kept within each composition. A resource of a module fps names no fingerprint for is refused, +// naming it. Data is the caller's to set, from the module's manifest. +func SplitComposed(node string, resources []json.RawMessage, fps map[string]string, filled []string, own NodeOwn) (map[string]Composed, error) { + nodeKey, err := ComposedNodeKey(node) + if err != nil { + return nil, err + } + out := map[string]Composed{nodeKey: {Node: node, Own: own, Resources: []json.RawMessage{}}} + pathOf := map[string]string{} + for _, raw := range resources { + var head struct { + ID string `json:"id"` + Type string `json:"type"` + Module string `json:"module"` + Path string `json:"path"` + } + if err := json.Unmarshal(raw, &head); err != nil { + return nil, fmt.Errorf("delivery: a resource of %s is not JSON: %w", node, err) + } + key := nodeKey + if head.Module != "" { + fp, ok := fps[head.Module] + if !ok { + return nil, fmt.Errorf("delivery: the resource %q is of %q, whose fingerprint on %s is not given", head.ID, head.Module, node) + } + if key, err = ComposedKey(node, head.Module, fp); err != nil { + return nil, err + } + if _, ok := out[key]; !ok { + out[key] = Composed{Node: node, Module: head.Module, FP: fp} + } + } + if head.Type == string(declaration.TypeFile) && head.Module != "" { + pathOf[head.Path] = key + } + c := out[key] + c.Resources = append(c.Resources, raw) + out[key] = c + } + for _, path := range filled { + key, ok := pathOf[path] + if !ok { + return nil, fmt.Errorf("delivery: the provisioner fills %q on %s, and no module's file declares it", path, node) + } + c := out[key] + c.Filled = append(c.Filled, path) + out[key] = c + } + return out, nil +} diff --git a/go/delivery/composed_value_test.go b/go/delivery/composed_value_test.go new file mode 100644 index 0000000..238c495 --- /dev/null +++ b/go/delivery/composed_value_test.go @@ -0,0 +1,317 @@ +package delivery + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "sort" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/declaration" +) + +// capturedDeclaration is a node's declaration as the controller's `plan` verb answered it on 2026-10-24 at 16:19 +// (testdata/declaration-.json.gz; the resources carrying a sealed value or a secret taken out, and the filled +// files that named them), never typed by hand. +type capturedDeclaration struct { + Builds map[string]string `json:"builds"` + Resources []json.RawMessage `json:"resources"` + Filled []string `json:"filled_files"` + Provisioner string `json:"provisioner_account"` +} + +func readCaptured(t *testing.T, node string) capturedDeclaration { + t.Helper() + f, err := os.Open("testdata/declaration-" + node + ".json.gz") + if err != nil { + t.Fatal(err) + } + defer f.Close() + z, err := gzip.NewReader(f) + if err != nil { + t.Fatal(err) + } + raw, err := io.ReadAll(z) + if err != nil { + t.Fatal(err) + } + var c capturedDeclaration + if err := json.Unmarshal(raw, &c); err != nil { + t.Fatal(err) + } + return c +} + +// fpOfBuild stands for each module's fingerprint in a captured declaration, which names builds, not fingerprints. +func fpOfBuild(build string) string { + var b strings.Builder + for b.Len() < 64 { + b.WriteString(fmt.Sprintf("%x", []byte(build))) + } + return SourcePrefix2 + b.String()[:64] +} + +func splitCaptured(t *testing.T, node string, c capturedDeclaration) map[string]Composed { + t.Helper() + fps := map[string]string{} + for m, b := range c.Builds { + fps[m] = fpOfBuild(b) + } + // mesh-wireguard's resources are composed with no build of it named; its fingerprint is the indexer's, as every + // module's is, and stood in for here by its name. + for _, r := range c.Resources { + var head struct { + Module string `json:"module"` + } + _ = json.Unmarshal(r, &head) + if _, ok := fps[head.Module]; head.Module != "" && !ok { + fps[head.Module] = fpOfBuild(head.Module) + } + } + split, err := SplitComposed(node, c.Resources, fps, c.Filled, NodeOwn{Provisioner: c.Provisioner}) + if err != nil { + t.Fatalf("SplitComposed(%s): %v", node, err) + } + return split +} + +func canonical(t *testing.T, raw json.RawMessage) string { + t.Helper() + var v any + d := json.NewDecoder(bytes.NewReader(raw)) + d.UseNumber() + if err := d.Decode(&v); err != nil { + t.Fatal(err) + } + out, _ := json.Marshal(v) + return string(out) +} + +// 1943/AC2: each module's slice of a captured declaration of each node, and the node's own resources, round-trip +// through the composition's encoding with the same digest; together they are the declaration's resources, each once, +// each in the composition of the module it says. +func TestCompositionsOfACapturedDeclaration(t *testing.T) { + for _, node := range []string{"ace", "g14"} { + c := readCaptured(t, node) + split := splitCaptured(t, node, c) + modules := map[string]bool{} + for m := range c.Builds { + modules[m] = true + } + var union []string + for key, comp := range split { + n, module, fp, isNode, err := ParseComposedKey(key) + if err != nil || n != node || comp.Node != node || comp.Module != module || comp.FP != fp { + t.Fatalf("%s: key %q holds %s/%s/%s (%v)", node, key, comp.Node, comp.Module, comp.FP, err) + } + if !isNode && !modules[module] && module != "mesh-wireguard" { + t.Errorf("%s: a composition of %s, which the declaration names no build of", node, module) + } + raw, err := EncodeComposed(comp) + if err != nil { + t.Fatalf("%s: EncodeComposed(%s): %v", node, key, err) + } + back, err := DecodeComposed(raw) + if err != nil { + t.Fatalf("%s: DecodeComposed(%s): %v", node, key, err) + } + if !SameComposed(back, comp) { + t.Errorf("%s: %s did not round-trip", node, key) + } + d1, err1 := ComposedDigestOf(comp) + d2, err2 := ComposedDigestOf(back) + if err1 != nil || err2 != nil || d1 != d2 || !digestForm.MatchString(d1) { + t.Errorf("%s: %s digests %q and %q (%v, %v)", node, key, d1, d2, err1, err2) + } + again, _ := EncodeComposed(back) + if !bytes.Equal(raw, again) { + t.Errorf("%s: %s encodes differently once read back", node, key) + } + for _, r := range back.Resources { + var head struct { + Module string `json:"module"` + } + _ = json.Unmarshal(r, &head) + if head.Module != module { + t.Errorf("%s: %s holds a resource of %q", node, key, head.Module) + } + union = append(union, canonical(t, r)) + } + } + var want []string + for _, r := range c.Resources { + want = append(want, canonical(t, r)) + } + sort.Strings(union) + sort.Strings(want) + if strings.Join(union, "\n") != strings.Join(want, "\n") { + t.Errorf("%s: the compositions hold %d resources, the declaration %d, or they differ", node, len(union), len(want)) + } + } +} + +// 1943/AC2: one changed byte of a resource is another digest; the same composition written with its keys in another +// order is the same digest. +func TestComposedDigestFollowsTheContent(t *testing.T) { + c := readCaptured(t, "g14") + split := splitCaptured(t, "g14", c) + var comp Composed + for _, v := range split { + if v.Module != "" && len(v.Resources) > 1 { + comp = v + break + } + } + d1, _ := ComposedDigestOf(comp) + var m map[string]json.RawMessage + _ = json.Unmarshal(comp.Resources[0], &m) + var keys []string + for k := range m { + keys = append(keys, k) + } + sort.Sort(sort.Reverse(sort.StringSlice(keys))) + var parts []string + for _, k := range keys { + parts = append(parts, fmt.Sprintf("%q : %s", k, m[k])) + } + reordered := json.RawMessage("{ " + strings.Join(parts, ",\n ") + " }") + shuffled := comp + shuffled.Resources = append([]json.RawMessage{reordered}, comp.Resources[1:]...) + if d2, _ := ComposedDigestOf(shuffled); d2 != d1 { + t.Errorf("the same resources in another order and spacing digest %s and %s", d1, d2) + } + changed := comp + changed.Resources = append([]json.RawMessage{json.RawMessage(strings.Replace(canonical(t, comp.Resources[0]), `"id":"`, `"id":"x`, 1))}, comp.Resources[1:]...) + if d3, _ := ComposedDigestOf(changed); d3 == d1 { + t.Errorf("a changed resource kept its digest") + } + revised := comp + revised.Data = !comp.Data + if d4, _ := ComposedDigestOf(revised); d4 == d1 { + t.Errorf("a changed data flag kept its digest") + } +} + +// 1943/AC2: a file over 64 KiB is carried by digest and size, its content handed back to be put in the node's object +// bucket; a composition that carries such a file whole is refused. +func TestAComposedFileOver64KiBIsCarriedByDigest(t *testing.T) { + c := readCaptured(t, "g14") + split := splitCaptured(t, "g14", c) + var comp Composed + var at int + for _, v := range split { + for i, r := range v.Resources { + var f map[string]any + _ = json.Unmarshal(r, &f) + if f["type"] == "file" && v.Module != "" && f["bytes"] == nil && f["into"] == nil { + comp, at = v, i + break + } + } + if comp.Module != "" { + break + } + } + var f map[string]any + _ = json.Unmarshal(comp.Resources[at], &f) + // Grown past the threshold from a captured file: no captured file of this node is over 64 KiB. + body := strings.Repeat("x", declaration.FileReferenceThreshold+1) + f["content"] = body + big, _ := json.Marshal(f) + comp.Resources = append([]json.RawMessage{}, comp.Resources...) + comp.Resources[at] = big + if _, err := EncodeComposed(comp); err == nil || !strings.Contains(err.Error(), "by digest") { + t.Fatalf("a file over 64 KiB carried whole = %v, want refused", err) + } + byDigest, objects, err := comp.ByDigest() + if err != nil { + t.Fatal(err) + } + var ref map[string]any + _ = json.Unmarshal(byDigest.Resources[at], &ref) + digest := declaration.DigestOf([]byte(body)) + if ref["digest"] != digest || ref["size"] != float64(len(body)) || ref["content"] != nil { + t.Errorf("the file became %v, want digest %s and size %d", ref, digest, len(body)) + } + if string(objects[digest]) != body || len(objects) != 1 { + t.Errorf("the objects handed back are %d, want the file's content under its digest", len(objects)) + } + if _, err := EncodeComposed(byDigest); err != nil { + t.Errorf("the composition by digest is refused: %v", err) + } +} + +// 1943/AC2: an unknown version is refused as one; a module's composition carrying the node's own parts, a resource of +// another module, a resource its kind refuses, or no fingerprint, is refused, naming why. +func TestComposedRefusals(t *testing.T) { + fp := fp2of('a') + file := json.RawMessage(`{"id":"m.f","type":"file","module":"m","path":"/etc/m","content":"x"}`) + ok := Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{file}} + raw, err := EncodeComposed(ok) + if err != nil { + t.Fatal(err) + } + if _, err := DecodeComposed(bytes.Replace(raw, []byte(`"v":1`), []byte(`"v":2`), 1)); !errors.Is(err, ErrUnknownVersion) { + t.Errorf("version 2 = %v, want ErrUnknownVersion", err) + } + refusals := []struct { + c Composed + why string + }{ + {Composed{Node: "ace", Module: "m", Resources: []json.RawMessage{file}}, "fingerprint"}, + {Composed{Node: "", Module: "m", FP: fp, Resources: []json.RawMessage{file}}, "node name is empty"}, + {Composed{Node: "ace", Module: "other", FP: fp, Resources: []json.RawMessage{file}}, `of "m"`}, + {Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{file}, Own: NodeOwn{Provisioner: "p"}}, "node's own"}, + {Composed{Node: "ace", FP: fp, Resources: []json.RawMessage{}}, "names no fingerprint"}, + {Composed{Node: "ace", Resources: []json.RawMessage{file}}, `of "m"`}, + {Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{json.RawMessage(`{"id":"m.f","type":"file","module":"m","path":"/etc/m","content":"x","image":"y"}`)}}, `"image"`}, + {Composed{Node: "ace", Module: "m", FP: fp, Resources: []json.RawMessage{json.RawMessage(`{"id":"m.a","type":"action","module":"m"}`)}}, "action"}, + } + for _, r := range refusals { + if _, err := EncodeComposed(r.c); err == nil || !strings.Contains(err.Error(), r.why) { + t.Errorf("EncodeComposed(%s/%s) = %v, want a refusal saying %q", r.c.Node, r.c.Module, err, r.why) + } + } +} + +// 1943/AC2 (issue 1150): a module whose name holds a dot keys its have, its want and its composition escaped, and the +// parsers give its name back; a name with a wildcard, whitespace or an escape of its own is still refused. +func TestADottedModuleKeysEscaped(t *testing.T) { + fp := fp2of('f') + have, err := HaveKey("ace", "novox.be") + if err != nil || have != "ace.novox=2ebe" { + t.Fatalf("HaveKey = %q, %v", have, err) + } + if n, m, isNode, err := ParseHaveKey(have); err != nil || n != "ace" || m != "novox.be" || isNode { + t.Errorf("ParseHaveKey(%q) = %q, %q, %v, %v", have, n, m, isNode, err) + } + want, err := WantKey("ace", "novox.be") + if err != nil || want != "want.ace.novox=2ebe" { + t.Fatalf("WantKey = %q, %v", want, err) + } + if n, m, err := ParseWantKey(want); err != nil || n != "ace" || m != "novox.be" { + t.Errorf("ParseWantKey(%q) = %q, %q, %v", want, n, m, err) + } + comp, err := ComposedKey("ace", "novox.be", fp) + if err != nil || comp != "ace.novox=2ebe."+strings.Repeat("f", 64) { + t.Fatalf("ComposedKey = %q, %v", comp, err) + } + if n, m, back, _, err := ParseComposedKey(comp); err != nil || n != "ace" || m != "novox.be" || back != fp { + t.Errorf("ParseComposedKey(%q) = %q, %q, %q, %v", comp, n, m, back, err) + } + for _, bad := range []string{"m*", "m x", "m=2e", "m>", ".m", "m.", "a..b"} { + if _, err := HaveKey("ace", bad); err == nil { + t.Errorf("HaveKey(ace, %q) accepted", bad) + } + } + for _, bad := range []string{"ace.m=2", "ace.m=2E", "ace.m=41", "ace.=6eode"} { + if _, _, _, err := ParseHaveKey(bad); err == nil { + t.Errorf("ParseHaveKey(%q) accepted", bad) + } + } +} diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index e4e541d..71ccf75 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -11,7 +11,8 @@ // `node`. A presence key is `` for a node and `part..` for a part's instance, so a node may // not be named `part`. A pulse key is `.`: the full name of the bucket watched, then that bucket's key, // which may hold dots, so it splits at the first dot. A name is refused when it is empty or holds a dot, a wildcard, -// whitespace or a character a bus key refuses, and the error names it. +// whitespace or a character a bus key refuses, and the error names it; but a module's name may hold dots between its +// parts (`novox.be`), and keys with each dot escaped as EscapeName writes it (`novox=2ebe`, issue 1150). // // **Values** are JSON of under about 500 bytes, each carrying its version in `v`. A reader refuses a value whose // version is missing or one it does not know, with an error that is ErrUnknownVersion (research 063 section 05 @@ -35,7 +36,7 @@ // builder's build keys (builder.go) and the checker's verdicts and kept verdicts (checker.go). A manifests value is the // one exception to the size above: a manifest is up to about 21 KB, whole, byte for byte. // -// Standard library only, and this module's own asks package. +// Standard library only, and this module's own asks and declaration packages (a composition holds declaration resources). package delivery import ( @@ -164,13 +165,45 @@ func HaveKey(node, module string) (string, error) { if err := checkName("node", node); err != nil { return "", err } - if err := checkName("module", module); err != nil { + part, err := moduleKeyPart(module) + if err != nil { return "", err } if module == "node" { return "", fmt.Errorf(`delivery: the module name "node" is refused: %s.node is the node's own key`, node) } - return node + "." + module, nil + return node + "." + part, nil +} + +var moduleNameOK = regexp.MustCompile(`^[-/_a-zA-Z0-9]+(\.[-/_a-zA-Z0-9]+)*$`) + +// moduleKeyPart is a module's name as a key part (issue 1150): a name may hold a dot (`novox.be`), which a key part +// may not, so a dot is escaped as EscapeName writes it (`novox=2ebe`); a name of letters, digits, `-`, `/` and `_` keys +// as it is. A name that is empty, or holds a wildcard, whitespace, `=`, or an empty part between dots, is refused. +func moduleKeyPart(module string) (string, error) { + if module == "" { + return "", fmt.Errorf("delivery: the module name is empty") + } + if !moduleNameOK.MatchString(module) { + return "", fmt.Errorf("delivery: the module name %q holds a wildcard, whitespace, '=', an empty part between dots or a character a key refuses", module) + } + return EscapeName(module), nil +} + +// parseModuleKeyPart is the inverse of moduleKeyPart; it refuses a part that is not the one moduleKeyPart writes for +// its name (an escape of a character that keys as it is, an upper-case escape, an escape of a refused character). +func parseModuleKeyPart(part string) (string, error) { + if err := checkName("module", part); err != nil { + return "", err + } + module, err := UnescapeName(part) + if err != nil { + return "", err + } + if back, err := moduleKeyPart(module); err != nil || back != part { + return "", fmt.Errorf("delivery: the module part %q is not a module name as a key writes it", part) + } + return module, nil } // HaveNodeKey is the key, in HaveBucket, of a node's own facts: `.node`. @@ -190,12 +223,13 @@ func ParseHaveKey(key string) (node, module string, isNode bool, err error) { if err := checkName("node", node); err != nil { return "", "", false, fmt.Errorf("delivery: %q is not a have key: %w", key, err) } - if err := checkName("module", module); err != nil { - return "", "", false, fmt.Errorf("delivery: %q is not a have key: %w", key, err) - } if module == "node" { return node, "", true, nil } + module, err = parseModuleKeyPart(module) + if err != nil { + return "", "", false, fmt.Errorf("delivery: %q is not a have key: %w", key, err) + } return node, module, false, nil } diff --git a/go/delivery/testdata/declaration-ace.json.gz b/go/delivery/testdata/declaration-ace.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..11f4a417b86fc45786453a439e22e77418906164 GIT binary patch literal 74424 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%0D%X@ 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%0DJHbv92P+m#{&wu6*)gU}1 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%0AYbVw_N_N{X#+ z)&9J6y4mPtc@5NsSyUR`16~m2Wkm{Yhs@<*X}%0AF2j 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 literal 0 HcmV?d00001 From d6cef5829bb7d02e74cf922ff52109e048c1fc5a Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:29:24 +0200 Subject: [PATCH 03/13] Have a want name its composition and a backup answer; give the node its own want, the seventh rule, the held phase and the backup and record asks (issue 1943, S3) The node agent applies only the composition its want names, and the assigner writes a want only once that composition exists (ADR 0539 clauses 1, 6, 14 and 16). --- go/delivery/assigner.go | 124 ++++++++++++++++++--- go/delivery/assigner_test.go | 6 +- go/delivery/delivery.go | 46 +++++--- go/delivery/view_answers.go | 74 +++++++++++-- go/delivery/want_composed_test.go | 176 ++++++++++++++++++++++++++++++ 5 files changed, 382 insertions(+), 44 deletions(-) create mode 100644 go/delivery/want_composed_test.go diff --git a/go/delivery/assigner.go b/go/delivery/assigner.go index 46560f2..b009088 100644 --- a/go/delivery/assigner.go +++ b/go/delivery/assigner.go @@ -1,6 +1,7 @@ package delivery import ( + "bytes" "encoding/json" "fmt" "reflect" @@ -16,6 +17,10 @@ import ( // only compared; the node agent applying them is release 6 (ADR 0429 §7). Only the assigner's credential writes these // buckets; the watchman, the view and the controller read them. // +// **Release 6** (ADR 0539 clauses 1, 6 and 16): a want also names the digest of the one composition it allows, written +// only once that composition exists (the seventh rule, RuleComposed), and carries the answer to a failed backup; the +// node's own want `want..node` names the digest of the node's own composition (NodeWant). +// // **The bucket is the first word, the key the rest** (as the builder's `builds..` is the key // `.` in its builds bucket): the differences bucket's keys are `.`. The wants bucket holds // two kinds of key, so each keeps its word: `want..` and `held..`. @@ -50,7 +55,33 @@ func WantKey(node, module string) (string, error) { return wantWord + "." + have, nil } -// ParseWantKey is the inverse of WantKey; it refuses a held key, a have key and any key of another bucket, naming it. +// WantNodeKey is the key, in AssignerWantsBucket, of a node's own want: `want..node`, naming the digest of the +// node's own composition (ADR 0539 clause 6; ADR 0429 §4's node key). The assigner writes it once the node's own +// composition exists. +func WantNodeKey(node string) (string, error) { + own, err := HaveNodeKey(node) + if err != nil { + return "", err + } + return wantWord + "." + own, nil +} + +// ParseWantNodeKey is the inverse of WantNodeKey; it refuses a module's want key and any key of another bucket, +// naming it. +func ParseWantNodeKey(key string) (node string, err error) { + rest, ok := strings.CutPrefix(key, wantWord+".") + if !ok { + return "", fmt.Errorf("delivery: %q is not a node's own want key, want..node", key) + } + node, _, isNode, err := ParseHaveKey(rest) + if err != nil || !isNode { + return "", fmt.Errorf("delivery: %q is not a node's own want key, want..node", key) + } + return node, nil +} + +// ParseWantKey is the inverse of WantKey; it refuses a held key, a have key, the node's own want key and any key of +// another bucket, naming it. func ParseWantKey(key string) (node, module string, err error) { rest, ok := strings.CutPrefix(key, wantWord+".") if !ok { @@ -100,7 +131,8 @@ func ParseDifferenceKey(key string) (node, module string, err error) { return node, module, nil } -// The six rules of ADR 0429 §6, by number, as a want or a difference names the one that holds a node back. +// The rules of ADR 0429 §6 and ADR 0539 clause 6, by number, as a want or a difference names the one that holds a node +// back. const ( RuleBuildDone = 1 // the build of F is done RuleNotHeld = 2 // F is not held @@ -108,15 +140,25 @@ const ( RuleStep = 4 // at most step other nodes are ahead (step 0: no limit, ADR 0528 §3) RuleProviders = 5 // every provider the module requires runs healthy on that node, or at its canary RuleGrant = 6 // the module's credential for F exists (grants..) + RuleComposed = 7 // the module's composition for that node at F exists (mesh-controller_composed, ADR 0539 §6) ) func checkRule(rule int, of string) error { - if rule < RuleBuildDone || rule > RuleGrant { - return fmt.Errorf("delivery: %s names the rule %d, which is not one of the six (1 to 6)", of, rule) + if rule < RuleBuildDone || rule > RuleComposed { + return fmt.Errorf("delivery: %s names the rule %d, which is not one of the seven (1 to 7)", of, rule) } return nil } +// The answers a want carries to a failed backup before an apply (ADR 0539 clause 16): the node agent reads them from +// its own want and from nothing else. +const ( + // BackupAgain is the answer to back the module up again before the apply. + BackupAgain = "again" + // BackupSkip is the answer to go on without a backup. + BackupSkip = "skip" +) + func checkFingerprint(fp, of string) error { if _, err := FingerprintKeyPart(fp); err != nil { return fmt.Errorf("delivery: %s: %w", of, err) @@ -124,29 +166,44 @@ func checkFingerprint(fp, of string) error { return nil } -// Want is a node's want for a module: the fingerprint FP the node should run, or Removed for a module that left the -// node's modules (then FP is empty). When the module's newest fingerprint is not yet allowed on this node, Newest names -// it and Rule the rule that holds the node back; both are empty when FP is the newest. Words say why in a sentence. -// The entry's own time says since when. +// Want is a node's want for a module: the fingerprint FP the node should run and the digest Composed of the one +// composition of it the node may apply (ADR 0539 clause 1), or Removed for a module that left the node's modules (then +// both are empty). When the module's newest fingerprint is not yet allowed on this node, Newest names it and Rule the +// rule that holds the node back; both are empty when FP is the newest. Backup is the answer to a failed backup before +// the apply, BackupAgain or BackupSkip (clause 16), empty when none was asked. Words say why in a sentence. The +// entry's own time says since when. type Want struct { - V int `json:"v"` - FP string `json:"fp,omitempty"` - Removed bool `json:"removed,omitempty"` - Newest string `json:"newest,omitempty"` - Rule int `json:"rule,omitempty"` - Words string `json:"words,omitempty"` + V int `json:"v"` + FP string `json:"fp,omitempty"` + Composed string `json:"composed,omitempty"` + Removed bool `json:"removed,omitempty"` + Newest string `json:"newest,omitempty"` + Rule int `json:"rule,omitempty"` + Backup string `json:"backup,omitempty"` + Words string `json:"words,omitempty"` } func (w Want) check() error { if w.Removed { - if w.FP != "" || w.Newest != "" || w.Rule != 0 { - return fmt.Errorf("delivery: a removed want names no fingerprint and no rule") + if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" { + return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule or backup answer") } return nil } if err := checkFingerprint(w.FP, "a want's fingerprint"); err != nil { return err } + if w.Composed == "" { + return fmt.Errorf("delivery: the want of %s names no composition: a want names the digest of the one composition it allows (ADR 0539 clause 1)", w.FP) + } + if err := checkDigest(w.Composed, "a want's composition digest"); err != nil { + return err + } + switch w.Backup { + case "", BackupAgain, BackupSkip: + default: + return fmt.Errorf("delivery: the backup answer %q is not %s or %s", w.Backup, BackupAgain, BackupSkip) + } if w.Newest == "" && w.Rule == 0 { return nil } @@ -236,6 +293,41 @@ func DecodeWant(data []byte) (Want, error) { // SameWant says whether two wants are the same value, so the assigner writes only when they differ. func SameWant(a, b Want) bool { return a == b } +// NodeWant is a node's own want (WantNodeKey): the digest Composed of the node's own composition it may apply. Words +// say why in a sentence. +type NodeWant struct { + V int `json:"v"` + Composed string `json:"composed"` + Words string `json:"words,omitempty"` +} + +// EncodeNodeWant is w as JSON at Version; it refuses a digest not of the sha256 form. +func EncodeNodeWant(w NodeWant) ([]byte, error) { + if err := checkDigest(w.Composed, "the node's own want's composition digest"); err != nil { + return nil, err + } + w.V = Version + return json.Marshal(w) +} + +// DecodeNodeWant reads a node's own want; it refuses an unknown version (ErrUnknownVersion), a field a node's own want +// does not have (a fingerprint among them), and what EncodeNodeWant refuses. +func DecodeNodeWant(data []byte) (NodeWant, error) { + if err := checkVersion(data); err != nil { + return NodeWant{}, err + } + var w NodeWant + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + if err := d.Decode(&w); err != nil { + return NodeWant{}, fmt.Errorf("delivery: a node's own want does not fit its shape: %w", err) + } + if err := checkDigest(w.Composed, "the node's own want's composition digest"); err != nil { + return NodeWant{}, err + } + return w, nil +} + // EncodeHeld is h as JSON at Version; it refuses a node name a key refuses. func EncodeHeld(h Held) ([]byte, error) { if err := h.check(); err != nil { diff --git a/go/delivery/assigner_test.go b/go/delivery/assigner_test.go index 3a2d5cc..a63f108 100644 --- a/go/delivery/assigner_test.go +++ b/go/delivery/assigner_test.go @@ -74,8 +74,8 @@ func TestDifferenceKey(t *testing.T) { // 1297/B1a: wants, holds and differences round-trip through their encoders unchanged, at Version. func TestAssignerValuesRoundTrip(t *testing.T) { wants := []Want{ - {FP: fp2of('a')}, - {FP: fp2of('a'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"}, + {FP: fp2of('a'), Composed: hashOf('1')}, + {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"}, {Removed: true, Words: "the module left g14"}, } for _, w := range wants { @@ -127,7 +127,7 @@ func TestAssignerValuesRefused(t *testing.T) { {Removed: true, FP: fp2of('a')}, {FP: fp2of('a'), Newest: fp2of('b')}, {FP: fp2of('a'), Newest: fp2of('a'), Rule: 3}, - {FP: fp2of('a'), Newest: fp2of('b'), Rule: 7}, + {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 8}, } { if _, err := EncodeWant(w); err == nil { t.Errorf("EncodeWant(%+v) accepted", w) diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 71ccf75..128c683 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -90,6 +90,10 @@ const ( // for which it had written a have key; the key is never deleted (issue 1369, ADR 0408 §4), so a reader can tell // that the module stopped. PhaseRemoved Phase = "removed" + // PhaseHeld is a module whose backup before an apply failed: it keeps running what it has, nothing of the new + // composition is applied, and the node waits for the answer in its want (novox/hq ADR 0539 clause 16). Detail + // carries the backup's own words. + PhaseHeld Phase = "held" ) // Health is what the module's own check says. @@ -316,24 +320,31 @@ func PulseTTL(moveAge time.Duration) (ttl time.Duration, stalled bool) { // Have is a module's state on a node, written by that node's node-engine under HaveKey. FP is the module's // fingerprint once the indexer exists (design 47 release 3); until then Build is the build the node was sent, and // the view compares by build. +// +// Composed is the digest of the composition the node runs for the module (novox/hq ADR 0539 clause 9): the +// controller judges a move against it (clause 14), and the assigner reads it to know a want was applied. type Have struct { - V int `json:"v"` - FP string `json:"fp,omitempty"` - Build string `json:"build,omitempty"` - Phase Phase `json:"phase"` - Health Health `json:"health"` - Detail string `json:"detail,omitempty"` + V int `json:"v"` + FP string `json:"fp,omitempty"` + Composed string `json:"composed,omitempty"` + Build string `json:"build,omitempty"` + Phase Phase `json:"phase"` + Health Health `json:"health"` + Detail string `json:"detail,omitempty"` } // HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole: // this package names the facts and does not type them. +// +// Composed is the digest of the node's own composition it runs (ADR 0539 clause 9). type HaveNode struct { - V int `json:"v"` - Windows json.RawMessage `json:"windows,omitempty"` - Busy json.RawMessage `json:"busy,omitempty"` - Strays json.RawMessage `json:"strays,omitempty"` - Filters json.RawMessage `json:"filters,omitempty"` - Profile json.RawMessage `json:"profile,omitempty"` + V int `json:"v"` + Composed string `json:"composed,omitempty"` + Windows json.RawMessage `json:"windows,omitempty"` + Busy json.RawMessage `json:"busy,omitempty"` + Strays json.RawMessage `json:"strays,omitempty"` + Filters json.RawMessage `json:"filters,omitempty"` + Profile json.RawMessage `json:"profile,omitempty"` } // Presence is whether a node or a part's instance is online; the entry's own time says since when. The watchman @@ -381,9 +392,14 @@ func decode(data []byte, into any) error { func (h Have) check() error { switch h.Phase { - case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved: + case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld: default: - return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed or removed", h.Phase) + return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed, removed or held", h.Phase) + } + if h.Composed != "" { + if err := checkDigest(h.Composed, "the have's composition digest"); err != nil { + return err + } } switch h.Health { case HealthOK, HealthFailed, HealthWaits, HealthUnknown: @@ -458,7 +474,7 @@ func SameHave(a, b Have) bool { return a == b } // SameHaveNode says whether two node facts are the same value, comparing each carried JSON without its spacing. func SameHaveNode(a, b HaveNode) bool { - return a.V == b.V && sameJSON(a.Windows, b.Windows) && sameJSON(a.Busy, b.Busy) && + return a.V == b.V && a.Composed == b.Composed && sameJSON(a.Windows, b.Windows) && sameJSON(a.Busy, b.Busy) && sameJSON(a.Strays, b.Strays) && sameJSON(a.Filters, b.Filters) && sameJSON(a.Profile, b.Profile) } diff --git a/go/delivery/view_answers.go b/go/delivery/view_answers.go index d8f15a3..68561fc 100644 --- a/go/delivery/view_answers.go +++ b/go/delivery/view_answers.go @@ -32,19 +32,27 @@ const ( AskKindHold = "hold" // AskKindRestore is the kind of the assigner's ask about a data module set back. AskKindRestore = "restore" + // AskKindBackup is the kind of the assigner's ask about a backup before an apply that failed: back up again, or go + // on without one (novox/hq ADR 0539 clause 16). + AskKindBackup = "backup" + // AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: + // a composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14). + AskKindRecord = "record" viewWord = "view" ) // AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's -// Module and fingerprint FP, a restore's Node, Module and FP. +// Module and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest +// Composed of the composition it would allow. type AskKey struct { - Kind string - Group string - Hash string - Node string - Module string - FP string + Kind string + Group string + Hash string + Node string + Module string + FP string + Composed string } // AskNeedsProof says whether an ask of this kind must prove who answered it, so the view may not answer it: the @@ -53,10 +61,11 @@ func AskNeedsProof(kind string) (bool, error) { switch kind { case AskKindMerge: return true, nil - case AskKindHold, AskKindRestore: + case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord: return false, nil } - return false, fmt.Errorf("delivery: %q is not a kind of ask, %s, %s or %s", kind, AskKindMerge, AskKindHold, AskKindRestore) + return false, fmt.Errorf("delivery: %q is not a kind of ask, %s, %s, %s, %s or %s", kind, AskKindMerge, AskKindHold, + AskKindRestore, AskKindBackup, AskKindRecord) } // HoldAskKey is the key, in AssignerAsksBucket, of the ask about a module's held fingerprint: `hold..`. @@ -81,7 +90,32 @@ func RestoreAskKey(node, module, fingerprint string) (string, error) { return AskKindRestore + "." + node + "." + key, nil } -// ParseAnyAskKey reads an ask key of any kind (MergeAskKey, HoldAskKey, RestoreAskKey); it refuses an unknown kind +// BackupAskKey is the key, in AssignerAsksBucket, of the ask about a module's failed backup on a node before it applies +// a fingerprint: `backup...`. +func BackupAskKey(node, module, fingerprint string) (string, error) { + key, err := ComposedKey(node, module, fingerprint) + if err != nil { + return "", err + } + return AskKindBackup + "." + key, nil +} + +// RecordAskKey is the key, in AssignerAsksBucket, of the ask about a move of a module on a node to the composition +// whose digest it names: `record...`, so a new revision at the same fingerprint is asked +// again (ADR 0539 clause 14). +func RecordAskKey(node, module, composed string) (string, error) { + have, err := HaveKey(node, module) + if err != nil { + return "", err + } + if err := checkDigest(composed, "a record ask's composition digest"); err != nil { + return "", err + } + return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil +} + +// ParseAnyAskKey reads an ask key of any kind (MergeAskKey, HoldAskKey, RestoreAskKey, BackupAskKey, RecordAskKey); it +// refuses an unknown kind // and a key whose parts do not fit its kind, naming the key. func ParseAnyAskKey(key string) (AskKey, error) { kind, rest, _ := strings.Cut(key, ".") @@ -114,6 +148,26 @@ func ParseAnyAskKey(key string) (AskKey, error) { return refused(err) } return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil + case AskKindBackup: + node, module, fp, isNode, err := ParseComposedKey(rest) + if err != nil || isNode { + return refused(fmt.Errorf("a backup ask is backup...")) + } + return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil + case AskKindRecord: + parts := strings.Split(rest, ".") + if len(parts) != 3 { + return refused(fmt.Errorf("a record ask is record...")) + } + node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1]) + if err != nil || isNode { + return refused(fmt.Errorf("a record ask is record...")) + } + composed := "sha256:" + parts[2] + if err := checkDigest(composed, "a record ask's composition digest"); err != nil { + return refused(err) + } + return AskKey{Kind: kind, Node: node, Module: module, Composed: composed}, nil } _, err := AskNeedsProof(kind) return refused(err) diff --git a/go/delivery/want_composed_test.go b/go/delivery/want_composed_test.go new file mode 100644 index 0000000..12512e8 --- /dev/null +++ b/go/delivery/want_composed_test.go @@ -0,0 +1,176 @@ +package delivery + +import ( + "strings" + "testing" +) + +func hashOf(c byte) string { return "sha256:" + digestOf(c) } + +// 1943/AC3: a want naming its fingerprint and the digest of the composition it allows round-trips, and so does one +// carrying the answer to a failed backup; a want naming a fingerprint without a digest, a digest not of the sha256 +// form, a removed want naming a digest, an unknown backup answer and a rule past the seventh are refused. +func TestAWantNamesItsComposition(t *testing.T) { + for _, w := range []Want{ + {FP: fp2of('a'), Composed: hashOf('1')}, + {FP: fp2of('a'), Composed: hashOf('1'), Backup: BackupAgain}, + {FP: fp2of('a'), Composed: hashOf('1'), Backup: BackupSkip, Words: "the operator: go on without a backup"}, + {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleComposed, Words: "no composition of b for ace yet"}, + {Removed: true}, + } { + raw, err := EncodeWant(w) + if err != nil { + t.Fatalf("EncodeWant(%+v): %v", w, err) + } + back, err := DecodeWant(raw) + w.V = Version + if err != nil || !SameWant(back, w) { + t.Errorf("want %+v came back %+v, %v", w, back, err) + } + } + refusals := []struct { + w Want + why string + }{ + {Want{FP: fp2of('a')}, "names no composition"}, + {Want{FP: fp2of('a'), Composed: "sha256:12"}, "not sha256"}, + {Want{Removed: true, Composed: hashOf('1')}, "removed want names no"}, + {Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`}, + {Want{Removed: true, Backup: BackupAgain}, "removed want names no"}, + {Want{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 8}, "rule 8"}, + } + for _, r := range refusals { + if _, err := EncodeWant(r.w); err == nil || !strings.Contains(err.Error(), r.why) { + t.Errorf("EncodeWant(%+v) = %v, want a refusal saying %q", r.w, err, r.why) + } + } + if _, err := DecodeWant([]byte(`{"v":1,"fp":"` + fp2of('a') + `"}`)); err == nil || !strings.Contains(err.Error(), "names no composition") { + t.Errorf("a want of release 5, naming no composition, was read: %v", err) + } + if RuleComposed != 7 { + t.Errorf("RuleComposed = %d, want the seventh rule", RuleComposed) + } +} + +// 1943/AC3: the node's own want is `want..node` and round-trips through its parser, while a module named node is +// still refused a want key; its value names the digest of the node's own composition and no fingerprint. +func TestTheNodesOwnWant(t *testing.T) { + key, err := WantNodeKey("ace") + if err != nil || key != "want.ace.node" { + t.Fatalf("WantNodeKey = %q, %v", key, err) + } + if node, err := ParseWantNodeKey(key); err != nil || node != "ace" { + t.Errorf("ParseWantNodeKey(%q) = %q, %v", key, node, err) + } + if _, err := WantKey("ace", "node"); err == nil { + t.Errorf(`WantKey(ace, "node") accepted`) + } + if _, _, err := ParseWantKey(key); err == nil { + t.Errorf("ParseWantKey read the node's own want as a module's") + } + for _, bad := range []string{"want.ace.mesh-mail", "want.node", "ace.node", "want.a.b.node", ""} { + if _, err := ParseWantNodeKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("ParseWantNodeKey(%q) = %v, want a refusal naming it", bad, err) + } + } + if _, err := WantNodeKey(""); err == nil { + t.Errorf("WantNodeKey(\"\") accepted") + } + w := NodeWant{Composed: hashOf('2'), Words: "the node's own composition"} + raw, err := EncodeNodeWant(w) + if err != nil { + t.Fatal(err) + } + back, err := DecodeNodeWant(raw) + w.V = Version + if err != nil || back != w { + t.Errorf("node want came back %+v, %v", back, err) + } + for _, bad := range []NodeWant{{}, {Composed: "sha256:1"}} { + if _, err := EncodeNodeWant(bad); err == nil { + t.Errorf("EncodeNodeWant(%+v) accepted", bad) + } + } + if _, err := DecodeNodeWant([]byte(`{"v":1,"composed":"` + hashOf('2') + `","fp":"` + fp2of('a') + `"}`)); err == nil { + t.Errorf("a node want naming a fingerprint was read") + } +} + +// 1943/AC3: a have held after a failed backup, naming the composition it runs, round-trips; so does the node's own have +// naming its composition; a have's digest not of the sha256 form is refused. +func TestAHaveNamesItsComposition(t *testing.T) { + for _, h := range []Have{ + {FP: fp2of('a'), Composed: hashOf('3'), Phase: PhaseHeld, Health: HealthOK, Detail: "the backup failed: no space left"}, + {FP: fp2of('a'), Composed: hashOf('3'), Phase: PhaseRunning, Health: HealthOK}, + } { + raw, err := EncodeHave(h) + if err != nil { + t.Fatalf("EncodeHave(%+v): %v", h, err) + } + back, err := DecodeHave(raw) + h.V = Version + if err != nil || !SameHave(back, h) { + t.Errorf("have %+v came back %+v, %v", h, back, err) + } + } + if _, err := EncodeHave(Have{Composed: "sha256:x", Phase: PhaseRunning, Health: HealthOK}); err == nil || !strings.Contains(err.Error(), "not sha256") { + t.Errorf("a have naming a bad digest = %v", err) + } + n := HaveNode{Composed: hashOf('4')} + raw, err := EncodeHaveNode(n) + if err != nil { + t.Fatal(err) + } + back, err := DecodeHaveNode(raw) + n.V = Version + if err != nil || !SameHaveNode(back, n) { + t.Errorf("node have came back %+v, %v", back, err) + } + if SameHaveNode(n, HaveNode{V: Version, Composed: hashOf('5')}) { + t.Errorf("node haves naming two compositions are the same") + } +} + +// 1943/AC3: the assigner's backup ask (back up again, or go on without one, ADR 0539 clause 16) and its record ask (a +// move a person must allow, clause 14) key by node, module and fingerprint or composition, round-trip through +// ParseAnyAskKey, and the view may answer both. +func TestTheBackupAndRecordAsks(t *testing.T) { + fp, digest := fp2of('a'), hashOf('6') + backup, err := BackupAskKey("ace", "mesh-mail", fp) + if err != nil || backup != "backup.ace.mesh-mail."+strings.Repeat("a", 64) { + t.Fatalf("BackupAskKey = %q, %v", backup, err) + } + record, err := RecordAskKey("ace", "mesh-mail", digest) + if err != nil || record != "record.ace.mesh-mail."+strings.Repeat("6", 64) { + t.Fatalf("RecordAskKey = %q, %v", record, err) + } + if got, err := ParseAnyAskKey(backup); err != nil || got != (AskKey{Kind: AskKindBackup, Node: "ace", Module: "mesh-mail", FP: fp}) { + t.Errorf("ParseAnyAskKey(%q) = %+v, %v", backup, got, err) + } + if got, err := ParseAnyAskKey(record); err != nil || got != (AskKey{Kind: AskKindRecord, Node: "ace", Module: "mesh-mail", Composed: digest}) { + t.Errorf("ParseAnyAskKey(%q) = %+v, %v", record, got, err) + } + for _, kind := range []string{AskKindBackup, AskKindRecord} { + if proof, err := AskNeedsProof(kind); err != nil || proof { + t.Errorf("AskNeedsProof(%s) = %v, %v; the view may answer it", kind, proof, err) + } + } + for _, k := range []string{backup, record} { + if _, err := ViewAnswerKey(k); err != nil { + t.Errorf("the view may not answer %s: %v", k, err) + } + } + for _, bad := range [][3]string{{"", "mesh-mail", fp}, {"ace", "node", fp}, {"ace", "mesh-mail", "src2:1"}} { + if _, err := BackupAskKey(bad[0], bad[1], bad[2]); err == nil { + t.Errorf("BackupAskKey(%v) accepted", bad) + } + } + if _, err := RecordAskKey("ace", "mesh-mail", "sha256:1"); err == nil { + t.Errorf("a record ask naming a bad digest was keyed") + } + for _, bad := range []string{"backup.ace.mesh-mail", "record.ace.mesh-mail.12", "record.ace." + strings.Repeat("6", 64)} { + if _, err := ParseAnyAskKey(bad); err == nil { + t.Errorf("ParseAnyAskKey(%q) accepted", bad) + } + } +} From d506f2196a7cf9d1d76d4094f898fb07d7c3db5f Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:32:11 +0200 Subject: [PATCH 04/13] Refuse the plan verbs by name on the controller and delivery seats (issue 1943, S4) With no delivery plan nothing starts, stops, retries or reads one; each removed verb stays a row with no input so a caller is told its replacement, optional on the delivery seat so the gate of before and of release 6 both hold it (ADR 0539 clauses 15, 17 and 18). --- go/catalogue/delivery_seat.go | 71 +++++--------- go/catalogue/delivery_seat_test.go | 104 -------------------- go/catalogue/verbs.go | 152 +++++++++-------------------- go/catalogue/verbs_r6_test.go | 103 +++++++++++++++++++ 4 files changed, 174 insertions(+), 256 deletions(-) create mode 100644 go/catalogue/verbs_r6_test.go diff --git a/go/catalogue/delivery_seat.go b/go/catalogue/delivery_seat.go index 7df4d86..9296f5e 100644 --- a/go/catalogue/delivery_seat.go +++ b/go/catalogue/delivery_seat.go @@ -9,7 +9,26 @@ package catalogue // DeliverySeat is the seat mesh-delivery holds. const DeliverySeat = "mesh-delivery" -// deliveryVerbs are the delivery seat's tools: six that read, and the acts of a person, a reviewer and healer H2. +// RemovedDeliveryVerbs are the delivery seat's verbs release 6 removed (novox/hq ADR 0539 clause 17): with no plan the +// gate keeps the merge's states and derives nothing after the merge. Each stays in deliveryVerbs as an optional row +// with no input, so the holder of before and the holder of release 6 both hold the seat and a caller is answered by +// name with its replacement, given here. +var RemovedDeliveryVerbs = map[string]string{ + "times": "the delivery view (nox-delivery): each machine's want and have, and since when", + "what-if": "the merge check's composition, which composes every machine a change touches", + "release": "the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", + "stop": "a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", + "retire-history": "nothing to retire: the gate keeps no delivery after its merge", +} + +// deliveryRemovedInR6 is the row of a verb release 6 removed from the delivery seat. +func deliveryRemovedInR6(name string) Verb { + return Verb{Name: name, Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: " + + RemovedDeliveryVerbs[name], Input: schema(nil, nil), Optional: true} +} + +// deliveryVerbs are the delivery seat's tools: what reads the merges and checks, and the acts of a person and healer H2 +// on a merge (ADR 0539 clause 17). func deliveryVerbs() []Verb { return []Verb{ {Name: "deliveries", Description: "Every delivery not final, and those that ended in the last day, one line " + @@ -18,15 +37,7 @@ func deliveryVerbs() []Verb { Input: schema(map[string]string{"state": "one state, e.g. delivering or held", "repository": "owner/repository", "group": "a group's id (its branch name)", "all": "\"true\": the final ones of the last thirty days too"}, nil, "all")}, - // Delivery time against the delivery budgets (novox/hq ADR 0282): what probe D16 reads, optional until its - // holder serves it. - {Name: "times", Description: "Delivery time: from a merge to every machine of its walk running the build. " + - "Each class's delivery budget (a leaf module three minutes, a core module ten), the last days' median and " + - "worst, how many within and over, the newest delivery of each class, every delivery over its budget with its " + - "longest phase, and the delivered ones whose time is not known. Given a delivery's id, its time phase by phase.", - Input: schema(map[string]string{"days": "how many days back (default 7)", "id": "one delivery's id: its phases"}, nil), - // Optional until mesh-delivery serves it: its holder lives in the catalogue (design 33 §7). - Optional: true}, + deliveryRemovedInR6("times"), {Name: "show", Description: "One delivery or group whole: its delivery plan (what it builds, what each " + "machine receives, what is not an ordinary send), every transition with when and why, the machine " + "steps of its walk, its group and its order.", @@ -35,12 +46,7 @@ func deliveryVerbs() []Verb { "(declared, built by, version skew, engine before controller, by name), its composed check and its " + "state, derived from its members.", Input: schema(map[string]string{"all": "\"true\": the groups that ended too"}, nil, "all")}, - {Name: "what-if", Description: "The delivery plan a change would have, asked of the controller's planner " + - "and kept nowhere: the modules it moves and their dependents in tiers, what each machine would receive, " + - "the steps that are not an ordinary send.", - Input: schema(map[string]string{"repository": "owner/repository", - "paths": "the files it changes, comma-separated, from the repository's root", - "base": "the branch it merges into (default main)"}, []string{"repository", "paths"})}, + deliveryRemovedInR6("what-if"), {Name: "checks", Description: "What the mesh's checks said of a pull request's head or of one commit: each " + "of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, " + "mesh/delivery, …) with its state, " + @@ -62,40 +68,13 @@ func deliveryVerbs() []Verb { "controller is asked for its check. With why.", Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"}, []string{"id", "why"})}, - {Name: "release", Description: "A person's word that a held delivery goes on: it starts delivering. With why.", - Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"}, - []string{"id", "why"})}, - {Name: "stop", Description: "Stop a delivery that is not final, with why; its walk is ended through the " + - "controller, and in a group every member after it is stopped too, naming it.", - Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"}, - []string{"id", "why"})}, + deliveryRemovedInR6("release"), + deliveryRemovedInR6("stop"), {Name: "close", Description: "Healer H2's verb: take the one transition the table names for a delivery held " + "past its bound. Refused for any other.", Input: schema(map[string]string{"id": "the delivery's id", "why": "the condition it answers"}, []string{"id", "why"})}, - {Name: "retire-history", Description: "Retire held deliveries that are history, on a person's word, with " + - "why: each made from the forge's word for a merge on the trunk heard long after it happened — replayed " + - "when the holder first held the seat — with no walk of it open. Never released, delivered or put back: " + - "retired, final. One by id, refused when the rule does not take it; or every held one, of one " + - "repository when named, answering how many it retired and naming those it left held. dry \"true\" " + - "answers what it would do and changes nothing.", - Input: schema(map[string]string{"why": "why, kept with every transition", - "id": "one delivery's id; every held one when empty", - "repository": "owner/repository: only its held deliveries", - "dry": "\"true\": what it would retire, changing nothing"}, []string{"why"}, "dry"), - // Added after the seat's holder shipped (novox/hq issue 313): optional until mesh-delivery serves it. - Optional: true}, - {Name: "mark-ready", Description: "A reviewer's word that a delivery's head is approved: the holder puts the " + - "label mesh/ready on its pull request and then writes \"ready at \", the order the forge counts, " + - "as the mesh's own forge account, naming who reviewed it and why. Refused when the delivery's commit is " + - "no longer its pull request's head. mesh-delivery then merges it once its checks pass (novox/hq ADR 0364).", - Input: schema(map[string]string{"id": "the delivery's id (owner/repository@commit): the head reviewed", - "reviewed-by": "who reviewed it, as who@node", "why": "the review's verdict in a line"}, - []string{"id", "reviewed-by", "why"}), - // Kept one more release, optional (novox/hq issue 1022): mesh-delivery no longer serves it, but the manifest - // it still has on the mesh does until its catalogue member merges, and a seat refuses a holder that serves - // a verb it does not promise. The next release removes this entry. - Optional: true}, + deliveryRemovedInR6("retire-history"), } } diff --git a/go/catalogue/delivery_seat_test.go b/go/catalogue/delivery_seat_test.go index 7d3a619..046b93d 100644 --- a/go/catalogue/delivery_seat_test.go +++ b/go/catalogue/delivery_seat_test.go @@ -58,107 +58,3 @@ func TestTheDeliverySeatPromisesChecks(t *testing.T) { t.Fatalf("a holder without %s: %v", before[0], err) } } - -// The seat retires history on a person's word (novox/hq issue 313): `retire-history`, with why, one delivery by -// id or every held one, dry to see first. Added after the holder shipped, it is optional, so the holder that -// does not serve it yet still holds the seat, and one that does is not refused for a verb the seat lacks. -func TestTheDeliverySeatPromisesRetireHistoryOptionally(t *testing.T) { - seat, _ := SeatNamed(DeliverySeat) - var retire *Verb - for i := range seat.Serves { - if seat.Serves[i].Name == "retire-history" { - retire = &seat.Serves[i] - } - } - if retire == nil || !retire.Optional { - t.Fatalf("the delivery seat promises %v, retire-history optionally", VerbNames(seat.Serves)) - } - props, _ := retire.Input["properties"].(map[string]any) - for _, arg := range []string{"why", "id", "repository", "dry"} { - if _, has := props[arg]; !has { - t.Errorf("retire-history takes no %q", arg) - } - } - if req, _ := retire.Input["required"].([]string); !reflect.DeepEqual(req, []string{"why"}) { - t.Errorf("retire-history requires %v, wanted only why", req) - } - var without []string - for _, v := range VerbNames(seat.Serves) { - if v != "retire-history" { - without = append(without, v) - } - } - m := Manifest{Module: "mesh-delivery", Claims: []Claim{{Name: DeliverySeat, Scope: ScopeMesh, Serves: without}}} - if err := CanHold(m, seat); err != nil { - t.Fatalf("a holder without retire-history yet: %v", err) - } - m.Claims[0].Serves = VerbNames(seat.Serves) - if err := CanHold(m, seat); err != nil { - t.Fatalf("a holder serving retire-history: %v", err) - } -} - -// The seat says delivery times (novox/hq ADR 0282, issue 382): `times`, over some days or for one delivery. Added -// after the holder shipped, it is optional, so the holder that does not serve it yet still holds the seat, and one -// that does is not refused for a verb the seat lacks. -func TestTheDeliverySeatPromisesTimesOptionally(t *testing.T) { - seat, _ := SeatNamed(DeliverySeat) - var times *Verb - for i := range seat.Serves { - if seat.Serves[i].Name == "times" { - times = &seat.Serves[i] - } - } - if times == nil || !times.Optional { - t.Fatalf("the delivery seat promises %v, times optionally", VerbNames(seat.Serves)) - } - props, _ := times.Input["properties"].(map[string]any) - for _, arg := range []string{"days", "id"} { - if _, has := props[arg]; !has { - t.Errorf("times takes no %q", arg) - } - } - var without []string - for _, v := range VerbNames(seat.Serves) { - if v != "times" { - without = append(without, v) - } - } - m := Manifest{Module: "mesh-delivery", Claims: []Claim{{Name: DeliverySeat, Scope: ScopeMesh, Serves: without}}} - if err := CanHold(m, seat); err != nil { - t.Fatalf("a holder without times yet: %v", err) - } - m.Claims[0].Serves = VerbNames(seat.Serves) - if err := CanHold(m, seat); err != nil { - t.Fatalf("a holder serving times: %v", err) - } -} - -// The seat still promises `mark-ready`, optionally, for one more release (issue 1022); it was added by novox/hq issue 913: `mark-ready`, with the delivery's id, who reviewed it -// and why, so a reviewer marks a pull request through one audited verb, label and comment in the order the forge -// counts, instead of two raw forge calls. Added after the holder shipped, it is optional. -func TestTheDeliverySeatPromisesMarkReadyOptionally(t *testing.T) { - seat, _ := SeatNamed(DeliverySeat) - var mark *Verb - for i := range seat.Serves { - if seat.Serves[i].Name == "mark-ready" { - mark = &seat.Serves[i] - } - } - if mark == nil || !mark.Optional { - t.Fatalf("the delivery seat promises %v, mark-ready optionally", VerbNames(seat.Serves)) - } - if req, _ := mark.Input["required"].([]string); !reflect.DeepEqual(req, []string{"id", "reviewed-by", "why"}) { - t.Errorf("mark-ready requires %v, wanted id, reviewed-by and why", req) - } - var without []string - for _, v := range VerbNames(seat.Serves) { - if v != "mark-ready" { - without = append(without, v) - } - } - m := Manifest{Module: "mesh-delivery", Claims: []Claim{{Name: DeliverySeat, Scope: ScopeMesh, Serves: without}}} - if err := CanHold(m, seat); err != nil { - t.Fatalf("a holder without mark-ready yet: %v", err) - } -} diff --git a/go/catalogue/verbs.go b/go/catalogue/verbs.go index aad0f6d..2218d95 100644 --- a/go/catalogue/verbs.go +++ b/go/catalogue/verbs.go @@ -74,6 +74,29 @@ func VerbNames(verbs []Verb) []string { return out } +// RemovedControllerVerbs are the controller seat's verbs release 6 removed (novox/hq ADR 0539, ADR 0429 §8): no +// delivery plan, batch, tier or push exists, so nothing starts, stops, retries, follows or reads one. Each stays in +// ControllerVerbs as a row with no input, so a caller is answered by name with its replacement, given here. +var RemovedControllerVerbs = map[string]string{ + "plans": "the delivery view (nox-delivery): each machine's want and have, read from the assigner's wants and the " + + "node-engine's haves", + "plan": "the node's compositions: mesh-controller_composed holds each module's digest and judgement per node, and " + + "mesh-compose prints one module's composition for one node", + "push": "a want: the assigner writes want.. by its rules, and by hand the operator's credential writes " + + "it, naming a kept composition (ADR 0539 clause 10)", + "deliver": "nothing to start: a merged change reaches each machine by its want", + "delivery-stop": "a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", + "delivery-backups-again": "the assigner's backup ask, answered on the phone or in the delivery view (ADR 0539 clause 16)", + "delivery-walks": "the delivery view (nox-delivery) and the wants, haves and compositions on the bus", + "delivery-order": "no order to keep: each module reaches its machines by its own want, its providers first by rule 5", +} + +// removedInR6 is the row of a verb release 6 removed from the controller's seat: no input, refused by name. +func removedInR6(name string) Verb { + return Verb{Name: name, Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: " + + RemovedControllerVerbs[name], Input: schema(nil, nil)} +} + // ControllerSeatName is the seat the control plane holds, whose tools are the mesh's own verbs. const ControllerSeatName = "mesh-controller" @@ -115,87 +138,23 @@ var ControllerVerbs = []Verb{ "log": "a build's id (as `builds` lists it): print what the build machine said, line by line", "limit": "how many builds to list (default 20); not with log", }, nil)}, - {Name: "plans", Description: "What the last merges produced and where each stands (novox/hq ADR 0162): " + - "the tiers, the tier a plan is at, what it waits for and since when; one plan whole, given its id; " + - "or, given wait, one plan followed until it moves.", - Input: schema(map[string]string{ - "id": "a plan's id (as `plans` lists them): that plan, tier by tier", - "wait": "a plan's id: wait until it moves, then answer it as data — the plan, and what ended the wait " + - "(moved, tier, done, or bound); ask again to follow it further (novox/hq issue 229)", - "until": "with wait: moved (any change, the default), tier (it reaches another tier) or done (it ends)", - "bound": "with wait: how long to wait at most, as 15s or seconds — 20s, the default, is the longest, " + - "inside what a call is answered within", - "stop": "a plan's id: stop it — what was asked still builds, nothing further is asked", - "close": "a plan's id: close a plan that will not move again, as failed by hand (novox/hq issue 254)", - "retry": "a failed plan's id: ask its failed builds again under new ids, and carry the plan on from that tier (novox/hq ADR 0219)", - "go": "a walk's id that waits for its delivery's word: start it by hand, in place of mesh-delivery — with why, " + - "recorded in the hand-act log (novox/hq ADR 0239)", - "repository": "owner/repository: the plan a merge there would produce, saving nothing (what-if); with paths or modules", - "paths": "with repository: the files the merge would change, comma-separated, from the repository's root", - "modules": "with repository: or the modules it would change, comma-separated", - "module-dirs": "with repository and paths: the directories holding a module.json at the commit, comma-separated, " + - "as the forge's announcer says them (novox/hq issue 278); without it, a directory is a module only when the paths hold its manifest", - "data": "with repository and paths: true answers the change plan as data, the document delivery-plan answers " + - "(moved, dependents, new, tiers, machines with what each receives and waits for, steps, summary, class, head, base); " + - "without it the answer is text (novox/hq ADR 0450 §2)", - "head": "with data: the commit at hand", - "base": "with data: the branch it merges into (default main)", - "removed": "with data: the files among paths the change deletes, comma-separated", - "limit": "how many plans to list (default 10); only when listing", - "why": "with stop, close or go: why it is done by hand — required, and recorded in the hand-act log (novox/hq to-be 45 §7)", - "cause": "with stop, close or go: the cause in a word, or a condition's kind (optional)", - }, nil, "data")}, + removedInR6("plans"), // The verbs the delivery's owner asks with (novox/hq ADR 0239): the planner's answer, a group's order and // composed check, and the start and end of a walk. Callable by a person too; mesh-delivery is who needs them. {Name: "delivery-plan", Description: "Removed in R4e (ADR 0501 §5 / ADR 0503 §3): refused by name, naming its replacement", Input: schema(nil, nil)}, - {Name: "delivery-order", Description: "A delivery group's order (novox/hq ADR 0419): its members in the order " + - "they are delivered, every pair and why — built by, verb before caller, version skew, engine before " + - "controller — read from the module graph and the manifests' change alone; no `after:` line is read. The " + - "cycle, with every module and edge of it, is the one refusal.", - Input: schema(map[string]string{"members": "the members, as JSON: [{id, repository, base, head, number, paths, " + - "module_dirs, module_dirs_said, removed}]"}, []string{"members"})}, + removedInR6("delivery-order"), {Name: "delivery-check", Description: "Answers that the checker (mesh-delivery-checker) judges a group (novox/hq " + "ADR 0503 §3); asks nothing. Its inputs stay, since hq's repository check still names it (ADR 0513 §4).", Input: schema(map[string]string{"group": "the group's id (none for one head checked again)", "members": "the members, as JSON, as delivery-order takes them", "ready": "\"true\" when the pull request is marked ready: its check is queued before every other check (novox/hq ADR 0406 §3)"}, []string{"members"})}, - {Name: "deliver", Description: "The delivery's word that a walk waiting for it may start (novox/hq ADR 0239): its " + - "first tier is asked at the controller's next pass. Refused for a walk that waits for nobody.", - Input: schema(map[string]string{"plan": "the walk's id", "why": "what lets it go: its turn, a person's release"}, - []string{"plan"})}, - {Name: "delivery-stop", Description: "End a walk on its delivery's word (novox/hq ADR 0239): failed, said as stopped " + - "by whom and why; what it asked still builds and registers, nothing further is asked or sent. Given merges to " + - "leave out, every other merge the walk answered is walked again — in the open batch of its kind, or a new one — " + - "without them, without each later merge of their repository, which contains one, and without each merge moving " + - "a module one moves, each left out too and named (novox/hq ADR 0299); a batch not yet cut drops the merges. The " + - "answer names the batch, the merges walked again, the merges left out, the modules the stopped walk sent that " + - "stay on their machines, and the merges elsewhere that will still deliver a stopped change.", - Input: schema(map[string]string{"plan": "the walk's id", "why": "why", "by": "who stopped the delivery", - "without": "merges to leave out, @, comma-separated"}, - []string{"plan", "why"})}, - {Name: "delivery-backups-again", Description: "Ask again the backups a walk was held for (novox/hq ADR 0404 " + - "decision 4): a walk whose backup before its send failed or did not answer is held, nothing asked or sent; this " + - "clears those failed backups and the next look at the plans asks them again, and only them. Refused for a walk " + - "with no failed backup. The delivery's release calls it. The answer names the walk and the backups asked again.", - Input: schema(map[string]string{"plan": "the held walk's id", "why": "a person's reason, as release passes it", - "by": "who released the delivery"}, []string{"plan", "why"})}, - {Name: "delivery-walks", Description: "The walks the controller keeps (novox/hq ADR 0239): every open one and the " + - "last ended ones, each whole — its tiers, each module's state, first machines and first-node gate with its readings, and its " + - "phases from its merge to every machine running it (novox/hq ADR 0282) — and whether the delivery seat has a " + - "holder on record. Given a plan, that one.", - Input: schema(map[string]string{"plan": "one walk's id", "limit": "how many ended walks beside the open ones (default 50)"}, - nil)}, - {Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it — " + - "or, with files, the files it would be given; or, with diff, what a push would change there against what " + - "it was last sent: resources added, removed and changed, a container's changed fields (it is recreated), " + - "and any mount whose directory on the machine would move (novox/hq ADR 0217).", - Input: schema(map[string]string{ - "node": "the machine's name", - "files": "\"true\": the files this machine would be given, instead of the declaration as JSON", - "diff": "\"true\": what a push would change on this machine, against what it was last sent; not with files", - }, []string{"node"}, "files", "diff")}, + removedInR6("deliver"), + removedInR6("delivery-stop"), + removedInR6("delivery-backups-again"), + removedInR6("delivery-walks"), + removedInR6("plan"), {Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there, " + "or when a seat its resources are applied through is held by nothing on the machine (novox/hq ADR 0207). " + "A module not registered yet is looked for (novox/hq issue 325, ADR 0261): while its build runs — a merge " + @@ -221,22 +180,7 @@ var ControllerVerbs = []Verb{ }, []string{"node", "provision", "from", "module"})}, {Name: "unpin", Description: "Take that choice back, putting the question to the mesh again.", Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})}, - {Name: "push", Description: "Send one machine everything it should be. With no machine named it is a push of the " + - "WHOLE mesh — every machine that is behind — and the answer says so first; behind says that outright. " + - "Answers at once that it is running, with a call id: `calls` with that id says what it sent and, for a " + - "machine named, how that machine applied it — applied, refused with its reason, or each failed resource — " + - "once it reports within wait (a push can reload the bus, which then refuses any answer still to come). A push by hand is a repair, " + - "and says why: recorded in the hand-act log (novox/hq to-be 45 §7). A machine whose push would give a " + - "running module's data another directory is held and named, the others sent (novox/hq ADR 0217): read " + - "`plan` with diff, and name the machine with move to send it.", - Input: schema(map[string]string{ - "node": "the machine's name; without it, every machine that is behind", - "move": "with node: the modules whose data may move with this push, comma-separated (optional)", - "wait": "with node: how long the call waits for the machine to report how it applied what it was sent, as \"90s\" or \"2m\"; \"0\" waits for nothing; without it, the controller's setting push-wait (optional)", - "behind": "\"true\": every machine that is behind, the whole mesh — the same as naming none, said outright; not with node", - "why": "why this is pushed by hand: recorded in the hand-act log", - "cause": "the cause in a word, or a condition's kind — the word a second push for the same reason uses (optional)", - }, []string{"why"}, "behind")}, + removedInR6("push"), {Name: "rotate", Description: "Replace a credential. A pair credential, by provision (and a consuming machine and module, " + "else every holder): both ends are re-sent together. Or a module's own secret, by machine, module and " + "name: made anew and the machine sent, so the module starts again on it — for a secret its definition " + @@ -457,26 +401,22 @@ var ControllerVerbs = []Verb{ "probe": "one probe's id (D14): its verdict alone, with every finding — the verdict shows at most twenty a probe", }, nil, "run", "probes", "signals")}, // How a module's new builds reach its machines, and the bus's planned step (novox/hq ADR 0236). - {Name: "upgrade", Description: "How each module's new builds reach its machines (novox/hq ADR 0236): rolled " + - "out one machine first and judged there at the gate, then the rest — or recorded, waiting for a person's " + - "push — with where that comes from (a person, the module, the bus, its irreplaceable data, the default) and " + - "why. With module, that one; with policy, a person's choice for it — roll-out, record (with why) or default " + - "to take the choice back. The bus is never rolled out.", + {Name: "upgrade", Description: "Whether each module's new builds wait for a person (novox/hq ADR 0236, ADR 0539 clause 14): " + + "for a module of policy record, the assigner writes no want naming a composition judged to recreate a container or move " + + "data until the operator answers its ask; with where the policy comes from (a person, the module, the bus, its " + + "irreplaceable data, the default) and why. With module, that one; with policy, a person's choice for it — roll-out, " + + "record (with why) or default to take the choice back. The bus is never moved by the assigner (ADR 0539 clause 15).", Input: schema(map[string]string{ - "module": "one module", - "policy": "with module: roll-out, record or default", - "together": "\"true\": with roll-out, every machine at once instead of one machine first", - "why": "with policy or release-backlog: why — required for record and for a release, kept", - "backlog": "\"true\": every build waiting for a gate, per machine — what a release plan would send", - "release-backlog": "\"true\": release what waits now, one machine at a time, each judged — after a release " + - "plan failed, the mesh waits for this; with why", - "cause": "with release-backlog: the cause in a word (optional)", - }, nil, "together", "backlog", "release-backlog")}, - {Name: "bus", Description: "The bus as a planned step (novox/hq to-be 45 §8, ADR 0236): what a bus upgrade " + - "would do — the bus's build on each machine against the one the mesh holds — and how the last step went. " + - "With upgrade, start one: a person's act with why, after the streams are snapshotted (snapshot-taken says " + - "where, while the mesh takes none itself), saying first whether it can be reverted; bus-maintenance is open " + - "while it runs and every stream, consumer and a round trip are checked after.", + "module": "one module", + "policy": "with module: roll-out, record or default", + "why": "with policy: why — required for record, kept", + }, nil)}, + {Name: "bus", Description: "The bus as a person's step (novox/hq to-be 45 §8, ADR 0236, ADR 0539 clause 15): what a bus " + + "upgrade would do — the bus's build on each machine against the one the mesh holds — and how the last step went. " + + "With upgrade, check one: a person's act with why, after the streams are snapshotted (snapshot-taken says where, " + + "while the mesh takes none itself), saying first whether it can be reverted; then it answers the want the operator " + + "writes with the operator's credential, want.., recorded — nothing is sent, and the assigner never " + + "moves the bus.", Input: schema(map[string]string{ "upgrade": "\"true\": start the bus's upgrade", "why": "with upgrade: why — required, recorded in the hand-act log", diff --git a/go/catalogue/verbs_r6_test.go b/go/catalogue/verbs_r6_test.go new file mode 100644 index 0000000..8e595db --- /dev/null +++ b/go/catalogue/verbs_r6_test.go @@ -0,0 +1,103 @@ +package catalogue + +import ( + "strings" + "testing" +) + +// 1943/AC4: the controller's seat no longer offers a verb that starts, stops, retries, follows or reads a delivery +// plan, nor a push: each is a row with no input whose description refuses it by name and names its replacement, and +// each is in the removed list the holder answers from. upgrade keeps its policy and drops the plan's backlog. +func TestTheControllerSeatHasNoPlanVerbs(t *testing.T) { + rows := map[string]Verb{} + for _, v := range ControllerVerbs { + rows[v.Name] = v + } + gone := []string{"plans", "plan", "push", "deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-order"} + for _, name := range gone { + v, ok := rows[name] + if !ok { + t.Errorf("%s is not in the table, so a caller is not answered by name", name) + continue + } + replacement, removed := RemovedControllerVerbs[name] + if !removed || replacement == "" { + t.Errorf("%s is not in the removed list with its replacement", name) + } + if !strings.HasPrefix(v.Description, "Removed in R6 (ADR 0539): refused by name") || !strings.Contains(v.Description, replacement) { + t.Errorf("%s = %q, want refused by name, naming %q", name, v.Description, replacement) + } + if props, _ := v.Input["properties"].(map[string]any); len(props) > 0 { + t.Errorf("%s still takes %v", name, props) + } + } + props, _ := rows["upgrade"].Input["properties"].(map[string]any) + for _, arg := range []string{"together", "backlog", "release-backlog", "cause"} { + if _, has := props[arg]; has { + t.Errorf("upgrade still takes %s, a plan's", arg) + } + } + for _, arg := range []string{"module", "policy", "why"} { + if _, has := props[arg]; !has { + t.Errorf("upgrade no longer takes %s", arg) + } + } + for _, word := range []string{"rolled", "gate", "backlog", "push"} { + if strings.Contains(rows["upgrade"].Description, word) { + t.Errorf("upgrade's description still speaks of %q: %q", word, rows["upgrade"].Description) + } + } + if !strings.Contains(rows["bus"].Description, "want") { + t.Errorf("bus does not say the operator writes the bus's want: %q", rows["bus"].Description) + } + for name := range RemovedControllerVerbs { + if v := rows[name]; !strings.HasPrefix(v.Description, "Removed in R6") { + t.Errorf("%s is in the removed list and its row says %q", name, v.Description) + } + } +} + +// 1943/AC4: the delivery seat keeps the merge's verbs and no longer offers times, what-if, release, stop or +// retire-history: each is a row with no input, optional so the holder of before and the holder of release 6 both hold +// the seat, refusing it by name; mark-ready, served by nobody since issue 1022, is gone. +func TestTheDeliverySeatHasNoPlanVerbs(t *testing.T) { + seat, ok := SeatNamed(DeliverySeat) + if !ok { + t.Fatal("the delivery seat is not in the set") + } + rows := map[string]Verb{} + for _, v := range seat.Serves { + rows[v.Name] = v + } + for _, name := range []string{"times", "what-if", "release", "stop", "retire-history"} { + v, ok := rows[name] + replacement, removed := RemovedDeliveryVerbs[name] + if !ok || !removed || replacement == "" { + t.Errorf("%s: in the table %v, in the removed list %v", name, ok, removed) + continue + } + if !strings.HasPrefix(v.Description, "Removed in R6 (ADR 0539): refused by name") || !strings.Contains(v.Description, replacement) || !v.Optional { + t.Errorf("%s = %q (optional %v), want an optional row refusing it by name", name, v.Description, v.Optional) + } + if props, _ := v.Input["properties"].(map[string]any); len(props) > 0 { + t.Errorf("%s still takes %v", name, props) + } + } + if _, ok := rows["mark-ready"]; ok { + t.Errorf("mark-ready is still promised") + } + for _, kept := range []string{"deliveries", "show", "groups", "checks", "table", "recheck", "close", "stalled"} { + if v, ok := rows[kept]; !ok || strings.HasPrefix(v.Description, "Removed") { + t.Errorf("%s is not kept (ADR 0539 clause 17)", kept) + } + } + gate := Manifest{Module: "mesh-delivery", Claims: []Claim{{Name: DeliverySeat, Scope: ScopeMesh, + Serves: []string{"deliveries", "show", "groups", "checks", "table", "stalled", "recheck", "close"}}}} + if err := CanHold(gate, seat); err != nil { + t.Errorf("the gate of release 6 cannot hold the seat: %v", err) + } + gate.Claims[0].Serves = append(gate.Claims[0].Serves, "times", "what-if", "release", "stop", "retire-history") + if err := CanHold(gate, seat); err != nil { + t.Errorf("the gate of release 5 cannot hold the seat: %v", err) + } +} From d8da8f1d3613f605067de3e01af33dc36c576dd0 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:36:14 +0200 Subject: [PATCH 05/13] Sign a composition with a trusted key and compose one by hand with mesh-compose (issue 1943, S5) The node-engine applies only a composition signed by the controller or the operator, and when the controller cannot compose its own fix the operator composes, signs and writes one at the terminal (ADR 0539 clauses 3 and 10). --- go/cmd/mesh-compose/main.go | 209 +++++++++++++++++++++++++ go/cmd/mesh-compose/main_test.go | 145 +++++++++++++++++ go/compose/composed.go | 94 +++++++++++ go/compose/composed_test.go | 72 +++++++++ go/delivery/composed_signature.go | 80 ++++++++++ go/delivery/composed_signature_test.go | 85 ++++++++++ go/delivery/composed_value.go | 20 ++- 7 files changed, 700 insertions(+), 5 deletions(-) create mode 100644 go/cmd/mesh-compose/main.go create mode 100644 go/cmd/mesh-compose/main_test.go create mode 100644 go/compose/composed.go create mode 100644 go/compose/composed_test.go create mode 100644 go/delivery/composed_signature.go create mode 100644 go/delivery/composed_signature_test.go diff --git a/go/cmd/mesh-compose/main.go b/go/cmd/mesh-compose/main.go new file mode 100644 index 0000000..0a849d8 --- /dev/null +++ b/go/cmd/mesh-compose/main.go @@ -0,0 +1,209 @@ +// mesh-compose composes one module for one node by hand, from the inputs the controller keeps, and signs it with the +// operator's key, for the hand path of novox/hq ADR 0539 clause 10: a fingerprint the controller never composed (a +// hand build), while the controller cannot compose its own fix. It composes with mesh-sdk's composition, the one the +// controller writes with (compose.Compositions), and prints the composition, its key and its digest; only with -write +// does it write them, with the operator's credential: the composition into the node's bucket composed-, each +// file over 64 KiB into the node's object bucket, and the digest into mesh-controller_composed. The want is the +// operator's next act, naming that digest; each act is recorded in the hand-act log. +// +// mesh-compose -input -node -module -fp -key +// [-write -nats -creds ] +// +// The inputs are a compose.Input as JSON. The key is the operator's ed25519 private key, PKCS#8 in PEM; the +// node-engine trusts it beside the controller's (ADR 0539 clause 3). +package main + +import ( + "context" + "crypto/ed25519" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "flag" + "fmt" + "io" + "os" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/declaration" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +func main() { + if err := run(os.Args[1:], os.Stdout); err != nil { + fmt.Fprintln(os.Stderr, "mesh-compose:", err) + os.Exit(1) + } +} + +// printed is what mesh-compose prints: where the composition goes, its digest, the composition, and whether it was +// written. +type printed struct { + Bucket string `json:"bucket"` + Key string `json:"key"` + Digest string `json:"digest"` + Composition json.RawMessage `json:"composition"` + Objects []string `json:"objects,omitempty"` + Written bool `json:"written"` + Next string `json:"next"` +} + +func run(args []string, stdout io.Writer) error { + fs := flag.NewFlagSet("mesh-compose", flag.ContinueOnError) + input := fs.String("input", "", "the controller's kept inputs, a compose.Input as JSON") + node := fs.String("node", "", "the node to compose for") + module := fs.String("module", "", "the module to compose") + fp := fs.String("fp", "", "the module's fingerprint, src2: and 64 hex digits") + keyFile := fs.String("key", "", "the operator's ed25519 private key, PKCS#8 in PEM") + write := fs.Bool("write", false, "write the composition, its files and its digest, with the operator's credential") + url := fs.String("nats", nats.DefaultURL, "with -write: the bus") + creds := fs.String("creds", "", "with -write: the operator's credential file") + if err := fs.Parse(args); err != nil { + return err + } + switch { + case *input == "" || *node == "" || *module == "" || *fp == "": + return errors.New("-input, -node, -module and -fp are each required") + case *keyFile == "": + return errors.New("-key is required: a composition made by hand is signed by the operator's key, and the node-engine applies no other") + case *write && *creds == "": + return errors.New("-write needs -creds: the operator's credential is the only hand writer of a composition") + } + key, err := readKey(*keyFile) + if err != nil { + return err + } + key2, err := delivery.ComposedKey(*node, *module, *fp) + if err != nil { + return err + } + bucket, err := delivery.ComposedBucketFor(*node) + if err != nil { + return err + } + raw, err := os.ReadFile(*input) + if err != nil { + return err + } + var in compose.Input + if err := json.Unmarshal(raw, &in); err != nil { + return fmt.Errorf("the inputs in %s are not a compose.Input: %w", *input, err) + } + all, _, err := compose.Compositions(in, *node, map[string]string{*module: *fp}, compose.Reading) + if err != nil { + return err + } + c, ok := all[key2] + if !ok { + return fmt.Errorf("%s does not run %s: its composition holds none of it", *node, *module) + } + c, objects, err := c.ByDigest() + if err != nil { + return err + } + if c, err = delivery.SignComposed(c, key); err != nil { + return err + } + encoded, err := delivery.EncodeComposed(c) + if err != nil { + return err + } + digest, err := delivery.ComposedDigestOf(c) + if err != nil { + return err + } + out := printed{Bucket: bucket, Key: key2, Digest: digest, Composition: encoded, + Next: fmt.Sprintf("write want.%s.%s naming fp %s and composed %s with the operator's credential, and record each act in the hand-act log", *node, *module, *fp, digest)} + for d := range objects { + out.Objects = append(out.Objects, d) + } + if *write { + if err := writeIt(*url, *creds, *node, bucket, key2, encoded, digest, objects); err != nil { + return err + } + out.Written = true + } + enc := json.NewEncoder(stdout) + enc.SetIndent("", " ") + return enc.Encode(out) +} + +func readKey(path string) (ed25519.PrivateKey, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + block, _ := pem.Decode(raw) + if block == nil { + return nil, fmt.Errorf("%s holds no PEM block", path) + } + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + key, ok := parsed.(ed25519.PrivateKey) + if !ok { + return nil, fmt.Errorf("%s is not an ed25519 key", path) + } + return key, nil +} + +// writeIt writes the files by digest, then the composition, then its digest: a reader that finds the digest finds the +// composition and its files already there. +func writeIt(url, creds, node, bucket, key string, encoded []byte, digest string, objects map[string][]byte) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + nc, err := nats.Connect(url, nats.UserCredentials(creds), nats.Name("mesh-compose")) + if err != nil { + return err + } + defer nc.Close() + js, err := jetstream.New(nc) + if err != nil { + return err + } + if len(objects) > 0 { + filesBucket, err := declaration.FileBucketFor(node) + if err != nil { + return err + } + store, err := js.ObjectStore(ctx, filesBucket) + if err != nil { + return fmt.Errorf("the node's object bucket %s: %w", filesBucket, err) + } + for d, body := range objects { + name, err := declaration.ObjectName(d) + if err != nil { + return err + } + if _, err := store.PutBytes(ctx, name, body); err != nil { + return fmt.Errorf("the file %s: %w", d, err) + } + } + } + composed, err := js.KeyValue(ctx, bucket) + if err != nil { + return fmt.Errorf("the bucket %s: %w", bucket, err) + } + if _, err := composed.Put(ctx, key, encoded); err != nil { + return fmt.Errorf("%s in %s: %w", key, bucket, err) + } + value, err := delivery.EncodeComposedDigest(delivery.ComposedDigest{Digest: digest, Judgement: delivery.JudgedCannot, + Words: "composed by hand with mesh-compose (ADR 0539 clause 10): the operator writes the want"}) + if err != nil { + return err + } + digests, err := js.KeyValue(ctx, delivery.ControllerComposedBucket) + if err != nil { + return fmt.Errorf("the bucket %s: %w", delivery.ControllerComposedBucket, err) + } + if _, err := digests.Put(ctx, key, value); err != nil { + return fmt.Errorf("%s in %s: %w", key, delivery.ControllerComposedBucket, err) + } + return nil +} diff --git a/go/cmd/mesh-compose/main_test.go b/go/cmd/mesh-compose/main_test.go new file mode 100644 index 0000000..f18c0f0 --- /dev/null +++ b/go/cmd/mesh-compose/main_test.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/overlay" +) + +// aMesh is the Input of mesh-controller's aMesh, as mesh-sdk's compose tests make it (compose/fixture_test.go): two +// machines on the private network, the module the mesh provides for itself assigned to both. The manifest is the one +// the controller provides (overlay.Manifest), read back as the catalogue reads it. +func aMesh(t *testing.T) compose.Input { + t.Helper() + var m catalogue.Manifest + b, _ := json.Marshal(overlay.Manifest()) + if err := json.Unmarshal(b, &m); err != nil { + t.Fatal(err) + } + in := compose.Input{Catalogue: map[string]catalogue.Manifest{overlay.Name: m}} + for i, name := range []string{"anchor", "laptop"} { + pub, _, _ := ed25519.GenerateKey(rand.Reader) + in.Nodes = append(in.Nodes, compose.Node{Name: name, + Place: compose.Place{Key: base64.StdEncoding.EncodeToString(pub), Endpoint: name + ".example:51820", Site: "here", Hub: name == "anchor", Address: []string{"10.77.0.1", "10.77.0.2"}[i]}, + Assigned: []string{overlay.Name}, Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}}) + } + return in +} + +func writeKey(t *testing.T, dir string) ed25519.PublicKey { + t.Helper() + pub, priv, _ := ed25519.GenerateKey(rand.Reader) + der, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "operator.key"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), 0o600); err != nil { + t.Fatal(err) + } + return pub +} + +// 1943/AC5: mesh-compose, given the kept inputs, a node, a module and its fingerprint, prints that module's +// composition for that node signed by the operator's key and its digest: its resources are the controller's for that +// module on that node (compose.Compose), and it verifies against the operator's key. Without --write it writes nothing. +func TestMeshComposeGivesTheControllersResourcesSignedByTheOperator(t *testing.T) { + dir := t.TempDir() + in := aMesh(t) + raw, err := json.Marshal(in) + if err != nil { + t.Fatal(err) + } + input := filepath.Join(dir, "input.json") + if err := os.WriteFile(input, raw, 0o600); err != nil { + t.Fatal(err) + } + operator := writeKey(t, dir) + fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) + + var out bytes.Buffer + if err := run([]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp, + "-key", filepath.Join(dir, "operator.key")}, &out); err != nil { + t.Fatal(err) + } + var printed struct { + Key string `json:"key"` + Bucket string `json:"bucket"` + Digest string `json:"digest"` + Composition json.RawMessage `json:"composition"` + Written bool `json:"written"` + } + if err := json.Unmarshal(out.Bytes(), &printed); err != nil { + t.Fatalf("%s: %v", out.String(), err) + } + c, err := delivery.DecodeComposed(printed.Composition) + if err != nil { + t.Fatal(err) + } + if digest, err := delivery.VerifyComposed(c, []ed25519.PublicKey{operator}); err != nil || digest != printed.Digest { + t.Errorf("the composition verifies %q, %v against the operator's key; printed %s", digest, err, printed.Digest) + } + wantKey, _ := delivery.ComposedKey("laptop", overlay.Name, fp) + if printed.Key != wantKey || printed.Bucket != "composed-laptop" || printed.Written { + t.Errorf("printed key %s in %s, written %v", printed.Key, printed.Bucket, printed.Written) + } + + d, _, err := compose.Compose(in, "laptop") + if err != nil { + t.Fatal(err) + } + var want, got []string + for _, r := range d.Resources { + want = append(want, r["id"].(string)) + } + for _, r := range c.Resources { + var head struct{ ID string } + _ = json.Unmarshal(r, &head) + got = append(got, head.ID) + } + sort.Strings(want) + sort.Strings(got) + if len(got) == 0 || strings.Join(got, " ") != strings.Join(want, " ") { + t.Errorf("mesh-compose gives %v, the controller's composition %v", got, want) + } +} + +// 1943/AC5: mesh-compose refuses what it cannot compose, naming why: no key, a fingerprint not of the new form, a +// module the node does not run, a node the mesh does not have. +func TestMeshComposeRefuses(t *testing.T) { + dir := t.TempDir() + raw, _ := json.Marshal(aMesh(t)) + input := filepath.Join(dir, "input.json") + _ = os.WriteFile(input, raw, 0o600) + writeKey(t, dir) + key := filepath.Join(dir, "operator.key") + fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) + for _, c := range []struct { + args []string + why string + }{ + {[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp}, "-key"}, + {[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", "src2:1", "-key", key}, "fingerprint"}, + {[]string{"-input", input, "-node", "laptop", "-module", "mesh-mail", "-fp", fp, "-key", key}, "does not run mesh-mail"}, + {[]string{"-input", input, "-node", "nowhere", "-module", overlay.Name, "-fp", fp, "-key", key}, "nowhere"}, + {[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp, "-key", key, "-write"}, "-creds"}, + } { + var out bytes.Buffer + if err := run(c.args, &out); err == nil || !strings.Contains(err.Error(), c.why) { + t.Errorf("mesh-compose %v = %v, want a refusal saying %q", c.args, err, c.why) + } + } +} diff --git a/go/compose/composed.go b/go/compose/composed.go new file mode 100644 index 0000000..953b896 --- /dev/null +++ b/go/compose/composed.go @@ -0,0 +1,94 @@ +package compose + +import ( + "encoding/json" + "fmt" + "sort" + + "git.novox.be/novox/mesh-sdk/go/declaration" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// Compositions is one node's compositions (novox/hq ADR 0539 clauses 1 and 2): its declaration composed over in, as +// the controller composes it, split into each module's composition at the fingerprint fps gives it and the node's own +// (`.node`), by key (delivery.ComposedKey). Each resource says its module, whatever the node reported it reads. +// A module fps gives no fingerprint for is not composed and is named in skipped, so a caller composing one module +// (mesh-compose) gets that one. Each module's Data says whether its manifest declares data. The compositions are not +// signed and not yet by digest: the writer does both (delivery.SignComposed, Composed.ByDigest). +func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped []string, err error) { + reading := in + reading.Nodes = append([]Node(nil), in.Nodes...) + found := false + for i := range reading.Nodes { + if reading.Nodes[i].Name == node { + reading.Nodes[i].ReadsModules = true + found = true + } + } + if !found { + return nil, nil, fmt.Errorf("compose: the mesh has no node %q", node) + } + d, problems, err := ComposeIn(reading, node, mode) + if err != nil { + return nil, nil, err + } + if len(problems) > 0 { + return nil, nil, fmt.Errorf("compose: %s's declaration is refused by the node-engine's validator: %v", node, problems) + } + body, err := d.Body() + if err != nil { + return nil, nil, err + } + var envelope struct { + Resources []json.RawMessage `json:"resources"` + Adoption *declaration.Adoption `json:"adoption"` + Filled []string `json:"filled_files"` + Provisioner string `json:"provisioner_account"` + } + if err := json.Unmarshal(body, &envelope); err != nil { + return nil, nil, err + } + var kept []json.RawMessage + keptPaths := map[string]bool{} + left := map[string]bool{} + for _, raw := range envelope.Resources { + var head struct { + Module string `json:"module"` + Type string `json:"type"` + Path string `json:"path"` + } + if err := json.Unmarshal(raw, &head); err != nil { + return nil, nil, err + } + if _, ok := fps[head.Module]; head.Module != "" && !ok { + left[head.Module] = true + continue + } + if head.Type == string(declaration.TypeFile) { + keptPaths[head.Path] = true + } + kept = append(kept, raw) + } + var filled []string + for _, p := range envelope.Filled { + if keptPaths[p] { + filled = append(filled, p) + } + } + out, err = delivery.SplitComposed(node, kept, fps, filled, + delivery.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner}) + if err != nil { + return nil, nil, err + } + for key, c := range out { + if m, ok := in.Catalogue[c.Module]; ok && c.Module != "" { + c.Data = len(m.DataItems()) > 0 + out[key] = c + } + } + for m := range left { + skipped = append(skipped, m) + } + sort.Strings(skipped) + return out, skipped, nil +} diff --git a/go/compose/composed_test.go b/go/compose/composed_test.go new file mode 100644 index 0000000..b6dd92e --- /dev/null +++ b/go/compose/composed_test.go @@ -0,0 +1,72 @@ +package compose + +import ( + "encoding/json" + "sort" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +func fpFor(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) } + +// 1943/AC5: a node's compositions are its declaration as Compose gives it, split by the module each resource belongs +// to: every resource once, each in its module's composition at the fingerprint given, the module's ownership said on +// it; a module given no fingerprint is named as skipped and composed nowhere. +func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { + in := aSmallMesh(t) + d, _, err := Compose(in, "laptop") + if err != nil { + t.Fatal(err) + } + var want []string + for _, r := range d.Resources { + want = append(want, r["type"].(string)+":"+r["id"].(string)) + } + sort.Strings(want) + + fps := map[string]string{"mesh-wireguard": fpFor('a'), "files": fpFor('b')} + out, skipped, err := Compositions(in, "laptop", fps, Foreseeing) + if err != nil || len(skipped) != 0 { + t.Fatalf("Compositions = %v, skipped %v", err, skipped) + } + var got []string + for key, c := range out { + node, module, fp, isNode, err := delivery.ParseComposedKey(key) + if err != nil || node != "laptop" || module != c.Module || fp != c.FP || (isNode != (module == "")) { + t.Errorf("key %s holds %s at %s (%v)", key, c.Module, c.FP, err) + } + if !isNode && fp != fps[module] { + t.Errorf("%s is composed at %s, given %s", module, fp, fps[module]) + } + if _, err := delivery.EncodeComposed(c); err != nil { + t.Errorf("%s: %v", key, err) + } + for _, raw := range c.Resources { + var r struct{ ID, Type, Module string } + _ = json.Unmarshal(raw, &r) + if r.Module != module { + t.Errorf("%s holds %s of %q", key, r.ID, r.Module) + } + got = append(got, r.Type+":"+r.ID) + } + } + sort.Strings(got) + if strings.Join(got, " ") != strings.Join(want, " ") { + t.Errorf("the compositions hold %v, the declaration %v", got, want) + } + + one, skipped, err := Compositions(in, "laptop", map[string]string{"files": fpFor('b')}, Foreseeing) + if err != nil || strings.Join(skipped, ",") != "mesh-wireguard" { + t.Fatalf("one module: %v, skipped %v", err, skipped) + } + for _, c := range one { + if c.Module == "mesh-wireguard" { + t.Errorf("a module given no fingerprint was composed") + } + } + if _, _, err := Compositions(in, "nowhere", fps, Foreseeing); err == nil { + t.Errorf("a node the mesh does not have was composed") + } +} diff --git a/go/delivery/composed_signature.go b/go/delivery/composed_signature.go new file mode 100644 index 0000000..df7219f --- /dev/null +++ b/go/delivery/composed_signature.go @@ -0,0 +1,80 @@ +package delivery + +import ( + "crypto/ed25519" + "encoding/base64" + "fmt" +) + +// A composition's signature (novox/hq ADR 0539 clause 3): the controller signs each composition it writes with its +// key, and the operator signs one made by hand with mesh-compose with theirs. The node-engine trusts exactly these two +// signers and applies nothing of a composition whose signature fails or whose signer is neither. The signature is an +// ed25519 signature, as a declaration's is, of the composition's digest (ComposedDigestOf) after a context line, so a +// signature made for anything else never verifies here. +const composedSigningContext = "novox composition v1\n" + +// Signature is who signed a composition and the signature: Signer the signer's ed25519 public key and Sig the +// signature, both in standard base64. +type Signature struct { + Signer string `json:"signer"` + Sig string `json:"sig"` +} + +func (s Signature) check() error { + if key, err := base64.StdEncoding.DecodeString(s.Signer); err != nil || len(key) != ed25519.PublicKeySize { + return fmt.Errorf("delivery: a composition's signer %q is not an ed25519 public key in base64", s.Signer) + } + if sig, err := base64.StdEncoding.DecodeString(s.Sig); err != nil || len(sig) != ed25519.SignatureSize { + return fmt.Errorf("delivery: a composition's signature is not an ed25519 signature in base64") + } + return nil +} + +// SignComposed is c signed with key: its digest signed, and the key's public half named as the signer. A signature c +// already carries is replaced. +func SignComposed(c Composed, key ed25519.PrivateKey) (Composed, error) { + if len(key) != ed25519.PrivateKeySize { + return Composed{}, fmt.Errorf("delivery: the key to sign a composition with is not an ed25519 private key") + } + digest, err := ComposedDigestOf(c) + if err != nil { + return Composed{}, err + } + sig := ed25519.Sign(key, []byte(composedSigningContext+digest)) + c.Signature = &Signature{Signer: base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey)), + Sig: base64.StdEncoding.EncodeToString(sig)} + return c, nil +} + +// VerifyComposed is c's digest when c is signed by one of the trusted keys and its signature verifies against its +// digest; otherwise it refuses, saying why, and nothing of c is to be applied: an unsigned composition, one whose +// signer is not a trusted key, and one changed after it was signed. +func VerifyComposed(c Composed, trusted []ed25519.PublicKey) (string, error) { + if c.Signature == nil { + return "", fmt.Errorf("delivery: the composition of %q on %s is unsigned, and only a signed one is applied", c.Module, c.Node) + } + if err := c.Signature.check(); err != nil { + return "", err + } + signer, _ := base64.StdEncoding.DecodeString(c.Signature.Signer) + var key ed25519.PublicKey + for _, t := range trusted { + if t.Equal(ed25519.PublicKey(signer)) { + key = t + } + } + if key == nil { + return "", fmt.Errorf("delivery: the composition of %q on %s is signed by %s, which is not a trusted signer (the controller's key or the operator's)", + c.Module, c.Node, c.Signature.Signer) + } + digest, err := ComposedDigestOf(c) + if err != nil { + return "", err + } + sig, _ := base64.StdEncoding.DecodeString(c.Signature.Sig) + if !ed25519.Verify(key, []byte(composedSigningContext+digest), sig) { + return "", fmt.Errorf("delivery: the composition of %q on %s does not match its signature: it was changed after it was signed, or signed for another", + c.Module, c.Node) + } + return digest, nil +} diff --git a/go/delivery/composed_signature_test.go b/go/delivery/composed_signature_test.go new file mode 100644 index 0000000..d0606c5 --- /dev/null +++ b/go/delivery/composed_signature_test.go @@ -0,0 +1,85 @@ +package delivery + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "strings" + "testing" +) + +func aSigningKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) { + t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + return pub, priv +} + +func aComposition() Composed { + return Composed{Node: "ace", Module: "m", FP: fp2of('a'), Resources: []json.RawMessage{ + json.RawMessage(`{"id":"m.f","type":"file","module":"m","path":"/etc/m","content":"x"}`)}} +} + +// 1943/AC5: a composition signed by the controller's key or by the operator's verifies against the two trusted keys, +// keeps its digest, and survives its encoding; one signed by another key, unsigned, or changed by one byte after it was +// signed does not verify, and nothing of it is to be applied. +func TestACompositionIsSignedByATrustedKey(t *testing.T) { + controllerPub, controller := aSigningKey(t) + operatorPub, operator := aSigningKey(t) + _, stranger := aSigningKey(t) + trusted := []ed25519.PublicKey{controllerPub, operatorPub} + c := aComposition() + want, _ := ComposedDigestOf(c) + + for name, key := range map[string]ed25519.PrivateKey{"controller": controller, "operator": operator} { + signed, err := SignComposed(c, key) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + raw, err := EncodeComposed(signed) + if err != nil { + t.Fatal(err) + } + back, err := DecodeComposed(raw) + if err != nil { + t.Fatal(err) + } + digest, err := VerifyComposed(back, trusted) + if err != nil || digest != want { + t.Errorf("%s's signature: %q, %v; want the digest %s", name, digest, err, want) + } + if d, _ := ComposedDigestOf(back); d != want { + t.Errorf("%s's signature changed the digest: %s", name, d) + } + + changed := strings.Replace(string(raw), `"content":"x"`, `"content":"y"`, 1) + if changed == string(raw) { + t.Fatal("the test changed nothing") + } + if tampered, err := DecodeComposed([]byte(changed)); err != nil { + t.Fatal(err) + } else if _, err := VerifyComposed(tampered, trusted); err == nil || !strings.Contains(err.Error(), "signature") { + t.Errorf("%s: a composition changed by one byte verified: %v", name, err) + } + } + + byStranger, _ := SignComposed(c, stranger) + if _, err := VerifyComposed(byStranger, trusted); err == nil || !strings.Contains(err.Error(), "not a trusted") { + t.Errorf("a stranger's signature = %v, want refused as no trusted signer", err) + } + if _, err := VerifyComposed(c, trusted); err == nil || !strings.Contains(err.Error(), "unsigned") { + t.Errorf("an unsigned composition = %v, want refused", err) + } + signed, _ := SignComposed(c, controller) + forged := signed + forged.Signature = &Signature{Signer: signed.Signature.Signer, Sig: byStranger.Signature.Sig} + if _, err := VerifyComposed(forged, trusted); err == nil { + t.Errorf("a stranger's signature under the controller's name verified") + } + if _, err := EncodeComposed(Composed{Node: "ace", Module: "m", FP: fp2of('a'), Resources: c.Resources, + Signature: &Signature{Signer: "not base64!", Sig: "x"}}); err == nil { + t.Errorf("a signature naming no key was encoded") + } +} diff --git a/go/delivery/composed_value.go b/go/delivery/composed_value.go index 8ec92bc..e490425 100644 --- a/go/delivery/composed_value.go +++ b/go/delivery/composed_value.go @@ -46,6 +46,9 @@ type Composed struct { Resources []json.RawMessage Filled []string Own NodeOwn + // Signature is the controller's or, made by hand, the operator's (ADR 0539 clause 3, SignComposed); it signs the + // digest and is never part of it. + Signature *Signature } // composedWire is a composition as it is written. @@ -58,6 +61,7 @@ type composedWire struct { Resources []json.RawMessage `json:"resources"` Filled []string `json:"filled_files,omitempty"` Own *NodeOwn `json:"own,omitempty"` + Signature *Signature `json:"signature,omitempty"` } // canonicalJSON is raw with its object keys sorted and its spacing dropped, its numbers kept as written. @@ -126,6 +130,11 @@ func (c Composed) check() error { } } } + if c.Signature != nil { + if err := c.Signature.check(); err != nil { + return err + } + } return nil } @@ -135,7 +144,7 @@ func (c Composed) wire() (composedWire, error) { return composedWire{}, err } w := composedWire{V: Version, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, Filled: c.Filled, - Resources: make([]json.RawMessage, len(c.Resources))} + Signature: c.Signature, Resources: make([]json.RawMessage, len(c.Resources))} for i, r := range c.Resources { canon, err := canonicalJSON(r) if err != nil { @@ -174,7 +183,7 @@ func DecodeComposed(data []byte) (Composed, error) { return Composed{}, fmt.Errorf("delivery: a composition does not fit its shape: %w", err) } c := Composed{V: w.V, Node: w.Node, Module: w.Module, FP: w.FP, Data: w.Data, Resources: w.Resources, - Filled: w.Filled} + Filled: w.Filled, Signature: w.Signature} if w.Own != nil { c.Own = *w.Own } @@ -184,9 +193,10 @@ func DecodeComposed(data []byte) (Composed, error) { return c, nil } -// ComposedDigestOf is a composition's digest, "sha256:", of its encoding. A want names it (ADR 0539 clause 1), -// and the controller writes it beside the composition. +// ComposedDigestOf is a composition's digest, "sha256:", of its encoding without its signature. A want names it +// (ADR 0539 clause 1), the controller writes it beside the composition, and the signature signs it. func ComposedDigestOf(c Composed) (string, error) { + c.Signature = nil raw, err := EncodeComposed(c) if err != nil { return "", err @@ -195,7 +205,7 @@ func ComposedDigestOf(c Composed) (string, error) { return "sha256:" + hex.EncodeToString(sum[:]), nil } -// SameComposed says whether two compositions are the same value, so the controller writes +// SameComposed says whether two compositions are the same value, their signatures included, so the controller writes // only when they differ. func SameComposed(a, b Composed) bool { ra, errA := EncodeComposed(a) From de5b33843f25de9511b7fc0f825b5aa92d26aeee Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:36:42 +0200 Subject: [PATCH 06/13] Run the captured declarations' test per node in parallel, inside the unit level's bound (issue 1943) --- go/delivery/composed_value_test.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/go/delivery/composed_value_test.go b/go/delivery/composed_value_test.go index 238c495..960e4c5 100644 --- a/go/delivery/composed_value_test.go +++ b/go/delivery/composed_value_test.go @@ -96,7 +96,17 @@ func canonical(t *testing.T, raw json.RawMessage) string { // through the composition's encoding with the same digest; together they are the declaration's resources, each once, // each in the composition of the module it says. func TestCompositionsOfACapturedDeclaration(t *testing.T) { + t.Parallel() for _, node := range []string{"ace", "g14"} { + t.Run(node, func(t *testing.T) { + t.Parallel() + oneCapturedDeclaration(t, node) + }) + } +} + +func oneCapturedDeclaration(t *testing.T, node string) { + { c := readCaptured(t, node) split := splitCaptured(t, node, c) modules := map[string]bool{} @@ -123,14 +133,9 @@ func TestCompositionsOfACapturedDeclaration(t *testing.T) { if !SameComposed(back, comp) { t.Errorf("%s: %s did not round-trip", node, key) } - d1, err1 := ComposedDigestOf(comp) - d2, err2 := ComposedDigestOf(back) - if err1 != nil || err2 != nil || d1 != d2 || !digestForm.MatchString(d1) { - t.Errorf("%s: %s digests %q and %q (%v, %v)", node, key, d1, d2, err1, err2) - } - again, _ := EncodeComposed(back) - if !bytes.Equal(raw, again) { - t.Errorf("%s: %s encodes differently once read back", node, key) + // SameComposed encodes both, so the digest of what was read back, a hash of its encoding, is the one written. + if d, err := ComposedDigestOf(back); err != nil || !digestForm.MatchString(d) { + t.Errorf("%s: %s digests %q (%v)", node, key, d, err) } for _, r := range back.Resources { var head struct { From 64a502d16aacaaf87c0acde4ae548f011a7020b1 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:40:45 +0200 Subject: [PATCH 07/13] Carry the module's upgrade policy and its source beside a composition's judgement (issue 1943; for issue 1906) The assigner holds a move of a module of policy record, and only the controller knows a person's own choice of policy (ADR 0539 clause 14). --- go/compose/composed_test.go | 16 +++++++++++ go/delivery/composed.go | 46 +++++++++++++++++++++++++++---- go/delivery/composed_keys_test.go | 10 +++++-- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/go/compose/composed_test.go b/go/compose/composed_test.go index b6dd92e..b525f5b 100644 --- a/go/compose/composed_test.go +++ b/go/compose/composed_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -70,3 +71,18 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { t.Errorf("a node the mesh does not have was composed") } } + +// 1943/AC1: the upgrade policies and sources a composition's value may name are the catalogue's own words. +func TestTheComposedPoliciesAreTheCatalogues(t *testing.T) { + for _, p := range []string{catalogue.PolicyRoll, catalogue.PolicyTogether, catalogue.PolicyRecord} { + for _, from := range []string{catalogue.FromPerson, catalogue.FromModule, catalogue.FromBus, catalogue.FromData, catalogue.FromDefault} { + v := delivery.ComposedDigest{Digest: "sha256:" + strings.Repeat("a", 64), Judgement: delivery.JudgedNeither, Policy: p, PolicyFrom: from} + if _, err := delivery.EncodeComposedDigest(v); err != nil { + t.Errorf("%s from %s: %v", p, from, err) + } + } + } + if delivery.PolicyRecord != catalogue.PolicyRecord { + t.Errorf("delivery.PolicyRecord = %q, the catalogue's %q", delivery.PolicyRecord, catalogue.PolicyRecord) + } +} diff --git a/go/delivery/composed.go b/go/delivery/composed.go index c5d30c7..a77b6ee 100644 --- a/go/delivery/composed.go +++ b/go/delivery/composed.go @@ -135,12 +135,37 @@ func checkDigest(digest, of string) error { // the Judgement of the move to it from the composition the node runs now, whose digest is Against (empty when the node // runs no composition of the module). Words say why, and are required for a move that cannot be judged. Rewritten when // the node's have changes. +// +// Policy is the module's upgrade policy as the controller holds it (ADR 0236, ADR 0539 clause 14), with PolicyFrom +// where it comes from and PolicyWhy why: the assigner holds a move of a module of policy record from it, the operator's +// own choice (`upgrade` with policy) included. Rewritten when the policy changes. Empty on a composition made by hand +// (mesh-compose), whose want the operator writes. type ComposedDigest struct { - V int `json:"v"` - Digest string `json:"digest"` - Judgement Judgement `json:"judgement"` - Against string `json:"against,omitempty"` - Words string `json:"words,omitempty"` + V int `json:"v"` + Digest string `json:"digest"` + Judgement Judgement `json:"judgement"` + Against string `json:"against,omitempty"` + Words string `json:"words,omitempty"` + Policy string `json:"policy,omitempty"` + PolicyFrom string `json:"policy-from,omitempty"` + PolicyWhy string `json:"policy-why,omitempty"` +} + +// The upgrade policies and their sources a composition's value names: catalogue.PolicyRoll, PolicyTogether and +// PolicyRecord, and catalogue.FromPerson, FromModule, FromBus, FromData and FromDefault, the same words (the compose +// package's test holds them equal; this package does not import the catalogue). +var ( + composedPolicies = []string{"roll", "together", "record"} + composedPolicyFroms = []string{"person", "module", "the bus", "irreplaceable data", "default"} +) + +func oneOf(v string, of []string) bool { + for _, o := range of { + if v == o { + return true + } + } + return false } func (c ComposedDigest) check() error { @@ -166,9 +191,20 @@ func (c ComposedDigest) check() error { return fmt.Errorf("delivery: the judgement %q is not %s, %s, %s or %s", c.Judgement, JudgedRecreates, JudgedMovesData, JudgedNeither, JudgedCannot) } + if c.Policy != "" || c.PolicyFrom != "" { + if !oneOf(c.Policy, composedPolicies) { + return fmt.Errorf("delivery: the upgrade policy %q is not one of %v", c.Policy, composedPolicies) + } + if !oneOf(c.PolicyFrom, composedPolicyFroms) { + return fmt.Errorf("delivery: the upgrade policy's source %q is not one of %v", c.PolicyFrom, composedPolicyFroms) + } + } return nil } +// PolicyRecord is the policy whose moves the assigner holds for a person's yes (ADR 0539 clause 14). +const PolicyRecord = "record" + // EncodeComposedDigest is c as JSON at Version; it refuses an unknown judgement, a digest not of the sha256 form, a // move judged to recreate or move data against nothing, and a move that cannot be judged without its reason. func EncodeComposedDigest(c ComposedDigest) ([]byte, error) { diff --git a/go/delivery/composed_keys_test.go b/go/delivery/composed_keys_test.go index dec6987..bd3630e 100644 --- a/go/delivery/composed_keys_test.go +++ b/go/delivery/composed_keys_test.go @@ -68,8 +68,9 @@ func TestComposedKeys(t *testing.T) { } // 1943/AC1: the controller's value beside a composition, its digest and the judgement of the move, round-trips; an -// unknown judgement, a digest not of the sha256 form, a move judged against nothing, and a cannot-judge without its -// reason are refused, naming why; an unknown version is refused as one. +// unknown judgement, a digest not of the sha256 form, a move judged against nothing, a cannot-judge without its +// reason, and an upgrade policy or source outside the catalogue's words are refused, naming why; an unknown version is +// refused as one. The module's upgrade policy and its source ride beside the judgement, for the assigner's hold. func TestComposedDigestValue(t *testing.T) { digest := "sha256:" + strings.Repeat("d", 64) against := "sha256:" + strings.Repeat("e", 64) @@ -79,6 +80,8 @@ func TestComposedDigestValue(t *testing.T) { {Digest: digest, Judgement: JudgedRecreates, Against: against, Words: "the container's image changes"}, {Digest: digest, Judgement: JudgedMovesData, Against: against}, {Digest: digest, Judgement: JudgedCannot, Words: "the composition the node runs is gone"}, + {Digest: digest, Judgement: JudgedNeither, Policy: "record", PolicyFrom: "person", PolicyWhy: "the operator: its data"}, + {Digest: digest, Judgement: JudgedNeither, Policy: "roll", PolicyFrom: "default"}, } { raw, err := EncodeComposedDigest(v) if err != nil { @@ -102,6 +105,9 @@ func TestComposedDigestValue(t *testing.T) { {ComposedDigest{Digest: digest, Judgement: JudgedMovesData}, "against"}, {ComposedDigest{Digest: digest, Judgement: JudgedCannot}, "why"}, {ComposedDigest{Digest: digest, Judgement: JudgedNeither, Against: "sha256:zz"}, "not sha256"}, + {ComposedDigest{Digest: digest, Judgement: JudgedNeither, Policy: "sometimes", PolicyFrom: "person"}, `policy "sometimes"`}, + {ComposedDigest{Digest: digest, Judgement: JudgedNeither, Policy: "record"}, `source ""`}, + {ComposedDigest{Digest: digest, Judgement: JudgedNeither, PolicyFrom: "person"}, `policy ""`}, } for _, r := range refusals { if _, err := EncodeComposedDigest(r.v); err == nil || !strings.Contains(err.Error(), r.why) { From e64486b0e6ab573ee089aaaae7915406f8a5a745 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 16:42:41 +0200 Subject: [PATCH 08/13] Sign a composition through a signer that keeps its private half (issue 1943, S5) The controller's identity store signs a message and never hands its key out, as it signed declarations. --- go/delivery/composed_signature.go | 26 +++++++++++++++++++++++--- go/delivery/composed_signature_test.go | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/go/delivery/composed_signature.go b/go/delivery/composed_signature.go index df7219f..1860a5a 100644 --- a/go/delivery/composed_signature.go +++ b/go/delivery/composed_signature.go @@ -36,13 +36,33 @@ func SignComposed(c Composed, key ed25519.PrivateKey) (Composed, error) { if len(key) != ed25519.PrivateKeySize { return Composed{}, fmt.Errorf("delivery: the key to sign a composition with is not an ed25519 private key") } + return SignComposedBy(c, key.Public().(ed25519.PublicKey), func(message []byte) ([]byte, error) { + return ed25519.Sign(key, message), nil + }) +} + +// SignComposedBy is SignComposed for a signer that keeps its private half to itself (the controller's identity store +// signs a message and hands back the signature): sign signs the message it is given with the key whose public half is +// signer. A signature that does not verify against signer is refused, so a composition is never written signed by a +// key other than the one it names. +func SignComposedBy(c Composed, signer ed25519.PublicKey, sign func(message []byte) ([]byte, error)) (Composed, error) { + if len(signer) != ed25519.PublicKeySize { + return Composed{}, fmt.Errorf("delivery: the signer of a composition is not an ed25519 public key") + } + c.Signature = nil digest, err := ComposedDigestOf(c) if err != nil { return Composed{}, err } - sig := ed25519.Sign(key, []byte(composedSigningContext+digest)) - c.Signature = &Signature{Signer: base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey)), - Sig: base64.StdEncoding.EncodeToString(sig)} + message := []byte(composedSigningContext + digest) + sig, err := sign(message) + if err != nil { + return Composed{}, err + } + if !ed25519.Verify(signer, message, sig) { + return Composed{}, fmt.Errorf("delivery: the signature made for the composition of %q on %s does not verify against its signer", c.Module, c.Node) + } + c.Signature = &Signature{Signer: base64.StdEncoding.EncodeToString(signer), Sig: base64.StdEncoding.EncodeToString(sig)} return c, nil } diff --git a/go/delivery/composed_signature_test.go b/go/delivery/composed_signature_test.go index d0606c5..816e8a8 100644 --- a/go/delivery/composed_signature_test.go +++ b/go/delivery/composed_signature_test.go @@ -83,3 +83,21 @@ func TestACompositionIsSignedByATrustedKey(t *testing.T) { t.Errorf("a signature naming no key was encoded") } } + +// 1943/AC5: a signer that keeps its private half signs through a function, and the composition verifies as one signed +// with the key; a function that signs with another key than the one named is refused. +func TestACompositionIsSignedByAKeptKey(t *testing.T) { + pub, priv := aSigningKey(t) + _, other := aSigningKey(t) + c := aComposition() + signed, err := SignComposedBy(c, pub, func(m []byte) ([]byte, error) { return ed25519.Sign(priv, m), nil }) + if err != nil { + t.Fatal(err) + } + if _, err := VerifyComposed(signed, []ed25519.PublicKey{pub}); err != nil { + t.Errorf("a composition signed through the function does not verify: %v", err) + } + if _, err := SignComposedBy(c, pub, func(m []byte) ([]byte, error) { return ed25519.Sign(other, m), nil }); err == nil { + t.Errorf("a signature by another key than the one named was accepted") + } +} From 5da87a0dda89f2b48b23fc0cb7765f0312f7eb92 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 17:03:54 +0200 Subject: [PATCH 09/13] Let the controller say a bucket it deleted at its first start (issue 1943; for issue 1945, B13) ADR 0539 clause 13 has the controller delete the plan-state buckets once and say each as its event. --- go/catalogue/seats.go | 5 ++++- go/catalogue/verbs_r6_test.go | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index a22827c..a0f73eb 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -118,7 +118,10 @@ var defaultSeats = append([]Seat{ "assignment-changed", "declared-config-changed", "predefined-changed", // A builder's key not taken in because the artifact store does not hold an artifact it names (novox/hq // issue 1937): the builder builds it again. - "artifacts-gone"}, + "artifacts-gone", + // A bucket of a state this release deleted, deleted at the controller's first start (novox/hq ADR 0539 + // clause 13): the merge window's, the silent sends' and the building plans' hold, each said once. + "bucket-deleted"}, Serves: ControllerVerbs}, // The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable, // served by whichever module holds the seat with tools of these names. diff --git a/go/catalogue/verbs_r6_test.go b/go/catalogue/verbs_r6_test.go index 8e595db..ddad237 100644 --- a/go/catalogue/verbs_r6_test.go +++ b/go/catalogue/verbs_r6_test.go @@ -101,3 +101,15 @@ func TestTheDeliverySeatHasNoPlanVerbs(t *testing.T) { t.Errorf("the gate of release 5 cannot hold the seat: %v", err) } } + +// 1943 (for 1945/AC14): the controller's seat says a bucket it deleted at its first start as its event, bucket-deleted +// (ADR 0539 clause 13). +func TestTheControllerSaysABucketItDeleted(t *testing.T) { + seat, ok := SeatNamed(ControllerSeatName) + if !ok { + t.Fatal("no controller seat") + } + if !SeatSays(seat.Emits, "bucket-deleted") { + t.Errorf("the controller's seat does not emit bucket-deleted: %v", seat.Emits) + } +} From a523a8cbb0c8b1a83fcccd1790224e1d94380785 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 18:15:32 +0200 Subject: [PATCH 10/13] Say beside a composition that it changes only the bus's user list (issue 1943; the review's A2, for issue 1906) A revision that changes only the user list is written into the bus node's want at once, outside the record hold (ADR 0539 clause 7), so the assigner must tell it apart from a move that recreates or moves data. --- go/delivery/composed.go | 21 +++++++++++++-------- go/delivery/composed_keys_test.go | 1 + 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/go/delivery/composed.go b/go/delivery/composed.go index a77b6ee..d22151a 100644 --- a/go/delivery/composed.go +++ b/go/delivery/composed.go @@ -141,14 +141,19 @@ func checkDigest(digest, of string) error { // own choice (`upgrade` with policy) included. Rewritten when the policy changes. Empty on a composition made by hand // (mesh-compose), whose want the operator writes. type ComposedDigest struct { - V int `json:"v"` - Digest string `json:"digest"` - Judgement Judgement `json:"judgement"` - Against string `json:"against,omitempty"` - Words string `json:"words,omitempty"` - Policy string `json:"policy,omitempty"` - PolicyFrom string `json:"policy-from,omitempty"` - PolicyWhy string `json:"policy-why,omitempty"` + V int `json:"v"` + Digest string `json:"digest"` + Judgement Judgement `json:"judgement"` + Against string `json:"against,omitempty"` + Words string `json:"words,omitempty"` + // UserListOnly says the composition differs from the one the node's have names only in the bus's user list (the + // bus module's file `.bus-users`), whatever the judgement says of its mount: the assigner writes such a + // revision into the bus node's want at once, outside the record hold, at the fingerprint the node runs (ADR 0539 + // clause 7). + UserListOnly bool `json:"user-list-only,omitempty"` + Policy string `json:"policy,omitempty"` + PolicyFrom string `json:"policy-from,omitempty"` + PolicyWhy string `json:"policy-why,omitempty"` } // The upgrade policies and their sources a composition's value names: catalogue.PolicyRoll, PolicyTogether and diff --git a/go/delivery/composed_keys_test.go b/go/delivery/composed_keys_test.go index bd3630e..fc03a5e 100644 --- a/go/delivery/composed_keys_test.go +++ b/go/delivery/composed_keys_test.go @@ -82,6 +82,7 @@ func TestComposedDigestValue(t *testing.T) { {Digest: digest, Judgement: JudgedCannot, Words: "the composition the node runs is gone"}, {Digest: digest, Judgement: JudgedNeither, Policy: "record", PolicyFrom: "person", PolicyWhy: "the operator: its data"}, {Digest: digest, Judgement: JudgedNeither, Policy: "roll", PolicyFrom: "default"}, + {Digest: digest, Judgement: JudgedNeither, Against: against, UserListOnly: true, Words: "only the bus's user list changes"}, } { raw, err := EncodeComposedDigest(v) if err != nil { From e45bdc0891bb7c90be639c4903d51bb5dc61a2ac Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 20:20:48 +0200 Subject: [PATCH 11/13] Drop plan-moved and sent from the controller seat's events (issue 1943; the review's notes on 1945) No plan is kept or sent in release 6 (ADR 0539), and the messenger now hears the delivery view's events instead. --- go/catalogue/seats.go | 5 +---- go/catalogue/verbs_r6_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index a0f73eb..b0327ce 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -110,10 +110,7 @@ var defaultSeats = append([]Seat{ "rolled-back", // A pull request's merge check, judged (novox/hq to-be 45 §9). "checked", - // A walk kept in a new state, for the delivery it walks (novox/hq ADR 0239). - "plan-moved", - // A tier sent to a machine, once the send is kept (novox/hq ADR 0406 §5, issue 974). - "sent", + // (A walk kept in a new state and a tier sent to a machine are gone with the plans, novox/hq ADR 0539.) // A key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3; design 57 F3). "assignment-changed", "declared-config-changed", "predefined-changed", // A builder's key not taken in because the artifact store does not hold an artifact it names (novox/hq diff --git a/go/catalogue/verbs_r6_test.go b/go/catalogue/verbs_r6_test.go index ddad237..f38d7d4 100644 --- a/go/catalogue/verbs_r6_test.go +++ b/go/catalogue/verbs_r6_test.go @@ -113,3 +113,17 @@ func TestTheControllerSaysABucketItDeleted(t *testing.T) { t.Errorf("the controller's seat does not emit bucket-deleted: %v", seat.Emits) } } + +// 1943 (the review's notes on 1945): the controller's seat no longer says a walk moved or a tier was sent: there is no +// plan (ADR 0539). +func TestTheControllerSaysNoPlanEvents(t *testing.T) { + seat, ok := SeatNamed(ControllerSeatName) + if !ok { + t.Fatal("no controller seat") + } + for _, gone := range []string{"plan-moved", "sent"} { + if SeatSays(seat.Emits, gone) { + t.Errorf("the controller's seat still emits %s", gone) + } + } +} From 7f9cc0e55c37930aaafe65e28e1ca2c0dcf2d16a Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 20:38:24 +0200 Subject: [PATCH 12/13] Keep plan-moved and sent among the controller seat's events until the catalogue's main consumes them no more (issue 1943) The controller's broker tests read the catalogue's main, whose gate and messenger still consume plan-moved; nothing emits them (ADR 0539). --- go/catalogue/seats.go | 5 ++++- go/catalogue/verbs_r6_test.go | 14 -------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index b0327ce..7337d8a 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -110,7 +110,10 @@ var defaultSeats = append([]Seat{ "rolled-back", // A pull request's merge check, judged (novox/hq to-be 45 §9). "checked", - // (A walk kept in a new state and a tier sent to a machine are gone with the plans, novox/hq ADR 0539.) + // A walk kept in a new state, and a tier sent to a machine: kept until the catalogue's main consumes them no + // more; nothing emits them (ADR 0539). + "plan-moved", + "sent", // A key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3; design 57 F3). "assignment-changed", "declared-config-changed", "predefined-changed", // A builder's key not taken in because the artifact store does not hold an artifact it names (novox/hq diff --git a/go/catalogue/verbs_r6_test.go b/go/catalogue/verbs_r6_test.go index f38d7d4..ddad237 100644 --- a/go/catalogue/verbs_r6_test.go +++ b/go/catalogue/verbs_r6_test.go @@ -113,17 +113,3 @@ func TestTheControllerSaysABucketItDeleted(t *testing.T) { t.Errorf("the controller's seat does not emit bucket-deleted: %v", seat.Emits) } } - -// 1943 (the review's notes on 1945): the controller's seat no longer says a walk moved or a tier was sent: there is no -// plan (ADR 0539). -func TestTheControllerSaysNoPlanEvents(t *testing.T) { - seat, ok := SeatNamed(ControllerSeatName) - if !ok { - t.Fatal("no controller seat") - } - for _, gone := range []string{"plan-moved", "sent"} { - if SeatSays(seat.Emits, gone) { - t.Errorf("the controller's seat still emits %s", gone) - } - } -} From d7a9b9688b768a11459bd9a65bb954361b35be77 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 21:17:14 +0200 Subject: [PATCH 13/13] Let the judgement beside a composition say unchanged: equal to the declaration the node last applied (issue 1943; ADR 0540 clause 9) The first want at the switch skips the record hold only where the composition changes nothing on the node. --- go/delivery/composed.go | 10 +++++++--- go/delivery/composed_keys_test.go | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/go/delivery/composed.go b/go/delivery/composed.go index d22151a..0509bbb 100644 --- a/go/delivery/composed.go +++ b/go/delivery/composed.go @@ -120,6 +120,10 @@ const ( JudgedNeither Judgement = "neither" // JudgedCannot is a move that cannot be judged, because the composition the node runs is gone or cannot be read. JudgedCannot Judgement = "cannot-judge" + // JudgedUnchanged is a composition equal to the module's slice of the declaration the node last applied, for a node + // whose have names no composition yet (the first want at the switch, novox/hq ADR 0540 clause 9): applying it + // changes nothing on the node, so it needs no person's yes. + JudgedUnchanged Judgement = "unchanged" ) var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) @@ -187,14 +191,14 @@ func (c ComposedDigest) check() error { if c.Against == "" { return fmt.Errorf("delivery: a move judged %s names the composition it is judged against", c.Judgement) } - case JudgedNeither: + case JudgedNeither, JudgedUnchanged: case JudgedCannot: if c.Words == "" { return fmt.Errorf("delivery: a move that cannot be judged says why") } default: - return fmt.Errorf("delivery: the judgement %q is not %s, %s, %s or %s", c.Judgement, JudgedRecreates, - JudgedMovesData, JudgedNeither, JudgedCannot) + return fmt.Errorf("delivery: the judgement %q is not %s, %s, %s, %s or %s", c.Judgement, JudgedRecreates, + JudgedMovesData, JudgedNeither, JudgedCannot, JudgedUnchanged) } if c.Policy != "" || c.PolicyFrom != "" { if !oneOf(c.Policy, composedPolicies) { diff --git a/go/delivery/composed_keys_test.go b/go/delivery/composed_keys_test.go index fc03a5e..5882208 100644 --- a/go/delivery/composed_keys_test.go +++ b/go/delivery/composed_keys_test.go @@ -80,6 +80,7 @@ func TestComposedDigestValue(t *testing.T) { {Digest: digest, Judgement: JudgedRecreates, Against: against, Words: "the container's image changes"}, {Digest: digest, Judgement: JudgedMovesData, Against: against}, {Digest: digest, Judgement: JudgedCannot, Words: "the composition the node runs is gone"}, + {Digest: digest, Judgement: JudgedUnchanged, Words: "equal to the declaration the node last applied"}, {Digest: digest, Judgement: JudgedNeither, Policy: "record", PolicyFrom: "person", PolicyWhy: "the operator: its data"}, {Digest: digest, Judgement: JudgedNeither, Policy: "roll", PolicyFrom: "default"}, {Digest: digest, Judgement: JudgedNeither, Against: against, UserListOnly: true, Words: "only the bus's user list changes"},