Merge pull request 'The vault the only issuer, placement read per node and module, hand acts copied once, routes only from the provisioner's plans (issue 774)' (#579) from feat/072-moves into main

This commit was merged in pull request #579.
This commit is contained in:
2026-10-31 10:31:01 +00:00
57 changed files with 952 additions and 303 deletions
+26
View File
@@ -1,6 +1,12 @@
// Package credentialform refuses an offer that does not say the form of its credential: an own secret, an issuer, or
// `form` pair or none (the lead, 2026-10-29). It reads every modules/*/module.json through mesh-sdk's
// CredentialFormProblems.
//
// It refuses a route received as a file too (issue 774, research 072 row 34, the operator on 2026-10-31: "there is only
// 1 path, the other is simply deprecated/invalid"): a route is a provision, served from the seat mesh-provisioner's
// plans alone, and a manifest's `receives.route` is the deprecated path. A module still moving off it is listed in
// stillReceivingRoutes with why; every refusal names them all, and a listed module that no longer receives one is
// refused too, so the list only shrinks.
package credentialform
import (
@@ -9,6 +15,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
@@ -21,6 +28,7 @@ func Problems(root string) ([]string, error) {
}
sort.Strings(files)
var out []string
var receiving []string
reads := map[string]bool{}
var binders []string
for _, f := range files {
@@ -33,6 +41,11 @@ func Problems(root string) ([]string, error) {
return nil, fmt.Errorf("%s: %w", f, err)
}
out = append(out, catalogue.CredentialFormProblems(m)...)
if _, received := m.Receives["route"]; received {
receiving = append(receiving, m.Module)
} else if _, listed := stillReceivingRoutes[m.Module]; listed {
out = append(out, fmt.Sprintf("%s no longer receives route; take it off stillReceivingRoutes", m.Module))
}
var kept struct {
Reads []string `json:"reads"`
State []json.RawMessage `json:"state"`
@@ -52,9 +65,22 @@ func Problems(root string) ([]string, error) {
out = append(out, fmt.Sprintf("%s provides and keeps a `bound` state, and mesh-provisioner's manifest does not read %s.bound", module, module))
}
}
for _, module := range receiving {
if _, listed := stillReceivingRoutes[module]; !listed {
out = append(out, fmt.Sprintf("%s receives route as a file: a route is a provision, served from the seat "+
"mesh-provisioner's plans alone (issue 774); still on the deprecated path: %s", module,
strings.Join(receiving, ", ")))
}
}
return out, nil
}
// stillReceivingRoutes is every module still receiving its routes as a file, and why it has not moved yet.
var stillReceivingRoutes = map[string]string{
"route-adapter": "migration scaffolding writing into a predecessor's proxy (novox/hq ADR 0104), deleted when that " +
"migration ends; it reads the received file its TypeScript adapter turns into the predecessor's route files",
}
// keepsBound says whether a manifest's `state` names `bound`, as a plain name or an object with a name.
func keepsBound(state []json.RawMessage) bool {
for _, s := range state {
+10 -5
View File
@@ -22,22 +22,27 @@ func aCatalogue(t *testing.T, manifests map[string]string) string {
return root
}
// An offer that says no form is refused and named; one that says pair, none or an own secret passes; and the real
// catalogue is clean.
// An offer that says no form is refused and named; one that says pair, none or an own secret passes; a route received
// as a file is refused but for the module still moving off it; and the real catalogue is clean.
func TestEveryOfferSaysTheFormOfItsCredential(t *testing.T) {
root := aCatalogue(t, map[string]string{
"silent": `{"module":"silent","provides":[{"name":"thing","scope":"mesh"}]}`,
"binder": `{"module":"binder","provides":[{"name":"d","credential":{"form":"pair"}}],"state":["bound"]}`,
"mesh-provisioner": `{"module":"mesh-provisioner","reads":["other.bound"]}`,
"says": `{"module":"says","provides":[{"name":"a","credential":{"form":"pair"}},{"name":"b","credential":{"form":"none"}},{"name":"c","credential":{"own":"jwt"}}]}`,
// A route received as a file (issue 774, research 072 row 34): refused, naming every module still on it; the one
// listed as still moving passes.
"old-proxy": `{"module":"old-proxy","receives":{"route":"/var/lib/old-proxy/mesh.json"}}`,
"route-adapter": `{"module":"route-adapter","receives":{"route":"/var/lib/route-adapter/mesh.json"}}`,
})
got, err := Problems(root)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || !strings.Contains(got[0], `silent provides "thing"`) ||
!strings.Contains(got[1], "binder") || !strings.Contains(got[1], "mesh-provisioner") {
t.Fatalf("refused %v, wanted the silent offer and the binder the provisioner does not read", got)
if len(got) != 3 || !strings.Contains(got[0], `silent provides "thing"`) ||
!strings.Contains(got[1], "binder") || !strings.Contains(got[1], "mesh-provisioner") ||
!strings.HasPrefix(got[2], "old-proxy receives route") || !strings.Contains(got[2], "old-proxy, route-adapter") {
t.Fatalf("refused %v, wanted the silent offer, the binder the provisioner does not read and the received route", got)
}
real, err := Problems(filepath.Join("..", ".."))
if err != nil {
+1 -1
View File
@@ -2,4 +2,4 @@ module checks/health
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
+2 -2
View File
@@ -1,2 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
+1 -1
View File
@@ -2,4 +2,4 @@ module checks/manifests
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030181247-4c1384faefa0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
+2 -2
View File
@@ -1,2 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030181247-4c1384faefa0 h1:t04baH2Xz/NUsOEV3ctKY59XjiY5WmEuststvH+XeOs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030181247-4c1384faefa0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
@@ -216,26 +216,26 @@ func (k *Keeper) Have(key string, value []byte, deleted bool) error {
return k.judge(node, module, "")
}
// Placement takes one change of the placement: which module holds which seat on a node.
// Placement takes one change of the placement: which module holds which seat on a node. The keys are read in either
// form, a node key and its module keys or the form before (delivery.PlacementKeys; research 072 row 37).
func (k *Keeper) Placement(key string, value []byte, deleted bool) {
node, err := delivery.ParsePlacementKey(key)
if err != nil {
return
}
k.mu.Lock()
defer k.mu.Unlock()
k.init()
node, err := k.placement.Put(key, value, 0, deleted)
if node == "" {
return
}
if err != nil {
k.Logf("the placement of %s is not readable as it stands, so its seats' holders are kept as last read: %v", node, err)
}
for hk := range k.holders {
if strings.HasPrefix(hk, node+".") {
delete(k.holders, hk)
}
}
if deleted {
return
}
p, err := delivery.DecodePlacement(value)
if err != nil {
k.Logf("the placement of %s is not readable, so its seats' holders are not known: %v", node, err)
p, _, ok := k.placement.Of(node)
if !ok {
return
}
for _, h := range p.Holds {
@@ -113,12 +113,19 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
// A seat's finding is about the module the placement says holds the seat on that node (the lead, 2026-10-29): held
// while the assigner is delivered on ace, though the seat is named mesh-placement.
r := newRig()
placed, err := delivery.EncodePlacement(delivery.Placement{Node: "ace", Modules: []delivery.PlacedModule{{Module: "mesh-delivery-assigner"}},
// Research 072 row 37: the placement in its keys per module and node; a stale legacy key beside them is not read.
stale, err := delivery.EncodePlacement(delivery.Placement{Node: "ace"})
if err != nil {
t.Fatal(err)
}
r.k.Placement("placement.ace", stale, false)
placed, err := delivery.PlacementValues(delivery.Placement{Node: "ace", Modules: []delivery.PlacedModule{{Module: "mesh-delivery-assigner"}},
Holds: []delivery.Holding{{Claim: "mesh-placement", Scope: "mesh", Module: "mesh-delivery-assigner"}}})
if err != nil {
t.Fatal(err)
}
r.k.Placement("placement.ace", placed, false)
r.k.Placement("ace.mesh-delivery-assigner", placed["ace.mesh-delivery-assigner"], false)
r.k.Placement("ace.node", placed["ace.node"], false)
_ = r.k.Want("want.ace.mesh-delivery-assigner", want(ctrlNext, ctrlNextComp), false)
_ = r.k.Have("ace.mesh-delivery-assigner", have(ctrlRunning, ctrlComposed, delivery.PhaseRunning, delivery.HealthOK), false)
seat, seatRaw := finding(t, selfCheck, health.ScopeSeat, "mesh-placement.ace", "holder-silent", "D3", health.Warning, r.now)
@@ -112,8 +112,10 @@ type Keeper struct {
haves map[string]delivery.Have
// holders are, by `<node>.<seat>`, the module the placement says holds the seat there.
holders map[string]string
stuck map[string]string
held map[string]held
// placement is the placement's keys as heard, assembled per node.
placement *delivery.PlacementKeys
stuck map[string]string
held map[string]held
// passed are the deliveries whose bound passed, by node and module, at the fingerprint wanted then; outOfHold the
// line a key raised out of a hold says (hold.go).
passed map[string]string
@@ -130,6 +132,7 @@ func (k *Keeper) init() {
map[string]string{}, map[string]held{}, map[string]clearedMemo{}
k.takeover = health.Takeover{V: health.TakeoverVersion, Follow: health.FollowController}
k.passed, k.outOfHold, k.holders = map[string]string{}, map[string]string{}, map[string]string{}
k.placement = delivery.NewPlacementKeys()
}
if k.Now == nil {
k.Now = time.Now
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-conditions
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -134,34 +134,58 @@ func (m *memStore) unassign(t *testing.T, node, module string) {
m.place(t, node, module, false)
}
// place writes a node's placement as the seat writes it: its node key and module keys (research 072 row 37), and for
// ace in the form before, `placement.ace`, which a reader still takes while the change is delivered.
func (m *memStore) place(t *testing.T, node, module string, on bool) {
t.Helper()
type placed struct {
Module string `json:"module"`
keys := map[string][]byte{}
for _, k := range m.keysOf(PlacementRead) {
e, _ := m.Get(PlacementRead, k)
keys[k] = e.Value
}
p := struct {
V int `json:"v"`
Node string `json:"node"`
Modules []placed `json:"modules"`
}{V: 1, Node: node}
if e, _ := m.Get(PlacementRead, "placement."+node); e != nil {
if err := json.Unmarshal(e.Value, &p); err != nil {
t.Fatal(err)
}
all, err := delivery.PlacementsOf(keys)
if err != nil {
t.Fatal(err)
}
var kept []placed
p := all[node]
p.Node = node
var kept []delivery.PlacedModule
for _, x := range p.Modules {
if x.Module != module {
kept = append(kept, x)
}
}
if on {
kept = append(kept, placed{Module: module})
kept = append(kept, delivery.PlacedModule{Module: module})
}
p.Modules = kept
if err := m.Put(PlacementRead, "placement."+node, p); err != nil {
for _, k := range m.keysOf(PlacementRead) {
if n, _, _, err := delivery.ParsePlacementKey(k); err == nil && n == node {
_ = m.Delete(PlacementRead, k)
}
}
if node == "ace" {
raw, err := delivery.EncodePlacement(p)
if err != nil {
t.Fatal(err)
}
_ = m.Put(PlacementRead, "placement.ace", json.RawMessage(raw))
return
}
values, err := delivery.PlacementValues(p)
if err != nil {
t.Fatal(err)
}
for k, raw := range values {
if err := m.Put(PlacementRead, k, json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
}
func (m *memStore) keysOf(state string) []string {
keys, _ := m.Keys(state)
return keys
}
// manifest keeps a manifest in the indexer's manifests by its digest, as the indexer writes it, and answers the digest.
+24 -24
View File
@@ -70,32 +70,32 @@ func (c *Config) sourcesFrom(source string) (Sources, error) {
return s, nil
}
// readPlacement is every module the seat mesh-placement places on a node (ADR 0567 §1, §9): each node's key
// `placement.<node>`.
// readPlacement is every module the seat mesh-placement places on a node (ADR 0567 §1, §9), from the state's keys in
// either form: a node key and its module keys, or the form before, `placement.<node>` (delivery.PlacementsOf;
// research 072 row 37). By node in name order, each node's modules in the order its placement names them.
func (c *Config) readPlacement(s *Sources) error {
placed := map[string]bool{}
return c.each(PlacementRead, func(key string, raw []byte) error {
node, err := delivery.ParsePlacementKey(key)
if err != nil {
return nil // a key of another kind
}
var p struct {
Node string `json:"node"`
Modules []struct {
Module string `json:"module"`
} `json:"modules"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.Node != node {
return fmt.Errorf("the placement %s is not one", key)
}
for _, m := range p.Modules {
if m.Module != "" && !placed[node+"."+m.Module] {
placed[node+"."+m.Module] = true
s.Assignments = append(s.Assignments, Assignment{Node: node, Module: m.Module})
}
}
keys := map[string][]byte{}
if err := c.each(PlacementRead, func(key string, raw []byte) error {
keys[key] = raw
return nil
})
}); err != nil {
return err
}
placements, err := delivery.PlacementsOf(keys)
if err != nil {
return fmt.Errorf("the placement is not read whole: %w", err)
}
nodes := make([]string, 0, len(placements))
for node := range placements {
nodes = append(nodes, node)
}
sort.Strings(nodes)
for _, node := range nodes {
for _, module := range placements[node].Assigned() {
s.Assignments = append(s.Assignments, Assignment{Node: node, Module: module})
}
}
return nil
}
// readDeclarations reads each placed module's declarations from its manifests as the delivery holds them: the trunk's
+1 -1
View File
@@ -2,7 +2,7 @@ module mesh-config
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030174137-a37181261c24
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
require (
github.com/klauspost/compress v1.20.0 // indirect
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030174137-a37181261c24 h1:s0XXZtR7CFPBbnaDygWnE4/Q827iEFt0YfEzujk4Q9g=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030174137-a37181261c24/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+7
View File
@@ -22,5 +22,12 @@ alone writes the log.
- `mesh-hand-acts.hand-acts`: both logs, this one and the controller's `mesh-controller_hand-acts` until its lease
ends, newest first, with the causes repaired by hand twice within a fortnight; `warrants=true` lists the warrants.
- `hand_acts_copied` and `hand_acts_copy` (research 072 row 26): the controller's hand acts are copied once into this
seat's log, each with its own key, time and author. It runs at the module's start while the old bucket is readable
and no marker (`copied`, key `controller-hand-acts`) says it was done, and by the tool; an act already in the log is
never appended again, and `hand-acts` counts a copied act once. `hand_acts_copied` says whether, when and how many.
The lease write has no hook to run it from, so the start is the trigger; `hand_acts_copy` with `top_up=true`, asked
after the lease write, appends any act the controller wrote since the copy (a finished copy is otherwise a no-op).
No act is written to both logs: every recorder calls this seat from its delivery, and the controller keeps its own
acts in its own bucket until it loses its lease.
@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"regexp"
"slices"
"sort"
"strconv"
"strings"
@@ -287,8 +288,18 @@ func (l *Log) HandActs(days int, cause, who string) Listing {
since := now.Add(-time.Duration(days) * 24 * time.Hour)
out := Listing{Acts: []Act{}, Repeated: map[string]int{}, Days: days, Kept: map[string]int{}}
var all []Act
// What was copied from the controller's log is in this one: it is read from this one only (copy.go).
copied := map[string]bool{}
for _, log := range []string{ActsLog, ControllerActs} {
acts, kept, err := l.read(log, now.Add(-max(time.Duration(days)*24*time.Hour, RepeatedWithin)))
if log == ActsLog && err == nil {
for _, a := range acts {
copied[a.ID] = a.ID != ""
}
}
if log == ControllerActs {
acts = slices.DeleteFunc(acts, func(a Act) bool { return a.ID != "" && copied[a.ID] })
}
if err != nil {
if out.Unread == nil {
out.Unread = map[string]string{}
@@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
@@ -284,3 +285,87 @@ func TestHandActsReadsBothLogs(t *testing.T) {
t.Errorf("warrants answered %v, %v", w, err)
}
}
// The controller's hand acts are copied into this seat's log once, each with its own key, time and author, and
// never twice: a marker makes it once, a key already in the log is not appended again, a log that cannot be read or
// written leaves no marker so the next call copies again, and hand-acts counts a copied act once (research 072 row 26).
func TestTheControllersHandActsAreCopiedOnce(t *testing.T) {
base := time.Date(2026, 10, 28, 0, 0, 0, 0, time.UTC)
old := []string{fmt.Sprintf("act-%d-1", base.UnixNano()), fmt.Sprintf("act-%d-2", base.UnixNano()+1),
fmt.Sprintf("act-%d-3", base.UnixNano()+2), "warrant-x-y"}
cases := []struct {
name string
already []string // keys in the new log before the copy
oldErr, newErr error
junk bool
wantErr string
wantCopied, wantAlready int
wantMarker bool
}{
{"all", nil, nil, nil, false, "", 4, 0, true},
{"finishes a half copy", []string{old[0], old[1]}, nil, nil, false, "", 2, 2, true},
{"an entry that is no act is copied unchanged", nil, nil, nil, true, "", 5, 0, true},
{"the old log cannot be read", nil, errors.New("away"), nil, false, "cannot be read", 0, 0, false},
{"the new log cannot be written", nil, nil, errors.New("full"), false, "could not be copied", 0, 0, false},
}
for _, tc := range cases {
l, store, _, _ := newLog()
for _, k := range old {
_ = store.Put(ControllerActs, k, Act{ID: k, At: base, By: "ace.mesh-config", Verb: VerbRecord, Why: "w", Cause: "c"})
}
if tc.junk {
_ = store.Put(ControllerActs, fmt.Sprintf("act-%d-4", base.UnixNano()+3), "not an act")
}
for _, k := range tc.already {
_ = store.Append(ActsLog, k, Act{ID: k})
}
store.keysErr[ControllerActs], store.keysErr[ActsLog] = tc.oldErr, tc.newErr
st, err := l.Copy("test", false)
if tc.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("%s: %v", tc.name, err)
}
} else if err != nil || !st.Done || st.Marker.Copied != tc.wantCopied || st.Marker.Already != tc.wantAlready {
t.Fatalf("%s: %+v, %v", tc.name, st, err)
}
_, marked, _ := store.Get(CopiedState, CopyKey)
if marked != tc.wantMarker || len(store.logKeys(ActsLog)) != map[bool]int{true: tc.wantCopied + tc.wantAlready, false: len(tc.already)}[tc.wantMarker] {
t.Errorf("%s: marker %v, log %v", tc.name, marked, store.logKeys(ActsLog))
}
if !tc.wantMarker {
continue
}
// Each act is kept with its own time and author, as the controller wrote it.
raw, _, _ := store.Last(ActsLog, old[2])
var kept Act
_ = json.Unmarshal(raw, &kept)
if kept.By != "ace.mesh-config" || !kept.At.Equal(base) {
t.Errorf("%s: kept %+v", tc.name, kept)
}
// Asked again: nothing more is appended, and hand-acts counts each act once.
before := len(store.logKeys(ActsLog))
if again, err := l.Copy("test", false); err != nil || !again.Done || !strings.Contains(again.Said, "already copied") ||
len(store.logKeys(ActsLog)) != before {
t.Errorf("%s: a second copy: %+v, %v", tc.name, again, err)
}
if got := l.HandActs(30, "", ""); got.Kept[ControllerActs] != len(old)+boolInt(tc.junk) || len(got.Acts) != len(old) {
t.Errorf("%s: %d acts listed from %d kept: %+v", tc.name, len(got.Acts), before, got.Kept)
}
// An act the controller wrote since is appended by a top-up, and only that one.
late := fmt.Sprintf("act-%d-9", base.UnixNano()+9)
_ = store.Put(ControllerActs, late, Act{ID: late, At: base, By: "ace.late", Verb: VerbRecord, Why: "w", Cause: "c"})
if up, err := l.Copy("test", true); err != nil || up.Marker.Copied != 1 || len(store.logKeys(ActsLog)) != before+1 {
t.Errorf("%s: a top-up: %+v, %v", tc.name, up, err)
}
if s, err := l.Copied(); err != nil || !s.Done || s.Marker == nil {
t.Errorf("%s: copied says %+v, %v", tc.name, s, err)
}
}
}
func boolInt(b bool) int {
if b {
return 1
}
return 0
}
@@ -0,0 +1,183 @@
package main
// The one-time copy of the controller's hand-act log into this seat's log (novox/hq research 072 row 26, ADR 0571
// §13). The controller kept every act of its life in its bucket `mesh-controller_hand-acts` (574 on 2026-10-31); from
// the lease write only this seat reads them, so they are copied once into `acts`, each as the controller wrote it: the
// same key, the same bytes, so its time and its author are what they were.
//
// **When it runs.** The lease write has no hook to run from (it is a person's step, ADR 0561 §6), so the copy runs at
// this module's start while the old log is readable and no marker says it was copied, and on the tool
// `hand_acts_copy`. Both do the same thing, and neither does it twice (a `top_up` of the tool appends only what is new): the marker `controller-hand-acts` of the state
// `copied` is created once, after every act is in the log; an act already in the log (by its key) is never appended
// again, so a copy that stopped half way is finished by the next one. Until the marker exists `hand-acts` hides an
// old act that is already copied, so the two logs never count one act twice. `hand_acts_copied` says where it stands.
import (
"encoding/json"
"errors"
"fmt"
"sort"
"time"
)
// CopiedState is the state that holds the marker, and CopyKey the marker's key.
const (
CopiedState = "copied"
CopyKey = "controller-hand-acts"
)
// CopyMarker is what the copy leaves behind once it is whole: when, by what, how many acts, and which entries could not
// be read as acts (copied unchanged all the same, so nothing is lost).
type CopyMarker struct {
At time.Time `json:"at"`
By string `json:"by"`
Source string `json:"source"`
Found int `json:"found"`
Copied int `json:"copied"`
Already int `json:"already-there"`
// Unreadable are the keys whose value is not an act; they were copied as they were.
Unreadable []string `json:"unreadable,omitempty"`
FirstKey string `json:"first-key,omitempty"`
LastKey string `json:"last-key,omitempty"`
}
// CopyStatus is what the tools answer: the marker if the copy is whole, and how the two logs stand now.
type CopyStatus struct {
Done bool `json:"done"`
Marker *CopyMarker `json:"marker,omitempty"`
// Said is a sentence for a person: what this call did or found.
Said string `json:"said"`
// Pending are the old acts not yet in this seat's log (counted only while the copy is not whole).
Pending *int `json:"pending,omitempty"`
Old *int `json:"old-log-keys,omitempty"`
New *int `json:"new-log-entries,omitempty"`
}
// Copy copies the old log into this one, once. by says what asked (`start` or a caller). With topUp it also runs after
// the marker exists, appending only the acts the log does not hold yet (the controller's, written since the copy), and
// leaves the marker as it was. An old log that cannot be
// read, or an act that cannot be appended, is an error and leaves no marker, so the next call copies again.
func (l *Log) Copy(by string, topUp bool) (CopyStatus, error) {
marked := false
if raw, found, err := l.Store.Get(CopiedState, CopyKey); err != nil {
return CopyStatus{}, fmt.Errorf("whether the controller's hand acts were copied cannot be read: %w", err)
} else if found {
var m CopyMarker
if err := json.Unmarshal(raw, &m); err != nil {
return CopyStatus{}, fmt.Errorf("the copy's marker is not readable: %w", err)
}
if !topUp {
return CopyStatus{Done: true, Marker: &m, Said: fmt.Sprintf("already copied at %s: %d acts of %s, nothing done",
m.At.Format(time.RFC3339), m.Copied+m.Already, m.Source)}, nil
}
marked = true
}
keys, err := l.Store.Keys(ControllerActs)
if err != nil {
return CopyStatus{}, fmt.Errorf("the controller's hand acts cannot be read, so none were copied: %w", err)
}
sortByTime(keys)
m := CopyMarker{By: by, Source: ControllerActs, Found: len(keys)}
l.Logf("[%s] copying the controller's hand acts into %s: %d found in %s", Module, ActsLog, len(keys), ControllerActs)
for i, k := range keys {
raw, found, err := l.Store.Get(ControllerActs, k)
if err != nil {
return CopyStatus{}, fmt.Errorf("%s of %s cannot be read after %d of %d acts were copied: %w", k, ControllerActs, m.Copied, len(keys), err)
}
if !found {
continue // removed since it was listed
}
if _, there, err := l.Store.Last(ActsLog, k); err != nil {
return CopyStatus{}, fmt.Errorf("whether %s is in %s cannot be read, after %d acts were copied: %w", k, ActsLog, m.Copied, err)
} else if there {
m.Already++
continue
}
var a Act
if json.Unmarshal(raw, &a) != nil {
m.Unreadable = append(m.Unreadable, k)
l.Logf("[%s] %s of %s is not an act; copied unchanged", Module, k, ControllerActs)
}
if err := l.Store.Append(ActsLog, k, json.RawMessage(raw)); err != nil {
return CopyStatus{}, fmt.Errorf("%s could not be copied into %s after %d acts: %w", k, ActsLog, m.Copied, err)
}
m.Copied++
if m.FirstKey == "" {
m.FirstKey = k
}
m.LastKey = k
if (i+1)%100 == 0 {
l.Logf("[%s] copied %d of %d", Module, i+1, len(keys))
}
}
m.At = l.Now().UTC()
if marked {
l.Logf("[%s] topped up the copy: %d acts copied, %d already there, of %d found", Module, m.Copied, m.Already, m.Found)
return CopyStatus{Done: true, Marker: &m, Said: fmt.Sprintf("topped up: %d acts copied that the first copy did not hold (%d already there)", m.Copied, m.Already)}, nil
}
if err := l.Store.Create(CopiedState, CopyKey, m); errors.Is(err, ErrExists) {
return CopyStatus{Said: "another instance finished the copy first; nothing more done"}, nil
} else if err != nil {
return CopyStatus{}, fmt.Errorf("every act was copied but the marker could not be written, so the next call checks them again: %w", err)
}
l.Logf("[%s] copied the controller's hand acts: %d copied, %d already there, %d not acts, of %d found (%s .. %s)",
Module, m.Copied, m.Already, len(m.Unreadable), m.Found, m.FirstKey, m.LastKey)
return CopyStatus{Done: true, Marker: &m, Said: fmt.Sprintf("copied %d acts (%d were already there) of %d found in %s",
m.Copied, m.Already, m.Found, ControllerActs)}, nil
}
// Copied says where the copy stands without doing anything: the marker, or how many old acts wait.
func (l *Log) Copied() (CopyStatus, error) {
raw, found, err := l.Store.Get(CopiedState, CopyKey)
if err != nil {
return CopyStatus{}, fmt.Errorf("the copy's marker cannot be read: %w", err)
}
var st CopyStatus
if found {
var m CopyMarker
if err := json.Unmarshal(raw, &m); err != nil {
return CopyStatus{}, fmt.Errorf("the copy's marker is not readable: %w", err)
}
st = CopyStatus{Done: true, Marker: &m, Said: fmt.Sprintf("copied at %s by %s: %d copied, %d already there, of %d in %s",
m.At.Format(time.RFC3339), m.By, m.Copied, m.Already, m.Found, m.Source)}
}
if f, err := l.Store.Fill(ActsLog); err == nil {
n := int(f.Entries)
st.New = &n
}
keys, err := l.Store.Keys(ControllerActs)
if err != nil {
if !st.Done {
st.Said = "not copied yet, and the controller's log cannot be read: " + err.Error()
}
return st, nil
}
n := len(keys)
st.Old = &n
if !st.Done {
pending := 0
for _, k := range keys {
if _, there, err := l.Store.Last(ActsLog, k); err != nil || !there {
pending++
}
}
st.Pending = &pending
st.Said = fmt.Sprintf("not copied yet: %d of %d old acts wait; hand_acts_copy copies them", pending, n)
}
return st, nil
}
// sortByTime orders keys oldest first by the time they name, those that name none after, by key.
func sortByTime(keys []string) {
sort.SliceStable(keys, func(i, j int) bool {
ti, oi := keyTime(keys[i])
tj, oj := keyTime(keys[j])
switch {
case oi && oj && !ti.Equal(tj):
return ti.Before(tj)
case oi != oj:
return oi
}
return keys[i] < keys[j]
})
}
@@ -77,6 +77,15 @@ func (runtimeStore) Fill(log string) (Fill, error) {
func main() {
l := &Log{Store: runtimeStore{}, Now: time.Now, Logf: logf,
Publish: stdio.SeatPublish}
// No hook of the lease write runs this module, so the one-time copy of the controller's hand acts runs at start
// and on the tool hand_acts_copy; the marker makes it once (copy.go).
go func() {
if st, err := l.Copy("start", false); err != nil {
logf("[%s] the copy of the controller's hand acts is NOT done: %v", Module, err)
} else {
logf("[%s] %s", Module, st.Said)
}
}()
if err := stdio.Serve("", Tools(l)); err != nil {
logf("%v", err)
os.Exit(1)
@@ -47,7 +47,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
t.Errorf("emits %v, reads %v", m.Emits, m.Reads)
}
readBy := map[string]string{ActsLog: HandActsTool, WarrantsState: HandActsTool, ControllerActs: HandActsTool,
RouterRecords: WarrantedTool}
RouterRecords: WarrantedTool, CopiedState: CopiedTool}
tools := map[string]bool{}
for _, tool := range Tools(&Log{}) {
tools[tool.Name] = true
@@ -58,8 +58,8 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
}
for _, raw := range m.State {
var name string
if json.Unmarshal(raw, &name) != nil || name != WarrantsState {
t.Errorf("the state %s is not the warrants, or ages", raw)
if json.Unmarshal(raw, &name) != nil || (name != WarrantsState && name != CopiedState) {
t.Errorf("the state %s is neither the warrants nor the copy's marker, or ages", raw)
}
if !tools[readBy[name]] {
t.Errorf("the state %s has no tool that reads it", name)
@@ -21,6 +21,9 @@ const (
HandActTool = Seat + ".hand-act"
HandActsTool = Seat + ".hand-acts"
WarrantedTool = Seat + ".warranted"
// CopiedTool reads where the one-time copy of the controller's hand acts stands, CopyTool does it (copy.go).
CopiedTool = "hand_acts_copied"
CopyTool = "hand_acts_copy"
)
func obj(props map[string]any, required ...string) map[string]any {
@@ -70,6 +73,21 @@ func Tools(l *Log) []stdio.Tool {
Run: func(a map[string]any) (any, error) {
return l.Warranted(stdio.CallerOf(a), arg(a, "asker"), arg(a, "ask"))
}},
{Name: CopiedTool,
Description: "Where the one-time copy of the controller's hand acts into this seat's log stands (novox/hq research 072 " +
"row 26): done, with when, by what and how many acts were copied, or how many old acts still wait, and how " +
"full both logs are. Changes nothing.",
Input: obj(map[string]any{}),
Run: func(map[string]any) (any, error) { return l.Copied() }},
{Name: CopyTool,
Description: "Copy the controller's hand acts (its bucket mesh-controller_hand-acts) into this seat's log once, each " +
"with its own key, time and author. Does nothing when the copy is already done; an act already in the log is " +
"never appended again, so a copy that stopped is finished by asking again. With top_up=true it also runs after the " +
"copy is done and appends only the acts written since. This also runs at the module's start.",
Input: obj(map[string]any{"top_up": str("\"true\": after the copy is done, append the acts written since")}),
Run: func(a map[string]any) (any, error) {
return l.Copy("tool, called by "+stdio.CallerOf(a), a["top_up"] == true || strings.EqualFold(arg(a, "top_up"), "true"))
}},
}
}
@@ -338,7 +338,7 @@ func TestTheHandActLogWholeRecordsOnTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, b := range []string{Module + "_" + WarrantsState, "mesh-controller_hand-acts", "messenger_asks"} {
for _, b := range []string{Module + "_" + WarrantsState, Module + "_" + CopiedState, "mesh-controller_hand-acts", "messenger_asks"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b}); err != nil {
t.Fatal(err)
}
@@ -354,6 +354,13 @@ func TestTheHandActLogWholeRecordsOnTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// One act of the controller's, kept before the module starts: the start copies it into the log.
oldActs, _ := js.KeyValue(ctx, "mesh-controller_hand-acts")
oldKey := fmt.Sprintf("act-%d-1", time.Now().Add(-time.Hour).UnixNano())
if _, err := oldActs.Put(ctx, oldKey, []byte(`{"id":"`+oldKey+`","at":"`+time.Now().Add(-time.Hour).UTC().Format(time.RFC3339)+
`","by":"ace.old","verb":"hand-act","why":"w","cause":"c"}`)); err != nil {
t.Fatal(err)
}
cmd := exec.CommandContext(ctx, bin)
cmd.Env = append(os.Environ(), "MESH_NODE=novox", "MESH_MODULE="+Module)
stdin, _ := cmd.StdinPipe()
@@ -395,8 +402,19 @@ func TestTheHandActLogWholeRecordsOnTheBus(t *testing.T) {
t.Fatal(err)
}
}
if info, err := logStream.Info(ctx); err != nil || info.State.Msgs != 2 {
t.Errorf("the log keeps %v, want the act and one warrant (%v)", info, err)
for i := 0; ; i++ {
if c, err := p.call(CopiedTool, "person.jochen", map[string]any{}); err == nil && strings.Contains(string(c), `"done":true`) {
break
} else if i > 50 {
t.Fatalf("the controller's hand acts were not copied at the start: %s, %v", c, err)
}
time.Sleep(200 * time.Millisecond)
}
if e, err := logStream.GetLastMsgForSubject(ctx, "mesh.log."+Module+"."+ActsLog+"."+oldKey); err != nil || !strings.Contains(string(e.Data), `"by":"ace.old"`) {
t.Errorf("the controller's act is not copied with its author: %v", err)
}
if info, err := logStream.Info(ctx); err != nil || info.State.Msgs != 3 {
t.Errorf("the log keeps %v, want the copied act, the act and one warrant (%v)", info, err)
}
listed, err := p.call(HandActsTool, "person.jochen", map[string]any{})
if err != nil || !strings.Contains(string(listed), "warrant-claude-code-a1") || !strings.Contains(string(listed), r.Act.ID) {
+2 -1
View File
@@ -24,7 +24,8 @@
}
],
"state": [
"warrants"
"warrants",
"copied"
],
"own-secrets": {
"broker": "${dir:state}/broker"
@@ -108,6 +108,10 @@ type PlannedProvision struct {
// CredentialTool is, for a software-issued offer, the provider's tool the vault asks for the value (issue 928);
// the vault acts on it and this client only reads it.
CredentialTool string `json:"credential-tool,omitempty"`
// Contributed is what this module gives the provider through the provision, composed, as one opaque payload
// (issue 774): the provider reads it with its own contract (route-proxy's for a route); the client keeps it unread,
// so the plan reads whole and no provision's fields are known here.
Contributed json.RawMessage `json:"contributed,omitempty"`
}
// issuedForHolder is whether the entry names a value its provider's software issued for this holder alone (issue
@@ -9,8 +9,9 @@ import (
// A plan provision carries everything the controller's PlanProvision writes (issue 954): what the consumer
// contributes, where the provider is and who the consumer is at its end (issue 788), the names the mesh calls it
// (issue 792) and, for a software-issued offer, the tool the vault asks (issue 928). The client still refuses a field
// nobody writes.
// (issue 792), for a software-issued offer, the tool the vault asks (issue 928), and what the consumer contributes as
// one opaque payload, carried unread (issue 774: route-proxy serves its routes from it, read with its own contract).
// The client still refuses a field nobody writes, but never looks inside the payload.
func TestAPlanProvisionWithEveryFieldTheControllerWritesIsRead(t *testing.T) {
p, err := json.Marshal(contributor(t, "shell:zsh:normal", "alias nox=mesh-cli"))
if err != nil {
@@ -18,7 +19,9 @@ func TestAPlanProvisionWithEveryFieldTheControllerWritesIsRead(t *testing.T) {
}
provision := `"provisions":[{"provision":"route","local":"web","provider":"novox","provider-module":"route-proxy",` +
`"credential-form":"pair","contributes":{"label":"git"},"at":"10.10.0.1","from":"novox","as":"gitea",` +
`"names":{"name-web":"git.example.org"}},{"name":"gitea-read","provision":"gitea-read","provider":"novox",` +
`"names":{"name-web":"git.example.org"},` +
`"contributed":{"v":1,"at":"10.10.0.2","each":[{"local":"web","values":{"name":"git.example.org","anything":1}}]}},` +
`{"name":"gitea-read","provision":"gitea-read","provider":"novox",` +
`"provider-module":"gitea","credential-form":"issued","credential-tool":"gitea_agent_token",` +
`"path":"/var/lib/claude-code/gitea-read.secret"}],"items":`
value := fmt.Sprintf(`{"node":"g14","module":"mesh-cli","provisioning":%s}`,
@@ -29,7 +32,8 @@ func TestAPlanProvisionWithEveryFieldTheControllerWritesIsRead(t *testing.T) {
}
if len(got.Provisions) != 2 || got.Provisions[0].At != "10.10.0.1" || got.Provisions[0].From != "novox" ||
got.Provisions[0].As != "gitea" || got.Provisions[0].Names["name-web"] != "git.example.org" ||
got.Provisions[1].CredentialTool != "gitea_agent_token" {
got.Provisions[1].CredentialTool != "gitea_agent_token" || string(got.Provisions[0].Contributed) !=
`{"v":1,"at":"10.10.0.2","each":[{"local":"web","values":{"name":"git.example.org","anything":1}}]}` {
t.Fatalf("the plan's provisions read as %+v", got.Provisions)
}
spoilt := strings.Replace(value, `"at":`, `"surprise":1,"at":`, 1)
@@ -903,9 +903,9 @@ func (h *Holder) put(key string, p provisions.Provision) {
// ---- what a change touches --------------------------------------------------------------------------------------
// NodesOfPlacement is the node a placement key names.
// NodesOfPlacement is the node a placement key names, in either form (research 072 row 37).
func NodesOfPlacement(key string) []string {
if n, err := delivery.ParsePlacementKey(key); err == nil {
if n, _, _, err := delivery.ParsePlacementKey(key); err == nil {
return []string{n}
}
return nil
@@ -938,25 +938,23 @@ func (h *Holder) NodesOfModule(module string) []string {
return sortedKeys(nodes)
}
// placedOn is every node a module is placed on, sorted.
// placedOn is every node a module is placed on, sorted, from the placement's keys in either form
// (delivery.PlacementsOf; research 072 row 37). A node whose keys do not read whole is read as they stand.
func (h *Holder) placedOn(module string) []string {
nodes := map[string]bool{}
raw := map[string][]byte{}
if keys, err := h.Store.Keys(PlacementRead); err == nil {
for _, k := range keys {
n, err := delivery.ParsePlacementKey(k)
if err != nil {
continue
if e, err := h.Store.Get(PlacementRead, k); err == nil && e != nil {
raw[k] = e.Value
}
e, err := h.Store.Get(PlacementRead, k)
if err != nil || e == nil {
continue
}
if p, err := delivery.DecodePlacement(e.Value); err == nil {
for _, m := range p.Modules {
if m.Module == module {
nodes[n] = true
}
}
}
}
placements, _ := delivery.PlacementsOf(raw)
for n, p := range placements {
for _, m := range p.Modules {
if m.Module == module {
nodes[n] = true
}
}
}
@@ -191,7 +191,17 @@ func TestAKeysStateFollowsItsProvidersOutcome(t *testing.T) {
// A provision's credential is the vault's own provision, on the consumer's node for a login of its own, and the
// provision depends on it (clauses 3, 6).
c := newMesh()
c.s.write(t, PlacementRead, "placement.novox", `{"v":1,"node":"novox","decided":{},"modules":[{"module":"mesh-vault"}]}`)
// The placement in its keys per module and node (research 072 row 37).
novox, err := delivery.PlacementValues(delivery.Placement{Node: "novox", Modules: []delivery.PlacedModule{{Module: "mesh-vault"}}})
if err != nil {
t.Fatal(err)
}
for k, v := range novox {
c.s.write(t, PlacementRead, k, string(v))
if n := NodesOfPlacement(k); !slices.Equal(n, []string{"novox"}) {
t.Errorf("NodesOfPlacement(%q) = %v", k, n)
}
}
pair := pgDB
pair.CredentialForm = "pair"
c.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(pair)}
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-provisioner
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030212912-6c5a8ef893b4
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
github.com/nats-io/nats.go v1.54.0
)
+2 -4
View File
@@ -1,7 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030182415-754edfae6924 h1:aIiKj066gYFEDokUOq0i2n4vsygu5g0XMKJRRaMI4fM=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030182415-754edfae6924/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030212912-6c5a8ef893b4 h1:uzXzKfjoWB8wCbAPgxUAWSArr+3DKwi8qrB3BUEOVOE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030212912-6c5a8ef893b4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -31,29 +31,26 @@ const (
ScopeMachine = health.ScopeMachine
)
// placements is every node's placement, by node.
// 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.<node>` (delivery.PlacementsOf; research 072 row 37).
func (c *Checker) placements() (map[string]delivery.Placement, error) {
keys, err := c.Store.Keys(PlacementRead)
if err != nil {
return nil, fmt.Errorf("the placement cannot be read: %w", err)
}
out := map[string]delivery.Placement{}
values := map[string][]byte{}
for _, k := range keys {
raw, ok, err := c.Store.Get(PlacementRead, k)
if err != nil {
return nil, fmt.Errorf("the placement of %s cannot be read: %w", k, err)
return nil, fmt.Errorf("the placement's key %s cannot be read: %w", k, err)
}
if !ok {
continue
if ok {
values[k] = raw
}
var p delivery.Placement
if err := json.Unmarshal(raw, &p); err != nil {
return nil, fmt.Errorf("the placement of %s is not readable: %w", k, err)
}
if p.Node == "" {
p.Node = k
}
out[p.Node] = p
}
out, err := delivery.PlacementsOf(values)
if err != nil {
return nil, fmt.Errorf("the placement is not readable: %w", err)
}
return out, nil
}
@@ -260,8 +260,12 @@ 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}} {
_ = s.Put(PlacementRead, p.node, delivery.Placement{V: 1, Node: p.node,
Decided: delivery.NodeDecided{Address: p.address, Endpoint: p.endpoint, Hub: p.endpoint != ""}})
pl := delivery.Placement{V: 1, Node: p.node,
Decided: delivery.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
}
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,
@@ -270,6 +274,31 @@ func machines() *memStore {
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) {
if p.Node == "g14" {
raw, _ := delivery.EncodePlacement(p)
_ = s.Put(PlacementRead, "placement.g14", json.RawMessage(raw))
return
}
values, _ := delivery.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 {
keys, _ := s.Keys(PlacementRead)
values := map[string][]byte{}
for _, k := range keys {
values[k], _, _ = s.Get(PlacementRead, k)
}
out, _ := delivery.PlacementsOf(values)
return out
}
// filling is a store holding the self-check's written fill finding of a stream: the condition open.
func filling(stream string) *memStore {
s := newStore()
@@ -285,11 +314,9 @@ func filling(stream string) *memStore {
func agents(verdicts map[string]string) *memStore {
s := machines()
for node, v := range verdicts {
raw, _, _ := s.Get(PlacementRead, node)
var p delivery.Placement
_ = json.Unmarshal(raw, &p)
p := placed(s)[node]
p.Decided.AgentAccount = "agent"
_ = s.Put(PlacementRead, node, p)
place(s, p)
h := delivery.NodeHealth{V: 1, Node: node, Contract: 13}
switch v {
case "":
@@ -303,7 +330,7 @@ func agents(verdicts map[string]string) *memStore {
h.Root = []delivery.RootVerdict{{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, _ := delivery.EncodeNodeHealth(h)
_ = s.Put(HealthRead, node, json.RawMessage(raw))
}
return s
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-self-check
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -323,9 +323,29 @@ func (v *Vault) AcceptTake(caller, call, sealed string) (Changed, error) {
if origin == "" {
origin = OriginAccepted
}
if origin == OriginIssued {
// **The vault's mint wins** (issue 774): during the delivery a controller older than the hand-over may still mint
// a covered account and hand it here; it never replaces an account the vault minted, judged under the store's
// lock, so the value and the hash the bus loaded stay the vault's.
return v.keepWith(c.secret, string(value), someoneOr(c.by, caller), caller, origin, keepOptions{refuse: v.vaultMinted})
}
return v.keep(c.secret, string(value), someoneOr(c.by, caller), caller, origin)
}
// vaultMinted refuses a value handed for a secret holding an account the vault minted itself (mint.go): one is never
// replaced but by the vault's own rotation.
func (v *Vault) vaultMinted(before Record) string {
if before.Origin != OriginMade || before.ChangedBy != "the vault" || before.MovedFrom != nil {
return ""
}
if was, err := openAtRest(before, v); err == nil && BusAccountValue(before.Name, was) {
return before.Name + " holds a bus account the vault minted (generation " + fmt.Sprint(before.Generation) +
"): a value the controller issued never replaces it, so nothing was taken; the vault is the one issuer " +
"(issue 774), and a new value is the vault's rotation"
}
return ""
}
// ---- secret-ask ---------------------------------------------------------------------------------------------
// AskOf is what an ask is for, and where.
+9 -17
View File
@@ -1,28 +1,20 @@
package main
// The switch on the controller's lease (novox/hq ADR 0561 §3): minting is on only while the lease is handed to a holder
// that is no controller, and off for a controller's value, a key deleted or expired, or one that cannot be read. The
// write that hands it away turns it on and mints every wanted account the vault holds no value for; a lease handed back
// turns it off. No clock and no setting turns it. A key's age passing is heard by no watch, so the lease is read afresh
// before every mint (LeaseRead): a lease that expired since mints nothing.
// The controller's lease as the vault reads it (novox/hq ADR 0561 §3): handed away only to a holder that is no
// controller, and not for a controller's value, a key deleted or expired, or one that cannot be read. It turns retirement
// (retire.go) and rotation (rotation.go), never minting: the vault mints every wanted account it holds no value for,
// whoever holds the lease (mint.go, issue 774). No clock and no setting turns it. A key's age passing is heard by no
// watch, so the lease is read afresh before a retirement or a rotation (LeaseRead).
import "git.novox.be/novox/mesh-sdk/go/buscompose"
// LeaseHeard is the watch of the controller's lease. When minting turns on or off, every wanted account is judged again:
// minted when on, held back with the new why when off.
// LeaseHeard is the watch of the controller's lease: it records whether it is handed away, and says when that turned.
func (v *Vault) LeaseHeard(raw []byte, found bool, readErr error) {
if !v.setLease(raw, found, readErr) {
return
}
on, why := v.mintingOn()
v.log("minting is %s: %s", map[bool]string{true: "ON", false: "off"}[on], why)
for _, user := range v.wantedUsers() {
if w, ok := v.wanted(user); ok {
if _, err := v.mintWanted(w, "the lease write"); err != nil {
v.log("%s is not minted: %v", user, err)
}
}
}
on, why := v.leaseHanded()
v.log("the controller's lease is %s: %s", map[bool]string{true: "HANDED AWAY", false: "not handed away"}[on], why)
}
// setLease records what the lease says, and answers whether minting turned on or off by it.
@@ -39,7 +31,7 @@ func (v *Vault) setLease(raw []byte, found bool, readErr error) bool {
return flipped
}
// leaseAfresh reads the lease again before a mint, where the vault can: a key that expired since is heard by no watch.
// leaseAfresh reads the lease again before a retirement or a rotation, where the vault can: a key that expired since is heard by no watch.
func (v *Vault) leaseAfresh() {
if v.LeaseRead == nil {
return
+61 -28
View File
@@ -10,10 +10,14 @@ package main
// bcrypt hash into `bus-accounts`; and says `account-issued`, then `secret-changed`. The node's engine asks `own` again
// on `secret-changed` and composes.
//
// **One issuer at every moment** (§3): minting is on only while the controller's lease is handed to a holder that is no
// controller (mesh-sdk LeaseHandedAway). Until then the controller mints, the vault keeps hashing what it is handed, and
// vault_status names every wanted account it holds back and why. **Nothing is minted anew** (§4): an account whose value
// the vault holds, or whose hash it adopted, is never minted; the node accounts and the controller's own are never wanted.
// **The vault is the one issuer** (issue 774, research 072's gap "vault mints after the lease"): it mints every wanted
// account it holds no value for, whoever holds the controller's lease, and the controller mints none once a vault is
// installed (mesh-controller forwards `module issue` and its assignment's issue to issue-account). No account depends
// on a node reporting to the controller. The lease still turns retirement and rotation (retire.go, rotation.go), and
// vault_status names it. **Never two issuers of one value** (§3, §4): an account whose value the vault holds, or whose
// hash it adopted, is never minted; the node accounts and the controller's own are never wanted; a value the controller
// moves in never replaces one held (move.go), and one it hands with origin issued never replaces an account the vault
// minted (AcceptTake): during the delivery an older controller may still mint, and the vault's value wins.
// **Never two values for one user** (§7): a user whose hash the vault already wrote from another of its secrets is held
// back, said, and never replaced but by a rotation.
@@ -43,8 +47,9 @@ const (
// AccountIssuedEvent is what the vault says when it minted an account (mesh-sdk buscompose.AccountIssued).
const AccountIssuedEvent = buscompose.AccountIssuedEvent
// minting is the switch and what the vault knows of the wanted accounts, kept in memory and read again from the bus at
// every start: the grants part's state and the lease are the record, never this.
// minting is what the vault knows of the wanted accounts and of the controller's lease, kept in memory and read again
// from the bus at every start: the grants part's state and the lease are the record, never this. on, why and since are
// the lease (handed away or not), which turns retirement and rotation, never minting.
type minting struct {
mu sync.Mutex
on bool
@@ -76,41 +81,77 @@ type Minted struct {
Hashed bool `json:"hashed"`
}
// MintState is what vault_status says of minting.
// MintState is what vault_status says of minting: always on (the vault is the one issuer, issue 774), and the lease,
// which turns retirement and rotation.
type MintState struct {
On bool `json:"on"`
Why string `json:"why"`
On bool `json:"on"`
Why string `json:"why"`
// Lease is whether the controller's lease is handed away, LeaseWhy why, and Since when that last turned.
Lease bool `json:"lease_handed_away"`
LeaseWhy string `json:"lease_why"`
Since time.Time `json:"since,omitzero"`
Wanted int `json:"wanted"`
HeldBack map[string]string `json:"held_back"`
Minted []Minted `json:"minted"`
}
// MintState answers how minting stands.
// mintingWhy is why the vault mints, whoever holds the lease.
const mintingWhy = "the vault is the one issuer of bus accounts: it mints every wanted account it holds no value for " +
"and adopted no hash of, whoever holds the controller's lease (issue 774)"
// MintState answers how minting stands. A user held back whose account the vault holds a value for meanwhile (moved in,
// handed, or minted by another path) is not said held back: it is not (issue 774, 2026-10-31).
func (v *Vault) MintState() MintState {
m := &v.minting
m.mu.Lock()
defer m.mu.Unlock()
out := MintState{On: m.on, Why: m.why, Since: m.since, Wanted: len(m.wants), HeldBack: map[string]string{}, Minted: []Minted{}}
if out.Why == "" {
out.Why = "the controller's lease has not been read yet, so nothing is minted"
out := MintState{On: true, Why: mintingWhy, Lease: m.on, LeaseWhy: m.why, Since: m.since, Wanted: len(m.wants),
HeldBack: map[string]string{}, Minted: []Minted{}}
if out.LeaseWhy == "" {
out.LeaseWhy = leaseUnread
}
wants := map[string]buscompose.WantedAccount{}
for u, why := range m.heldBack {
out.HeldBack[u] = why
if w, ok := m.wants[u]; ok {
wants[u] = w
}
}
for _, x := range m.minted {
out.Minted = append(out.Minted, x)
}
m.mu.Unlock()
for u, w := range wants {
if v.holdsAccount(w) {
delete(out.HeldBack, u)
}
}
sort.Slice(out.Minted, func(i, j int) bool { return out.Minted[i].User < out.Minted[j].User })
return out
}
// mintingOn says whether the vault mints now, and why.
func (v *Vault) mintingOn() (bool, string) {
// holdsAccount says the vault holds a bus account as w's own secret: a value that is an account.
func (v *Vault) holdsAccount(w buscompose.WantedAccount) bool {
secret, err := AccountSecret(w)
if err != nil {
return false
}
rec, held, err := v.Store.Read(secret)
if err != nil || !held {
return false
}
value, err := openAtRest(rec, v)
return err == nil && BusAccountValue(secret, value)
}
// leaseUnread is why nothing is retired or rotated before the lease is read.
const leaseUnread = "the controller's lease has not been read yet, so nothing is retired or rotated"
// leaseHanded says whether the controller's lease is handed away, and why: what retirement and rotation wait for.
func (v *Vault) leaseHanded() (bool, string) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
if v.minting.why == "" {
return false, "the controller's lease has not been read yet, so nothing is minted"
return false, leaseUnread
}
return v.minting.on, v.minting.why
}
@@ -248,11 +289,6 @@ func (v *Vault) mintOnce(w buscompose.WantedAccount, by string) (Minted, error)
v.holdBack(w.User, why)
return Minted{}, errHeldBack{why}
}
v.leaseAfresh()
if on, why := v.mintingOn(); !on {
v.holdBack(w.User, "held back: "+why)
return Minted{}, errHeldBack{why}
}
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return Minted{}, err
@@ -360,8 +396,9 @@ func heldBackOrMinted(m Minted, err error) (any, error) {
}
// IssueAccount is the seat's verb issue-account (ADR 0561 §9): one wanted user minted now, its key read afresh from the
// grants part's state. The recovery when a want was not heard: it mints nothing a want would not, and is refused while
// minting is off, for a node's account and the controller's (never minted here, §4), and for a user nobody wants.
// grants part's state. The recovery when a want was not heard, and what the controller's `module issue` forwards to
// (issue 774): it mints nothing a want would not, answers "held already" for an account it holds, and is refused for a
// node's account and the controller's (never minted here, §4) and for a user nobody wants.
func (v *Vault) IssueAccount(caller, user string) (any, error) {
user = strings.TrimSpace(user)
if user == "" {
@@ -371,10 +408,6 @@ func (v *Vault) IssueAccount(caller, user string) (any, error) {
return nil, fmt.Errorf("%s is never minted by the vault: a node's account and the controller's keep the hash they "+
"have (ADR 0561 §4)", user)
}
v.leaseAfresh()
if on, why := v.mintingOn(); !on {
return nil, fmt.Errorf("not minted: %s", why)
}
raw, found, err := []byte(nil), false, error(nil)
if v.WantRead != nil {
raw, found, err = v.WantRead(user)
@@ -5,9 +5,9 @@ package main
// The vault whole against a real bus (novox/hq ADR 0561 §1 to §4, issue 774's acceptance 12): the built binary, played
// over its real stdin and stdout as the node's tool runtime plays it (tools/call with the caller in _meta; mesh/state.*
// and mesh/publish answered from a real nats-server's key-value buckets and subjects), its store in a directory of its
// own. A want written to the grants part's state is minted only once the controller's lease is handed away: the hash is
// in bus-accounts, own answers the value sealed to the node, the value is the controller's shape and its hash's
// password, account-issued is said; an adopted user is never minted, and a lease handed back mints nothing.
// own. A want written to the grants part's state is minted whoever holds the controller's lease (issue 774: the vault is
// the one issuer): the hash is in bus-accounts, own answers the value sealed to the node, the value is the controller's
// shape and its hash's password, account-issued is said; an adopted user is never minted.
import (
"bufio"
@@ -297,7 +297,7 @@ func eventually(t *testing.T, what string, ok func() bool) {
}
// 774/AC12.
func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) {
func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
dir := t.TempDir()
@@ -358,21 +358,16 @@ func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) {
_ = json.Unmarshal(raw, &s)
return s.Minting
}
// The controller holds the lease: a want is held back, nothing is minted.
// The controller holds the lease, and the want is minted all the same.
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
x, y, z := aWant("mod-x"), aWant("mod-y"), aWant("mod-z")
x, y := aWant("mod-x"), aWant("mod-y")
p.put(t, "mesh-vault.bus-accounts", y.User, AccountHash{User: y.User, Hash: "$2a$04$adoptedadoptedadoptedadoptedadoptedadoptedadopte", Adopted: true})
p.put(t, "mesh-grants.accounts", x.User, x)
p.put(t, "mesh-grants.accounts", y.User, y)
eventually(t, "the want held back under the controller's lease", func() bool {
return strings.Contains(status().HeldBack[x.User], "controller holds the lease")
})
if p.hashOf(t, x.User) != nil {
t.Fatal("minted under the controller's lease")
eventually(t, "the account minted under the controller's lease", func() bool { return p.hashOf(t, x.User) != nil })
if st := status(); !st.On || st.Lease || len(st.HeldBack) != 0 {
t.Errorf("vault_status says %+v", st)
}
// The write that hands the lease away mints it.
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
eventually(t, "the account minted at the lease write", func() bool { return p.hashOf(t, x.User) != nil })
nodePub, nodePriv := aSealingKey(t)
if _, err := p.call(Seat+".node-key", "node.ace", map[string]any{"key": nodePub}); err != nil {
t.Fatal(err)
@@ -410,13 +405,4 @@ func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) {
if !strings.Contains(said, AccountIssuedEvent+` {"`) || !strings.Contains(said, `"user":"ace.mod-x"`) || strings.Contains(said, held.Password) {
t.Errorf("the vault said: %s", said)
}
// Handed back to a controller: a new want mints nothing.
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/2/2026-10-28", "epoch": 58})
p.put(t, "mesh-grants.accounts", z.User, z)
eventually(t, "the new want held back once the lease is the controller's again", func() bool {
return strings.Contains(status().HeldBack[z.User], "controller holds the lease")
})
if p.hashOf(t, z.User) != nil {
t.Error("minted after the lease was handed back")
}
}
+35 -32
View File
@@ -106,51 +106,55 @@ func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) {
}
}
// 774/AC8 (novox/hq ADR 0561 §3): while the controller holds the lease nothing is minted and vault_status says why and
// names each wanted account held back (never an adopted one); the write that hands the lease away mints them; a lease
// handed back, expired or unreadable turns minting off again, read afresh before every mint.
func TestNothingIsMintedUntilTheLeaseIsHandedAway(t *testing.T) {
// 774/AC8, after research 072's gap "vault mints after the lease": the vault is the one issuer, so a wanted account is
// minted while the controller still holds the lease (never an adopted one), vault_status says minting is on and the
// lease apart, and a want held back is not said held back once the vault holds its value. During the delivery an older
// controller may still mint: its value handed with origin issued never replaces the account the vault minted.
func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) {
v, accounts, _ := mintingVault(t, false)
accounts.keys = map[string]AccountHash{"ace.mod-y": {User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}}
for _, m := range []string{"mod-x", "mod-y"} {
w := aWant(m)
v.WantHeard(w.User, wantRaw(t, w), false)
}
st := v.MintState()
if st.On || !strings.Contains(st.Why, "controller holds the lease") || len(st.HeldBack) != 1 ||
!strings.Contains(st.HeldBack["ace.mod-x"], "controller holds the lease") {
t.Fatalf("under the controller's lease vault_status says %+v", st)
}
if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); held {
t.Fatal("minted while the controller holds the lease")
}
// Handed away: the wanted account is minted at that write, the adopted one is not.
lease := []byte(`{"handed-to":"the operator's hand-over"}`)
v.LeaseRead = func() ([]byte, bool, error) { return lease, true, nil }
v.LeaseHeard(lease, true, nil)
if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); !held {
t.Fatalf("not minted at the lease write: %+v", v.MintState())
minted, held, _ := v.Store.Read("own/ace/mod-x/broker")
if !held {
t.Fatalf("not minted under the controller's lease: %+v", v.MintState())
}
if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held {
t.Error("an adopted account was minted at the lease write")
t.Error("an adopted account was minted")
}
if st := v.MintState(); !st.On || len(st.HeldBack) != 0 {
t.Errorf("after the lease write vault_status says %+v", st)
st := v.MintState()
if !st.On || st.Lease || !strings.Contains(st.LeaseWhy, "controller holds the lease") || len(st.HeldBack) != 0 {
t.Errorf("under the controller's lease vault_status says %+v", st)
}
// The key expired (no watch hears a key's age pass): read afresh before the next mint, nothing is minted.
v.LeaseRead = func() ([]byte, bool, error) { return nil, false, nil }
// An older controller's covered account, handed with origin issued: refused, the vault's value kept.
x := aWant("mod-x")
theirs, _ := buscompose.HeldCredential(x, "the-controllers-password")
if _, err := handIssued(t, v, ControllerCaller, "own/ace/mod-x/broker", string(theirs)); err == nil ||
!strings.Contains(err.Error(), "the vault minted") {
t.Errorf("a controller's issued value over the vault's mint: %v", err)
}
if rec, _, _ := v.Store.Read("own/ace/mod-x/broker"); rec.Generation != minted.Generation || rec.ChangedBy != "the vault" {
t.Errorf("the vault's minted account was replaced: %+v", rec)
}
// Held back, and then held (moved in, handed): no longer said held back.
z := aWant("mod-z")
v.WantHeard(z.User, wantRaw(t, z), false)
if _, held, _ := v.Store.Read("own/ace/mod-z/broker"); held {
t.Error("minted after the lease's key expired")
v.minting.mu.Lock()
v.minting.wants[z.User] = z
v.minting.mu.Unlock()
v.holdBack(z.User, "held back for a reason that has passed")
value, _ := buscompose.HeldCredential(z, "moved-in")
if _, err := v.keep("own/ace/mod-z/broker", string(value), "the move", ControllerCaller, OriginMade); err != nil {
t.Fatal(err)
}
if st := v.MintState(); st.On || !strings.Contains(st.HeldBack["ace.mod-z"], "no lease is written") {
t.Errorf("after the key expired vault_status says %+v", st)
if st := v.MintState(); st.HeldBack[z.User] != "" {
t.Errorf("an account the vault holds is said held back: %+v", st.HeldBack)
}
}
// 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh; it is refused while
// minting is off, for a node's account and the controller's, and for a user nobody wants; a user held already is said so.
// 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh, whoever holds the lease;
// it is refused for a node's account and the controller's, and for a user nobody wants; a user held already is said so.
func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
v, _, _ := mintingVault(t, false)
w := aWant("mod-x")
@@ -162,7 +166,6 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
return nil, false, nil
}
for user, says := range map[string]string{
"ace.mod-x": "controller holds the lease",
"node.ace": "never minted",
"controller": "never minted",
} {
@@ -170,7 +173,7 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
t.Errorf("%s: %v, want a refusal saying %q", user, err, says)
}
}
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
// Minted while the controller still holds the lease (issue 774): `module issue` forwards here.
if _, err := v.IssueAccount("person.jochen", "ace.nobody"); err == nil || !strings.Contains(err.Error(), "no wanted account") {
t.Errorf("a user nobody wants: %v", err)
}
+28 -11
View File
@@ -8,7 +8,6 @@ package main
// provisioner is left out: its client fills its secrets from `get` (ADR 0349 §17).
import (
"encoding/json"
"errors"
"fmt"
"slices"
@@ -38,6 +37,11 @@ type OwnAnswer struct {
Own []NodeOwnSecret `json:"own"`
Credentials []NodeCredential `json:"credentials,omitempty"`
Missing []CredentialAsked `json:"missing,omitempty"`
// Handed is, by module of the machine, whether the vault judged it handed to the provisioner (catalogue.AssignmentHands
// of the plan in its assignment), and so left its own secrets out of Own: the node composing itself hands exactly
// those (mesh-sdk compose/frombus HandedReader, issue 774). Every module it holds a secret of, and every module
// placed there whose plan reads; one it could not judge is not named, and the node judges it as before.
Handed map[string]bool `json:"handed,omitempty"`
}
// CredentialAsked names one pair credential by its ends, as a node composing its own asks for it (mesh-sdk
@@ -163,6 +167,15 @@ func (p *planReader) placedOn(module string) (bool, error) {
return slices.Contains(p.placed, module), p.placedErr
}
// placedModules is the machine's placed modules, read once for the answer.
func (p *planReader) placedModules() ([]string, error) {
if p.placement == nil {
return nil, errors.New("the machine's placement is not read here")
}
p.placedOn("")
return p.placed, p.placedErr
}
// read is module's assignment on the machine, nil when there is none, or why the vault reads none.
func (p *planReader) read(module string) ([]byte, string, error) {
if p.why != "" {
@@ -283,7 +296,7 @@ func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e
}
plan := v.plansOf(node)
var handings []Handing
out := OwnAnswer{Node: node, Key: key}
out := OwnAnswer{Node: node, Key: key, Handed: map[string]bool{}}
for _, r := range all {
parts := strings.Split(r.Name, "/")
if len(parts) != 4 || parts[0] != "own" || parts[1] != node {
@@ -293,6 +306,7 @@ func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e
if err != nil {
return OwnAnswer{}, err
}
out.Handed[parts[2]] = handed
if handed {
continue
}
@@ -308,6 +322,17 @@ func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e
out.Own = append(out.Own, NodeOwnSecret{Module: parts[2], Name: parts[3], Origin: composeOrigin(r.Origin),
Generation: r.Generation, Sealed: sealed})
}
// And every module placed there that it holds no secret of yet, where its plan reads: the node hands it as judged here.
if placed, err := plan.placedModules(); err == nil {
for _, module := range placed {
if _, judged := out.Handed[module]; judged {
continue
}
if handed, err := plan.handed(module); err == nil {
out.Handed[module] = handed
}
}
}
sort.Slice(out.Own, func(i, j int) bool {
if out.Own[i].Module != out.Own[j].Module {
return out.Own[i].Module < out.Own[j].Module
@@ -392,15 +417,7 @@ func (p *planReader) handed(module string) (bool, error) {
// handed) hands nothing, and that module's files and own secrets stay its node's composition. Whether a plan exists
// is never the rule; a reader that asks only who binds whom reads the provisions and says so ("not handing", which
// the catalogue check checks/planhands holds every module to).
func handsOf(raw []byte) (bool, error) {
var a struct {
Provisioning *catalogue.Plan `json:"provisioning"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return false, err
}
return a.Provisioning != nil && a.Provisioning.Hands(), nil
}
func handsOf(raw []byte) (bool, error) { return catalogue.AssignmentHands(raw) }
// hands is handsOf for a value already read as the controller's: one that cannot be read hands nothing.
func hands(raw []byte) bool {
@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
@@ -286,4 +287,8 @@ func TestOwnLeavesOutOnlyAModuleWhosePlanHandsIt(t *testing.T) {
if len(got.Own) != 1 || got.Own[0].Module != "grafana" || got.Own[0].Name != "admin" {
t.Errorf("anchor was answered %+v; want grafana's admin alone (its plan keys a route and hands nothing)", got.Own)
}
// And it says how it judged each, so the node composing itself hands exactly those (issue 774, postgres on novox).
if want := map[string]bool{"grafana": false, "web": true, "db": true}; !reflect.DeepEqual(got.Handed, want) {
t.Errorf("own judged %v handed, want %v", got.Handed, want)
}
}
+48 -26
View File
@@ -25,7 +25,8 @@ type PlacementReader struct {
Read func(key string) ([]byte, error)
}
// placements is every node's placement, by node; none when the vault reads no placement.
// placements is every node's placement, by node, from the state's keys in either form (delivery.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{}
if v.Placement.Keys == nil || v.Placement.Read == nil {
@@ -35,52 +36,73 @@ func (v *Vault) placements() (map[string]delivery.Placement, error) {
if err != nil {
return nil, fmt.Errorf("the placement's keys could not be read: %w", err)
}
values := map[string][]byte{}
for _, k := range keys {
node, err := delivery.ParsePlacementKey(k)
if err != nil {
if _, _, _, err := delivery.ParsePlacementKey(k); err != nil {
continue
}
raw, err := v.Placement.Read(k)
if err != nil {
return nil, fmt.Errorf("the placement of %s could not be read: %w", node, err)
return nil, fmt.Errorf("the placement's key %s could not be read: %w", k, err)
}
if raw == nil {
continue
if raw != nil {
values[k] = raw
}
p, err := delivery.DecodePlacement(raw)
if err != nil || p.Node != node {
continue
}
out[node] = p
}
out, _ = delivery.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>.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.<node>`. 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)
if err != nil {
return nil, decided, false, err
}
raw, err := v.Placement.Read(key)
if err != nil {
return nil, decided, false, err
}
if raw != nil {
n, err := delivery.DecodePlacementNode(raw)
if err != nil {
return nil, decided, false, fmt.Errorf("the placement of %s is not readable: %w", node, err)
}
for _, r := range n.Modules {
modules = append(modules, r.Module)
}
return modules, n.Decided, true, nil
}
legacy, err := delivery.LegacyPlacementKey(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)
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
}
// placedOn says module is placed on node by the catalogue.
func (v *Vault) placedOn(node, module string) (bool, error) {
placed, err := v.placedModules(node)
return slices.Contains(placed, module), err
}
// placedModules is every module the catalogue places on node, read from the node's one key of the placement; none when
// the vault reads no placement or the node has no key.
// placedModules is every module the catalogue places on node, read from the node's own key of the placement
// (nodePlaced); none when the vault reads no placement or the node has no key.
func (v *Vault) placedModules(node string) ([]string, error) {
if v.Placement.Read == nil {
return nil, nil
}
key, err := delivery.PlacementKey(node)
if err != nil {
return nil, err
}
raw, err := v.Placement.Read(key)
if err != nil || raw == nil {
return nil, err
}
p, err := delivery.DecodePlacement(raw)
if err != nil {
return nil, fmt.Errorf("the placement of %s is not readable: %w", node, err)
}
return p.Assigned(), nil
modules, _, _, err := v.nodePlaced(node)
return modules, err
}
// standsOn says module stands on node: placed there, or assigned there while the assignments stand. A read that fails is
@@ -16,12 +16,14 @@ func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
}
raw, err := delivery.EncodePlacement(p)
// Research 072 row 37: the node key and a key per module.
kv, err := delivery.PlacementValues(p)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.PlacementKey(node)
values[key] = raw
for k, raw := range kv {
values[k] = raw
}
}
return PlacementReader{
Keys: func() ([]string, error) {
+2 -2
View File
@@ -136,7 +136,7 @@ func (v *Vault) RetirePass() {
return
}
v.leaseAfresh()
on, leaseWhy := v.mintingOn()
on, leaseWhy := v.leaseHanded()
m := &v.minting
m.mu.Lock()
read := m.retiring.wantsRead
@@ -239,7 +239,7 @@ func (v *Vault) RetireAccounts(caller string, users []string, why string) (any,
// As the passes: never while the controller holds the lease (it forgets the accounts it mints), nor before the wanted
// accounts are read whole (an account not read yet is not known to be gone).
v.leaseAfresh()
if on, why := v.mintingOn(); !on {
if on, why := v.leaseHanded(); !on {
return nil, fmt.Errorf("nothing was retired: %s", why)
}
v.minting.mu.Lock()
@@ -42,7 +42,7 @@ func (v *Vault) BeginRotation(user string) (Rotating, error) {
if nodeOrController(user) {
return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user)
}
if on, why := v.mintingOn(); !on {
if on, why := v.leaseHanded(); !on {
return Rotating{}, fmt.Errorf("no rotation begins: %s", why)
}
v.minting.mu.Lock()
+3 -11
View File
@@ -303,22 +303,14 @@ func (v *Vault) internalName(node string) (string, error) {
if v.Placement.Read == nil {
return "", errors.New("the vault reads no placement, so it cannot tell a node's internal name")
}
key, err := delivery.PlacementKey(node)
if err != nil {
return "", err
}
raw, err := v.Placement.Read(key)
_, decided, found, err := v.nodePlaced(node)
if err != nil {
return "", fmt.Errorf(notYetPrefix+"the placement of %s could not be read: %w", node, err)
}
if raw == nil {
if !found {
return "", fmt.Errorf("%s has no placement of the seat mesh-placement, so it has no name inside the mesh", node)
}
p, err := delivery.DecodePlacement(raw)
if err != nil {
return "", fmt.Errorf("the placement of %s is not readable: %w", node, err)
}
if p.Decided.Address == "" {
if decided.Address == "" {
return "", fmt.Errorf("%s is not on the private network (its placement names no address), so it has no name inside the mesh to be certified for", node)
}
return overlay.InternalName(node), nil
@@ -40,16 +40,21 @@ func TestTheVaultSigns(t *testing.T) {
"g14": {Node: "g14", Decided: delivery.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, err := delivery.ParsePlacementKey(key)
node, _, kind, err := delivery.ParsePlacementKey(key)
if err != nil {
return nil, err
}
p, ok := placements[node]
if !ok {
if !ok || (node == "g14") != (kind == delivery.PlacementLegacyKind) {
return nil, nil
}
return delivery.EncodePlacement(p)
if kind == delivery.PlacementLegacyKind {
return delivery.EncodePlacement(p)
}
values, err := delivery.PlacementValues(p)
return values[key], err
}}
records := map[string]string{"ace": base64.StdEncoding.EncodeToString(servingPub), "plain": base64.StdEncoding.EncodeToString(servingPub)}
v.Haves = func(key string) ([]byte, error) {
+6 -5
View File
@@ -306,9 +306,9 @@ func definitions(env *Env) []toolDef {
}},
{Name: Seat + ".issue-account",
Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): the recovery when the vault did not " +
"hear a want. Reads the wanted key afresh from mesh-grants.accounts and mints it as a want would: only while " +
"the controller's lease is handed away, never for a node's account or the controller's, never a user nobody " +
"wants, and never anew for one whose value the vault holds or whose hash it adopted (it says so). Answers " +
"hear a want, and what the controller's `module issue` forwards to (issue 774). Reads the wanted key afresh from " +
"mesh-grants.accounts and mints it as a want would, whoever holds the controller's lease: never for a node's " +
"account or the controller's, never a user nobody wants, and never anew for one whose value the vault holds or whose hash it adopted (it says so). Answers " +
"whether it minted, the secret and generation, never the value or the hash.",
Input: map[string]any{"user": str("the bus user, e.g. ace.mesh-build-go")},
run: func(caller string, a map[string]any) (any, error) {
@@ -633,8 +633,9 @@ func definitions(env *Env) []toolDef {
{Name: "vault_status",
Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " +
"key the mesh's export holds if the vault refused it, how often the record of hand-outs was rolled, and " +
"minting (novox/hq ADR 0561 §3, §9): whether the vault mints bus accounts now and why (only while the " +
"controller's lease is handed away), every wanted account it holds back with why, and those it minted.",
"minting (novox/hq ADR 0561 §9, issue 774): the vault mints every wanted bus account it holds no value for, " +
"whoever holds the controller's lease; whether that lease is handed away (it turns retirement and rotation), " +
"every wanted account it holds back with why (never one whose value it holds), and those it minted.",
run: func(string, map[string]any) (any, error) {
if env.Vault == nil {
return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-vault
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030225009-17fc9b9b8115
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
golang.org/x/crypto v0.57.0
+6 -2
View File
@@ -1,5 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030225009-17fc9b9b8115 h1:ezKg8aAKQoZ5XEAWQQVk4y/x0h2qC73GcUKBmbqY5jw=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030225009-17fc9b9b8115/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535 h1:Bgrm3YfWCxr+FEm03kEP5fDIe9hNbe+j0CDVatGAGKg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082933-085f17e935db h1:PJ1V9tDjNqp/aogm/r52RRePxcHyW58f7lYBceMMY9c=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082933-085f17e935db/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+2 -2
View File
@@ -112,12 +112,12 @@ func TestTheBussManifestMountsItsProgramsIntoTheUpstreamImage(t *testing.T) {
}
// The tools that change the bus (issue 1952 N1-N3): listed, their audit events declared, and the credential
// they dial with the module's own secret `broker`.
for _, want := range []string{"nats_consumer_remove", "nats_bucket_remove"} {
for _, want := range []string{"nats_consumer_remove", "nats_bucket_remove", "nats_stream_remove"} {
if !slices.Contains(m.Tools, want) {
t.Errorf("tools do not list %s", want)
}
}
for _, want := range []string{"consumer-removed", "bucket-keys-removed"} {
for _, want := range []string{"consumer-removed", "bucket-keys-removed", "stream-removed"} {
if !slices.Contains(m.Emits, want) {
t.Errorf("emits do not list %s: the removal tools say it as the audit line", want)
}
+94 -3
View File
@@ -1,7 +1,7 @@
package main
// The two tools of this module that change the bus (novox/hq ADR 0564 clause 5, issue 1952 notes 6, 7 and 9):
// remove one named consumer, and remove one node's keys under a prefix of a key-value bucket. Everything else
// The three tools of this module that change the bus (novox/hq ADR 0564 clause 5, issue 1952 notes 6, 7 and 9):
// remove one named consumer, remove one named stream, and remove one node's keys under a prefix of a key-value bucket. Everything else
// here reads.
//
// **Narrow by what the tool needs, and narrower by what the bus grants.** The tools reach the bus on the
@@ -34,6 +34,7 @@ type Said func(event string, body any) error
const (
consumerRemoved = "consumer-removed"
bucketKeysRemoved = "bucket-keys-removed"
streamRemoved = "stream-removed"
// maxKeysShown bounds the keys an answer lists; the count is always whole.
maxKeysShown = 200
)
@@ -120,9 +121,13 @@ func RemoveConsumer(ctx context.Context, bus Bus, said Said, a map[string]any) (
sctx, cancel = step(ctx)
_, err = js.Consumer(sctx, stream, consumer)
cancel()
// Only the bus saying it is not there is a removal; a read that failed otherwise confirms nothing.
if err == nil {
return nil, fmt.Errorf("consumer %s of %s is still there after the removal was accepted", consumer, stream)
}
if !errors.Is(err, jetstream.ErrConsumerNotFound) && !errors.Is(err, jetstream.ErrStreamNotFound) {
return nil, unanswered(err, fmt.Sprintf("the removal of consumer %s of %s was accepted but could not be confirmed", consumer, stream))
}
out := ConsumerRemoved{Stream: stream, Consumer: consumer, Why: w, Was: was, Removed: true, At: time.Now().UTC()}
if err := said(consumerRemoved, out); err != nil {
return nil, fmt.Errorf("consumer %s of %s WAS removed (%s) but the audit event could not be said: %w", consumer, stream, w, err)
@@ -227,7 +232,89 @@ func RemoveKeys(ctx context.Context, bus Bus, said Said, a map[string]any) (any,
return out, nil
}
// removalTools are the module's two tools that change the bus; every other tool here reads.
// StreamRemoved is what nats_stream_remove answers and says.
type StreamRemoved struct {
Stream string `json:"stream"`
Why string `json:"why"`
DryRun bool `json:"dry_run"`
Present bool `json:"present"`
Messages uint64 `json:"messages"`
Bytes uint64 `json:"bytes"`
Consumers int `json:"consumers"`
Removed bool `json:"removed"`
Said string `json:"said"`
At time.Time `json:"at"`
}
// RemoveStream removes one named stream whole, after reading it, and says so (the lease write's step for the stream
// ASSIGNMENTS, novox/hq research 072 row 33). **A stream already gone is no error**: the removal is the same act
// however often it is asked, and says it found nothing. A stream with a consumer is refused, naming how many: a reader
// is still attached, and that is for a person to look at before the stream goes.
func RemoveStream(ctx context.Context, bus Bus, said Said, a map[string]any) (any, error) {
w, err := why(a)
if err != nil {
return nil, err
}
stream := arg(a, "stream")
if !name.MatchString(stream) {
return nil, errors.New("stream is required, one stream's name (letters, digits, - and _): nothing was removed")
}
dry, _ := a["dry_run"].(bool)
js, closeBus, err := bus(ctx)
if err != nil {
return nil, err
}
defer closeBus()
out := StreamRemoved{Stream: stream, Why: w, DryRun: dry, At: time.Now().UTC()}
sctx, cancel := step(ctx)
s, err := js.Stream(sctx, stream)
cancel()
if errors.Is(err, jetstream.ErrStreamNotFound) {
out.Said = stream + " is not there: nothing to remove"
return out, nil
}
if err != nil {
return nil, unanswered(err, fmt.Sprintf("the stream %s could not be read, so it was not removed", stream))
}
sctx, cancel = step(ctx)
info, err := s.Info(sctx)
cancel()
if err != nil {
return nil, unanswered(err, fmt.Sprintf("the stream %s could not be read, so it was not removed", stream))
}
out.Present, out.Messages, out.Bytes, out.Consumers = true, info.State.Msgs, info.State.Bytes, info.State.Consumers
if info.State.Consumers > 0 {
return nil, fmt.Errorf("%s has %d consumers: a reader is attached, so it was not removed", stream, info.State.Consumers)
}
if dry {
out.Said = fmt.Sprintf("%s holds %d messages (%d bytes) and no consumer; it would be removed", stream, out.Messages, out.Bytes)
return out, nil
}
sctx, cancel = step(ctx)
err = js.DeleteStream(sctx, stream)
cancel()
if err != nil {
return nil, unanswered(err, fmt.Sprintf("stream %s was not removed", stream))
}
sctx, cancel = step(ctx)
_, err = js.Stream(sctx, stream)
cancel()
// Only the bus saying it is not there is a removal; a read that failed otherwise confirms nothing.
if err == nil {
return nil, fmt.Errorf("stream %s is still there after the removal was accepted", stream)
}
if !errors.Is(err, jetstream.ErrStreamNotFound) {
return nil, unanswered(err, fmt.Sprintf("the removal of stream %s was accepted but could not be confirmed", stream))
}
out.Removed = true
out.Said = fmt.Sprintf("%s removed: %d messages (%d bytes) and no consumer", stream, out.Messages, out.Bytes)
if err := said(streamRemoved, out); err != nil {
return nil, fmt.Errorf("stream %s WAS removed (%s) but the audit event could not be said: %w", stream, w, err)
}
return out, nil
}
// removalTools are the module's three tools that change the bus; every other tool here reads.
func removalTools(bus Bus, said Said) []stdio.Tool {
run := func(f func(context.Context, Bus, Said, map[string]any) (any, error)) func(map[string]any) (any, error) {
return func(a map[string]any) (any, error) { return f(context.Background(), bus, said, a) }
@@ -237,6 +324,10 @@ func removalTools(bus Bus, said Said) []stdio.Tool {
Description: "Removes (deletes) one named JetStream consumer of one stream, e.g. the consumer `controller` of DEAD_LETTER_NOTICES at the lease write (novox/hq ADR 0564 clause 5). Needs `why`, kept in the audit trail; refuses an unknown stream or consumer; reads the consumer first and answers what it was (filters, pending, unacknowledged); the stream and its other consumers are not touched. The bus refuses any consumer the module's credential is not granted.",
Input: map[string]any{"stream": str("the stream, e.g. DEAD_LETTER_NOTICES"), "consumer": str("the consumer's name, e.g. controller"), "why": str("why it is removed, for the audit trail (required)")},
Run: run(RemoveConsumer)},
{Name: "nats_stream_remove",
Description: "Removes (deletes) one named JetStream stream whole, e.g. ASSIGNMENTS at the lease write, beside the consumer of DEAD_LETTER_NOTICES (novox/hq research 072 row 33). Needs `why`, kept in the audit trail; reads the stream first and answers its messages, bytes and consumers; refuses a stream that still has a consumer; a stream already gone is answered as such and is no error, so asking again is safe. `dry_run` only reads. The bus refuses any stream the module's credential is not granted.",
Input: map[string]any{"stream": str("the stream, e.g. ASSIGNMENTS"), "why": str("why it is removed, for the audit trail (required)"), "dry_run": map[string]any{"type": "boolean", "description": "read the stream, remove nothing"}},
Run: run(RemoveStream)},
{Name: "nats_bucket_remove",
Description: "Removes (purges) keys from a key-value bucket: one node's keys (`node`, the keys starting `<node>.`) or the keys under a `prefix`, e.g. a full acts bucket of the node-engine. Needs `why`, kept in the audit trail; never the whole bucket. Counts the keys first and lists them (`dry_run` only counts; `expect` refuses when the count differs); removes only those keys, and their history. See the buckets and their keys with nats_buckets. The bus refuses any bucket the module's credential is not granted.",
Input: map[string]any{"bucket": str("the bucket as nats_buckets lists it, e.g. mesh-host_acts"), "node": str("a node whose keys (`<node>.`) go"), "prefix": str("key prefix instead of node"),
+35 -1
View File
@@ -31,6 +31,8 @@ accounts { MESH { jetstream: enabled, users: [
publish: { allow: [
"$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller",
"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller",
"$JS.API.STREAM.INFO.ASSIGNMENTS",
"$JS.API.STREAM.DELETE.ASSIGNMENTS",
"$JS.API.STREAM.INFO.KV_mesh-host_acts",
"$KV.mesh-host_acts.>" ] },
subscribe: { allow: ["_INBOX.nats-tools.>"] } } }
@@ -73,6 +75,14 @@ accounts { MESH { jetstream: enabled, users: [
if err != nil {
t.Fatal(err)
}
if _, err := ajs.CreateStream(ctx, jetstream.StreamConfig{Name: "ASSIGNMENTS", Subjects: []string{"mesh.assignment.*.*"}, MaxMsgsPerSubject: 1}); err != nil {
t.Fatal(err)
}
for _, sub := range []string{"mesh.assignment.g14.a", "mesh.assignment.ace.b"} {
if _, err := ajs.Publish(ctx, sub, []byte("{}")); err != nil {
t.Fatal(err)
}
}
other, _ := ajs.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "other_state"})
for _, k := range []string{"g14.1", "g14.2", "g14.sub.3", "g14x.4", "ace.5"} {
acts.Put(ctx, k, []byte("v"))
@@ -131,6 +141,30 @@ accounts { MESH { jetstream: enabled, users: [
t.Fatalf("a consumer already gone: %v", err)
}
// Research 072 row 33: the stream ASSIGNMENTS goes whole, once; read first, and asking again is no error; a stream
// outside the grant is not touched.
if _, err := RemoveStream(ctx, bus, say, map[string]any{"stream": "ASSIGNMENTS"}); err == nil || !strings.Contains(err.Error(), "why is required") {
t.Fatalf("a stream without why: %v", err)
}
if _, err := RemoveStream(ctx, bus, say, map[string]any{"stream": "DEAD_LETTER_NOTICES", "why": "x"}); err == nil || !strings.Contains(err.Error(), "grant") {
t.Fatalf("a stream outside the grant: %v", err)
}
if d, err := RemoveStream(ctx, bus, say, map[string]any{"stream": "ASSIGNMENTS", "why": "look", "dry_run": true}); err != nil || d.(StreamRemoved).Messages != 2 || d.(StreamRemoved).Removed {
t.Fatalf("a dry run: %+v %v", d, err)
}
if _, err := ajs.Stream(ctx, "ASSIGNMENTS"); err != nil {
t.Fatalf("a dry run removed the stream: %v", err)
}
if r, err := RemoveStream(ctx, bus, say, map[string]any{"stream": "ASSIGNMENTS", "why": "the lease write"}); err != nil || !r.(StreamRemoved).Removed || r.(StreamRemoved).Messages != 2 {
t.Fatalf("the removal: %+v %v", r, err)
}
if _, err := ajs.Stream(ctx, "ASSIGNMENTS"); err == nil {
t.Fatal("ASSIGNMENTS is still there")
}
if r, err := RemoveStream(ctx, bus, say, map[string]any{"stream": "ASSIGNMENTS", "why": "again"}); err != nil || r.(StreamRemoved).Removed || r.(StreamRemoved).Present {
t.Fatalf("a second removal: %+v %v", r, err)
}
// N3: only the node's keys go; counted first; refused without why, for a whole bucket, and outside the grant.
if _, err := RemoveKeys(ctx, bus, say, map[string]any{"bucket": "mesh-host_acts", "node": "g14"}); err == nil || !strings.Contains(err.Error(), "why is required") {
t.Fatalf("without why: %v", err)
@@ -167,7 +201,7 @@ accounts { MESH { jetstream: enabled, users: [
if k := keys(other); k != "g14.1" {
t.Fatalf("another bucket changed: %s", k)
}
if strings.Join(said, ",") != "consumer-removed,bucket-keys-removed" {
if strings.Join(said, ",") != "consumer-removed,stream-removed,bucket-keys-removed" {
t.Fatalf("the audit events said: %v", said)
}
}
@@ -234,6 +234,10 @@ func TestTheRemovalToolsRefuseBeforeTheyReachTheBus(t *testing.T) {
{"nats_bucket_remove", map[string]any{"bucket": "mesh-host_acts", "node": "g14", "prefix": "g", "why": "x"}, "exactly one of node"},
{"nats_bucket_remove", map[string]any{"bucket": "mesh-host_acts", "prefix": "g*", "why": "x"}, "without wildcards"},
{"nats_bucket_remove", map[string]any{"bucket": "", "node": "g14", "why": "x"}, "bucket is required"},
{"nats_stream_remove", map[string]any{"stream": "ASSIGNMENTS"}, "why is required"},
{"nats_stream_remove", map[string]any{"stream": "ASSIGNMENTS", "why": " "}, "why is required"},
{"nats_stream_remove", map[string]any{"stream": "A.>", "why": "x"}, "stream is required"},
{"nats_stream_remove", map[string]any{"stream": "", "why": "x"}, "stream is required"},
} {
var run func(map[string]any) (any, error)
for _, tl := range removalTools(bus, said) {
+1 -1
View File
@@ -3,7 +3,7 @@ module nats-tools
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a
github.com/klauspost/compress v1.20.0
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+6 -3
View File
@@ -30,7 +30,8 @@
],
"emits": [
"consumer-removed",
"bucket-keys-removed"
"bucket-keys-removed",
"stream-removed"
],
"consumes": [],
"listens": [
@@ -54,7 +55,8 @@
"nats_user_can",
"nats_snapshot_verify",
"nats_consumer_remove",
"nats_bucket_remove"
"nats_bucket_remove",
"nats_stream_remove"
],
"guards": [
8222
@@ -208,7 +210,8 @@
],
"bus": {
"removes": [
"DEAD_LETTER_NOTICES.controller"
"DEAD_LETTER_NOTICES.controller",
"ASSIGNMENTS"
]
},
"env": {