diff --git a/checks/readable/go.mod b/checks/readable/go.mod index 5e10aa28e..8de6e7097 100644 --- a/checks/readable/go.mod +++ b/checks/readable/go.mod @@ -2,6 +2,11 @@ module checks/readable go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f +require ( + git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-catalog/modules/restic/backupgood v0.0.0-00010101000000-000000000000 +) -require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect +replace git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings => ../../modules/disk-load/diskreadings + +replace git.novox.be/novox/mesh-catalog/modules/restic/backupgood => ../../modules/restic/backupgood diff --git a/checks/readable/main.go b/checks/readable/main.go index 8fa81fba9..b455f0f35 100644 --- a/checks/readable/main.go +++ b/checks/readable/main.go @@ -6,7 +6,7 @@ // the owner's manifest went with clause 5, and announcetest holds a module to its own list, not to its readers. // // An owner announces from its code. A Go module announces when its code imports mesh-sdk's go/announce: every -// announce.State literal there is read, its Name a string, a constant of the package, or a constant of mesh-sdk this +// announce.State literal there is read, its Name a string, a constant of the package, or a constant of an owner's package this // check knows (Known). A TypeScript module announces when its code says the event `states` or calls the SDK's keeps: // every `{ name: "...", readable: true }` there is read. An owner that announces nothing from code is read as before // on the mesh, and is not judged; neither is an owner outside the catalogue (another repository's). @@ -29,13 +29,15 @@ import ( "strconv" "strings" - "git.novox.be/novox/mesh-sdk/go/health" + "git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings" + "git.novox.be/novox/mesh-catalog/modules/restic/backupgood" ) -// Known are the mesh-sdk constants an announcement may name a state by, by the selector the code writes. +// Known are the constants of the owners' published packages an announcement may name a state by, by the selector the +// code writes: disk-load's readings and restic's last good night (ADR 0634 §4: a reading's layout is its owner's). var Known = map[string]string{ - "health.GoodState": health.GoodState, - "health.ReadingsState": health.ReadingsState, + "backupgood.GoodState": backupgood.GoodState, + "diskreadings.ReadingsState": diskreadings.ReadingsState, } const announcePkg = `"git.novox.be/novox/mesh-sdk/go/announce"` diff --git a/checks/readable/main_test.go b/checks/readable/main_test.go index 3a15e322c..808b5326e 100644 --- a/checks/readable/main_test.go +++ b/checks/readable/main_test.go @@ -30,7 +30,7 @@ func TestFindings(t *testing.T) { {"go, a known mesh-sdk name and readers", map[string]string{ "modules/r/module.json": `{"module":"r","reads":["o.good","o.s"]}`, "modules/o/module.json": `{"module":"o"}`, - "modules/o/cmd/o/main.go": goOwner + "var _ = []announce.State{{Name: health.GoodState, Readable: true}, {Name: \"s\", Readers: []string{\"r\"}}}\n"}, ""}, + "modules/o/cmd/o/main.go": goOwner + "var _ = []announce.State{{Name: backupgood.GoodState, Readable: true}, {Name: \"s\", Readers: []string{\"r\"}}}\n"}, ""}, {"go, a name that cannot be read", map[string]string{ "modules/r/module.json": `{"module":"r","reads":["o.s"]}`, "modules/o/module.json": `{"module":"o"}`, diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/gone.go b/modules/artifact-store-tools/cmd/artifact-store-tools/gone.go index e9804f483..cca4a96df 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/gone.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/gone.go @@ -4,7 +4,8 @@ import ( "encoding/json" "fmt" "sort" - "time" + + sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" ) // The holders whose states this module reads, one constant each (a reader names its holder once; ADR 0634 §4). @@ -13,38 +14,26 @@ const ( indexerModule = "mesh-delivery-indexer" ) -// ArtifactsGoneEvent is the event the seat mesh-artifact-sweep says when the store does not hold an artifact a -// builder's key names. The seat is a seat-list seat, which has no generated types, so its two bodies are the holder's -// own: this module is the one that says the event and answers the verb held. -const ArtifactsGoneEvent = "artifacts-gone" +// The seat mesh-artifact-sweep types the answer of its verb held and the body of its event artifacts-gone; this module +// holds the seat, so it answers and says them in those types. +type ( + // HeldAnswer is the answer of the seat's verb held. + HeldAnswer = sweep.HeldAnswer + // ArtifactsGone is the body of the seat's event artifacts-gone. + ArtifactsGone = sweep.ArtifactsGone +) -// HeldAnswer is the answer of the seat's verb held: the references the store holds, and those it answered 404 for. A -// reference the store could not be asked about is in neither, and named in Unknown with why. -type HeldAnswer struct { - Store string `json:"store,omitempty"` - Held []string `json:"held"` - Gone []string `json:"gone"` - Unknown map[string]string `json:"unknown,omitempty"` -} +// GoneSubject is where the seat says artifacts-gone. +const GoneSubject = sweep.SubjectArtifactsGone -// ArtifactsGone is the body of ArtifactsGoneEvent: the module whose builder's key names a gone artifact, the key and -// its fingerprint, and every reference of it the store does not hold. -type ArtifactsGone struct { - Event string `json:"event"` - Module string `json:"module"` - Key string `json:"key"` - Fingerprint string `json:"fingerprint"` - Gone []string `json:"gone"` - At time.Time `json:"at"` -} - -// EncodeArtifactsGone is the event's body, the references sorted. +// EncodeArtifactsGone is the event's body, the references sorted; it refuses one naming no module, no fingerprint or no +// gone reference. func EncodeArtifactsGone(e ArtifactsGone) ([]byte, error) { - e.Event = ArtifactsGoneEvent + e.Event = sweep.EventArtifactsGone e.Gone = append([]string(nil), e.Gone...) sort.Strings(e.Gone) if e.Module == "" || e.Fingerprint == "" || len(e.Gone) == 0 { - return nil, fmt.Errorf("%s names a module, a fingerprint and at least one gone reference", ArtifactsGoneEvent) + return nil, fmt.Errorf("%s names a module, a fingerprint and at least one gone reference", sweep.EventArtifactsGone) } return json.Marshal(e) } diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go b/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go index 1d46218cc..1f3d8f09b 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/holder.go @@ -49,8 +49,6 @@ const ( FindingsState = meshconditions.StateFindings // Module is this module, the observer its findings name. Module = "artifact-store-tools" - // GoneSubject is where the seat says artifacts-gone. - GoneSubject = "mesh.seat." + catalogue.ArtifactSweepSeat + ".event." + ArtifactsGoneEvent ) // The sweep's bounds, the controller's (cmd/mesh-controller/collect.go mostPerSweep, sweepBudget). @@ -628,7 +626,8 @@ func (h *Holder) sayGone(j *judged, refs []string, how string) { for _, b := range ref.Builds { g := byKey[b.Key] if g == nil { - g = &ArtifactsGone{Module: b.Module, Key: b.Key, Fingerprint: b.FP, At: h.now().UTC()} + at := h.now().UTC() + g = &ArtifactsGone{Module: b.Module, Key: b.Key, Fingerprint: b.FP, At: &at} byKey[b.Key] = g keys = append(keys, b.Key) } diff --git a/modules/artifact-store-tools/go.mod b/modules/artifact-store-tools/go.mod index 66c3b9f23..adcd679f0 100644 --- a/modules/artifact-store-tools/go.mod +++ b/modules/artifact-store-tools/go.mod @@ -3,6 +3,7 @@ module artifactstoretools go 1.26.0 require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 @@ -22,6 +23,7 @@ require ( require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep v0.0.0-00010101000000-000000000000 github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.1 // indirect diff --git a/modules/distribution/internal/holder/window.go b/modules/distribution/internal/holder/window.go index 8ebae12af..4ba0971e8 100644 --- a/modules/distribution/internal/holder/window.go +++ b/modules/distribution/internal/holder/window.go @@ -3,7 +3,7 @@ package holder import ( "fmt" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/window" "slices" "sort" "time" @@ -29,11 +29,11 @@ const ( ) // Window is one window's record, kept in distribution's state windows: what registry_old_name and -// registry_direct_writes answer. Its open, holding, since, closed and last holding are mesh-sdk's delivery.WindowHeld, -// the part every window keeper shares and steps by one rule (delivery.StepWindow). +// registry_direct_writes answer. Its open, holding, since, closed and last holding are mesh-delivery's sdk/window.WindowHeld, +// the part every window keeper shares and steps by one rule (window.StepWindow). type Window struct { Name string `json:"name"` - delivery.WindowHeld + window.WindowHeld Dropped []DroppedNode `json:"dropped,omitempty"` // Why says, of each node holding the window, why it holds it: what its read answered, a node-engine whose // manifest-reads did not answer (an away node, an older node-engine, a timeout) included, so a timeout taken as @@ -134,7 +134,7 @@ func isDropped(dropped []DroppedNode, node string) bool { return slices.ContainsFunc(dropped, func(d DroppedNode) bool { return d.Node == node }) } -// Step moves the window by mesh-sdk's one rule (delivery.StepWindow): named is every node the read named, dropped ones +// Step moves the window by mesh-sdk's one rule (window.StepWindow): named is every node the read named, dropped ones // included, holding those holding the window at now. A read naming no node judges nothing. It answers the next record, // the closing event when it closed in this step (said once), and the nodes holding it past WindowFindingAfter (the // finding; cleared when it closes). A window once closed stays closed: a node that holds it later is no reason to serve @@ -144,7 +144,7 @@ func (w Window) Step(named, holding []string, now time.Time) (next Window, close if !w.Open && !w.Closed.IsZero() { return next, nil, nil } - held, shut, overdue := delivery.StepWindow(w.WindowHeld, named, holding, now, WindowFindingAfter) + held, shut, overdue := window.StepWindow(w.WindowHeld, named, holding, now, WindowFindingAfter) next.WindowHeld = held if !held.Open { next.Why = nil diff --git a/modules/gitea/cmd/package-registry/whole_integration_test.go b/modules/gitea/cmd/package-registry/whole_integration_test.go index 401adb78c..788c81c8d 100644 --- a/modules/gitea/cmd/package-registry/whole_integration_test.go +++ b/modules/gitea/cmd/package-registry/whole_integration_test.go @@ -37,7 +37,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/testbus" "gitea/internal/packageseats" @@ -118,7 +118,7 @@ func (p *player) handle(m wire) { } _ = json.Unmarshal(m.Params, &a) switch { - case m.Method == "mesh/ask" && a.Key == delivery.BuilderVersionsAddress: + case m.Method == "mesh/ask" && a.Key == builds.BuilderVersionsAddress: var q struct{ Name, Version, Digest string } _ = json.Unmarshal(a.Body, &q) d, ok := p.numbered[q.Name+"@"+q.Version] diff --git a/modules/gitea/internal/packageseats/holder.go b/modules/gitea/internal/packageseats/holder.go index 2450607c6..244f1322d 100644 --- a/modules/gitea/internal/packageseats/holder.go +++ b/modules/gitea/internal/packageseats/holder.go @@ -126,7 +126,7 @@ type Version struct { Published time.Time } -// Builder answers whether the builder numbered a version with a digest (mesh-sdk delivery.BuilderVersions). +// Builder answers whether the builder numbered a version with a digest (the builder package's builds.BuilderVersions). type Builder interface { Recorded(name, version, digest string) (ok bool, why string, err error) } diff --git a/modules/mesh-config/cmd/mesh-config/proposals.go b/modules/mesh-config/cmd/mesh-config/proposals.go index 7307c49ed..d3e689dc9 100644 --- a/modules/mesh-config/cmd/mesh-config/proposals.go +++ b/modules/mesh-config/cmd/mesh-config/proposals.go @@ -580,7 +580,7 @@ func (c *Config) propose(call Call, action string) (Proposal, oc.Ask, []string, } trusted = false k, kept := s.Kept[keptKey(call.Pull, level.Node, level.Module, call.Key)] - was, had = Value{Value: k.Value}, kept + was, had = Value{Value: heldValue(k)}, kept } else { var derr error r, trusted, derr = c.declared(s, level, call.Key) diff --git a/modules/mesh-config/cmd/mesh-config/pulls.go b/modules/mesh-config/cmd/mesh-config/pulls.go index 283d99270..0051d8781 100644 --- a/modules/mesh-config/cmd/mesh-config/pulls.go +++ b/modules/mesh-config/cmd/mesh-config/pulls.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient" "regexp" "strconv" "strings" @@ -61,32 +62,25 @@ type PullDecl struct { At time.Time `json:"at"` } -// KeptValue is an approved held value, kept apart from the values mesh-config resolves, in this module's own layout -// (the owner's: its state `kept`), which the checker and the assigner's composition check read (novox/hq ADR 0565 §5). -type KeptValue struct { - Pull string `json:"pull"` - Head string `json:"head"` - Digest string `json:"declaration"` - Node string `json:"node"` - Module string `json:"module"` - Key string `json:"key"` - // Value is the value held, as the operator approved it. - Value json.RawMessage `json:"value"` - By string `json:"by"` - Via string `json:"via"` - Why string `json:"why"` - // Approved is who approved it, as the proposal kept it. - Approved string `json:"approved"` - Proposal string `json:"proposal"` - At time.Time `json:"at"` -} +// KeptValue is an approved held value, kept apart from the values mesh-config resolves, in the layout of the seat's state +// `kept`, which the checker and the assigner's composition check read (novox/hq ADR 0565 §5, ADR 0634 §4). +type KeptValue = meshconfigclient.Kept // keptKey is a held value's key in `kept`: the pull request's key form (pullKey), the node, the module and the config // key, joined by dots. func keptKey(pull, node, module, key string) string { - return pullKey(pull) + "." + node + "." + module + "." + key + k, _ := meshconfigclient.HeldValueKey(pull, node, module, key) // a held value of no pull request has no key, and is found by none + return k } +// heldValue is the value held, as the operator approved it, in its JSON form. +func heldValue(k KeptValue) json.RawMessage { + raw, _ := meshconfigclient.ValueOf(k) // a value that was read from JSON writes as JSON + return raw +} + +func heldAt(t time.Time) *time.Time { return &t } + func keptStateKey(k KeptValue) string { return keptKey(k.Pull, k.Node, k.Module, k.Key) } func keptLevel(k KeptValue) Level { return Level{Kind: LevelModule, Node: k.Node, Module: k.Module} } @@ -123,9 +117,9 @@ func (c *Config) readPulls(s *Sources) error { return fmt.Errorf("the pull requests' declarations cannot be read: %w", err) } if err := c.each(KeptState, func(key string, raw []byte) error { - var k KeptValue - if err := json.Unmarshal(raw, &k); err != nil || k.Pull == "" { - return nil + k, err := meshconfigclient.DecodeKept(key, raw) + if err != nil { + return nil // not a held value of the seat's layout under its own key } s.Kept[key] = k return nil @@ -176,11 +170,11 @@ func (c *Config) declarePull(pull, head string, declares map[string]map[string]D switch { case !declared: why = "the head no longer adds or changes its declaration" - case declDigest(d) != kv.Digest: + case declDigest(d) != kv.Declaration: why = "the head declares the key otherwise than the value was approved against" default: var v any - _ = json.Unmarshal(kv.Value, &v) + _ = json.Unmarshal(heldValue(kv), &v) if p := d.rule().check(v); len(p) > 0 { why = "the value no longer passes: " + strings.Join(p, "; ") } @@ -208,13 +202,13 @@ func (c *Config) declarePull(pull, head string, declares map[string]map[string]D // config-changed; what could not be done is answered, not dropped. func (c *Config) heldChange(kv KeptValue, action, why, approved string) []string { level := keptLevel(kv) - ch := Change{Action: action, Level: &level, Key: kv.Key, Value: kv.Value, By: kv.By, Via: kv.Via, Why: why, + ch := Change{Action: action, Level: &level, Key: kv.Key, Value: heldValue(kv), By: kv.By, Via: kv.Via, Why: why, Approved: approved, Pull: kv.Pull, Head: kv.Head, At: c.now(), Moved: []Moved{}} unsaid := c.record(level.ValueKey(kv.Key), ch) cause := "a value held for " + kv.Pull what := fmt.Sprintf("config %s %s of %s for %s, held for %s", action, kv.Key, kv.Module, kv.Node, kv.Pull) if action != ActionDrop { - what += " (" + string(kv.Value) + ")" + what += " (" + string(heldValue(kv)) + ")" } if err := c.Store.HandAct(what, why, cause); err != nil { unsaid = append(unsaid, "the hand-act log does not hold it: "+err.Error()) @@ -260,8 +254,8 @@ type HeldSkipped struct { } func heldValueOf(k KeptValue) HeldValue { - return HeldValue{Pull: k.Pull, Head: k.Head, Node: k.Node, Module: k.Module, Key: k.Key, Value: compact(k.Value), - Approved: k.Approved, By: k.By, Why: k.Why, At: k.At} + return HeldValue{Pull: k.Pull, Head: k.Head, Node: k.Node, Module: k.Module, Key: k.Key, Value: compact(heldValue(k)), + Approved: k.Approved, By: k.By, Why: k.Why, At: timeOf(k.At)} } // Held answers the approved held values for the pull requests asked, at the heads asked, to the controller alone: a @@ -373,13 +367,13 @@ func (c *Config) keep(p Proposal) (string, error) { "propose it again", p.Pull, p.Key) } was, had := s.Kept[keptKey(p.Pull, p.Level.Node, p.Level.Module, p.Key)] - if had != p.HadValue || valueDigest(was.Value, had) != p.WasDigest { + if had != p.HadValue || valueDigest(heldValue(was), had) != p.WasDigest { return "", fmt.Errorf("the value held for %s changed since the operator was shown the change: propose it "+ "again", p.Pull) } - kv := KeptValue{Pull: p.Pull, Head: pd.Head, Digest: p.DeclDigest, Node: p.Level.Node, Module: p.Level.Module, + kv := KeptValue{Pull: p.Pull, Head: pd.Head, Declaration: p.DeclDigest, Node: p.Level.Node, Module: p.Level.Module, Key: p.Key, Value: p.Value, By: "the operator", Via: p.From, Why: p.Why, Approved: p.Approved, Proposal: p.ID, - At: c.now()} + At: heldAt(c.now())} if err := c.Store.Put(KeptState, keptStateKey(kv), kv); err != nil { return "", fmt.Errorf("the value was not kept: %v", err) } @@ -434,7 +428,7 @@ func (c *Config) settlePull(pull string, merged bool, head string) []string { why = "the merged head is not known, so the value cannot be held to it" case !strings.EqualFold(head, kv.Head): why = fmt.Sprintf("it is held at head %s and the pull request merged at %s", short(kv.Head), short(head)) - case !known || !declared || declDigest(d) != kv.Digest: + case !known || !declared || declDigest(d) != kv.Declaration: why = "the merged head does not declare the key as the operator was shown" } if why != "" { @@ -444,7 +438,7 @@ func (c *Config) settlePull(pull string, merged bool, head string) []string { continue } vk := keptLevel(kv).ValueKey(kv.Key) - v := Value{Value: compact(kv.Value), By: kv.By, Via: kv.Via, Why: kv.Why + " (held for " + kv.Pull + + v := Value{Value: compact(heldValue(kv)), By: kv.By, Via: kv.Via, Why: kv.Why + " (held for " + kv.Pull + ", merged)", Approved: kv.Approved, At: c.now()} if err := c.Store.Put(ValuesState, vk, v); err != nil { said = append(said, fmt.Sprintf("%s of %s on %s held for %s is not written yet: %v; it is tried "+ diff --git a/modules/mesh-config/cmd/mesh-config/pulls_test.go b/modules/mesh-config/cmd/mesh-config/pulls_test.go index 3877fde41..45b16a5a4 100644 --- a/modules/mesh-config/cmd/mesh-config/pulls_test.go +++ b/modules/mesh-config/cmd/mesh-config/pulls_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "errors" + "git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient" "strings" "testing" "time" @@ -317,8 +318,11 @@ func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *tes holdIt(t, c, m, r) approved := func() string { raw, _ := m.Get(KeptState, "novox.mesh-catalog.434.ace.n8n.media-gid") - var k KeptValue - _ = json.Unmarshal(raw.Value, &k) + // the state kept is the seat's layout, under the seat's key: the checker and the assigner read it with this + k, err := meshconfigclient.DecodeKept("novox.mesh-catalog.434.ace.n8n.media-gid", raw.Value) + if err != nil || string(heldValue(k)) != "2000" || k.Declaration == "" || k.At == nil { + t.Fatalf("the held value is not the seat's: %+v, %v", k, err) + } return k.Approved }() m.events, m.handActs = nil, nil diff --git a/modules/mesh-config/go.mod b/modules/mesh-config/go.mod index cb405a36a..d2c1c718f 100644 --- a/modules/mesh-config/go.mod +++ b/modules/mesh-config/go.mod @@ -3,6 +3,7 @@ module mesh-config go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 @@ -21,6 +22,7 @@ require ( ) require ( + git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect diff --git a/modules/mesh-self-check/cmd/mesh-self-check/main.go b/modules/mesh-self-check/cmd/mesh-self-check/main.go index 27d588ffe..6ddc930a6 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/main.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/main.go @@ -9,12 +9,13 @@ import ( "context" "encoding/json" "fmt" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "os" "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" "git.novox.be/novox/mesh-sdk/go/views" @@ -76,10 +77,10 @@ func NewChecker() *Checker { // self-check's own (first-view-unanswered), written into its state findings. func newViews() *views.Reader { r := &views.Reader{Module: Module, Logf: logf, Findings: meshconditionsclient.FindingWriter{State: stdio.State("findings")}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}} - r.Follow(delivery.AssignerModule, delivery.PlacementState, PlacementTool) - r.Follow(delivery.AssignerModule, delivery.SeatHoldersState, SeatHoldersTool) - r.Follow(delivery.IndexerModule, "modules", ModulesTool) - r.Follow(delivery.IndexerModule, "manifests", ManifestsTool) + r.Follow(assignerclient.Module, placing.StatePlacement, PlacementTool) + r.Follow(assignerclient.Module, placing.StateSeatHolders, SeatHoldersTool) + r.Follow(indexerModule, "modules", ModulesTool) + r.Follow(indexerModule, "manifests", ManifestsTool) return r } diff --git a/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go b/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go index a73b0edda..406d0d051 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/probes_data.go @@ -22,15 +22,14 @@ import ( "git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings" "git.novox.be/novox/mesh-catalog/modules/restic/backupgood" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // The states D13 reads (declared under `reads`). const ( ReadingsRead = diskreadings.ReadingsRead GoodRead = backupgood.GoodRead - ModulesRead = delivery.IndexerModule + ".modules" - ManifestsRead = delivery.IndexerModule + ".manifests" + ModulesRead = indexerModule + ".modules" + ManifestsRead = indexerModule + ".manifests" ) // The kinds D13 raises. diff --git a/modules/mesh-self-check/cmd/mesh-self-check/probes_mesh.go b/modules/mesh-self-check/cmd/mesh-self-check/probes_mesh.go index 878e053d2..ed2eeb2f7 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/probes_mesh.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/probes_mesh.go @@ -10,6 +10,9 @@ import ( "context" "encoding/json" "fmt" + watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "net" "regexp" "slices" @@ -18,22 +21,21 @@ import ( "sync" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // The states the machine probes read (declared under `reads`), and the verb D8 asks each node. const ( - PlacementRead = delivery.AssignerModule + "." + "placement" - SeatHoldersRead = delivery.SeatHoldersRead - PresenceRead = delivery.WatchmanModule + ".presence" + PlacementRead = assignerclient.Module + "." + "placement" + SeatHoldersRead = assignerclient.SeatHoldersRead + PresenceRead = watchmanModule + ".presence" BannedSeat = "node-intrusion-prevention" BannedVerb = "seat:" + BannedSeat + ".banned" ScopeMachine = meshconditionsclient.ScopeMachine ) // placements is every node's placement, by node, from the state's keys in either form: a node key and its module keys, -// or the form before, `placement.` (delivery.PlacementsOf; research 072 row 37). -func (c *Checker) placements() (map[string]delivery.Placement, error) { +// or the form before, `placement.` (assignerclient.PlacementsOf; research 072 row 37). +func (c *Checker) placements() (map[string]placing.Placement, error) { keys, err := c.Store.Keys(PlacementRead) if err != nil { return nil, fmt.Errorf("the placement cannot be read: %w", err) @@ -48,7 +50,7 @@ func (c *Checker) placements() (map[string]delivery.Placement, error) { values[k] = raw } } - out, err := delivery.PlacementsOf(values) + out, err := assignerclient.PlacementsOf(values) if err != nil { return nil, fmt.Errorf("the placement is not readable: %w", err) } @@ -63,15 +65,15 @@ func (c *Checker) heard() (map[string]bool, error) { } out := map[string]bool{} for _, k := range keys { - node, _, _, err := delivery.ParsePresenceKey(k) - if err != nil || node == "" { + node, ok := watchdogs.ParsePresenceByNodeKey(k) + if !ok || node == "" { continue } raw, ok, err := c.Store.Get(PresenceRead, k) if err != nil { return nil, fmt.Errorf("the presence of %s cannot be read: %w", node, err) } - var p delivery.Presence + var p watchdogs.PresenceByNodeValue if ok && json.Unmarshal(raw, &p) == nil && p.Online { out[node] = true } @@ -94,7 +96,7 @@ func (c *Checker) holdersOf(seat string) ([]string, error) { if err != nil { return nil, fmt.Errorf("the holders of %s cannot be read: %w", k, err) } - var sh delivery.SeatHolders + var sh placing.SeatHolders if !ok || json.Unmarshal(raw, &sh) != nil || sh.Seat != seat || sh.Node == "" || len(sh.Holders) == 0 { continue } @@ -214,3 +216,10 @@ func probeBans(ctx context.Context, c *Checker) ([]Observation, error) { } return sorted(out), nil } + +// The holders whose states the probes read, one constant each (a reader names its holder once; ADR 0634 §4). +const ( + watchmanModule = "mesh-delivery-watchman" + indexerModule = "mesh-delivery-indexer" + nodeEngineModule = "mesh-host" +) diff --git a/modules/mesh-self-check/cmd/mesh-self-check/probes_root.go b/modules/mesh-self-check/cmd/mesh-self-check/probes_root.go index c52c72ff8..ee20deaa9 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/probes_root.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/probes_root.go @@ -9,18 +9,19 @@ package main import ( "context" "fmt" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "sort" "strings" "sync" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // The engine's words and bounds the judgement reads (mesh-host rootsearch). const ( // HealthRead is the node-engines' health reports. - HealthRead = delivery.NodeHealthRead + HealthRead = (nodeEngineModule + "." + nodeseat.StateHealth) // rootPending starts a verdict's reason while the engine's setuid search runs and no complete one judges. rootPending = "not judged yet (search running)" // rootQuiet is how long nothing is raised while an agent account waits for that search, counted from when this @@ -32,17 +33,17 @@ const ( ) // report is a node's health report, and whether it has one. -func (c *Checker) report(node string) (delivery.NodeHealth, bool, error) { +func (c *Checker) report(node string) (nodeseat.HealthByNodeValue, bool, error) { raw, ok, err := c.Store.Get(HealthRead, node) if err != nil { - return delivery.NodeHealth{}, false, fmt.Errorf("the health report of %s cannot be read: %w", node, err) + return nodeseat.HealthByNodeValue{}, false, fmt.Errorf("the health report of %s cannot be read: %w", node, err) } if !ok { - return delivery.NodeHealth{}, false, nil + return nodeseat.HealthByNodeValue{}, false, nil } - h, err := delivery.DecodeNodeHealth(raw) + h, err := engineclient.DecodeHealth(raw) if err != nil { - return delivery.NodeHealth{}, false, fmt.Errorf("the health report of %s: %w", node, err) + return nodeseat.HealthByNodeValue{}, false, fmt.Errorf("the health report of %s: %w", node, err) } return h, true, nil } @@ -83,7 +84,7 @@ const ( // judgedConfined is the controller's judgement over one machine's report: confined only when the machine is heard // from, its engine judges root, and every verdict on the agent account is healthy. why is said either way. -func judgedConfined(agent string, h delivery.NodeHealth, had, heard bool) (bool, string) { +func judgedConfined(agent string, h nodeseat.HealthByNodeValue, had, heard bool) (bool, string) { switch { case !had: return false, fmt.Sprintf("the agent account %s is not judged: the machine's node-engine has stated "+ @@ -91,10 +92,10 @@ func judgedConfined(agent string, h delivery.NodeHealth, had, heard bool) (bool, case !heard: return false, fmt.Sprintf("the agent account %s is not judged: the machine is not heard from, and a verdict "+ "from before is not a verdict on now", agent) - case h.Contract < delivery.RootContract: + case h.Contract < engineclient.RootContract: return false, fmt.Sprintf("the agent account %s is not judged: the machine's node-engine is older than "+ "the judging of an account's root (its statement's contract is %d, the judging is %d)", - agent, h.Contract, delivery.RootContract) + agent, h.Contract, engineclient.RootContract) } verdicts := verdictsOn(agent, h) if len(verdicts) == 0 { @@ -118,8 +119,8 @@ func judgedConfined(agent string, h delivery.NodeHealth, had, heard bool) (bool, } // verdictsOn is every verdict on the account, sorted by module and resource. -func verdictsOn(agent string, h delivery.NodeHealth) []delivery.RootVerdict { - var out []delivery.RootVerdict +func verdictsOn(agent string, h nodeseat.HealthByNodeValue) []nodeseat.RootAccountVerdict { + var out []nodeseat.RootAccountVerdict for _, v := range h.Root { if v.Account == agent { out = append(out, v) @@ -131,8 +132,8 @@ func verdictsOn(agent string, h delivery.NodeHealth) []delivery.RootVerdict { // rootVerdictKind reads a report for the agent account: pending when every verdict on it is healthy or waits for the // setuid search, at least one of them that; complete when every verdict is healthy or one found a way to root. -func rootVerdictKind(agent string, h delivery.NodeHealth, had, heard bool) int { - if !had || !heard || h.Contract < delivery.RootContract { +func rootVerdictKind(agent string, h nodeseat.HealthByNodeValue, had, heard bool) int { + if !had || !heard || h.Contract < engineclient.RootContract { return verdictOther } pending, any := false, false @@ -159,7 +160,7 @@ func rootVerdictKind(agent string, h delivery.NodeHealth, had, heard bool) int { // agentQuiet says the one thing keeping a machine's agent account unjudged is the engine's setuid search, first seen // waiting less than rootQuiet ago. -func (c *Checker) agentQuiet(node, agent string, h delivery.NodeHealth, had, heard bool) bool { +func (c *Checker) agentQuiet(node, agent string, h nodeseat.HealthByNodeValue, had, heard bool) bool { switch rootVerdictKind(agent, h, had, heard) { case verdictComplete: c.pending.judged(node) @@ -186,7 +187,7 @@ type confinement struct { } // confined judges node's agent account from its placement, its report and its presence. -func (c *Checker) confined(node string, place delivery.Placement, heard bool) (confinement, error) { +func (c *Checker) confined(node string, place placing.Placement, heard bool) (confinement, error) { agent := place.Decided.AgentAccount if agent == "" { return confinement{Why: fmt.Sprintf("%s names no agent account: agents run as the operator account (%s)", diff --git a/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go b/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go index 2165a844a..5e569af83 100644 --- a/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go +++ b/modules/mesh-self-check/cmd/mesh-self-check/probes_test.go @@ -4,6 +4,13 @@ import ( "context" "encoding/json" "errors" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "slices" "strings" "testing" @@ -13,7 +20,6 @@ import ( "git.novox.be/novox/mesh-catalog/modules/restic/backupgood" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // bus is the bus module's answers as a probe asks them: every mesh stream and consumer as defined, unless changed. @@ -257,42 +263,42 @@ func machines() *memStore { node, address, endpoint string online bool }{{"novox", "10.10.0.1/16", "203.0.113.5:51820", true}, {"ace", "10.10.0.2/16", "", true}, {"g14", "10.10.0.3/16", "", false}} { - pl := delivery.Placement{V: 1, Node: p.node, - Decided: delivery.NodeDecided{Address: p.address, Endpoint: p.endpoint, Hub: p.endpoint != ""}} + pl := placing.Placement{V: 1, Node: p.node, + Decided: placing.NodeDecided{Address: p.address, Endpoint: p.endpoint, Hub: p.endpoint != ""}} if p.node == "ace" { - pl.Modules = []delivery.PlacedModule{{Module: "sshd"}} // a module key beside the node key + pl.Modules = []placing.PlacedModule{{Module: "sshd"}} // a module key beside the node key } place(s, pl) - _ = s.Put(PresenceRead, p.node, delivery.Presence{V: 1, Online: p.online}) - key, _ := delivery.SeatHoldersKey(BannedSeat, p.node) - _ = s.Put(SeatHoldersRead, key, delivery.SeatHolders{V: 1, Seat: BannedSeat, Scope: "node", Node: p.node, - Holders: []delivery.SeatHolder{{Module: "fail2ban", Node: p.node}}}) + _ = s.Put(PresenceRead, p.node, watchdogs.PresenceByNodeValue{V: 1, Online: p.online}) + key, _ := assignerclient.SeatHoldersKey(BannedSeat, p.node) + _ = s.Put(SeatHoldersRead, key, placing.SeatHolders{V: 1, Seat: BannedSeat, Scope: "node", Node: p.node, + Holders: []placing.SeatHoldersHoldersItem{{Module: "fail2ban", Node: p.node}}}) } return s } // place writes a node's placement as the seat does, its node key and a key per module (research 072 row 37); g14's in // the form before, `placement.g14`, which the probes still read while the change is delivered. -func place(s *memStore, p delivery.Placement) { +func place(s *memStore, p placing.Placement) { if p.Node == "g14" { - raw, _ := delivery.EncodePlacement(p) + raw, _ := assignerclient.EncodePlacement(p) _ = s.Put(PlacementRead, "placement.g14", json.RawMessage(raw)) return } - values, _ := delivery.PlacementValues(p) + values, _ := assignerclient.PlacementValues(p) for k, raw := range values { _ = s.Put(PlacementRead, k, json.RawMessage(raw)) } } // placed is every node's placement the store holds. -func placed(s *memStore) map[string]delivery.Placement { +func placed(s *memStore) map[string]placing.Placement { keys, _ := s.Keys(PlacementRead) values := map[string][]byte{} for _, k := range keys { values[k], _, _ = s.Get(PlacementRead, k) } - out, _ := delivery.PlacementsOf(values) + out, _ := assignerclient.PlacementsOf(values) return out } @@ -314,20 +320,20 @@ func agents(verdicts map[string]string) *memStore { p := placed(s)[node] p.Decided.AgentAccount = "agent" place(s, p) - h := delivery.NodeHealth{V: 1, Node: node, Contract: 13} + h := nodeseat.HealthByNodeValue{V: 1, Node: node, Contract: 13} switch v { case "": case "old": h.Contract = 2 - h.Root = []delivery.RootVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: stateHealthy}} + h.Root = []nodeseat.RootAccountVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: stateHealthy}} case "pending": - h.Root = []delivery.RootVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", + h.Root = []nodeseat.RootAccountVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: stateUnknown, Reason: rootPending + ": 3 of 9 trees"}} default: - h.Root = []delivery.RootVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: v, + h.Root = []nodeseat.RootAccountVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: v, Reason: "can become root without a person: a sudo rule"}} } - raw, _ := delivery.EncodeNodeHealth(h) + raw, _ := engineclient.EncodeHealth(h) _ = s.Put(HealthRead, node, json.RawMessage(raw)) } return s @@ -429,10 +435,10 @@ func (r *dataRig) firstSeen(node, module, item string, ago time.Duration) *dataR } func (r *dataRig) holder(node string) { - key, _ := delivery.SeatHoldersKey("node-backup", node) - _ = r.s.Put(SeatHoldersRead, key, delivery.SeatHolders{V: 1, Seat: "node-backup", Scope: "node", Node: node, - Holders: []delivery.SeatHolder{{Module: "restic", Node: node}}}) - _ = r.s.Put(PresenceRead, node, delivery.Presence{V: 1, Online: true}) + key, _ := assignerclient.SeatHoldersKey("node-backup", node) + _ = r.s.Put(SeatHoldersRead, key, placing.SeatHolders{V: 1, Seat: "node-backup", Scope: "node", Node: node, + Holders: []placing.SeatHoldersHoldersItem{{Module: "restic", Node: node}}}) + _ = r.s.Put(PresenceRead, node, watchdogs.PresenceByNodeValue{V: 1, Online: true}) } // dataMesh builds the mesh: pg declares data (backed up within 48 h, written all the time within 24 h) and dumps; plex @@ -450,9 +456,9 @@ func dataMesh(change func(*dataRig)) *memStore { man.Data.Own = append(man.Data.Own, it) } raw, _ := json.Marshal(man) - digest := delivery.ManifestDigest(raw) - _ = r.s.Put(ManifestsRead, digest, delivery.Manifest{V: 1, Module: module, JSON: string(raw)}) - _ = r.s.Put(ModulesRead, module, delivery.Module{V: 1, Manifest: digest}) + digest := catalogclient.ManifestDigest(raw) + _ = r.s.Put(ManifestsRead, digest, catalog.ManifestsByDigestValue{V: 1, Module: module, Manifest: string(raw)}) + _ = r.s.Put(ModulesRead, module, catalog.ModulesByModuleValue{V: 1, Manifest: digest}) } declare("pg", map[string]catalogue.DataItem{ "data": {Class: catalogue.ClassValuable, Within: "48h", Active: "24h"}, diff --git a/modules/mesh-self-check/go.mod b/modules/mesh-self-check/go.mod index 999679c50..c1d1d7419 100644 --- a/modules/mesh-self-check/go.mod +++ b/modules/mesh-self-check/go.mod @@ -3,6 +3,13 @@ module mesh-self-check go 1.26.0 require ( + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f diff --git a/modules/mesh-vault/cmd/mesh-vault/adopt.go b/modules/mesh-vault/cmd/mesh-vault/adopt.go index 60129cf92..1367aee81 100644 --- a/modules/mesh-vault/cmd/mesh-vault/adopt.go +++ b/modules/mesh-vault/cmd/mesh-vault/adopt.go @@ -5,7 +5,7 @@ package main // The controller's own account, the node accounts and the enrolled devices were never handed to the vault, so it cannot // hash them, and the grants part's list would leave them out. The one part that has their hashes is the node-engine of // the node holding mesh-broker, in the list the bus loaded: it hands every loaded user's hash in the bus module's have, -// sealed to this vault's key (mesh-sdk delivery.AccountsHanded), which it learns from the vault's answer to node-key. +// sealed to this vault's key (mesh-sdk nodeseat.AccountsHanded), which it learns from the vault's answer to node-key. // The vault reads that have (its manifest `reads` node-engine.have), as a watch of the bus module's have and again by // hand with the tool `account_adopt`, and opens it with its own key. // @@ -19,11 +19,10 @@ package main import ( "errors" "fmt" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "sort" "strings" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // HaveRead is the node-engines' have bucket as the vault reads it (its manifest's `reads`). @@ -186,12 +185,12 @@ func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption, return Adoption{}, fmt.Errorf("%s does not hold the bus (the assignments name %s): its handing is not adopted", node, bus) } - have, err := delivery.DecodeHave(raw) + have, err := engineclient.DecodeHave(raw) if err != nil { return Adoption{}, fmt.Errorf("the have of %s on %s is not a have: %w", BusModule, node, err) } if have.UserList != nil { - v.LoadedListHeard(have.UserList.Revision) + v.LoadedListHeard(uint64(have.UserList.Revision)) } if have.UserList == nil || have.UserList.Handed == nil { return Adoption{}, fmt.Errorf("the have of %s on %s hands no hashes yet (its node-engine hands them once it "+ @@ -205,7 +204,7 @@ func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption, if seen && !force { return Adoption{}, nil } - accounts, err := delivery.OpenHanded(h, v.Own.PublicKey, v.Own.PrivateKey) + accounts, err := engineclient.OpenHanded(h, v.Own.PublicKey, v.Own.PrivateKey) if err != nil { return Adoption{}, err } @@ -232,7 +231,7 @@ func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption, a.Refused[user] = why } for _, acc := range accounts { - if !delivery.IsBcrypt(acc.Hash) || !namePart.MatchString(acc.User) { + if !engineclient.IsBcrypt(acc.Hash) || !namePart.MatchString(acc.User) { refuse(acc.User, "not a bus user with a bcrypt hash") continue } diff --git a/modules/mesh-vault/cmd/mesh-vault/enrolment.go b/modules/mesh-vault/cmd/mesh-vault/enrolment.go index 04c48b236..a562add1a 100644 --- a/modules/mesh-vault/cmd/mesh-vault/enrolment.go +++ b/modules/mesh-vault/cmd/mesh-vault/enrolment.go @@ -21,25 +21,27 @@ import ( "encoding/pem" "errors" "fmt" + enrollerclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" "os" "path/filepath" "strings" "time" - "git.novox.be/novox/mesh-contracts/go/seats/meshvault" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "golang.org/x/crypto/bcrypt" ) // The names enrolment gives: the module that holds the seat mesh-node-enroller, the prefix of a token's bus user, and the // vault's state of each node's logins. const ( - EnrolmentModule = delivery.EnrolmentModule - EnrolUserPrefix = delivery.EnrolmentUserPrefix - NodeLoginsState = delivery.NodeLoginsState + EnrolmentModule = "mesh-enrolment" + EnrolUserPrefix = enrollerclient.UserPrefix + NodeLoginsState = meshvault.StateNodeLogins // MeshHostModule is the module a node's own login secret is kept under (`own//mesh-host/`, ADR 0583 §3). MeshHostModule = "mesh-host" + // vaultModule is this module: the holder of the seat mesh-vault, whose states the reads and the bus names carry. + vaultModule = "mesh-vault" ) // Events the vault says for enrolment's accounts are account-hashed (accounts.go), never carrying a hash or a value. @@ -144,7 +146,7 @@ type TokenBase struct { type EnrolmentTokenArgs struct { Node string Expires string - Tunnel *delivery.TokenTunnel + Tunnel *enrollerclient.TokenTunnel Sealed bool Drop bool } @@ -197,7 +199,7 @@ func (v *Vault) issueEnrolmentToken(caller string, a EnrolmentTokenArgs) (Enrolm return EnrolmentTokenAnswer{}, errors.New("enrolment-token at issue carries the machine's first tunnel (`tunnel`: key, address, range, " + "hub_key, hub_endpoint), so the machine reaches the bus over the private network (novox/hq ADR 0587 §3)") } - if missing := (delivery.Token{Version: 1, Broker: base.Broker, Fingerprint: base.Fingerprint, Signer: signer, + if missing := (enrollerclient.Token{Version: 1, Broker: base.Broker, Fingerprint: base.Fingerprint, Signer: signer, Secret: "-", Tunnel: a.Tunnel}).Missing(); len(missing) > 0 { return EnrolmentTokenAnswer{}, fmt.Errorf("the token of %s would lack %s: nothing was issued", a.Node, strings.Join(missing, ", ")) } @@ -264,7 +266,7 @@ func (v *Vault) showEnrolmentToken(caller, node string) (EnrolmentTokenAnswer, e if err != nil { return EnrolmentTokenAnswer{}, fmt.Errorf("the token carries the vault's signing key, which is not held yet: %w", err) } - var tunnel *delivery.TokenTunnel + var tunnel *enrollerclient.TokenTunnel if trec, theld, err := v.Store.Read(enrolTunnel(node)); err != nil { return EnrolmentTokenAnswer{}, err } else if theld { @@ -272,13 +274,13 @@ func (v *Vault) showEnrolmentToken(caller, node string) (EnrolmentTokenAnswer, e if err != nil { return EnrolmentTokenAnswer{}, fmt.Errorf("the first tunnel of %s does not open with the vault's key: %w", node, err) } - tunnel = &delivery.TokenTunnel{} + tunnel = &enrollerclient.TokenTunnel{} if err := json.Unmarshal([]byte(raw), tunnel); err != nil { return EnrolmentTokenAnswer{}, fmt.Errorf("the first tunnel of %s is not readable: %w", node, err) } } signer := keys.key.Public().(ed25519.PublicKey) - tok := delivery.Token{Version: 1, Node: node, Broker: base.Broker, Fingerprint: base.Fingerprint, + tok := enrollerclient.Token{Version: 1, Node: node, Broker: base.Broker, Fingerprint: base.Fingerprint, Signer: []byte(signer), Secret: secret, Tunnel: tunnel} if tunnel == nil { return EnrolmentTokenAnswer{}, fmt.Errorf("the vault keeps no first tunnel for %s's token: issue it again with its tunnel", node) diff --git a/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go b/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go index e25511f69..080e731a4 100644 --- a/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go @@ -9,6 +9,10 @@ import ( "encoding/json" "encoding/pem" "fmt" + enrollerclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "slices" "sort" "strings" @@ -16,8 +20,7 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-contracts/go/seats/meshvault" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/nacl/box" ) @@ -41,7 +44,7 @@ func (l *loginsState) Get(node string) (*NodeLogin, error) { func (l *loginsState) Put(n NodeLogin) error { l.mu.Lock() defer l.mu.Unlock() - raw, err := delivery.EncodeNodeLogin(n) + raw, err := vaultclient.EncodeNodeLogin(n) if err != nil { return err } @@ -127,10 +130,10 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { } // new1 is placed with its private-network address, as mesh-enrolment writes it before it asks for the certificate. values := map[string][]byte{} - for node, p := range map[string]delivery.Placement{"novox": {Node: "novox", Modules: []delivery.PlacedModule{{Module: "nats"}}}, - "g14": {Node: "g14"}, "new1": {Node: "new1", Decided: delivery.NodeDecided{Address: "10.77.0.9"}}} { - p.V = delivery.Version - kv, err := delivery.PlacementValues(p) + for node, p := range map[string]placing.Placement{"novox": {Node: "novox", Modules: []placing.PlacedModule{{Module: "nats"}}}, + "g14": {Node: "g14"}, "new1": {Node: "new1", Decided: placing.NodeDecided{Address: "10.77.0.9"}}} { + p.V = vaultclient.Version + kv, err := assignerclient.PlacementValues(p) if err != nil { t.Fatal(err) } @@ -175,7 +178,7 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { serving, _, _ := ed25519.GenerateKey(rand.Reader) keys := map[string]any{"public_key": base64.StdEncoding.EncodeToString(identity), "overlay_key": "overlay-pub", "sealing_key": sealingPub, "serving_key": base64.StdEncoding.EncodeToString(serving)} - digest := delivery.KeysDigest(keys["public_key"].(string), "overlay-pub", sealingPub, keys["serving_key"].(string)) + digest := enrollerclient.KeysDigest(keys["public_key"].(string), "overlay-pub", sealingPub, keys["serving_key"].(string)) with := func(base map[string]any, more map[string]any) map[string]any { out := map[string]any{} for k, x := range base { @@ -186,7 +189,7 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { } return out } - reply := delivery.RedeemAnswer{Node: "new1", Login: "node.new1", Account: "sealed", Certificate: "pem", Signer: signerB64} + reply := enrollerclient.RedeemAnswer{Node: "new1", Login: "node.new1", Account: "sealed", Certificate: "pem", Signer: signerB64} var issued, sealed, dropped meshvault.EnrolmentTokenAnswer var account, accountAgain, retired meshvault.NodeAccountAnswer @@ -203,8 +206,8 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { {"a node asks a token", "enrolment-token", "node.g14", map[string]any{"node": "new1", "tunnel": tunnel}, nil, nil, "mesh-enrolment"}, {"the controller asks a token", "enrolment-token", "controller", map[string]any{"node": "new1", "tunnel": tunnel}, nil, nil, "mesh-enrolment"}, {"a person asks an account", "node-account", "person.jochen", map[string]any{"node": "new1"}, nil, nil, "mesh-enrolment"}, - {"unproven asks an enrolment signed", "sign", "", map[string]any{"kind": delivery.KindEnrolment, "node": "new1", "reply": "AAEC"}, nil, nil, "proved"}, - {"a grants part asks an enrolment signed", "sign", "novox.mesh-grants", map[string]any{"kind": delivery.KindEnrolment, "node": "new1", "reply": "AAEC"}, nil, nil, "mesh-enrolment"}, + {"unproven asks an enrolment signed", "sign", "", map[string]any{"kind": vaultclient.KindEnrolment, "node": "new1", "reply": "AAEC"}, nil, nil, "proved"}, + {"a grants part asks an enrolment signed", "sign", "novox.mesh-grants", map[string]any{"kind": vaultclient.KindEnrolment, "node": "new1", "reply": "AAEC"}, nil, nil, "mesh-enrolment"}, // Insight 2026-11-02: the first tunnel is whole at issue, or nothing is issued. {"no tunnel", "enrolment-token", holder, map[string]any{"node": "new1"}, nil, nil, "first tunnel"}, {"a tunnel without the hub's key", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("hub_key")}, nil, nil, "hub's tunnel key"}, @@ -222,11 +225,11 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { {"node-account for other keys", "node-account", holder, map[string]any{"node": "new1", "sealing_key": sealingPub, "keys_digest": "sha256:other"}, nil, nil, "another set of keys"}, {"node-account of an adopted node", "node-account", holder, map[string]any{"node": "g14", "sealing_key": sealingPub, "keys_digest": digest}, nil, nil, "never minted anew"}, // The certificate for the serving key presented, checked against node-account's digest; the reply signed. - {"a certificate without the keys", "sign", holder, map[string]any{"kind": delivery.KindNodeCertificate, "node": "new1", "serving_key": keys["serving_key"]}, nil, nil, "keys it presented"}, - {"a certificate for another serving key", "sign", holder, with(keys, map[string]any{"kind": delivery.KindNodeCertificate, "node": "new1", "serving_key": base64.StdEncoding.EncodeToString(identity)}), nil, nil, "not the ones its account was made for"}, - {"a certificate", "sign", holder, with(keys, map[string]any{"kind": delivery.KindNodeCertificate, "node": "new1"}), &cert, []string{"kind", "node", "certificate", "authority", "signer", "digest"}, ""}, - {"the reply signed", "sign", holder, map[string]any{"kind": delivery.KindEnrolment, "node": "new1", "reply": base64.StdEncoding.EncodeToString(reply.SignedBytes())}, &replySig, []string{"kind", "node", "signature", "signer", "digest"}, ""}, - {"a reply of a node with no token", "sign", holder, map[string]any{"kind": delivery.KindEnrolment, "node": "g14", "reply": "AAEC"}, nil, nil, "no enrolment token"}, + {"a certificate without the keys", "sign", holder, map[string]any{"kind": vaultclient.KindNodeCertificate, "node": "new1", "serving_key": keys["serving_key"]}, nil, nil, "keys it presented"}, + {"a certificate for another serving key", "sign", holder, with(keys, map[string]any{"kind": vaultclient.KindNodeCertificate, "node": "new1", "serving_key": base64.StdEncoding.EncodeToString(identity)}), nil, nil, "not the ones its account was made for"}, + {"a certificate", "sign", holder, with(keys, map[string]any{"kind": vaultclient.KindNodeCertificate, "node": "new1"}), &cert, []string{"kind", "node", "certificate", "authority", "signer", "digest"}, ""}, + {"the reply signed", "sign", holder, map[string]any{"kind": vaultclient.KindEnrolment, "node": "new1", "reply": base64.StdEncoding.EncodeToString(reply.SignedBytes())}, &replySig, []string{"kind", "node", "signature", "signer", "digest"}, ""}, + {"a reply of a node with no token", "sign", holder, map[string]any{"kind": vaultclient.KindEnrolment, "node": "g14", "reply": "AAEC"}, nil, nil, "no enrolment token"}, // A revoke: the account retired, the token and its hash dropped. {"retire", "node-account", holder, map[string]any{"node": "new1", "retire": "true"}, &retired, []string{"node", "retired"}, ""}, {"drop", "enrolment-token", holder, map[string]any{"node": "new1", "drop": "true"}, &dropped, []string{"node", "dropped"}, ""}, @@ -276,7 +279,7 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { t.Errorf("issue answered %+v", issued) } // sealed issued nothing new: the token it seals is the one whose hash is enrol.new1, with its tunnel, and usable whole. - tok, err := delivery.ParseToken(openOperator(t, e.operatorPublic, e.operatorPrivate, sealed.Sealed)) + tok, err := enrollerclient.ParseToken(openOperator(t, e.operatorPublic, e.operatorPrivate, sealed.Sealed)) if err != nil || len(tok.Missing()) != 0 || tok.Node != "new1" || tok.Broker != issued.Broker || tok.Tunnel == nil || tok.Tunnel.HubEndpoint != "203.0.113.1:51820" || base64.StdEncoding.EncodeToString(tok.Signer) != signerB64 || tok.Secret != secret { @@ -304,7 +307,7 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { if err != nil || !ed25519.PublicKey(serving).Equal(parsed.PublicKey) { t.Errorf("the certificate is of another key: %v", err) } - ctx, _ := delivery.SignContext(delivery.KindEnrolment) + ctx, _ := vaultclient.SignContext(vaultclient.KindEnrolment) sig, _ := base64.StdEncoding.DecodeString(replySig.Signature) if !ed25519.Verify(signer.key.Public().(ed25519.PublicKey), append([]byte(ctx), reply.SignedBytes()...), sig) { t.Error("the redeem answer's signature does not verify under the enrolment context") diff --git a/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go b/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go index efeaa308e..59160165f 100644 --- a/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go @@ -9,11 +9,10 @@ import ( "encoding/json" "errors" "fmt" + enrollerclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" "sort" "strings" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // EnrolmentHeld is one enrolment token the vault holds, as secret_holders lists it: never the token or its hash. @@ -175,7 +174,7 @@ func enrolmentTools(env *Env) []toolDef { // tunnelArg reads enrolment-token's `tunnel`, an object or its JSON as a string, as mesh-sdk's TokenTunnel; nil when // absent. -func tunnelArg(v any) (*delivery.TokenTunnel, error) { +func tunnelArg(v any) (*enrollerclient.TokenTunnel, error) { var raw []byte switch t := v.(type) { case nil: @@ -192,7 +191,7 @@ func tunnelArg(v any) (*delivery.TokenTunnel, error) { } raw = b } - var tt delivery.TokenTunnel + var tt enrollerclient.TokenTunnel dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if err := dec.Decode(&tt); err != nil { diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go index 8e42369c9..a7b4549ad 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go @@ -31,7 +31,7 @@ import ( "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/nacl/box" - "git.novox.be/novox/mesh-contracts/go/seats/meshvault" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/seal" "git.novox.be/novox/mesh-sdk/go/testbus" diff --git a/modules/mesh-vault/cmd/mesh-vault/move_test.go b/modules/mesh-vault/cmd/mesh-vault/move_test.go index c40028456..2f55ed3bc 100644 --- a/modules/mesh-vault/cmd/mesh-vault/move_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/move_test.go @@ -10,11 +10,13 @@ package main import ( "crypto/rand" "encoding/base64" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" "strings" "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/seal" "golang.org/x/crypto/nacl/box" ) @@ -383,11 +385,11 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { later -= time.Hour // own_drift reads it from the node's latest shadow answer, mesh-wide; the id as a node runs it, its module's name // first (captured from novox's shadow answer, 2026-10-28: only-office.needs-jwt). - answer := delivery.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{ - Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "postgres.needs-superuser-password", - Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}} - have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"pro": answer}, - Composes: delivery.ComposesOwn}) + answer := nodeseat.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: nodeseat.CandidateAnswerVerdictDiffers, Differs: []nodeseat.CandidateDiffers{{ + Module: "postgres", Why: "the resource differs", Bodies: []nodeseat.CandidateBodies{{ID: "postgres.needs-superuser-password", + Composed: `{"sealed":"` + vaultclient.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + vaultclient.MaskedAnotherValue + `"}`}}}}} + have, err := engineclient.EncodeHaveNode(nodeseat.HaveByNodeNodeValue{Candidates: map[string]nodeseat.CandidateAnswer{"pro": answer}, + Composes: engineclient.ComposesOwn}) if err != nil { t.Fatal(err) } @@ -399,7 +401,7 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { } if len(drift.Secrets) != 1 || drift.Secrets[0].Secret != "own/anchor/postgres/superuser-password" || drift.Secrets[0].Generation != 2 || drift.Secrets[0].Previous != 1 || drift.Secrets[0].Takeable || - len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["pro"] != 1 || drift.Nodes[0].Composes != delivery.ComposesOwn { + len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["pro"] != 1 || drift.Nodes[0].Composes != engineclient.ComposesOwn { t.Errorf("own_drift: %+v", drift) } } diff --git a/modules/mesh-vault/cmd/mesh-vault/noderotation.go b/modules/mesh-vault/cmd/mesh-vault/noderotation.go index 8409482d8..22468b75a 100644 --- a/modules/mesh-vault/cmd/mesh-vault/noderotation.go +++ b/modules/mesh-vault/cmd/mesh-vault/noderotation.go @@ -22,6 +22,8 @@ import ( "encoding/json" "errors" "fmt" + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "slices" "sort" "strings" @@ -29,20 +31,19 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "golang.org/x/crypto/bcrypt" ) // The events a rotation says and the stages of a node's key are mesh-sdk's (delivery/node_logins.go); a rotation warranted // by a hand act is warranted for a day. const ( - RotationBegunEvent = delivery.RotationBegunEvent - NodeAccountReadyEvent = delivery.NodeAccountReadyEvent - RotationDoneEvent = delivery.RotationDoneEvent + RotationBegunEvent = meshvault.HolderEventRotationBegun + NodeAccountReadyEvent = meshvault.HolderEventNodeAccountReady + RotationDoneEvent = meshvault.HolderEventRotationDone - StageBegun = delivery.RotationBegun - StageReady = delivery.RotationReady - StageConfirmed = delivery.RotationConfirmed + StageBegun = vaultclient.RotationBegun + StageReady = vaultclient.RotationReady + StageConfirmed = vaultclient.RotationConfirmed // HandActWithin is how recent a hand act is that warrants a rotation. HandActWithin = 24 * time.Hour @@ -50,7 +51,7 @@ const ( // NodeLogin is one key of the vault's state node-logins (mesh-sdk's contract): the login the node's node-engine dials as, // and while a rotation runs the other login with its stage. Never a value or a hash. -type NodeLogin = delivery.NodeLogin +type NodeLogin = meshvault.NodeLoginsByNodeValue // NodeLoginStore is the vault's state `node-logins`, by node. type NodeLoginStore interface { @@ -68,7 +69,7 @@ func (busNodeLogins) Get(node string) (*NodeLogin, error) { if err != nil || e == nil { return nil, err } - n, err := delivery.DecodeNodeLogin(e.Value) + n, err := vaultclient.DecodeNodeLogin(e.Value) if err != nil { return nil, fmt.Errorf("%s %s is not a node's login: %w", NodeLoginsState, node, err) } @@ -76,7 +77,7 @@ func (busNodeLogins) Get(node string) (*NodeLogin, error) { } func (busNodeLogins) Put(n NodeLogin) error { - raw, err := delivery.EncodeNodeLogin(n) + raw, err := vaultclient.EncodeNodeLogin(n) if err != nil { return err } @@ -201,8 +202,8 @@ func (v *Vault) checkAct(caller, node, id string) error { return fmt.Errorf("the hand act %s was recorded by %s, not by %s who asks: a rotation is warranted by its own caller's act", id, act.By, caller) case v.Now().Sub(act.At) > HandActWithin: return fmt.Errorf("the hand act %s is older than %s: record a new one", id, HandActWithin) - case act.Verb != delivery.RotateNodeVerb && !strings.HasSuffix(act.Verb, "."+delivery.RotateNodeVerb): - return fmt.Errorf("the hand act %s is %q, not %s: it warrants no rotation", id, act.Verb, delivery.RotateNodeVerb) + case act.Verb != meshvault.VerbRotateNode && !strings.HasSuffix(act.Verb, "."+meshvault.VerbRotateNode): + return fmt.Errorf("the hand act %s is %q, not %s: it warrants no rotation", id, act.Verb, meshvault.VerbRotateNode) case !slices.Contains(act.Args, node): return fmt.Errorf("the hand act %s is a rotation of %s, not of %s", id, strings.Join(act.Args, " "), node) } @@ -385,7 +386,7 @@ func (v *Vault) beginNodeRotation(by, node, why, act string, revision uint64) (N if err := v.Accounts.Put(marked); err != nil { return NodeLogin{}, fmt.Errorf("%s could not be marked rotating: %w", cur, err) } - nl := NodeLogin{Node: node, Login: cur, Rotating: second, Stage: StageBegun, Since: at, ListRevision: revision} + nl := NodeLogin{Node: node, Login: cur, Rotating: second, Stage: StageBegun, Since: at, ListRevision: int64(revision)} if err := v.putLogin(nl); err != nil { return NodeLogin{}, fmt.Errorf("%s %s could not be written: %w", NodeLoginsState, node, err) } diff --git a/modules/mesh-vault/cmd/mesh-vault/owntake.go b/modules/mesh-vault/cmd/mesh-vault/owntake.go index 1e4f6f2f4..3f65249e7 100644 --- a/modules/mesh-vault/cmd/mesh-vault/owntake.go +++ b/modules/mesh-vault/cmd/mesh-vault/owntake.go @@ -26,6 +26,9 @@ import ( "encoding/json" "errors" "fmt" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" "os" "path/filepath" "sort" @@ -33,7 +36,6 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // ReplaceRunningArg is the argument of `move-in` that makes it take a running own secret over the one the vault holds. @@ -253,7 +255,7 @@ type Drift struct { } // OwnDrift reads every node's own have key (`.node`) and lists, from its latest answers to the placement -// candidates, every own secret whose shown bodies say another value (delivery.OwnValuesDiffer), with what the vault holds +// candidates, every own secret whose shown bodies say another value (vaultclient.OwnValuesDiffer), with what the vault holds // of it and whether move-in with replace_running would take the running value now. func (v *Vault) OwnDrift() (Drift, error) { if v.Haves == nil || v.HaveKeys == nil { @@ -268,7 +270,7 @@ func (v *Vault) OwnDrift() (Drift, error) { out := Drift{Secrets: []DriftRow{}, Nodes: []DriftNode{}, Note: "from each node's latest shadow answers; a value is never shown: move-in with replace_running from that node replaces it"} for _, key := range keys { - node, isNode := strings.CutSuffix(key, "."+delivery.HaveNodeWord) + node, isNode := strings.CutSuffix(key, "."+engineclient.NodeWord) if !isNode || !namePart.MatchString(node) { continue } @@ -279,7 +281,7 @@ func (v *Vault) OwnDrift() (Drift, error) { out.Nodes = append(out.Nodes, dn) continue } - have, err := delivery.DecodeHaveNode(raw) + have, err := engineclient.DecodeHaveNode(raw) if err != nil { dn.Unread = err.Error() out.Nodes = append(out.Nodes, dn) @@ -287,13 +289,13 @@ func (v *Vault) OwnDrift() (Drift, error) { } dn.Composes = have.Composes for group, a := range have.Candidates { - if n := delivery.ValuesDiffer(a); n > 0 { + if n := vaultclient.ValuesDiffer(bodiesOf(a)); n > 0 { if dn.ValuesDiffer == nil { dn.ValuesDiffer = map[string]int{} } dn.ValuesDiffer[group] = n } - for _, o := range delivery.OwnValuesDiffer(a) { + for _, o := range vaultclient.OwnValuesDiffer(bodiesOf(a)) { secret, err := OwnSecret(node, o.Module, o.Name) if err != nil { continue @@ -399,3 +401,17 @@ func (v *Vault) ownTakeOff() string { } return "" } + +// bodiesOf is the shown bodies of the modules a node's answer to a candidate says differ, in the form the vault client +// reads them (the node-engine's answer is the seat's type, the reading of masked values the vault's). +func bodiesOf(a nodeseat.CandidateAnswer) []vaultclient.ModuleBodies { + out := make([]vaultclient.ModuleBodies, 0, len(a.Differs)) + for _, d := range a.Differs { + m := vaultclient.ModuleBodies{Module: d.Module} + for _, b := range d.Bodies { + m.Bodies = append(m.Bodies, vaultclient.Body{ID: b.ID, Composed: b.Composed, Running: b.Running}) + } + out = append(out, m) + } + return out +} diff --git a/modules/mesh-vault/cmd/mesh-vault/placed.go b/modules/mesh-vault/cmd/mesh-vault/placed.go index 4c072fa9b..febd082e1 100644 --- a/modules/mesh-vault/cmd/mesh-vault/placed.go +++ b/modules/mesh-vault/cmd/mesh-vault/placed.go @@ -9,15 +9,15 @@ package main import ( "errors" "fmt" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "slices" "sort" "strings" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // PlacementRead is the seat mesh-module-assigner's placement, as the vault's manifest reads it (mesh-sdk's contract). -const PlacementRead = delivery.PlacementRead +const PlacementRead = assignerclient.PlacementRead // PlacementReader reads the seat's placement: every key, and one key's value (nil when there is none). type PlacementReader struct { @@ -25,10 +25,10 @@ type PlacementReader struct { Read func(key string) ([]byte, error) } -// placements is every node's placement, by node, from the state's keys in either form (delivery.PlacementsOf; research +// placements is every node's placement, by node, from the state's keys in either form (assignerclient.PlacementsOf; research // 072 row 37); none when the vault reads no placement. A node whose keys do not read whole is read as they stand. -func (v *Vault) placements() (map[string]delivery.Placement, error) { - out := map[string]delivery.Placement{} +func (v *Vault) placements() (map[string]placing.Placement, error) { + out := map[string]placing.Placement{} if v.Placement.Keys == nil || v.Placement.Read == nil { return out, nil } @@ -38,7 +38,7 @@ func (v *Vault) placements() (map[string]delivery.Placement, error) { } values := map[string][]byte{} for _, k := range keys { - if _, _, _, err := delivery.ParsePlacementKey(k); err != nil { + if _, _, _, err := assignerclient.ParseKey(k); err != nil { continue } raw, err := v.Placement.Read(k) @@ -49,15 +49,15 @@ func (v *Vault) placements() (map[string]delivery.Placement, error) { values[k] = raw } } - out, _ = delivery.PlacementsOf(values) + out, _ = assignerclient.PlacementsOf(values) return out, nil } // nodePlaced is what node's own key of the placement says: the modules placed there and what a person decided of it, // read from its node key `.node` alone (which names every module placed, research 072 row 37), or, for a node not // converted yet, from its key in the form before, `placement.`. found is false when it has neither. -func (v *Vault) nodePlaced(node string) (modules []string, decided delivery.NodeDecided, found bool, err error) { - key, err := delivery.PlacementNodeKey(node) +func (v *Vault) nodePlaced(node string) (modules []string, decided placing.NodeDecided, found bool, err error) { + key, err := assignerclient.NodeKey(node) if err != nil { return nil, decided, false, err } @@ -66,7 +66,7 @@ func (v *Vault) nodePlaced(node string) (modules []string, decided delivery.Node return nil, decided, false, err } if raw != nil { - n, err := delivery.DecodePlacementNode(raw) + n, err := assignerclient.DecodeNode(raw) if err != nil { return nil, decided, false, fmt.Errorf("the placement of %s is not readable: %w", node, err) } @@ -75,18 +75,18 @@ func (v *Vault) nodePlaced(node string) (modules []string, decided delivery.Node } return modules, n.Decided, true, nil } - legacy, err := delivery.LegacyPlacementKey(node) + legacy, err := assignerclient.LegacyKey(node) if err != nil { return nil, decided, false, err } if raw, err = v.Placement.Read(legacy); err != nil || raw == nil { return nil, decided, false, err } - p, err := delivery.DecodePlacement(raw) + p, err := assignerclient.DecodePlacement(raw) if err != nil { return nil, decided, false, fmt.Errorf("the placement of %s is not readable: %w", node, err) } - return p.Assigned(), p.Decided, true, nil + return assignerclient.Assigned(p), p.Decided, true, nil } // placedOn says module is placed on node by the catalogue. @@ -132,7 +132,7 @@ func (v *Vault) busNode() (string, error) { } var nodes []string for node, p := range placed { - if slices.Contains(p.Assigned(), BusModule) { + if slices.Contains(assignerclient.Assigned(p), BusModule) { nodes = append(nodes, node) } } diff --git a/modules/mesh-vault/cmd/mesh-vault/placed_test.go b/modules/mesh-vault/cmd/mesh-vault/placed_test.go index 122d9f247..36c0aeef9 100644 --- a/modules/mesh-vault/cmd/mesh-vault/placed_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/placed_test.go @@ -1,10 +1,11 @@ package main import ( + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // placementsOf is the seat mesh-module-assigner's placement of each node, as the vault reads it. @@ -12,12 +13,12 @@ func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader { t.Helper() values := map[string][]byte{} for node, modules := range nodes { - p := delivery.Placement{V: delivery.Version, Node: node} + p := placing.Placement{V: vaultclient.Version, Node: node} for _, m := range modules { - p.Modules = append(p.Modules, delivery.PlacedModule{Module: m}) + p.Modules = append(p.Modules, placing.PlacedModule{Module: m}) } // Research 072 row 37: the node key and a key per module. - kv, err := delivery.PlacementValues(p) + kv, err := assignerclient.PlacementValues(p) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-vault/cmd/mesh-vault/placementview.go b/modules/mesh-vault/cmd/mesh-vault/placementview.go index 81003eb5c..d4d224d08 100644 --- a/modules/mesh-vault/cmd/mesh-vault/placementview.go +++ b/modules/mesh-vault/cmd/mesh-vault/placementview.go @@ -10,20 +10,20 @@ import ( "context" "encoding/json" "fmt" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "sort" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/views" ) // PlacementTool is the assigner's ordinary read tool of its placement; PlacementViewsEvent the event of its views family. const ( - PlacementTool = delivery.AssignerModule + ".assigner_read_placement" - PlacementViewsEvent = delivery.AssignerModule + ".views.placement" + PlacementTool = assignerclient.Module + ".assigner_read_placement" + PlacementViewsEvent = assignerclient.Module + ".views.placement" FirstViewsTool = "vault_first_views" FirstViewAgainTool = "vault_first_view_again" ) @@ -38,7 +38,7 @@ var placementView *views.Follower // it is held (a first view's keys too). func newPlacementView(ask func(key string, body any) (json.RawMessage, error), gone func(key string), changed func(), findings views.FindingWriter) *views.Follower { - return &views.Follower{Owner: delivery.AssignerModule, Tool: PlacementTool, State: "placement", Logf: say, + return &views.Follower{Owner: assignerclient.Module, Tool: PlacementTool, State: "placement", Logf: say, Ask: func(_ context.Context, after string) (views.Answer, error) { body := map[string]any{"values": true} if after != "" { diff --git a/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go b/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go index 6972ba798..cc185eba1 100644 --- a/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go @@ -15,6 +15,11 @@ import ( "encoding/base64" "encoding/json" "errors" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "os" "os/exec" "path/filepath" @@ -28,7 +33,6 @@ import ( "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/nacl/box" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/seal" "git.novox.be/novox/mesh-sdk/go/testbus" ) @@ -113,24 +117,24 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) { t.Fatal(err) } sort.Strings(keys) - var accounts []delivery.HandedAccount + var accounts []engineclient.HandedAccount loaded = map[string]string{} for _, k := range keys { a := p.hashOf(t, k) - if a == nil || !delivery.IsBcrypt(a.Hash) { + if a == nil || !engineclient.IsBcrypt(a.Hash) { continue } - accounts = append(accounts, delivery.HandedAccount{User: k, Hash: a.Hash}) + accounts = append(accounts, engineclient.HandedAccount{User: k, Hash: a.Hash}) loaded[k] = a.Hash } revision++ list, _ := json.Marshal(accounts) - handed, err := delivery.HandAccounts(accounts, vaultKey.PublicKey, Fingerprint(string(list)), time.Now()) + handed, err := engineclient.HandAccounts(accounts, vaultKey.PublicKey, Fingerprint(string(list)), time.Now()) if err != nil { t.Fatal(err) } - raw, err := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK, - UserList: &delivery.UserListWritten{Source: "k", Revision: revision, Users: len(accounts), Handed: &handed}}) + raw, err := engineclient.EncodeHave(nodeseat.HaveByNodeModuleValue{Phase: engineclient.PhaseRunning, Health: engineclient.HealthOK, + UserList: &nodeseat.UserListWritten{Source: "k", Revision: int64(revision), Users: int64(len(accounts)), Handed: &handed}}) if err != nil { t.Fatal(err) } @@ -161,11 +165,11 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) { // held the bus, and adopts from it (learning the loaded list's revision) when the placement names novox. p.firstViewAnswered() for node, modules := range map[string][]string{"novox": {"nats"}, "g14": {"web"}} { - pl := delivery.Placement{V: delivery.Version, Node: node} + pl := placing.Placement{V: vaultclient.Version, Node: node} for _, m := range modules { - pl.Modules = append(pl.Modules, delivery.PlacedModule{Module: m}) + pl.Modules = append(pl.Modules, placing.PlacedModule{Module: m}) } - values, err := delivery.PlacementValues(pl) + values, err := assignerclient.PlacementValues(pl) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-vault/cmd/mesh-vault/signing.go b/modules/mesh-vault/cmd/mesh-vault/signing.go index 8500f12c2..8ed4b0ff8 100644 --- a/modules/mesh-vault/cmd/mesh-vault/signing.go +++ b/modules/mesh-vault/cmd/mesh-vault/signing.go @@ -16,12 +16,14 @@ import ( "encoding/pem" "errors" "fmt" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + enrollerclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" "strings" "time" - "git.novox.be/novox/mesh-contracts/go/seats/meshvault" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/overlay" ) @@ -37,7 +39,7 @@ const ( // signingSecret says name is one of the vault's signing secrets, which no asker is ever handed. func signingSecret(name string) bool { return name == SigningKeySecret || name == AuthorityKeySecret } -// SignersStore is the vault's state `signers` (delivery.SignersBucket): a key's value, or nil; and a write. +// SignersStore is the vault's state `signers` ((vaultModule + "_" + meshvault.StateSigners)): a key's value, or nil; and a write. type SignersStore interface { Get(key string) ([]byte, error) Put(key string, value []byte) error @@ -47,7 +49,7 @@ type SignersStore interface { type busSigners struct{} func (busSigners) Get(key string) ([]byte, error) { - e, err := stdio.State(delivery.SignersState).Get(key) + e, err := stdio.State(meshvault.StateSigners).Get(key) if err != nil || e == nil { return nil, err } @@ -55,7 +57,7 @@ func (busSigners) Get(key string) ([]byte, error) { } func (busSigners) Put(key string, value []byte) error { - _, err := stdio.State(delivery.SignersState).Put(key, json.RawMessage(value)) + _, err := stdio.State(meshvault.StateSigners).Put(key, json.RawMessage(value)) return err } @@ -94,11 +96,11 @@ func (v *Vault) EnsureSigning() (bool, error) { "would be trusted by no node; put it back from its operator copy (novox/hq ADR 0113 §21, ADR 0550 §2)", missing) } if v.Signers != nil { - if raw, err := v.Signers.Get(delivery.SignersKeyKey); err != nil { - return false, fmt.Errorf("the vault's state %s could not be read, so no signing key is made: %w", delivery.SignersRead, err) + if raw, err := v.Signers.Get(meshvault.SignersByKeyKey()); err != nil { + return false, fmt.Errorf("the vault's state %s could not be read, so no signing key is made: %w", (vaultModule + "." + meshvault.StateSigners), err) } else if raw != nil { return false, fmt.Errorf("THE VAULT'S SIGNING KEYS ARE MISSING WHILE %s PUBLISHES ONE: nothing is made, since every "+ - "node trusts the published key; put them back from their operator copies (novox/hq ADR 0113 §21)", delivery.SignersRead) + "node trusts the published key; put them back from their operator copies (novox/hq ADR 0113 §21)", (vaultModule + "." + meshvault.StateSigners)) } } for _, name := range []string{SigningKeySecret, AuthorityKeySecret} { @@ -147,7 +149,7 @@ func (v *Vault) keys() (signingKeys, error) { return signingKeys{}, err } pub := auth.Public().(ed25519.PublicKey) - tmpl := delivery.AuthorityTemplate(pub, authMade) + tmpl := vaultclient.AuthorityTemplate(pub, authMade) der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, auth) if err != nil { return signingKeys{}, fmt.Errorf("the vault's authority certificate could not be made: %w", err) @@ -161,31 +163,31 @@ func (v *Vault) keys() (signingKeys, error) { } // SignersValues are the two keys of `signers` as the vault holds them now. -func (v *Vault) SignersValues() (delivery.SignerKey, delivery.SignerAuthority, error) { +func (v *Vault) SignersValues() (meshvault.SignersByKeyValue, meshvault.SignersByAuthorityValue, error) { k, err := v.keys() if err != nil { - return delivery.SignerKey{}, delivery.SignerAuthority{}, err + return meshvault.SignersByKeyValue{}, meshvault.SignersByAuthorityValue{}, err } - fp, err := delivery.AuthorityFingerprint(k.certificate) + fp, err := vaultclient.AuthorityFingerprint(k.certificate) if err != nil { - return delivery.SignerKey{}, delivery.SignerAuthority{}, err + return meshvault.SignersByKeyValue{}, meshvault.SignersByAuthorityValue{}, err } - return delivery.NewSignerKey(k.key.Public().(ed25519.PublicKey), k.keyMade), - delivery.SignerAuthority{Certificate: k.certificate, Fingerprint: fp, Made: k.authorityMade.UTC()}, nil + return vaultclient.NewSignerKey(k.key.Public().(ed25519.PublicKey), k.keyMade), + meshvault.SignersByAuthorityValue{Certificate: k.certificate, Fingerprint: fp, Made: k.authorityMade.UTC()}, nil } // PublishSigners writes `signers` from the keys the vault holds, each key only when its value differs (or always with // force, signers_reassert), and answers which keys it wrote. func (v *Vault) PublishSigners(force bool) ([]string, error) { if v.Signers == nil { - return nil, errors.New("the vault has no state " + delivery.SignersRead + " to write") + return nil, errors.New("the vault has no state " + (vaultModule + "." + meshvault.StateSigners) + " to write") } key, auth, err := v.SignersValues() if err != nil { return nil, err } var written []string - for name, value := range map[string]any{delivery.SignersKeyKey: key, delivery.SignersAuthorityKey: auth} { + for name, value := range map[string]any{meshvault.SignersByKeyKey(): key, meshvault.SignersByAuthorityKey(): auth} { raw, err := json.Marshal(value) if err != nil { return written, err @@ -193,19 +195,19 @@ func (v *Vault) PublishSigners(force bool) ([]string, error) { if !force { held, err := v.Signers.Get(name) if err != nil { - return written, fmt.Errorf("%s %s could not be read: %w", delivery.SignersRead, name, err) + return written, fmt.Errorf("%s %s could not be read: %w", (vaultModule + "." + meshvault.StateSigners), name, err) } if same(held, raw) { continue } } if err := v.Signers.Put(name, raw); err != nil { - return written, fmt.Errorf("%s %s could not be written: %w", delivery.SignersRead, name, err) + return written, fmt.Errorf("%s %s could not be written: %w", (vaultModule + "." + meshvault.StateSigners), name, err) } written = append(written, name) } if len(written) > 0 { - v.log("wrote %s %s (ADR 0550 §2)", delivery.SignersRead, strings.Join(written, " and ")) + v.log("wrote %s %s (ADR 0550 §2)", (vaultModule + "." + meshvault.StateSigners), strings.Join(written, " and ")) } return written, nil } @@ -242,12 +244,12 @@ type SignArgs struct { Node string Reply string // Keys are, for a joining node's certificate asked by mesh-enrolment, the public keys the machine presented at its - // redeem (delivery.Redeem's): the serving key is certified, the four are checked against the digest node-account + // redeem (enrollerclient.Redeem's): the serving key is certified, the four are checked against the digest node-account // recorded. Keys PresentedKeys } -// PresentedKeys are the public keys a joining machine presented at its redeem, as delivery.Redeem names them. +// PresentedKeys are the public keys a joining machine presented at its redeem, as enrollerclient.Redeem names them. type PresentedKeys struct { Public, Overlay, Sealing, Serving string } @@ -260,21 +262,21 @@ func (v *Vault) Sign(caller string, a SignArgs) (meshvault.SignAnswer, error) { return meshvault.SignAnswer{}, errors.New("the vault signs only for a caller the runtime proved") } switch a.Kind { - case delivery.KindNodeCertificate: + case vaultclient.KindNodeCertificate: if v.fromEnrolment(caller, "sign") == nil { return v.signRedeemingCertificate(caller, a.Node, a.Keys) } return v.signNodeCertificate(caller, a.Node) - case delivery.KindEnrolment: + case vaultclient.KindEnrolment: if err := v.fromEnrolment(caller, "sign an enrolment"); err != nil { return meshvault.SignAnswer{}, fmt.Errorf("%s may not ask for an enrolment's signature: only the seat %s, which "+ "decides who joins, is named for it (novox/hq ADR 0561 §5, ADR 0587 §6)", caller, EnrolmentModule) } return v.signEnrolment(caller, a.Node, a.Reply) - case delivery.KindAuthority: + case vaultclient.KindAuthority: return meshvault.SignAnswer{}, fmt.Errorf("the authority is the vault's own, signed for itself; %s may not ask it", caller) } - return meshvault.SignAnswer{}, fmt.Errorf("%q is not a kind the vault signs (%s)", a.Kind, strings.Join(delivery.SignKinds, ", ")) + return meshvault.SignAnswer{}, fmt.Errorf("%q is not a kind the vault signs (%s)", a.Kind, strings.Join(vaultclient.SignKinds, ", ")) } // redeeming says node has an enrolment token held: a machine joining, which the seat mesh-node-enroller redeems. @@ -312,7 +314,7 @@ func (v *Vault) signRedeemingCertificate(caller, node string, keys PresentedKeys return meshvault.SignAnswer{}, errors.New("a joining node's certificate is asked with the keys it presented at the redeem " + "(`public_key`, `overlay_key`, `sealing_key`, `serving_key`), which are checked against node-account's digest") } - if got := delivery.KeysDigest(keys.Public, keys.Overlay, keys.Sealing, keys.Serving); got != acct.KeysDigest { + if got := enrollerclient.KeysDigest(keys.Public, keys.Overlay, keys.Sealing, keys.Serving); got != acct.KeysDigest { return meshvault.SignAnswer{}, fmt.Errorf("the keys presented for %s are not the ones its account was made for: nothing is signed", node) } pub, err := base64.StdEncoding.DecodeString(keys.Serving) @@ -336,7 +338,7 @@ func (v *Vault) signEnrolment(caller, node, reply string) (meshvault.SignAnswer, if err != nil || len(msg) == 0 { return meshvault.SignAnswer{}, errors.New("an enrolment's signature is asked for the reply's bytes in base64 (`reply`)") } - context, err := delivery.SignContext(delivery.KindEnrolment) + context, err := vaultclient.SignContext(vaultclient.KindEnrolment) if err != nil { return meshvault.SignAnswer{}, err } @@ -344,9 +346,9 @@ func (v *Vault) signEnrolment(caller, node, reply string) (meshvault.SignAnswer, if err != nil { return meshvault.SignAnswer{}, err } - answer := meshvault.SignAnswer{Kind: delivery.KindEnrolment, Node: node, - Signature: base64.StdEncoding.EncodeToString(delivery.SignMessage(k.key, context, msg)), - Signer: delivery.SignerFingerprint(k.key.Public().(ed25519.PublicKey)), Digest: delivery.DigestOf(msg)} + answer := meshvault.SignAnswer{Kind: vaultclient.KindEnrolment, Node: node, + Signature: base64.StdEncoding.EncodeToString(vaultclient.SignMessage(k.key, context, msg)), + Signer: vaultclient.SignerFingerprint(k.key.Public().(ed25519.PublicKey)), Digest: vaultclient.DigestOf(msg)} if err := v.saySigned(caller, SigningKeySecret, answer); err != nil { return meshvault.SignAnswer{}, err } @@ -385,14 +387,14 @@ func (v *Vault) signCertificateWith(caller, node, name string, serving ed25519.P if err != nil { return meshvault.SignAnswer{}, err } - der, err := x509.CreateCertificate(rand.Reader, delivery.NodeCertificateTemplate(name, serving, k.authorityMade), + der, err := x509.CreateCertificate(rand.Reader, vaultclient.NodeCertificateTemplate(name, serving, k.authorityMade), k.parsed, serving, k.authority) if err != nil { return meshvault.SignAnswer{}, fmt.Errorf("the certificate of %s could not be signed: %w", name, err) } - fp, _ := delivery.AuthorityFingerprint(k.certificate) - answer := meshvault.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Authority: k.certificate, - Certificate: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), Signer: fp, Digest: delivery.DigestOf(der)} + fp, _ := vaultclient.AuthorityFingerprint(k.certificate) + answer := meshvault.SignAnswer{Kind: vaultclient.KindNodeCertificate, Node: node, Authority: k.certificate, + Certificate: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), Signer: fp, Digest: vaultclient.DigestOf(der)} if err := v.saySigned(caller, AuthorityKeySecret, answer); err != nil { return meshvault.SignAnswer{}, err } @@ -423,7 +425,7 @@ func (v *Vault) servingKey(node string) (ed25519.PublicKey, error) { if v.Haves == nil { return nil, errors.New("the vault cannot read the have bucket (" + HaveRead + "), so it cannot read a node's record") } - key, err := delivery.HaveNodeKey(node) + key, err := engineclient.HaveNodeKey(node) if err != nil { return nil, err } @@ -434,7 +436,7 @@ func (v *Vault) servingKey(node string) (ed25519.PublicKey, error) { if raw == nil { return nil, fmt.Errorf("%s has written no record (%s %s), so it states no serving key", node, HaveRead, key) } - h, err := delivery.DecodeHaveNode(raw) + h, err := engineclient.DecodeHaveNode(raw) if err != nil { return nil, fmt.Errorf("the record of %s is not readable: %w", node, err) } @@ -525,7 +527,7 @@ func (v *Vault) SigningState() map[string]any { } published := map[string]any{} match := true - for name, want := range map[string]string{delivery.SignersKeyKey: key.Fingerprint, delivery.SignersAuthorityKey: auth.Fingerprint} { + for name, want := range map[string]string{meshvault.SignersByKeyKey(): key.Fingerprint, meshvault.SignersByAuthorityKey(): auth.Fingerprint} { raw, err := v.Signers.Get(name) var got struct { Fingerprint string `json:"fingerprint"` diff --git a/modules/mesh-vault/cmd/mesh-vault/signing_test.go b/modules/mesh-vault/cmd/mesh-vault/signing_test.go index 815cf5bc2..8a3d27930 100644 --- a/modules/mesh-vault/cmd/mesh-vault/signing_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/signing_test.go @@ -5,10 +5,15 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" + assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/overlay" "git.novox.be/novox/mesh-sdk/go/seal" ) @@ -35,25 +40,25 @@ func TestTheVaultSigns(t *testing.T) { state := &signersState{keys: map[string][]byte{}} v.Signers = state servingPub, _, _ := ed25519.GenerateKey(rand.Reader) - placements := map[string]delivery.Placement{ - "ace": {Node: "ace", Decided: delivery.NodeDecided{Address: "10.77.0.3"}}, - "g14": {Node: "g14", Decided: delivery.NodeDecided{Address: "10.77.0.4"}}, + placements := map[string]placing.Placement{ + "ace": {Node: "ace", Decided: placing.NodeDecided{Address: "10.77.0.3"}}, + "g14": {Node: "g14", Decided: placing.NodeDecided{Address: "10.77.0.4"}}, "plain": {Node: "plain"}, } // Research 072 row 37: ace and plain in their keys per module and node, g14 still in the form before. v.Placement = PlacementReader{Read: func(key string) ([]byte, error) { - node, _, kind, err := delivery.ParsePlacementKey(key) + node, _, kind, err := assignerclient.ParseKey(key) if err != nil { return nil, err } p, ok := placements[node] - if !ok || (node == "g14") != (kind == delivery.PlacementLegacyKind) { + if !ok || (node == "g14") != (kind == assignerclient.LegacyKind) { return nil, nil } - if kind == delivery.PlacementLegacyKind { - return delivery.EncodePlacement(p) + if kind == assignerclient.LegacyKind { + return assignerclient.EncodePlacement(p) } - values, err := delivery.PlacementValues(p) + values, err := assignerclient.PlacementValues(p) return values[key], err }} records := map[string]string{"ace": base64.StdEncoding.EncodeToString(servingPub), "plain": base64.StdEncoding.EncodeToString(servingPub)} @@ -63,7 +68,7 @@ func TestTheVaultSigns(t *testing.T) { if !ok { return nil, nil } - return delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: node, ServingKey: serving}}) + return engineclient.EncodeHaveNode(nodeseat.HaveByNodeNodeValue{Record: &nodeseat.NodeRecord{Node: node, ServingKey: serving}}) } // s2: made once, kept with their operator copies; a second pass makes nothing new and writes nothing again (s5). @@ -97,10 +102,10 @@ func TestTheVaultSigns(t *testing.T) { if key2 != key1 || auth2 != auth1 || state.writes != writes { t.Errorf("a second pass changed the keys or wrote signers again (%d writes)", state.writes) } - if _, err := delivery.DecodeSignerKey(state.keys[delivery.SignersKeyKey]); err != nil { + if _, err := vaultclient.DecodeSignerKey(state.keys[meshvault.SignersByKeyKey()]); err != nil { t.Errorf("signers' key: %v", err) } - authority, err := delivery.DecodeSignerAuthority(state.keys[delivery.SignersAuthorityKey]) + authority, err := vaultclient.DecodeSignerAuthority(state.keys[meshvault.SignersByAuthorityKey()]) if err != nil { t.Fatalf("signers' authority: %v", err) } @@ -121,14 +126,14 @@ func TestTheVaultSigns(t *testing.T) { for _, row := range []struct { name, caller, kind, node, refused string }{ - {"a module is not a node-engine", "ace.mesh-grants", delivery.KindNodeCertificate, "", "not a node-engine"}, - {"another node's certificate", "node.g14", delivery.KindNodeCertificate, "ace", "its own alone"}, - {"a node not on the private network", "node.plain", delivery.KindNodeCertificate, "", "not on the private network"}, - {"a node that states no serving key", "node.g14", delivery.KindNodeCertificate, "", "no record"}, - {"an enrolment has no caller yet", "node.ace", delivery.KindEnrolment, "", "ADR 0561 §5"}, - {"the authority is the vault's own", "node.ace", delivery.KindAuthority, "", "the vault's own"}, - {"an unproven caller", "", delivery.KindNodeCertificate, "", "proved"}, - {"the node's own certificate", "node.ace", delivery.KindNodeCertificate, "ace", ""}, + {"a module is not a node-engine", "ace.mesh-grants", vaultclient.KindNodeCertificate, "", "not a node-engine"}, + {"another node's certificate", "node.g14", vaultclient.KindNodeCertificate, "ace", "its own alone"}, + {"a node not on the private network", "node.plain", vaultclient.KindNodeCertificate, "", "not on the private network"}, + {"a node that states no serving key", "node.g14", vaultclient.KindNodeCertificate, "", "no record"}, + {"an enrolment has no caller yet", "node.ace", vaultclient.KindEnrolment, "", "ADR 0561 §5"}, + {"the authority is the vault's own", "node.ace", vaultclient.KindAuthority, "", "the vault's own"}, + {"an unproven caller", "", vaultclient.KindNodeCertificate, "", "proved"}, + {"the node's own certificate", "node.ace", vaultclient.KindNodeCertificate, "ace", ""}, } { v.bus.raw = nil got, err := v.Sign(row.caller, SignArgs{Kind: row.kind, Node: row.node}) @@ -144,7 +149,7 @@ func TestTheVaultSigns(t *testing.T) { if err != nil { t.Fatalf("%s: %v", row.name, err) } - if err := delivery.CheckNodeCertificate(got.Certificate, []string{authority.Certificate}, overlay.InternalName("ace"), + if err := vaultclient.CheckNodeCertificate(got.Certificate, []string{authority.Certificate}, overlay.InternalName("ace"), servingPub, authority.Made.Add(1)); err != nil { t.Errorf("%s: the certificate does not check against signers' authority: %v", row.name, err) } @@ -158,15 +163,15 @@ func TestTheVaultSigns(t *testing.T) { t.Errorf("%s: signed not said: %s", row.name, said) } rows, _ := v.Signatures(10) - if len(rows) != 1 || rows[0].Caller != "node.ace" || rows[0].Kind != delivery.KindNodeCertificate || rows[0].Digest != got.Digest { + if len(rows) != 1 || rows[0].Caller != "node.ace" || rows[0].Kind != vaultclient.KindNodeCertificate || rows[0].Digest != got.Digest { t.Errorf("%s: vault_signed lists %+v", row.name, rows) } status, _ := json.Marshal(v.SigningState()) for _, seed := range seeds { raw, _ := base64.StdEncoding.DecodeString(seed) priv := ed25519.NewKeyFromSeed(raw) - for _, text := range []string{string(answer), said, string(status), string(state.keys[delivery.SignersKeyKey]), - string(state.keys[delivery.SignersAuthorityKey])} { + for _, text := range []string{string(answer), said, string(status), string(state.keys[meshvault.SignersByKeyKey()]), + string(state.keys[meshvault.SignersByAuthorityKey()])} { if strings.Contains(text, seed) || strings.Contains(text, base64.StdEncoding.EncodeToString(priv)) { t.Errorf("%s: a signing key's private half is in what the vault says: %s", row.name, text) } @@ -176,15 +181,15 @@ func TestTheVaultSigns(t *testing.T) { // A serving key changed in the record is the one certified next, never the one before. other, _, _ := ed25519.GenerateKey(rand.Reader) records["ace"] = base64.StdEncoding.EncodeToString(other) - got, err := v.Sign("node.ace", SignArgs{Kind: delivery.KindNodeCertificate}) - if err != nil || delivery.CheckNodeCertificate(got.Certificate, []string{authority.Certificate}, overlay.InternalName("ace"), + got, err := v.Sign("node.ace", SignArgs{Kind: vaultclient.KindNodeCertificate}) + if err != nil || vaultclient.CheckNodeCertificate(got.Certificate, []string{authority.Certificate}, overlay.InternalName("ace"), servingPub, authority.Made.Add(1)) == nil { t.Errorf("a certificate for a serving key the record no longer states: %v", err) } // s2: one secret lost beside the other, or both lost while signers publishes a key, makes nothing. lost := newVault(t, nil) - lost.Signers = &signersState{keys: map[string][]byte{delivery.SignersKeyKey: state.keys[delivery.SignersKeyKey]}} + lost.Signers = &signersState{keys: map[string][]byte{meshvault.SignersByKeyKey(): state.keys[meshvault.SignersByKeyKey()]}} if made, err := lost.EnsureSigning(); made || err == nil || !strings.Contains(err.Error(), "MISSING") { t.Errorf("keys made while signers publishes one: %v, %v", made, err) } diff --git a/modules/mesh-vault/cmd/mesh-vault/tools_test.go b/modules/mesh-vault/cmd/mesh-vault/tools_test.go index 4d48b4ed2..9ff081e36 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools_test.go @@ -6,13 +6,14 @@ package main import ( "encoding/base64" "encoding/json" + engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "os" "path/filepath" "strings" "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/seal" ) @@ -234,20 +235,20 @@ func TestSecretOwnListsFingerprintsOfOwnSecretsOnly(t *testing.T) { handing := map[string]string{"controller": h("c"), "node.ace": h("n"), "g14.dunst": h("d")} sealedTo := v.Own.PublicKey have := func() []byte { - var accounts []delivery.HandedAccount + var accounts []engineclient.HandedAccount for _, u := range []string{"controller", "node.ace", "g14.dunst", "bad.user"} { if hash, ok := handing[u]; ok { - accounts = append(accounts, delivery.HandedAccount{User: u, Hash: hash}) + accounts = append(accounts, engineclient.HandedAccount{User: u, Hash: hash}) } } // The list's digest, as the node says it: another list (another hash or user) is another digest. list, _ := json.Marshal(accounts) - handed, err := delivery.HandAccounts(accounts, sealedTo, Fingerprint(string(list)), time.Now()) + handed, err := engineclient.HandAccounts(accounts, sealedTo, Fingerprint(string(list)), time.Now()) if err != nil { t.Fatal(err) } - raw, _ := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK, - UserList: &delivery.UserListWritten{Source: "k", Users: 3, Handed: &handed}}) + raw, _ := engineclient.EncodeHave(nodeseat.HaveByNodeModuleValue{Phase: engineclient.PhaseRunning, Health: engineclient.HealthOK, + UserList: &nodeseat.UserListWritten{Source: "k", Users: 3, Handed: &handed}}) return raw } v.Assignments = Mirror{Keys: func() ([]string, error) { return []string{"novox.nats", "novox.mesh-vault", "ace.dunst"}, nil }} diff --git a/modules/mesh-vault/cmd/mesh-vault/vault_test.go b/modules/mesh-vault/cmd/mesh-vault/vault_test.go index 1eabe93d5..30c44b300 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault_test.go @@ -156,7 +156,7 @@ func TestNoBucketHoldsASecret(t *testing.T) { "grants part's wanted accounts alone (the placement is the assigner's, through its tool and events, ADR 0615), never the controller's lease (issue 1126)", r) } sources, _ := filepath.Glob("*.go") - bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`) + bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|\bKeyValue\b`) for _, f := range sources { if strings.HasSuffix(f, "_test.go") { continue @@ -182,8 +182,8 @@ func TestNoBucketHoldsASecret(t *testing.T) { } // The signing keys' public halves, in signing.go alone: one read and one write of its own bucket (ADR 0550 §2). if f == "signing.go" { - code = []byte(strings.Replace(string(code), "stdio.State(delivery.SignersState).Get(", "", 1)) - code = []byte(strings.Replace(string(code), "stdio.State(delivery.SignersState).Put(", "", 1)) + code = []byte(strings.Replace(string(code), "stdio.State(meshvault.StateSigners).Get(", "", 1)) + code = []byte(strings.Replace(string(code), "stdio.State(meshvault.StateSigners).Put(", "", 1)) } // The haves, in main.go alone: one read, one watch and their keys (own_drift), never a write; one watch of the // assignments, which the vault's rules read from memory once it caught up (issue 1126); and one watch of the wanted diff --git a/modules/mesh-vault/go.mod b/modules/mesh-vault/go.mod index 41e94d087..0e2eb7cae 100644 --- a/modules/mesh-vault/go.mod +++ b/modules/mesh-vault/go.mod @@ -3,8 +3,14 @@ module mesh-vault go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-contracts/go/seats/meshvault v0.0.0-20261104174344-ae5266e77639 + git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/nats/cmd/nats-tools/keep.go b/modules/nats/cmd/nats-tools/keep.go index 09d7bd408..f1a529cf5 100644 --- a/modules/nats/cmd/nats-tools/keep.go +++ b/modules/nats/cmd/nats-tools/keep.go @@ -21,8 +21,9 @@ import ( "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/seatdef" @@ -36,24 +37,28 @@ import ( // each verdict asks for, and acts on the persons' requests its tools put in its own state requests (requests.go) as they // are written or answered. It serves nothing (ADR 0436 §3). -// AcceptedOf reads a manifest-accepted body: mesh-sdk's delivery.ManifestAccepted, with the indexer's revision of the -// verdict when the event carries it (a field mesh-sdk is asked to add; 0 until then). +// AcceptedOf reads a manifest-accepted body: the seat mesh-catalog's typed event, with the indexer's revision of the +// verdict when the event carries it (0 from an indexer that does not say it). func AcceptedOf(body []byte) (keeper.Accepted, error) { - var a struct { - delivery.ManifestAccepted - Revision uint64 `json:"revision"` - } + var a catalog.ManifestAcceptedEvent if err := json.Unmarshal(body, &a); err != nil { return keeper.Accepted{}, err } if a.Module == "" { return keeper.Accepted{}, fmt.Errorf("a manifest-accepted names no module") } - return keeper.Accepted{Module: a.Module, Digest: a.Digest, Revision: a.Revision, BusState: a.BusState}, nil + var state []catalogue.BusStateEntry // the seat types an entry as the manifest wrote it; the keeper reads the catalogue's form + if raw, err := json.Marshal(a.BusState); err != nil || json.Unmarshal(raw, &state) != nil { + return keeper.Accepted{}, fmt.Errorf("the bus-state of the manifest-accepted of %s is not readable", a.Module) + } + if len(a.BusState) == 0 { + state = nil + } + return keeper.Accepted{Module: a.Module, Digest: a.Digest, Revision: uint64(max(a.Revision, 0)), BusState: state}, nil } // AcceptedSubject is the seat mesh-catalog's manifest-accepted on the shared events stream. -var AcceptedSubject = "mesh.seat.mesh-catalog.event." + delivery.ManifestAcceptedEvent +var AcceptedSubject = catalog.SubjectManifestAccepted // The router's records of asks, read under this module's own name (ADR 0259 §3), as the mail sender reads them. const asksBucket = "messenger_asks" @@ -324,7 +329,7 @@ func Keep(ctx context.Context, k *keeper.Keeper, hear func(context.Context, func wake(asked)() return nil } - if subject != AcceptedSubject && !strings.HasSuffix(subject, ".event."+delivery.ManifestAcceptedEvent) { + if subject != AcceptedSubject && !strings.HasSuffix(subject, ".event."+catalog.EventManifestAccepted) { return nil } a, err := AcceptedOf(body) diff --git a/modules/nats/go.mod b/modules/nats/go.mod index 951629f3f..5a01c69c4 100644 --- a/modules/nats/go.mod +++ b/modules/nats/go.mod @@ -3,6 +3,7 @@ module nats-tools go 1.26.0 require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000 @@ -14,6 +15,7 @@ require ( ) require ( + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect