diff --git a/modules/mesh-config/cmd/mesh-config/delivery.go b/modules/mesh-config/cmd/mesh-config/delivery.go index 7e4171d4..2698bbe2 100644 --- a/modules/mesh-config/cmd/mesh-config/delivery.go +++ b/modules/mesh-config/cmd/mesh-config/delivery.go @@ -104,7 +104,7 @@ func notServed(err error) bool { } // toolVault asks the seat mesh-vault to retire a secret: the hand act that warrants it is recorded first, by this -// module, naming the node, module and secret; the vault reads it back by its id. +// module, naming the node, module, secret and the ask of the Approve; the vault reads it back by its id (`warrant`). type toolVault struct { ask func(key string, body any) (json.RawMessage, error) recorder handacts.Recorder @@ -112,7 +112,7 @@ type toolVault struct { // Retire records the warrant and calls the vault's verb. func (v toolVault) Retire(r RetireRequest) error { - taken, err := v.recorder.Record(handacts.Request{What: r.Node + " " + r.Module + " " + r.Name, + taken, err := v.recorder.Record(handacts.Request{What: r.Node + " " + r.Module + " " + r.Name + " " + r.Ask, Why: "the operator approved retiring " + r.Module + "'s secret " + r.Name + " on " + r.Node + " (ask " + r.Ask + ")", Cause: "the operator's Approve of ask " + r.Ask, Verb: "mesh-vault.retire"}) if err != nil { @@ -127,7 +127,6 @@ func (v toolVault) Retire(r RetireRequest) error { if json.Unmarshal(answer, &rec) != nil || rec.Act.ID == "" { return fmt.Errorf("mesh-hand-acts recorded the warrant and named no act") } - _, err = v.ask(VaultRetireAsk, map[string]any{"node": r.Node, "module": r.Module, "name": r.Name, "act": rec.Act.ID, - "ask": r.Ask}) + _, err = v.ask(VaultRetireAsk, map[string]any{"node": r.Node, "module": r.Module, "name": r.Name, "warrant": rec.Act.ID}) return err } diff --git a/modules/mesh-config/cmd/mesh-config/main.go b/modules/mesh-config/cmd/mesh-config/main.go index 40ad38d2..899b30c3 100644 --- a/modules/mesh-config/cmd/mesh-config/main.go +++ b/modules/mesh-config/cmd/mesh-config/main.go @@ -272,6 +272,23 @@ func hearingDelivery(c *Config) { } } +// seatRename is one open rename as the seat's definition answers it: module, from, to, at and the nodes holding it open. +type seatRename struct { + Module string `json:"module"` + From string `json:"from"` + To string `json:"to"` + At time.Time `json:"at"` + Open []string `json:"open"` +} + +func seatRenames(rows []RenameRow) []seatRename { + out := make([]seatRename, 0, len(rows)) + for _, r := range rows { + out = append(out, seatRename{Module: r.Module, From: r.From, To: r.To, At: r.At, Open: r.Open}) + } + return out +} + // Tools is the seat's verb and the module's own tools of the switch, as the runtime serves them. func Tools(c *Config) []stdio.Tool { renames := stdio.Tool{Name: Seat + "." + RenamesVerb, @@ -283,7 +300,7 @@ func Tools(c *Config) []stdio.Tool { Run: func(a map[string]any) (any, error) { m, _ := a["module"].(string) rows, err := c.Renames(strings.TrimSpace(m), true) - return map[string]any{"renames": rows}, err + return map[string]any{"renames": seatRenames(rows)}, err }} return append(switchTools(c), renames, stdio.Tool{ Name: Seat + "." + Verb, diff --git a/modules/mesh-config/cmd/mesh-config/renames.go b/modules/mesh-config/cmd/mesh-config/renames.go index b2e29f79..e82958fb 100644 --- a/modules/mesh-config/cmd/mesh-config/renames.go +++ b/modules/mesh-config/cmd/mesh-config/renames.go @@ -6,7 +6,7 @@ package main // (its Value, whole), and opens a **rename window** per node: until that node's node-engine has applied a build of the // module that reads the new name, the old name still answers with the new value (Resolve). A window closes on the // status view's `delivered` event and, for a node that was away, on the node-engine seat's `manifest-reads-changed` -// (Windows). Its state is `renames`, key `.`; what it says is `events.renamed` on the stream `config-events`. +// (Windows). Its state is `renames`, key `.`; what it says is `renamed` on the stream `config-events`. import ( "encoding/json" @@ -20,11 +20,11 @@ import ( // The state, the stream's events and the bounds this part keeps. const ( RenamesState = "renames" - // EventRenamed is `renamed`, EventRetiring `retiring` and EventRetired `retired`, the family `config.events` - // (design 26): each carried by the stream StreamName and kept seven days. - EventRenamed = "events.renamed" - EventRetiring = "events.retiring" - EventRetired = "events.retired" + // EventRenamed is `renamed`, EventRetiring `retiring` and EventRetired `retired`, the family `config.events` of the + // seat's definition (design 26), under the holder's subjects: each carried by the stream StreamName, kept seven days. + EventRenamed = "renamed" + EventRetiring = "retiring" + EventRetired = "retired" StreamName = "config-events" // HeldFinding is how long a window or a retirement may stay open before it is a finding naming the node. HeldFinding = 7 * 24 * time.Hour diff --git a/modules/mesh-config/cmd/mesh-config/whole_integration_test.go b/modules/mesh-config/cmd/mesh-config/whole_integration_test.go index 87a94ee7..ebad48c3 100644 --- a/modules/mesh-config/cmd/mesh-config/whole_integration_test.go +++ b/modules/mesh-config/cmd/mesh-config/whole_integration_test.go @@ -30,6 +30,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-contracts/go/seats/meshconfig" "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" @@ -343,6 +344,33 @@ func (p *player) event(key string, body any) { } } +// conforms decodes what the module published under key strictly into the seat definition's type (a field the type does +// not know is a fault here, though a reader drops it) and checks the subject the runtime publishes it on is the one the +// definition names: CAT-V4, the definition's fixtures matched by a real event. +func (p *player) conforms(key string, into any, subject func(holder string) string) { + p.t.Helper() + p.mu.Lock() + defer p.mu.Unlock() + found := false + for _, s := range p.said { + if s.Key != key { + continue + } + found = true + dec := json.NewDecoder(strings.NewReader(string(s.Body))) + dec.DisallowUnknownFields() + if err := dec.Decode(into); err != nil { + p.t.Errorf("the published %s %s does not match the seat's definition: %v", key, s.Body, err) + } + if got, want := "mesh.mod."+Module+".event."+s.Key, subject(Module); got != want { + p.t.Errorf("%s is published on %s, the definition names %s", key, got, want) + } + } + if !found { + p.t.Errorf("%s was never published", key) + } +} + func (p *player) publishedCount(key string) int { p.mu.Lock() defer p.mu.Unlock() @@ -533,6 +561,8 @@ func TestConfigWholeRenamesAndRetiresAgainstTheBus(t *testing.T) { }) eventually(t, "renamed is said", func() bool { return p.publishedCount(EventRenamed) == 1 }) + p.conforms(EventRenamed, &meshconfig.RenamedHolderEvent{}, meshconfig.HolderSubjectRenamed) + // A refused move writes its finding: dock declares both names. p.manifestIn("dock", dock) p.buildIn("g14", "dock", fp1, dock) @@ -569,6 +599,19 @@ func TestConfigWholeRenamesAndRetiresAgainstTheBus(t *testing.T) { t.Fatalf("the window after the restart: %s", raw) } + // The seat's verb answers the open window in the definition's shape (CAT-C10). + raw, err = p2.call(Seat+"."+RenamesVerb, map[string]any{"module": "player"}) + if err != nil { + t.Fatal(err) + } + var open meshconfig.RenamesAnswer + dec := json.NewDecoder(strings.NewReader(string(raw))) + dec.DisallowUnknownFields() + if err := dec.Decode(&open); err != nil || len(open.Renames) != 1 || open.Renames[0].To != "level" || + strings.Join(open.Renames[0].Open, ",") != "g14,novox" { + t.Fatalf("renames answered %s (%v), want player's level open on g14 and novox", raw, err) + } + // g14 applies the build that reads the new name; the delivery says so; its window closes, novox's stays. p2.buildIn("g14", "player", fp2, newPlayer) p2.mu.Lock() @@ -619,5 +662,7 @@ func TestConfigWholeRenamesAndRetiresAgainstTheBus(t *testing.T) { _, still := p2.get(ValuesState, "module.g14.radio.band") return !still && p2.publishedCount(EventRetired) == 1 }) + p2.conforms(EventRetiring, &meshconfig.RetiringHolderEvent{}, meshconfig.HolderSubjectRetiring) + p2.conforms(EventRetired, &meshconfig.RetiredHolderEvent{}, meshconfig.HolderSubjectRetired) eventually(t, "the retirement is dropped", func() bool { _, ok := p2.get(RetiringState, "radio.band"); return !ok }) } diff --git a/modules/mesh-config/go.mod b/modules/mesh-config/go.mod index 504c44dd..aae59185 100644 --- a/modules/mesh-config/go.mod +++ b/modules/mesh-config/go.mod @@ -3,6 +3,7 @@ module mesh-config go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-config/module.json b/modules/mesh-config/module.json index ae37f8d2..ee35885e 100644 --- a/modules/mesh-config/module.json +++ b/modules/mesh-config/module.json @@ -14,9 +14,9 @@ ], "emits": [ "config-changed", - "events.renamed", - "events.retiring", - "events.retired" + "renamed", + "retiring", + "retired" ], "uses": [ "operator-channel" @@ -110,7 +110,9 @@ { "name": "config-events", "events": [ - "events.**" + "renamed", + "retiring", + "retired" ], "max-age-days": 7 } diff --git a/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go b/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go index aab751af..3eda041d 100644 --- a/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/enrolment_tools.go @@ -132,17 +132,17 @@ func enrolmentTools(env *Env) []toolDef { Description: "Retire one module's own secret on a node because no manifest reads it any more (design 32): removed " + "whole, and own-retired said (node, module, name, the ask, the caller; never a value). Answered to the holder of " + "the seat mesh-config alone, as the vault's list retire-callers names it, and only for a hand act it recorded " + - "through mesh-hand-acts for this verb naming exactly this node, module and name (`act`), read back by its id.", + "through mesh-hand-acts for this verb naming exactly this node, module and name and the ask of the operator's Approve " + + "(`warrant`, the act's id), read back here.", Input: map[string]any{"node": node, "module": str("the module whose secret it is"), "name": str("the secret's name"), - "act": str("the id of the hand act recorded through mesh-hand-acts that warrants it"), - "ask": str("the id of the operator's ask whose Approve this answers, kept in what is said")}, + "warrant": str("the operator's warrant for this node, module and name: the id of the hand act recorded through mesh-hand-acts")}, run: func(caller string, a map[string]any) (any, error) { v, err := env.ready() if err != nil { return nil, err } return v.RetireOwn(caller, RetireArgs{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), - Act: arg(a, "act"), Ask: arg(a, "ask")}) + Warrant: arg(a, "warrant")}) }}, {Name: "node_logins", Description: "Every node's key of the vault's state node-logins: the login its node-engine dials as and, while a rotation " + diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go index f5ee911e..024f0b31 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go @@ -31,6 +31,7 @@ import ( "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/nacl/box" + "git.novox.be/novox/mesh-contracts/go/seats/meshvault" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/seal" "git.novox.be/novox/mesh-sdk/go/testbus" @@ -300,6 +301,16 @@ func (p *player) hashOf(t *testing.T, user string) *AccountHash { return &a } +// strictly decodes raw into v, a field the type does not know being a fault (the definition's type is the contract). +func strictly(t *testing.T, raw []byte, v any) { + t.Helper() + dec := json.NewDecoder(strings.NewReader(string(raw))) + dec.DisallowUnknownFields() + if err := dec.Decode(v); err != nil { + t.Errorf("%s does not match the seat's definition: %v", raw, err) + } +} + func eventually(t *testing.T, what string, ok func() bool) { t.Helper() for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(50 * time.Millisecond) { @@ -385,8 +396,8 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) { return nil, errors.New("nats: no responders available for request") } return map[string]any{"acts": []HandActSeen{ - {ID: "act-ok", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst token"}}, - {ID: "act-other", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst other"}}}}, nil + {ID: "act-ok", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst token ret-1"}}, + {ID: "act-other", By: "novox.mesh-config", At: time.Now(), Verb: "mesh-vault.retire", Args: []string{"g14 dunst other ret-1"}}}}, nil } go p.serve(stdout) @@ -454,21 +465,35 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) { t.Fatal(err) } for _, c := range []struct{ caller, act string }{{"g14.claude-code", "act-ok"}, {"novox.mesh-config", "act-other"}, {"novox.mesh-config", ""}} { - if _, err := p.call(Seat+".retire", c.caller, map[string]any{"node": "g14", "module": "dunst", "name": "token", "act": c.act}); err == nil { + if _, err := p.call(Seat+".retire", c.caller, map[string]any{"node": "g14", "module": "dunst", "name": "token", "warrant": c.act}); err == nil { t.Errorf("%s retired the token with act %q", c.caller, c.act) } } if m, err := waiter.NextMsg(300 * time.Millisecond); err == nil { t.Fatalf("a refused retire said %s", m.Subject) } - if _, err := p.call(Seat+".retire", "novox.mesh-config", map[string]any{"node": "g14", "module": "dunst", "name": "token", "act": "act-ok", - "ask": "ret-1"}); err != nil { + // The call is the definition's input, the answer its answer, and the event its body on its subject (CAT-V4). + input, _ := json.Marshal(meshvault.RetireInput{Node: "g14", Module: "dunst", Name: "token", Warrant: "act-ok"}) + var args map[string]any + _ = json.Unmarshal(input, &args) + raw, err = p.call(Seat+"."+meshvault.VerbRetire, "novox.mesh-config", args) + if err != nil { t.Fatalf("retire with its warrant: %v", err) } + var answered meshvault.RetireAnswer + strictly(t, raw, &answered) + if !answered.Retired { + t.Errorf("retire answered %s", raw) + } m, err := waiter.NextMsg(10 * time.Second) - if err != nil || m.Subject != "mesh.mod.mesh-vault.event."+OwnRetiredEvent || !strings.Contains(string(m.Data), `"ask":"ret-1"`) { + if err != nil || m.Subject != meshvault.HolderSubjectOwnRetired("mesh-vault") { t.Fatalf("retire said %v, %v", m, err) } + var event meshvault.OwnRetiredHolderEvent + strictly(t, m.Data, &event) + if event.Ask != "ret-1" || event.Caller != "novox.mesh-config" || event.Node != "g14" || event.Module != "dunst" || event.Name != "token" { + t.Errorf("own-retired said %s", m.Data) + } for _, other := range []string{"rotated", OwnReplacedEvent} { if extra, err := waiter.NextMsg(300 * time.Millisecond); err == nil { t.Errorf("a waiter on %s heard %s", other, extra.Subject) diff --git a/modules/mesh-vault/cmd/mesh-vault/ownretire.go b/modules/mesh-vault/cmd/mesh-vault/ownretire.go index ec76c185..ce265be6 100644 --- a/modules/mesh-vault/cmd/mesh-vault/ownretire.go +++ b/modules/mesh-vault/cmd/mesh-vault/ownretire.go @@ -7,8 +7,9 @@ package main // - the caller is on the vault's list `retire-callers`, a trusted config value of the vault (ADR 0363's form, as // mesh-mail-reader's `callers`), which names the holder of mesh-config alone, checked against the caller the tool // runner proved (stdio.Callers); and -// - the warrant: a hand act the caller recorded through mesh-hand-acts for this verb, read back here by its id, recent, -// naming exactly this node, module and secret (ADR 0259 §6). An act for another name warrants nothing. +// - the warrant (the verb's argument, as the seat's definition names it): the id of a hand act the caller recorded +// through mesh-hand-acts for this verb, read back here, recent, naming exactly this node, module and secret and the +// ask whose Approve it answers (ADR 0259 §6). An act for another name warrants nothing. // // The vault then removes the record whole and says `own-retired` (node, module, name, the ask's id, the caller it proved) // beside `rotated` and `own-replaced`, under its own subject: a waiter on either does not wake on it. Never a value. @@ -26,21 +27,24 @@ const OwnRetiredEvent = "own-retired" // RetireVerb is the seat's verb, and the verb of the hand act that warrants it (`mesh-vault.retire`). const RetireVerb = "retire" -// RetireArgs are `retire`'s arguments: the secret by node, module and name, the hand act that warrants it, and the ask of -// the operator's Approve it answers (kept in what is said). +// RetireArgs are `retire`'s arguments, as the seat's definition names them: the secret by node, module and name, and the +// warrant, the id of the hand act that warrants it. type RetireArgs struct { - Node, Module, Name, Act, Ask string + Node, Module, Name, Warrant string } -// OwnRetired is what one `retire` did. Never a value. -type OwnRetired struct { - Secret string `json:"secret"` +// OwnRetiredBody is the body of own-retired, exactly the seat definition's (node, module, name, ask, caller). +type OwnRetiredBody struct { Node string `json:"node"` Module string `json:"module"` Name string `json:"name"` - Ask string `json:"ask,omitempty"` + Ask string `json:"ask"` Caller string `json:"caller"` - Act string `json:"act"` +} + +// OwnRetired is what one `retire` answers, the seat definition's answer. +type OwnRetired struct { + Retired bool `json:"retired"` } // fromRetireCallers refuses every caller the vault's list `retire-callers` does not name. @@ -55,38 +59,39 @@ func (v *Vault) fromRetireCallers(caller string) error { return nil } -// warrantsRetire reads the hand act back and refuses what it does not warrant for exactly this secret. -func (v *Vault) warrantsRetire(caller string, a RetireArgs) error { - if strings.TrimSpace(a.Act) == "" { - return fmt.Errorf("retire carries the hand act that warrants it (`act`): record it through mesh-hand-acts first (design 32)") +// warrantsRetire reads the hand act back and refuses what it does not warrant for exactly this secret; it answers the +// ask the act names. +func (v *Vault) warrantsRetire(caller string, a RetireArgs) (ask string, err error) { + if strings.TrimSpace(a.Warrant) == "" { + return "", fmt.Errorf("retire carries the hand act that warrants it (`warrant`): record it through mesh-hand-acts first (design 32)") } if v.ReadAct == nil { - return fmt.Errorf("the vault reads no hand acts, so it retires nothing") + return "", fmt.Errorf("the vault reads no hand acts, so it retires nothing") } - act, err := v.ReadAct(a.Act) + act, err := v.ReadAct(a.Warrant) if err != nil { - return fmt.Errorf("the hand act %s could not be read back from mesh-hand-acts: %w", a.Act, err) + return "", fmt.Errorf("the hand act %s could not be read back from mesh-hand-acts: %w", a.Warrant, err) } switch { case act == nil: - return fmt.Errorf("mesh-hand-acts holds no hand act %s: record one first", a.Act) + return "", fmt.Errorf("mesh-hand-acts holds no hand act %s: record one first", a.Warrant) case act.By != caller: - return fmt.Errorf("the hand act %s was recorded by %s, not by %s who asks", a.Act, act.By, caller) + return "", fmt.Errorf("the hand act %s was recorded by %s, not by %s who asks", a.Warrant, act.By, caller) case v.Now().Sub(act.At) > HandActWithin: - return fmt.Errorf("the hand act %s is older than %s: record a new one", a.Act, HandActWithin) + return "", fmt.Errorf("the hand act %s is older than %s: record a new one", a.Warrant, HandActWithin) case act.Verb != Seat+"."+RetireVerb: - return fmt.Errorf("the hand act %s is %q, not %s.%s: it warrants no retirement", a.Act, act.Verb, Seat, RetireVerb) + return "", fmt.Errorf("the hand act %s is %q, not %s.%s: it warrants no retirement", a.Warrant, act.Verb, Seat, RetireVerb) } var named []string for _, arg := range act.Args { named = append(named, strings.Fields(arg)...) } - // The warrant is bound to the node, module and name it was asked for: all three, and no others. - if len(named) != 3 || named[0] != a.Node || named[1] != a.Module || named[2] != a.Name { - return fmt.Errorf("the hand act %s names %q, not %s %s %s: a warrant retires nothing else", - a.Act, strings.Join(named, " "), a.Node, a.Module, a.Name) + // The warrant is bound to the node, module and name it was asked for, and names the ask: those four words, no others. + if len(named) != 4 || named[0] != a.Node || named[1] != a.Module || named[2] != a.Name { + return "", fmt.Errorf("the hand act %s names %q, not %s %s %s and its ask: a warrant retires nothing else", + a.Warrant, strings.Join(named, " "), a.Node, a.Module, a.Name) } - return nil + return named[3], nil } // RetireOwn removes one module's own secret on a node on the warrant of a hand act, and says `own-retired`. @@ -98,7 +103,8 @@ func (v *Vault) RetireOwn(caller string, a RetireArgs) (OwnRetired, error) { if err != nil { return OwnRetired{}, err } - if err := v.warrantsRetire(caller, a); err != nil { + ask, err := v.warrantsRetire(caller, a) + if err != nil { return OwnRetired{}, err } switch { @@ -115,10 +121,10 @@ func (v *Vault) RetireOwn(caller string, a RetireArgs) (OwnRetired, error) { if err := v.Store.Delete(secret); err != nil { return OwnRetired{}, fmt.Errorf("%s is not retired: %w", secret, err) } - out := OwnRetired{Secret: secret, Node: a.Node, Module: a.Module, Name: a.Name, Ask: a.Ask, Caller: caller, Act: a.Act} - v.log("retired %s on the warrant of the hand act %s, asked by %s (ask %s)", secret, a.Act, caller, a.Ask) + out := OwnRetired{Retired: true} + v.log("retired %s on the warrant of the hand act %s, asked by %s (ask %s)", secret, a.Warrant, caller, ask) if v.Emit != nil { - body := map[string]any{"secret": secret, "node": a.Node, "module": a.Module, "name": a.Name, "ask": a.Ask, "caller": caller} + body := map[string]any{"node": a.Node, "module": a.Module, "name": a.Name, "ask": ask, "caller": caller} if err := v.Emit(OwnRetiredEvent, body); err != nil { return out, fmt.Errorf("%s is retired, but own-retired was not said: %w", secret, err) } diff --git a/modules/mesh-vault/cmd/mesh-vault/retire_test.go b/modules/mesh-vault/cmd/mesh-vault/retire_test.go index 29ce7262..7dd42b7d 100644 --- a/modules/mesh-vault/cmd/mesh-vault/retire_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/retire_test.go @@ -96,13 +96,14 @@ func TestRetireRemovesAnOwnSecretOnlyOnTheWarrantOfTheListedCaller(t *testing.T) act := func(id, by, verb, what string, age time.Duration) { acts[id] = &HandActSeen{ID: id, By: by, At: v.Now().Add(-age), Verb: verb, Args: []string{what}} } - act("ok", caller, "mesh-vault.retire", "g14 dunst token", time.Minute) - act("other-name", caller, "mesh-vault.retire", "g14 dunst other", time.Minute) - act("other-node", caller, "mesh-vault.retire", "ace dunst token", time.Minute) - act("other-caller", "g14.claude-code", "mesh-vault.retire", "g14 dunst token", time.Minute) - act("other-verb", caller, "mesh-vault.rotate", "g14 dunst token", time.Minute) - act("ledgers", caller, "mesh-vault.retire", "g14 node-tools ledgers", time.Minute) - act("old", caller, "mesh-vault.retire", "g14 dunst token", 25*time.Hour) + act("ok", caller, "mesh-vault.retire", "g14 dunst token ret-1", time.Minute) + act("other-name", caller, "mesh-vault.retire", "g14 dunst other ret-1", time.Minute) + act("other-node", caller, "mesh-vault.retire", "ace dunst token ret-1", time.Minute) + act("other-caller", "g14.claude-code", "mesh-vault.retire", "g14 dunst token ret-1", time.Minute) + act("other-verb", caller, "mesh-vault.rotate", "g14 dunst token ret-1", time.Minute) + act("ledgers", caller, "mesh-vault.retire", "g14 node-tools ledgers ret-1", time.Minute) + act("no-ask", caller, "mesh-vault.retire", "g14 dunst token", time.Minute) + act("old", caller, "mesh-vault.retire", "g14 dunst token ret-1", 25*time.Hour) const secret = "own/g14/dunst/token" for _, name := range []string{secret, "own/g14/dunst/other", "own/g14/node-tools/ledgers"} { if _, err := v.keep(name, "a-value-of-"+name, "the test", "g14.dunst", OriginMade); err != nil { @@ -115,15 +116,16 @@ func TestRetireRemovesAnOwnSecretOnlyOnTheWarrantOfTheListedCaller(t *testing.T) want string }{ {"no act", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token"}, "carries the hand act"}, - {"an act that does not exist", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "nope"}, "holds no hand act"}, - {"an act for another name", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-name"}, "a warrant retires nothing else"}, - {"an act for another node", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-node"}, "a warrant retires nothing else"}, - {"an act of another caller", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-caller"}, "was recorded by g14.claude-code"}, - {"an act for another verb", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-verb"}, "warrants no retirement"}, - {"an act older than a day", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "old"}, "older than"}, - {"a caller not on the list", "g14.claude-code", RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "other-caller"}, "retire-callers"}, - {"an unproven caller", stdio.Unproven, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "ok"}, "retire-callers"}, - {"the tool runner's ledgers", caller, RetireArgs{Node: "g14", Module: "node-tools", Name: "ledgers", Act: "ledgers"}, "bus account or the tool runner's ledgers"}, + {"an act that does not exist", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "nope"}, "holds no hand act"}, + {"an act for another name", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-name"}, "a warrant retires nothing else"}, + {"an act for another node", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-node"}, "a warrant retires nothing else"}, + {"an act of another caller", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-caller"}, "was recorded by g14.claude-code"}, + {"an act for another verb", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-verb"}, "warrants no retirement"}, + {"an act that names no ask", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "no-ask"}, "a warrant retires nothing else"}, + {"an act older than a day", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "old"}, "older than"}, + {"a caller not on the list", "g14.claude-code", RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-caller"}, "retire-callers"}, + {"an unproven caller", stdio.Unproven, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok"}, "retire-callers"}, + {"the tool runner's ledgers", caller, RetireArgs{Node: "g14", Module: "node-tools", Name: "ledgers", Warrant: "ledgers"}, "bus account or the tool runner's ledgers"}, } { t.Run(tc.name, func(t *testing.T) { before := len(v.bus.events) @@ -139,8 +141,8 @@ func TestRetireRemovesAnOwnSecretOnlyOnTheWarrantOfTheListedCaller(t *testing.T) }) } before := len(v.bus.events) - got, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "ok", Ask: "ret-1"}) - if err != nil || got.Secret != secret || got.Caller != caller { + got, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok"}) + if err != nil || !got.Retired { t.Fatalf("retire answered %+v, %v", got, err) } if _, held, _ := v.Store.Read(secret); held { @@ -152,7 +154,7 @@ func TestRetireRemovesAnOwnSecretOnlyOnTheWarrantOfTheListedCaller(t *testing.T) if n := eventsNamed(v, before, OwnRetiredEvent); n != 1 || len(v.bus.events)-before != 1 { t.Fatalf("said %d own-retired among %d events, want one and no other", n, len(v.bus.events)-before) } - if body, _ := v.bus.events[before]["body"].(map[string]any); body["ask"] != "ret-1" || body["caller"] != caller || body["node"] != "g14" || + if body, _ := v.bus.events[before]["body"].(map[string]any); len(body) != 5 || body["ask"] != "ret-1" || body["caller"] != caller || body["node"] != "g14" || body["module"] != "dunst" || body["name"] != "token" { t.Errorf("own-retired said %+v", body) } @@ -160,7 +162,7 @@ func TestRetireRemovesAnOwnSecretOnlyOnTheWarrantOfTheListedCaller(t *testing.T) t.Error("own-retired shares a name with an event a waiter hears") } // Asked again for what is gone: nothing to retire. - if _, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Act: "ok"}); err == nil || + if _, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok"}); err == nil || !strings.Contains(err.Error(), "nothing to retire") { t.Errorf("a second retire answered %v", err) } diff --git a/modules/mesh-vault/go.mod b/modules/mesh-vault/go.mod index 55f6b0da..86199626 100644 --- a/modules/mesh-vault/go.mod +++ b/modules/mesh-vault/go.mod @@ -3,6 +3,7 @@ module mesh-vault go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103174208-b7abe203a929 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0