From 47ed384d9722231f88c0a97d0a6c372b180eb6f8 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 09:37:54 +0100 Subject: [PATCH 1/6] The vault takes the value that runs on a machine over the one it was first handed, from that machine alone (issue 1126, cause C4) move-in never replaces a value the vault holds, so where the controller made an own secret anew after the move, the new value runs while the vault keeps the first. The verb own-take-running takes it from the node-engine of the machine it is for, keeps the replaced value as the previous generation, raises the generation and says own-replaced without a value; own_drift lists every own secret a node's shadow answer says another value of, mesh-wide. The module needs the mesh-sdk head of fix/1126-seed-same-own-resync (delivery.OwnValuesDiffer, the seat's verb). --- modules/mesh-vault/README.md | 16 + modules/mesh-vault/cmd/mesh-vault/intake.go | 4 +- modules/mesh-vault/cmd/mesh-vault/main.go | 3 +- .../mesh-vault/cmd/mesh-vault/move_test.go | 117 +++++++ modules/mesh-vault/cmd/mesh-vault/owntake.go | 326 ++++++++++++++++++ modules/mesh-vault/cmd/mesh-vault/store.go | 14 + modules/mesh-vault/cmd/mesh-vault/tools.go | 40 +++ modules/mesh-vault/cmd/mesh-vault/vault.go | 35 +- .../mesh-vault/cmd/mesh-vault/vault_test.go | 3 +- modules/mesh-vault/module.json | 9 +- 10 files changed, 560 insertions(+), 7 deletions(-) create mode 100644 modules/mesh-vault/cmd/mesh-vault/owntake.go diff --git a/modules/mesh-vault/README.md b/modules/mesh-vault/README.md index 429b861a..a64d59c1 100644 --- a/modules/mesh-vault/README.md +++ b/modules/mesh-vault/README.md @@ -206,10 +206,26 @@ account (and, until R1, the controller) and no other caller, and only sealed to answers the controller alone today; it must also answer `.mesh-vault` when it moves with the media catalogue, after release 57. +## Taking what runs: `own-take-running` (issue 1126, cause C4) + +`move-in` never replaces a value the vault holds, so where the controller made an own secret anew after the move (a +rotation at start, a re-issue), the vault kept the first value while the new one runs. The node-engine of the machine +the secret is for hands the vault what runs, in an intake's two steps (`owntake.go`): module, name and why, then the +value sealed to the call's key. Its own machine's secrets alone (the machine is the caller's, as the bus proved it); a +value the vault holds, never a person's (`accept`) or a provider's software's (`issued`, but for a covered bus +account); never over a value the vault changed in the last 15 minutes. The same value changes nothing and says so. +The replaced value is kept sealed as the record's `previous` (dropped by any later change), the generation is raised, +`secret-changed` and `own-replaced` are said, and the act is in `secret_handed` (`own-replaced`, `own-same`, +`own-take-refused`). A covered bus account so replaced has its hash written again. `own_drift` lists, from every +node's latest shadow answer (`.node` in `node-engine.have`), each own secret whose node says another value, +with what the vault holds and whether the node's ask would be taken now. + ## Events - `secret-changed` — `name`, `by`, `at`, `generation`: which secret, who changed it, when. Never a value or a sealed copy. +- `own-replaced` — `secret`, `node`, `module`, `name`, `from_generation`, `to_generation`, `by`, `why`, `at`: + an own secret replaced by what runs on its machine. Never a value or a fingerprint. - `provisioned`, `rotated` — the custody ledger's, by fingerprint, as before. - `provisioner.retirement` (ADR 0230 §35) — `waiting`, `settled`, `approved`, `rejected`, `retired`, `reenabled`, `deleted`, naming holders, never a value. diff --git a/modules/mesh-vault/cmd/mesh-vault/intake.go b/modules/mesh-vault/cmd/mesh-vault/intake.go index 33e91639..be8012d3 100644 --- a/modules/mesh-vault/cmd/mesh-vault/intake.go +++ b/modules/mesh-vault/cmd/mesh-vault/intake.go @@ -91,7 +91,9 @@ type callKey struct { // ask is the ask's record for `secret-ask`, which the call's id is. ask *AskRecord // move is what `move-in` takes: the row of the controller's store (move.go). - move *MoveOf + move *MoveOf + // why is what `own-take-running` was asked for, in the caller's words (owntake.go). + why string public [32]byte private [32]byte until time.Time diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index c74e0449..e78a7ff7 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -51,7 +51,8 @@ func build() *Env { // Where it sees a consumer bind a provider (pair.go): the same mirror, every key. env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: readAssignment, Store: store}, Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask, - Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave} + Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, + HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }} return env } diff --git a/modules/mesh-vault/cmd/mesh-vault/move_test.go b/modules/mesh-vault/cmd/mesh-vault/move_test.go index d2e41c1b..5e41e511 100644 --- a/modules/mesh-vault/cmd/mesh-vault/move_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/move_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/seal" "golang.org/x/crypto/nacl/box" ) @@ -319,3 +321,118 @@ func TestMoveInThroughTheTool(t *testing.T) { t.Fatalf("asks through the tool: %+v, %v", moved, err) } } + +// Issue 1126, cause C4: a machine's node-engine hands the vault what runs of its own machine's own secret, where the +// vault holds another. Refused for another machine's secret, one that is not an own secret or not held, a person's or a +// provider's software's value, an empty value, and one the vault changed in the last 15 minutes; the same value changes +// nothing. A replacement keeps the old value as the previous generation, raises the generation, and says own-replaced +// with both generations and never a value; own_drift lists it from the node's shadow answer. +func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { + v := newVault(t, nil) + tick, later := v.Now, time.Duration(0) + v.Now = func() time.Time { return tick().Add(later) } + old, running := "the-value-moved-in-first", "the-value-the-controller-made-at-start" + if _, err := moveIn(t, v, "node.anchor", superuser(), old); err != nil { + t.Fatal(err) + } + for _, origin := range []string{"accepted", "issued"} { + m := superuser() + m.Name, m.Origin = "from-"+origin, origin + if _, err := moveIn(t, v, "node.anchor", m, "value-"+origin); err != nil { + t.Fatal(err) + } + } + later = time.Hour + take := func(caller string, of OwnTakeOf, value string) (OwnReplaced, error) { + opened, err := v.OwnTakeOpen(caller, of) + if err != nil { + return OwnReplaced{}, err + } + return v.OwnTakeTake(caller, opened.Call, sealToCall(t, opened.SealTo, value)) + } + pg := OwnTakeOf{Module: "postgres", Name: "superuser-password", Why: "the shadow answer says another value"} + for name, row := range map[string]struct { + caller string + of OwnTakeOf + value string + says string + }{ + "another machine's, named": {"node.shanks", OwnTakeOf{Node: "anchor", Module: "postgres", Name: "superuser-password"}, running, "anchor's"}, + "another machine's, whole name": {"node.shanks", OwnTakeOf{Secret: "own/anchor/postgres/superuser-password"}, running, "anchor's"}, + "not a node-engine": {ControllerCaller, pg, running, "not a node-engine"}, + "a module's tools": {"anchor.node-tools", pg, running, "not a node-engine"}, + "not an own secret": {"node.anchor", OwnTakeOf{Secret: "pair/anchor/database/shanks/gitea"}, running, "not a module's own secret"}, + "not held": {"node.anchor", OwnTakeOf{Module: "postgres", Name: "never-made"}, running, "nothing to replace"}, + "a person's value": {"node.anchor", OwnTakeOf{Module: "postgres", Name: "from-accepted"}, running, "by a person"}, + "a provider's software's value": {"node.anchor", OwnTakeOf{Module: "postgres", Name: "from-issued"}, running, "issued by its provider"}, + "an empty value": {"node.anchor", pg, " ", "empty"}, + } { + if _, err := take(row.caller, row.of, row.value); err == nil || !strings.Contains(err.Error(), row.says) { + t.Errorf("%s: %v, want refused saying %q", name, err, row.says) + } + } + if rec, _, _ := v.Store.Read("own/anchor/postgres/superuser-password"); rec.Generation != 1 || rec.Previous != nil { + t.Fatalf("a refusal changed the secret: generation %d, previous %+v", rec.Generation, rec.Previous) + } + // The value the vault holds already changes nothing, and says so. + if same, err := take("node.anchor", pg, old); err != nil || !same.Unchanged || same.To != 1 { + t.Errorf("the same value: %+v %v", same, err) + } + // What runs replaces it: one generation more, the old kept as the previous, origin kept, its fingerprint answered. + got, err := take("node.anchor", pg, running) + if err != nil { + t.Fatal(err) + } + rec, _, _ := v.Store.Read("own/anchor/postgres/superuser-password") + if got.From != 1 || got.To != 2 || got.Unchanged || got.Fingerprint != Fingerprint(running) || rec.Generation != 2 || + rec.Origin != OriginMade || rec.ChangedBy != "node.anchor" || rec.Previous == nil || rec.Previous.Generation != 1 { + t.Fatalf("replaced %+v, the record at generation %d origin %q by %s, previous %+v", got, rec.Generation, rec.Origin, + rec.ChangedBy, rec.Previous) + } + if now, _ := seal.Open(rec.AtRest, v.Own.PrivateKey); now != running { + t.Error("the vault does not hold what runs") + } + if was, _ := seal.Open(rec.Previous.AtRest, v.Own.PrivateKey); was != old || + openOperator(t, v.operatorPublic, v.operatorPrivate, rec.Previous.Operator.Sealed) != old { + t.Error("the previous value is not kept, for the vault and the operator") + } + // Said once, with both generations and why, never a value. + var replaced []map[string]any + for _, e := range v.bus.events { + if e["event"] == OwnReplacedEvent { + replaced = append(replaced, e["body"].(map[string]any)) + } + } + if len(replaced) != 1 || replaced[0]["secret"] != "own/anchor/postgres/superuser-password" || + replaced[0]["from_generation"] != 1 || replaced[0]["to_generation"] != 2 || replaced[0]["node"] != "anchor" || + replaced[0]["why"] != pg.Why { + t.Errorf("own-replaced said %v", replaced) + } + if all := everything(t, v, got); strings.Contains(all, running) || strings.Contains(all, old) { + t.Fatal("a value is in something the vault said, answered or wrote in the clear") + } + // Taken again at once, over the value the vault just took: refused. + if _, err := take("node.anchor", pg, "a-value-older-than-the-vaults"); err == nil || !strings.Contains(err.Error(), "less than") { + t.Errorf("a running value over one the vault just took: %v", err) + } + // own_drift reads it from the node's latest shadow answer, mesh-wide. + answer := delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{ + Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "needs-superuser-password", + Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}} + have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": answer}, + Composes: delivery.ComposesController}) + if err != nil { + t.Fatal(err) + } + v.HaveKeys = func() ([]string, error) { return []string{"anchor.node", "anchor.postgres"}, nil } + v.Haves = func(key string) ([]byte, error) { return have, nil } + drift, err := v.OwnDrift() + if err != nil { + t.Fatal(err) + } + if len(drift.Secrets) != 1 || drift.Secrets[0].Secret != "own/anchor/postgres/superuser-password" || + drift.Secrets[0].Generation != 2 || drift.Secrets[0].Previous != 1 || drift.Secrets[0].Takeable || + len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["g"] != 1 || drift.Nodes[0].Composes != delivery.ComposesController { + t.Errorf("own_drift: %+v", drift) + } +} diff --git a/modules/mesh-vault/cmd/mesh-vault/owntake.go b/modules/mesh-vault/cmd/mesh-vault/owntake.go new file mode 100644 index 00000000..c05ab440 --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/owntake.go @@ -0,0 +1,326 @@ +package main + +// The vault takes what runs (novox/hq issue 1126, cause C4; ADR 0550 §5: a module's own secrets are the vault's). +// +// `move-in` never replaces a value the vault holds (move.go), so its first import of a machine's own secret is fixed. +// Where the controller later made that secret anew (a rotation at start, a re-issue) without handing it here, the new +// value is what runs and what its consumers hold, and the vault keeps the old one. Only the machine the value is sealed +// to can open what runs, so its node-engine hands it in, with the verb `own-take-running`, in the two steps of every +// intake (intake.go): name the module and the secret and why, and get a call and a key made for it; then send the value +// sealed to that key. +// +// **From the node-engine of the machine the secret is for, about its own machine alone**: the machine is the caller's, +// as the bus proved it (`node.`), never an argument; a machine named that is not the caller's is refused, and so +// is a name that is not an own secret. **Only a value the vault holds is replaced**, and never a person's (accept) or a +// provider's software's (issued) but for a covered bus account, nor one the vault changed less than RecentlyChanged ago: +// a running value is older than a value the vault just took (a re-issue the machine has not applied yet), and is never +// put back over it. The same value again changes nothing and says so; an empty one is refused. +// +// The value replaced is kept as the record's previous generation (Record.Previous), the generation is raised, +// `secret-changed` and `own-replaced` are said (the secret, the machine, both generations and why; never a value), and the +// act is written in the record of hand-outs (secret_handed). A covered bus account so replaced has its hash written again +// (accounts.go), so the bus-accounts key is of the value the bus runs. + +import ( + "errors" + "fmt" + "sort" + "strings" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// OwnTakeRunningVerb is the seat verb a machine's node-engine hands the vault a running own secret with. +const OwnTakeRunningVerb = "own-take-running" + +// OwnReplacedEvent is what the vault says when it replaced an own secret with what runs: never a value. +const OwnReplacedEvent = "own-replaced" + +// RecentlyChanged is how long after the vault changed a secret a running value is not taken over it. +var RecentlyChanged = 15 * time.Minute + +// OwnTakeOf names the secret of one `own-take-running`: Module and Name (or Secret whole), Node only to be checked +// against the caller's machine, and Why in the caller's words. +type OwnTakeOf struct { + Node, Module, Name, Secret, Why string +} + +// OwnReplaced is what one `own-take-running` did: the secret, the generation it replaced and the one it holds now, the +// fingerprint of the value held now (what the node-engine checks against the value it handed), whether the value was +// held already. Never a value. +type OwnReplaced struct { + Secret string `json:"secret"` + Node string `json:"node"` + Module string `json:"module"` + Name string `json:"name"` + From int `json:"from_generation"` + To int `json:"to_generation"` + Unchanged bool `json:"unchanged,omitempty"` + Fingerprint string `json:"fingerprint"` + Said string `json:"said"` +} + +// ownTakeSecret is the secret a node-engine names, and its machine: refused for a caller that is no node-engine, for +// another machine, and for a name that is not an own secret. +func ownTakeSecret(caller string, of OwnTakeOf) (secret, node string, err error) { + node, ok := nodeEngineOf(caller) + if !ok { + if caller == "" || caller == stdio.Unproven { + caller = "an unproven caller" + } + return "", "", fmt.Errorf("%s takes a value from a machine's node-engine alone, about its own machine; %s is "+ + "not a node-engine; nothing is taken", OwnTakeRunningVerb, caller) + } + if of.Secret != "" { + parts := strings.Split(of.Secret, "/") + if len(parts) != 4 || parts[0] != "own" { + return "", "", fmt.Errorf("%s is not a module's own secret (own///): %s replaces "+ + "those alone; nothing is taken", of.Secret, OwnTakeRunningVerb) + } + if of.Node != "" && of.Node != parts[1] { + return "", "", fmt.Errorf("%s is not on %s; nothing is taken", of.Secret, of.Node) + } + of.Node, of.Module, of.Name = parts[1], parts[2], parts[3] + } + if of.Node != "" && of.Node != node { + return "", "", fmt.Errorf("%s may hand the vault its own machine's secrets alone, and %s/%s is %s's: nothing "+ + "is taken", caller, of.Module, of.Name, of.Node) + } + secret, err = OwnSecret(node, of.Module, of.Name) + if err != nil { + return "", "", fmt.Errorf("%w; nothing is taken", err) + } + return secret, node, nil +} + +// ownTakeRefusal says why the held record rec is not replaced by a running value, or "". +func ownTakeRefusal(rec Record, now time.Time) string { + switch { + case rec.Origin == OriginAccepted: + return fmt.Sprintf("%s was given by a person, and a person alone gives it anew (accept); nothing is taken", rec.Name) + case rec.Origin == OriginIssued && !Covered(rec.Name): + return fmt.Sprintf("%s is issued by its provider's software, and `issued` is the only way it enters the vault "+ + "(ADR 0305 §10); nothing is taken", rec.Name) + case now.Sub(rec.ChangedAt) < RecentlyChanged: + return fmt.Sprintf("the vault changed %s at %s (generation %d, by %s), less than %s ago: a running value is "+ + "taken only over an older one, so a value the vault just took is never put back; nothing is taken", + rec.Name, rec.ChangedAt.Format(time.RFC3339), rec.Generation, rec.ChangedBy, RecentlyChanged) + } + return "" +} + +// OwnTakeOpen opens a call to take the running value of one of the caller's machine's own secrets. +func (v *Vault) OwnTakeOpen(caller string, of OwnTakeOf) (Opened, error) { + secret, _, err := ownTakeSecret(caller, of) + if err != nil { + return Opened{}, err + } + why := strings.TrimSpace(of.Why) + if len(why) > 300 || strings.ContainsAny(why, "\r\n\x00") { + return Opened{}, errors.New("why is not one short line; nothing is taken") + } + rec, held, err := v.Store.Read(secret) + if err != nil { + return Opened{}, err + } + refused := "" + if !held { + refused = fmt.Sprintf("the vault holds no secret named %s, so there is nothing to replace; nothing is taken", secret) + } else { + refused = ownTakeRefusal(rec, v.Now()) + } + if refused != "" { + _ = v.recordHanding(Handing{To: caller, Name: secret, Generation: rec.Generation, Outcome: "own-take-refused", Why: refused}) + return Opened{}, errors.New(refused) + } + id, key, err := v.openCall(OwnTakeRunningVerb, secret, caller, nil) + if err != nil { + return Opened{}, err + } + v.callsMu.Lock() + if c := v.pending[id]; c != nil { + c.why = why + } + v.callsMu.Unlock() + return Opened{Call: id, SealTo: key, Secret: secret}, nil +} + +// OwnTakeTake opens the value sealed to the call's key and replaces the secret with it, keeping the value it replaces. +func (v *Vault) OwnTakeTake(caller, call, sealed string) (OwnReplaced, error) { + if _, ok := nodeEngineOf(caller); !ok { + return OwnReplaced{}, fmt.Errorf("%s takes a value from a machine's node-engine alone; %q is not one", + OwnTakeRunningVerb, caller) + } + c, err := v.takeCall(OwnTakeRunningVerb, call) + if err != nil { + return OwnReplaced{}, err + } + if c.by != caller { + return OwnReplaced{}, fmt.Errorf("the %s call %q was opened by another caller; nothing was taken", OwnTakeRunningVerb, call) + } + value, err := c.open(sealed) + if err != nil { + return OwnReplaced{}, err + } + defer zero(value) + parts := strings.Split(c.secret, "/") + out := OwnReplaced{Secret: c.secret, Node: parts[1], Module: parts[2], Name: parts[3]} + if strings.TrimSpace(string(value)) == "" { + why := "the value handed is empty; nothing was taken" + _ = v.recordHanding(Handing{To: caller, Name: c.secret, Outcome: "own-take-refused", Why: why}) + return OwnReplaced{}, errors.New(why) + } + now := v.Now() + changed, err := v.keepWith(c.secret, string(value), caller, "", "", keepOptions{replaceOnly: true, keepPrevious: true, + refuse: func(before Record) string { return ownTakeRefusal(before, now) }}) + if err != nil { + _ = v.recordHanding(Handing{To: caller, Name: c.secret, Outcome: "own-take-refused", Why: err.Error()}) + return OwnReplaced{}, err + } + out.Fingerprint = Fingerprint(string(value)) + if changed.Unchanged { + out.From, out.To, out.Unchanged = changed.Generation, changed.Generation, true + out.Said = fmt.Sprintf("the vault holds this value of %s already (generation %d): nothing changed", c.secret, changed.Generation) + _ = v.recordHanding(Handing{To: caller, Name: c.secret, Generation: changed.Generation, Outcome: "own-same", Why: c.why}) + v.log("%s handed %s's running value, the one the vault holds (generation %d): nothing changed", caller, c.secret, + changed.Generation) + return out, nil + } + out.From, out.To = changed.Generation-1, changed.Generation + out.Said = fmt.Sprintf("%s is now the value that runs on %s, generation %d; generation %d is kept as the previous", + c.secret, out.Node, out.To, out.From) + if err := v.recordHanding(Handing{To: caller, Name: c.secret, Generation: out.To, Outcome: "own-replaced", Why: c.why}); err != nil { + out.Said += "; the record of hand-outs could not be written: " + err.Error() + } + v.log("replaced %s (generation %d → %d) with the value that runs, handed by %s: %s", c.secret, out.From, out.To, + caller, someoneOr(c.why, "no reason given")) + if v.Emit != nil { + body := map[string]any{"secret": c.secret, "node": out.Node, "module": out.Module, "name": out.Name, + "from_generation": out.From, "to_generation": out.To, "by": caller, "why": c.why, + "at": changed.At.Format(time.RFC3339Nano)} + if err := v.Emit(OwnReplacedEvent, body); err != nil { + v.log("%s is replaced at generation %d, but %s could not be said: %v", c.secret, out.To, OwnReplacedEvent, err) + } + } + return out, nil +} + +// ---- own_drift ---------------------------------------------------------------------------------------------- + +// DriftRow is one own secret a node's latest shadow answer says the vault holds another value of than runs there. +type DriftRow struct { + Secret string `json:"secret"` + Node string `json:"node"` + Module string `json:"module"` + Name string `json:"name"` + Groups []string `json:"groups"` + Held bool `json:"held"` + Generation int `json:"generation,omitempty"` + Changed time.Time `json:"changed,omitzero"` + ChangedBy string `json:"changed_by,omitempty"` + Origin string `json:"origin,omitempty"` + Previous int `json:"previous_generation,omitempty"` + // Takeable says own-take-running would take the running value now; Refused why not. + Takeable bool `json:"takeable"` + Refused string `json:"refused,omitempty"` +} + +// DriftNode is one node's answers as read: whose compositions it applies, and per group how many values differ. +type DriftNode struct { + Node string `json:"node"` + Composes string `json:"composes,omitempty"` + ValuesDiffer map[string]int `json:"values_differ,omitempty"` + Unread string `json:"unread,omitempty"` +} + +// Drift is own_drift's answer. +type Drift struct { + Secrets []DriftRow `json:"secrets"` + Nodes []DriftNode `json:"nodes"` + Note string `json:"note"` +} + +// OwnDrift reads every node's own have key (`.node`) and lists, from its latest answers to the placement +// candidates, every own secret whose shown bodies say another value (delivery.OwnValuesDiffer), with what the vault holds +// of it and whether own-take-running would take the running value now. +func (v *Vault) OwnDrift() (Drift, error) { + if v.Haves == nil || v.HaveKeys == nil { + return Drift{}, errors.New("the vault reads no node's have, so it cannot say what drifted") + } + keys, err := v.HaveKeys() + if err != nil { + return Drift{}, fmt.Errorf("the keys of %s could not be listed: %w", HaveRead, err) + } + sort.Strings(keys) + rows := map[string]*DriftRow{} + out := Drift{Secrets: []DriftRow{}, Nodes: []DriftNode{}, + Note: "from each node's latest shadow answers; a value is never shown: own-take-running from that node replaces it"} + for _, key := range keys { + node, isNode := strings.CutSuffix(key, "."+delivery.HaveNodeWord) + if !isNode || !namePart.MatchString(node) { + continue + } + dn := DriftNode{Node: node} + raw, err := v.Haves(key) + if err != nil || raw == nil { + dn.Unread = fmt.Sprintf("its have could not be read: %v", err) + out.Nodes = append(out.Nodes, dn) + continue + } + have, err := delivery.DecodeHaveNode(raw) + if err != nil { + dn.Unread = err.Error() + out.Nodes = append(out.Nodes, dn) + continue + } + dn.Composes = have.Composes + for group, a := range have.Candidates { + if n := delivery.ValuesDiffer(a); n > 0 { + if dn.ValuesDiffer == nil { + dn.ValuesDiffer = map[string]int{} + } + dn.ValuesDiffer[group] = n + } + for _, o := range delivery.OwnValuesDiffer(a) { + secret, err := OwnSecret(node, o.Module, o.Name) + if err != nil { + continue + } + r := rows[secret] + if r == nil { + r = &DriftRow{Secret: secret, Node: node, Module: o.Module, Name: o.Name} + rows[secret] = r + } + r.Groups = append(r.Groups, group) + } + } + out.Nodes = append(out.Nodes, dn) + } + names := make([]string, 0, len(rows)) + for n := range rows { + names = append(names, n) + } + sort.Strings(names) + for _, n := range names { + r := rows[n] + sort.Strings(r.Groups) + rec, held, err := v.Store.Read(n) + switch { + case err != nil: + r.Refused = err.Error() + case !held: + r.Refused = "the vault holds no secret named " + n + default: + r.Held, r.Generation, r.Changed, r.ChangedBy, r.Origin = true, rec.Generation, rec.ChangedAt, rec.ChangedBy, rec.Origin + if rec.Previous != nil { + r.Previous = rec.Previous.Generation + } + r.Refused = ownTakeRefusal(rec, v.Now()) + r.Takeable = r.Refused == "" + } + out.Secrets = append(out.Secrets, *r) + } + return out, nil +} diff --git a/modules/mesh-vault/cmd/mesh-vault/store.go b/modules/mesh-vault/cmd/mesh-vault/store.go index dd98a506..49b4bf8b 100644 --- a/modules/mesh-vault/cmd/mesh-vault/store.go +++ b/modules/mesh-vault/cmd/mesh-vault/store.go @@ -48,6 +48,20 @@ type Record struct { // MovedFrom is, for a value the move took from the controller's store (design 57's V3), the node-engine that // handed it in and the controller's generation for it, kept as it was. MovedFrom *MovedFrom `json:"moved-from,omitempty"` + // Previous is the value this one replaced when a machine's node-engine handed the vault what runs there + // (own-take-running, owntake.go): kept sealed as it was, so the replacement can be read and undone. Any later + // change of the secret drops it. + Previous *PreviousValue `json:"previous,omitempty"` +} + +// PreviousValue is a replaced value as the store kept it: its generation, both its seals, and who changed it when. +type PreviousValue struct { + Generation int `json:"generation"` + AtRest seal.Box `json:"at-rest"` + Operator OperatorCopy `json:"operator"` + ChangedAt time.Time `json:"changed-at"` + ChangedBy string `json:"changed-by"` + Origin string `json:"origin,omitempty"` } // OperatorCopy is a value sealed to the operator key named in Key. diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index 2ef64c8d..f1397ceb 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -195,6 +195,43 @@ func definitions(env *Env) []toolDef { } return v.RotateFor(caller, secret, one, arg(a, "by"), arg(a, "why")) }}, + {Name: Seat + "." + OwnTakeRunningVerb, + Description: "Replace one own secret of the asking machine (own///, the machine as the " + + "bus proved the caller) with the value that runs there (issue 1126, cause C4), from that machine's " + + "node-engine alone, in two steps: name module, name and why, and get a call and a key made for it; then " + + "send the call and the value sealed to that key. Keeps the old value as the previous generation, raises " + + "the generation and says own-replaced, never the value; answers both generations and the fingerprint of " + + "what it holds. Refused: another machine's secret, one that is not an own secret or not held, an empty " + + "value, a person's value, and a value the vault changed in the last 15 minutes. The same value is a no-op.", + Input: intakeInput(map[string]string{"module": "the module", "name": "its own secret's name", + "node": "the machine, when named: the caller's own, or refused", + "secret": "or the secret's whole name, own///", + "why": "why the value is handed, in words"}), + run: func(caller string, a map[string]any) (any, error) { + v, err := env.ready() + if err != nil { + return nil, err + } + if call := arg(a, "call"); call != "" { + return v.OwnTakeTake(caller, call, arg(a, "sealed")) + } + return v.OwnTakeOpen(caller, OwnTakeOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), + Secret: arg(a, "secret"), Why: arg(a, "why")}) + }}, + {Name: "own_drift", + Description: "Every own secret whose node's latest shadow answer says the vault holds another value than runs " + + "there (the masked word \"another value\", issue 1126), for all nodes in one call: the secret, the " + + "groups that show it, the vault's generation, origin and previous generation, and whether the node's " + + "own-take-running would take the running value now (or why not); and per node whose compositions it " + + "applies and how many values differ per group. Never a value.", + Input: map[string]any{"type": "object", "properties": map[string]any{}}, + run: func(_ string, _ map[string]any) (any, error) { + v, err := env.ready() + if err != nil { + return nil, err + } + return v.OwnDrift() + }}, {Name: Seat + ".move-in", Description: "Take one value of the controller's store into the vault, as it is (design 57's V3): from the " + "node-engine of a machine it was sealed to, and no other caller, in two steps — name the row (node, " + @@ -782,6 +819,9 @@ func ownAnswer(v *Vault, node, module, prefix string) (map[string]any, error) { if r.ChangedVia != "" { row["changedVia"] = r.ChangedVia } + if r.Previous != nil { + row["previousGeneration"] = r.Previous.Generation + } if note != nil { row["error"] = note } diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 6734901d..00a61548 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -49,6 +49,8 @@ type Vault struct { // Haves reads the node-engines' have bucket, where the bus node hands the loaded list's hashes (adopt.go): nil // adopts nothing. Haves HaveReader + // HaveKeys lists the keys of the node-engines' have bucket, for own_drift (owntake.go): nil lists none. + HaveKeys func() ([]string, error) mu sync.Mutex // hashMu keeps one hashing at a time, and hashes what is owed (accounts.go). @@ -106,6 +108,11 @@ type keepOptions struct { neverReplace bool madeAt time.Time movedFrom *MovedFrom + // replaceOnly, keepPrevious and refuse are what own-take-running keeps (owntake.go): only a value held is replaced, + // keeping its origin and the value it replaces, and refuse, under the store's lock, says why a held value is not. + replaceOnly bool + keepPrevious bool + refuse func(before Record) string } // keepWith is keep with the move's options. A covered bus account kept anew has its hash written once it is kept @@ -147,10 +154,18 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions) if err != nil { return Changed{}, err } + if !held && opt.replaceOnly { + return Changed{}, fmt.Errorf("the vault holds no secret named %s, so there is nothing to replace; nothing was taken", name) + } if held { if old, err := seal.Open(before.AtRest, v.Own.PrivateKey); err == nil && old == value { return Changed{Name: name, Generation: before.Generation, At: before.ChangedAt, By: before.ChangedBy, Unchanged: true}, nil } + if opt.refuse != nil { + if why := opt.refuse(before); why != "" { + return Changed{}, errors.New(why) + } + } if opt.neverReplace { return Changed{}, fmt.Errorf("the vault already holds another value for %s (generation %d, changed by %s "+ "at %s), and the move never replaces a value the vault holds: nothing was taken", name, @@ -174,6 +189,13 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions) MadeAt: now, ChangedAt: now, ChangedBy: by, ChangedVia: via, Origin: origin} if held { rec.Generation, rec.MadeAt = before.Generation+1, before.MadeAt + if opt.replaceOnly { + rec.Origin, rec.MovedFrom = before.Origin, before.MovedFrom + } + if opt.keepPrevious { + rec.Previous = &PreviousValue{Generation: before.Generation, AtRest: before.AtRest, Operator: before.Operator, + ChangedAt: before.ChangedAt, ChangedBy: before.ChangedBy, Origin: before.Origin} + } } else if !opt.madeAt.IsZero() { rec.MadeAt = opt.madeAt.UTC() } @@ -343,7 +365,8 @@ func (v *Vault) reseal(key string) (int, error) { n := 0 var failed []error for _, rec := range all { - if rec.Operator.Key == key && rec.Operator.Box == OperatorBox { + if rec.Operator.Key == key && rec.Operator.Box == OperatorBox && + (rec.Previous == nil || rec.Previous.Operator.Key == key && rec.Previous.Operator.Box == OperatorBox) { continue } value, err := seal.Open(rec.AtRest, v.Own.PrivateKey) @@ -352,6 +375,16 @@ func (v *Vault) reseal(key string) (int, error) { continue } sealed, err := operatorSeal(key, []byte(value)) + if err == nil && rec.Previous != nil { + // And the value it replaced (owntake.go): a copy of every value the vault keeps is the operator's. + var old string + if old, err = seal.Open(rec.Previous.AtRest, v.Own.PrivateKey); err == nil { + var prev string + if prev, err = operatorSeal(key, []byte(old)); err == nil { + rec.Previous.Operator = OperatorCopy{Box: OperatorBox, Key: key, Sealed: prev} + } + } + } if err == nil { rec.Operator = OperatorCopy{Box: OperatorBox, Key: key, Sealed: sealed} err = v.Store.Write(rec) diff --git a/modules/mesh-vault/cmd/mesh-vault/vault_test.go b/modules/mesh-vault/cmd/mesh-vault/vault_test.go index fefbff17..1ae1cb08 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault_test.go @@ -167,10 +167,11 @@ func TestNoBucketHoldsASecret(t *testing.T) { code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Put(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Keys(", "", 1)) } - // The haves, in main.go alone: one read and one watch, never a write. + // The haves, in main.go alone: one read, one watch and their keys (own_drift), never a write. if f == "main.go" { code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Get(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Watch(", "", 1)) + code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Keys(", "", 1)) } if m := bucket.Find(code); m != nil { t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m) diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 304b95df..6872935d 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -15,7 +15,8 @@ "provisioned", "rotated", "secret-changed", - "account-hashed" + "account-hashed", + "own-replaced" ], "reads": [ "mesh-controller.assignments", @@ -163,7 +164,8 @@ "provisioner_retire_reject", "provisioner_delete", "vault_status", - "vault_health" + "vault_health", + "own_drift" ], "claims": [ { @@ -178,7 +180,8 @@ "move-in", "node-key", "move-asks", - "own" + "own", + "own-take-running" ] } ] From 671a6a1f47451f0b0c5a834438c722e6d5b73f97 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 09:39:16 +0100 Subject: [PATCH 2/6] own-take-running has a person's switch, own_take_switch, kept in the vault's store (issue 1126, cause C4) --- modules/mesh-vault/README.md | 4 +- .../mesh-vault/cmd/mesh-vault/move_test.go | 12 +++ modules/mesh-vault/cmd/mesh-vault/owntake.go | 83 +++++++++++++++++-- modules/mesh-vault/cmd/mesh-vault/tools.go | 18 ++++ modules/mesh-vault/module.json | 3 +- 5 files changed, 112 insertions(+), 8 deletions(-) diff --git a/modules/mesh-vault/README.md b/modules/mesh-vault/README.md index a64d59c1..102a21dc 100644 --- a/modules/mesh-vault/README.md +++ b/modules/mesh-vault/README.md @@ -218,7 +218,9 @@ The replaced value is kept sealed as the record's `previous` (dropped by any lat `secret-changed` and `own-replaced` are said, and the act is in `secret_handed` (`own-replaced`, `own-same`, `own-take-refused`). A covered bus account so replaced has its hash written again. `own_drift` lists, from every node's latest shadow answer (`.node` in `node-engine.have`), each own secret whose node says another value, -with what the vault holds and whether the node's ask would be taken now. +with what the vault holds and whether the node's ask would be taken now. **The switch** is the vault's: a person +switches every machine's asks off or on with `own_take_switch` (`on`, `why`), kept in the vault's store over a +restart; it is on until switched off, and `own_drift` shows it. ## Events diff --git a/modules/mesh-vault/cmd/mesh-vault/move_test.go b/modules/mesh-vault/cmd/mesh-vault/move_test.go index 5e41e511..9edc92fa 100644 --- a/modules/mesh-vault/cmd/mesh-vault/move_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/move_test.go @@ -415,6 +415,18 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { if _, err := take("node.anchor", pg, "a-value-older-than-the-vaults"); err == nil || !strings.Contains(err.Error(), "less than") { t.Errorf("a running value over one the vault just took: %v", err) } + // Switched off by a person, every ask is refused, saying who and why; on again, taken again. + later += time.Hour + if _, err := v.SwitchOwnTake("jochen", false, "the test"); err != nil { + t.Fatal(err) + } + if _, err := take("node.anchor", pg, "after-the-switch"); err == nil || !strings.Contains(err.Error(), "switched off (by jochen") { + t.Errorf("an ask while switched off: %v", err) + } + if _, err := v.SwitchOwnTake("jochen", true, "the test"); err != nil { + t.Fatal(err) + } + later -= time.Hour // own_drift reads it from the node's latest shadow answer, mesh-wide. answer := delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{ Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "needs-superuser-password", diff --git a/modules/mesh-vault/cmd/mesh-vault/owntake.go b/modules/mesh-vault/cmd/mesh-vault/owntake.go index c05ab440..9c59b3b2 100644 --- a/modules/mesh-vault/cmd/mesh-vault/owntake.go +++ b/modules/mesh-vault/cmd/mesh-vault/owntake.go @@ -22,8 +22,11 @@ package main // (accounts.go), so the bus-accounts key is of the value the bus runs. import ( + "encoding/json" "errors" "fmt" + "os" + "path/filepath" "sort" "strings" "time" @@ -125,10 +128,12 @@ func (v *Vault) OwnTakeOpen(caller string, of OwnTakeOf) (Opened, error) { if err != nil { return Opened{}, err } - refused := "" - if !held { + refused := v.ownTakeOff() + switch { + case refused != "": + case !held: refused = fmt.Sprintf("the vault holds no secret named %s, so there is nothing to replace; nothing is taken", secret) - } else { + default: refused = ownTakeRefusal(rec, v.Now()) } if refused != "" { @@ -237,9 +242,10 @@ type DriftNode struct { // Drift is own_drift's answer. type Drift struct { - Secrets []DriftRow `json:"secrets"` - Nodes []DriftNode `json:"nodes"` - Note string `json:"note"` + Switch OwnTakeSwitch `json:"switch"` + Secrets []DriftRow `json:"secrets"` + Nodes []DriftNode `json:"nodes"` + Note string `json:"note"` } // OwnDrift reads every node's own have key (`.node`) and lists, from its latest answers to the placement @@ -303,6 +309,10 @@ func (v *Vault) OwnDrift() (Drift, error) { names = append(names, n) } sort.Strings(names) + if out.Switch, err = v.Store.OwnTakeSwitch(); err != nil { + return Drift{}, err + } + off := v.ownTakeOff() for _, n := range names { r := rows[n] sort.Strings(r.Groups) @@ -318,9 +328,70 @@ func (v *Vault) OwnDrift() (Drift, error) { r.Previous = rec.Previous.Generation } r.Refused = ownTakeRefusal(rec, v.Now()) + if off != "" { + r.Refused = off + } r.Takeable = r.Refused == "" } out.Secrets = append(out.Secrets, *r) } return out, nil } + +// ---- own_take_switch ---------------------------------------------------------------------------------------- + +// OwnTakeSwitch is whether own-take-running takes values, who switched it last, when and why. On until a person +// switches it off; kept in the vault's own store, so it holds over a restart. +type OwnTakeSwitch struct { + On bool `json:"on"` + By string `json:"by,omitempty"` + At time.Time `json:"at,omitzero"` + Why string `json:"why,omitempty"` +} + +func (s *Store) ownTakeSwitchPath() string { return filepath.Join(s.dir, "own-take-running.json") } + +// OwnTakeSwitch is the switch as kept: on when nothing was ever switched. +func (s *Store) OwnTakeSwitch() (OwnTakeSwitch, error) { + raw, err := os.ReadFile(s.ownTakeSwitchPath()) + if errors.Is(err, os.ErrNotExist) { + return OwnTakeSwitch{On: true}, nil + } + if err != nil { + return OwnTakeSwitch{}, err + } + var sw OwnTakeSwitch + if err := json.Unmarshal(raw, &sw); err != nil { + return OwnTakeSwitch{}, fmt.Errorf("the switch of %s is not readable: %w", OwnTakeRunningVerb, err) + } + return sw, nil +} + +// SwitchOwnTake switches own-take-running on or off, a person's act with why; the caller is recorded as the bus +// proved it. +func (v *Vault) SwitchOwnTake(caller string, on bool, why string) (OwnTakeSwitch, error) { + why = strings.TrimSpace(why) + if why == "" || len(why) > 300 || strings.ContainsAny(why, "\r\n\x00") { + return OwnTakeSwitch{}, errors.New("say why in one short line; nothing was switched") + } + sw := OwnTakeSwitch{On: on, By: someoneOr(caller, stdio.Unproven), At: v.Now().UTC(), Why: why} + raw, _ := json.MarshalIndent(sw, "", " ") + if err := writeWhole(v.Store.ownTakeSwitchPath(), append(raw, '\n')); err != nil { + return OwnTakeSwitch{}, err + } + v.log("%s is switched %s by %s: %s", OwnTakeRunningVerb, map[bool]string{true: "on", false: "off"}[on], sw.By, why) + return sw, nil +} + +// ownTakeOff says why own-take-running takes nothing now, or "". +func (v *Vault) ownTakeOff() string { + sw, err := v.Store.OwnTakeSwitch() + if err != nil { + return err.Error() + "; nothing is taken" + } + if !sw.On { + return fmt.Sprintf("%s is switched off (by %s at %s: %s); nothing is taken", OwnTakeRunningVerb, sw.By, + sw.At.Format(time.RFC3339), sw.Why) + } + return "" +} diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index f1397ceb..e7ff1b4f 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -218,6 +218,24 @@ func definitions(env *Env) []toolDef { return v.OwnTakeOpen(caller, OwnTakeOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), Secret: arg(a, "secret"), Why: arg(a, "why")}) }}, + {Name: "own_take_switch", + Description: "Switch own-take-running on or off for every machine, a person's act with why (on until " + + "switched off; kept over a restart). While off, every machine's ask is refused, saying who switched it " + + "off and why. own_drift shows the switch.", + Input: map[string]any{"type": "object", "required": []string{"on", "why"}, "properties": map[string]any{ + "on": map[string]any{"type": "boolean", "description": "true takes running values, false takes none"}, + "why": str("why, in one line")}}, + run: func(caller string, a map[string]any) (any, error) { + v, err := env.ready() + if err != nil { + return nil, err + } + on, ok := a["on"].(bool) + if !ok { + return nil, errors.New("say on: true or false; nothing was switched") + } + return v.SwitchOwnTake(caller, on, arg(a, "why")) + }}, {Name: "own_drift", Description: "Every own secret whose node's latest shadow answer says the vault holds another value than runs " + "there (the masked word \"another value\", issue 1126), for all nodes in one call: the secret, the " + diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 6872935d..12a50350 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -165,7 +165,8 @@ "provisioner_delete", "vault_status", "vault_health", - "own_drift" + "own_drift", + "own_take_switch" ], "claims": [ { From da5da4f6f78f14f85868df6ebd4ebf26a974ebf3 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 09:48:14 +0100 Subject: [PATCH 3/6] The vault takes what runs through move-in's replace_running, not a verb of its own, so no seat promises anything new (issue 1126, cause C4) --- modules/mesh-vault/README.md | 7 ++- modules/mesh-vault/cmd/mesh-vault/intake.go | 12 +++- .../mesh-vault/cmd/mesh-vault/move_test.go | 12 +++- modules/mesh-vault/cmd/mesh-vault/owntake.go | 50 +++++++++------- modules/mesh-vault/cmd/mesh-vault/store.go | 2 +- modules/mesh-vault/cmd/mesh-vault/tools.go | 60 +++++++++++-------- modules/mesh-vault/cmd/mesh-vault/vault.go | 2 +- modules/mesh-vault/module.json | 3 +- 8 files changed, 89 insertions(+), 59 deletions(-) diff --git a/modules/mesh-vault/README.md b/modules/mesh-vault/README.md index 102a21dc..c633a034 100644 --- a/modules/mesh-vault/README.md +++ b/modules/mesh-vault/README.md @@ -206,12 +206,13 @@ account (and, until R1, the controller) and no other caller, and only sealed to answers the controller alone today; it must also answer `.mesh-vault` when it moves with the media catalogue, after release 57. -## Taking what runs: `own-take-running` (issue 1126, cause C4) +## Taking what runs: `move-in` with `replace_running` (issue 1126, cause C4) `move-in` never replaces a value the vault holds, so where the controller made an own secret anew after the move (a rotation at start, a re-issue), the vault kept the first value while the new one runs. The node-engine of the machine -the secret is for hands the vault what runs, in an intake's two steps (`owntake.go`): module, name and why, then the -value sealed to the call's key. Its own machine's secrets alone (the machine is the caller's, as the bus proved it); a +the secret is for hands the vault what runs, through `move-in` with `replace_running: true`, in an intake's two steps +(`owntake.go`): module, name and why, then the value sealed to the call's key. It is `move-in`'s and no verb of its +own, so the node-engine asks it on the subject it may publish already and the seat promises nothing new. Its own machine's secrets alone (the machine is the caller's, as the bus proved it); a value the vault holds, never a person's (`accept`) or a provider's software's (`issued`, but for a covered bus account); never over a value the vault changed in the last 15 minutes. The same value changes nothing and says so. The replaced value is kept sealed as the record's `previous` (dropped by any later change), the generation is raised, diff --git a/modules/mesh-vault/cmd/mesh-vault/intake.go b/modules/mesh-vault/cmd/mesh-vault/intake.go index be8012d3..b1eb1811 100644 --- a/modules/mesh-vault/cmd/mesh-vault/intake.go +++ b/modules/mesh-vault/cmd/mesh-vault/intake.go @@ -92,7 +92,7 @@ type callKey struct { ask *AskRecord // move is what `move-in` takes: the row of the controller's store (move.go). move *MoveOf - // why is what `own-take-running` was asked for, in the caller's words (owntake.go). + // why is what `move-in` with replace_running was asked for, in the caller's words (owntake.go). why string public [32]byte private [32]byte @@ -151,6 +151,16 @@ func (v *Vault) openCall(verb, secret, by string, ask *AskRecord) (string, strin return id, base64.StdEncoding.EncodeToString(pub[:]), nil } +// callKind is the kind of the open call id, or "" when none is open: what its second step is for. +func (v *Vault) callKind(id string) string { + v.callsMu.Lock() + defer v.callsMu.Unlock() + if c, ok := v.pending[id]; ok { + return c.verb + } + return "" +} + // takeCall ends a call: its key is forgotten whatever follows. func (v *Vault) takeCall(verb, id string) (*callKey, error) { v.callsMu.Lock() diff --git a/modules/mesh-vault/cmd/mesh-vault/move_test.go b/modules/mesh-vault/cmd/mesh-vault/move_test.go index 9edc92fa..9ff90894 100644 --- a/modules/mesh-vault/cmd/mesh-vault/move_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/move_test.go @@ -343,12 +343,20 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { } } later = time.Hour + // Through move-in's tool as the node-engine asks it: replace_running in the first step, the second known by its call. + moveInTool := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".move-in") take := func(caller string, of OwnTakeOf, value string) (OwnReplaced, error) { - opened, err := v.OwnTakeOpen(caller, of) + got, err := moveInTool.runAs(caller, map[string]any{ReplaceRunningArg: true, "node": of.Node, "module": of.Module, + "name": of.Name, "secret": of.Secret, "why": of.Why}) if err != nil { return OwnReplaced{}, err } - return v.OwnTakeTake(caller, opened.Call, sealToCall(t, opened.SealTo, value)) + opened := got.(Opened) + got, err = moveInTool.runAs(caller, map[string]any{"call": opened.Call, "sealed": sealToCall(t, opened.SealTo, value)}) + if err != nil { + return OwnReplaced{}, err + } + return got.(OwnReplaced), nil } pg := OwnTakeOf{Module: "postgres", Name: "superuser-password", Why: "the shadow answer says another value"} for name, row := range map[string]struct { diff --git a/modules/mesh-vault/cmd/mesh-vault/owntake.go b/modules/mesh-vault/cmd/mesh-vault/owntake.go index 9c59b3b2..1e4f6f2f 100644 --- a/modules/mesh-vault/cmd/mesh-vault/owntake.go +++ b/modules/mesh-vault/cmd/mesh-vault/owntake.go @@ -5,9 +5,10 @@ package main // `move-in` never replaces a value the vault holds (move.go), so its first import of a machine's own secret is fixed. // Where the controller later made that secret anew (a rotation at start, a re-issue) without handing it here, the new // value is what runs and what its consumers hold, and the vault keeps the old one. Only the machine the value is sealed -// to can open what runs, so its node-engine hands it in, with the verb `own-take-running`, in the two steps of every -// intake (intake.go): name the module and the secret and why, and get a call and a key made for it; then send the value -// sealed to that key. +// to can open what runs, so its node-engine hands it in through `move-in` with replace_running: true, in the two steps +// of every intake (intake.go): name the module and the secret and why, and get a call and a key made for it; then send +// the value sealed to that key. It is move-in's, not a verb of its own, so a node-engine asks it on the subject it may +// already publish and no seat promises anything new (the lead, 2026-10-28). // // **From the node-engine of the machine the secret is for, about its own machine alone**: the machine is the caller's, // as the bus proved it (`node.`), never an argument; a machine named that is not the caller's is refused, and so @@ -35,8 +36,11 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" ) -// OwnTakeRunningVerb is the seat verb a machine's node-engine hands the vault a running own secret with. -const OwnTakeRunningVerb = "own-take-running" +// ReplaceRunningArg is the argument of `move-in` that makes it take a running own secret over the one the vault holds. +const ReplaceRunningArg = "replace_running" + +// ReplaceRunning names the act in what the vault says, and is the kind of its open calls. +const ReplaceRunning = "move-in with " + ReplaceRunningArg // OwnReplacedEvent is what the vault says when it replaced an own secret with what runs: never a value. const OwnReplacedEvent = "own-replaced" @@ -44,13 +48,13 @@ const OwnReplacedEvent = "own-replaced" // RecentlyChanged is how long after the vault changed a secret a running value is not taken over it. var RecentlyChanged = 15 * time.Minute -// OwnTakeOf names the secret of one `own-take-running`: Module and Name (or Secret whole), Node only to be checked +// OwnTakeOf names the secret of one `move-in` with replace_running: Module and Name (or Secret whole), Node only to be checked // against the caller's machine, and Why in the caller's words. type OwnTakeOf struct { Node, Module, Name, Secret, Why string } -// OwnReplaced is what one `own-take-running` did: the secret, the generation it replaced and the one it holds now, the +// OwnReplaced is what one `move-in` with replace_running did: the secret, the generation it replaced and the one it holds now, the // fingerprint of the value held now (what the node-engine checks against the value it handed), whether the value was // held already. Never a value. type OwnReplaced struct { @@ -74,13 +78,13 @@ func ownTakeSecret(caller string, of OwnTakeOf) (secret, node string, err error) caller = "an unproven caller" } return "", "", fmt.Errorf("%s takes a value from a machine's node-engine alone, about its own machine; %s is "+ - "not a node-engine; nothing is taken", OwnTakeRunningVerb, caller) + "not a node-engine; nothing is taken", ReplaceRunning, caller) } if of.Secret != "" { parts := strings.Split(of.Secret, "/") if len(parts) != 4 || parts[0] != "own" { return "", "", fmt.Errorf("%s is not a module's own secret (own///): %s replaces "+ - "those alone; nothing is taken", of.Secret, OwnTakeRunningVerb) + "those alone; nothing is taken", of.Secret, ReplaceRunning) } if of.Node != "" && of.Node != parts[1] { return "", "", fmt.Errorf("%s is not on %s; nothing is taken", of.Secret, of.Node) @@ -140,7 +144,7 @@ func (v *Vault) OwnTakeOpen(caller string, of OwnTakeOf) (Opened, error) { _ = v.recordHanding(Handing{To: caller, Name: secret, Generation: rec.Generation, Outcome: "own-take-refused", Why: refused}) return Opened{}, errors.New(refused) } - id, key, err := v.openCall(OwnTakeRunningVerb, secret, caller, nil) + id, key, err := v.openCall(ReplaceRunning, secret, caller, nil) if err != nil { return Opened{}, err } @@ -156,14 +160,14 @@ func (v *Vault) OwnTakeOpen(caller string, of OwnTakeOf) (Opened, error) { func (v *Vault) OwnTakeTake(caller, call, sealed string) (OwnReplaced, error) { if _, ok := nodeEngineOf(caller); !ok { return OwnReplaced{}, fmt.Errorf("%s takes a value from a machine's node-engine alone; %q is not one", - OwnTakeRunningVerb, caller) + ReplaceRunning, caller) } - c, err := v.takeCall(OwnTakeRunningVerb, call) + c, err := v.takeCall(ReplaceRunning, call) if err != nil { return OwnReplaced{}, err } if c.by != caller { - return OwnReplaced{}, fmt.Errorf("the %s call %q was opened by another caller; nothing was taken", OwnTakeRunningVerb, call) + return OwnReplaced{}, fmt.Errorf("the %s call %q was opened by another caller; nothing was taken", ReplaceRunning, call) } value, err := c.open(sealed) if err != nil { @@ -227,7 +231,7 @@ type DriftRow struct { ChangedBy string `json:"changed_by,omitempty"` Origin string `json:"origin,omitempty"` Previous int `json:"previous_generation,omitempty"` - // Takeable says own-take-running would take the running value now; Refused why not. + // Takeable says move-in with replace_running would take the running value now; Refused why not. Takeable bool `json:"takeable"` Refused string `json:"refused,omitempty"` } @@ -250,7 +254,7 @@ type Drift struct { // OwnDrift reads every node's own have key (`.node`) and lists, from its latest answers to the placement // candidates, every own secret whose shown bodies say another value (delivery.OwnValuesDiffer), with what the vault holds -// of it and whether own-take-running would take the running value now. +// of it and whether move-in with replace_running would take the running value now. func (v *Vault) OwnDrift() (Drift, error) { if v.Haves == nil || v.HaveKeys == nil { return Drift{}, errors.New("the vault reads no node's have, so it cannot say what drifted") @@ -262,7 +266,7 @@ func (v *Vault) OwnDrift() (Drift, error) { sort.Strings(keys) rows := map[string]*DriftRow{} out := Drift{Secrets: []DriftRow{}, Nodes: []DriftNode{}, - Note: "from each node's latest shadow answers; a value is never shown: own-take-running from that node replaces it"} + Note: "from each node's latest shadow answers; a value is never shown: move-in with replace_running from that node replaces it"} for _, key := range keys { node, isNode := strings.CutSuffix(key, "."+delivery.HaveNodeWord) if !isNode || !namePart.MatchString(node) { @@ -340,7 +344,7 @@ func (v *Vault) OwnDrift() (Drift, error) { // ---- own_take_switch ---------------------------------------------------------------------------------------- -// OwnTakeSwitch is whether own-take-running takes values, who switched it last, when and why. On until a person +// OwnTakeSwitch is whether move-in with replace_running takes values, who switched it last, when and why. On until a person // switches it off; kept in the vault's own store, so it holds over a restart. type OwnTakeSwitch struct { On bool `json:"on"` @@ -349,7 +353,7 @@ type OwnTakeSwitch struct { Why string `json:"why,omitempty"` } -func (s *Store) ownTakeSwitchPath() string { return filepath.Join(s.dir, "own-take-running.json") } +func (s *Store) ownTakeSwitchPath() string { return filepath.Join(s.dir, "replace-running.json") } // OwnTakeSwitch is the switch as kept: on when nothing was ever switched. func (s *Store) OwnTakeSwitch() (OwnTakeSwitch, error) { @@ -362,12 +366,12 @@ func (s *Store) OwnTakeSwitch() (OwnTakeSwitch, error) { } var sw OwnTakeSwitch if err := json.Unmarshal(raw, &sw); err != nil { - return OwnTakeSwitch{}, fmt.Errorf("the switch of %s is not readable: %w", OwnTakeRunningVerb, err) + return OwnTakeSwitch{}, fmt.Errorf("the switch of %s is not readable: %w", ReplaceRunning, err) } return sw, nil } -// SwitchOwnTake switches own-take-running on or off, a person's act with why; the caller is recorded as the bus +// SwitchOwnTake switches move-in with replace_running on or off, a person's act with why; the caller is recorded as the bus // proved it. func (v *Vault) SwitchOwnTake(caller string, on bool, why string) (OwnTakeSwitch, error) { why = strings.TrimSpace(why) @@ -379,18 +383,18 @@ func (v *Vault) SwitchOwnTake(caller string, on bool, why string) (OwnTakeSwitch if err := writeWhole(v.Store.ownTakeSwitchPath(), append(raw, '\n')); err != nil { return OwnTakeSwitch{}, err } - v.log("%s is switched %s by %s: %s", OwnTakeRunningVerb, map[bool]string{true: "on", false: "off"}[on], sw.By, why) + v.log("%s is switched %s by %s: %s", ReplaceRunning, map[bool]string{true: "on", false: "off"}[on], sw.By, why) return sw, nil } -// ownTakeOff says why own-take-running takes nothing now, or "". +// ownTakeOff says why move-in with replace_running takes nothing now, or "". func (v *Vault) ownTakeOff() string { sw, err := v.Store.OwnTakeSwitch() if err != nil { return err.Error() + "; nothing is taken" } if !sw.On { - return fmt.Sprintf("%s is switched off (by %s at %s: %s); nothing is taken", OwnTakeRunningVerb, sw.By, + return fmt.Sprintf("%s is switched off (by %s at %s: %s); nothing is taken", ReplaceRunning, sw.By, sw.At.Format(time.RFC3339), sw.Why) } return "" diff --git a/modules/mesh-vault/cmd/mesh-vault/store.go b/modules/mesh-vault/cmd/mesh-vault/store.go index 49b4bf8b..426132c4 100644 --- a/modules/mesh-vault/cmd/mesh-vault/store.go +++ b/modules/mesh-vault/cmd/mesh-vault/store.go @@ -49,7 +49,7 @@ type Record struct { // handed it in and the controller's generation for it, kept as it was. MovedFrom *MovedFrom `json:"moved-from,omitempty"` // Previous is the value this one replaced when a machine's node-engine handed the vault what runs there - // (own-take-running, owntake.go): kept sealed as it was, so the replacement can be read and undone. Any later + // (move-in with replace_running, owntake.go): kept sealed as it was, so the replacement can be read and undone. Any later // change of the secret drops it. Previous *PreviousValue `json:"previous,omitempty"` } diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index e7ff1b4f..62709e5b 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -195,31 +195,8 @@ func definitions(env *Env) []toolDef { } return v.RotateFor(caller, secret, one, arg(a, "by"), arg(a, "why")) }}, - {Name: Seat + "." + OwnTakeRunningVerb, - Description: "Replace one own secret of the asking machine (own///, the machine as the " + - "bus proved the caller) with the value that runs there (issue 1126, cause C4), from that machine's " + - "node-engine alone, in two steps: name module, name and why, and get a call and a key made for it; then " + - "send the call and the value sealed to that key. Keeps the old value as the previous generation, raises " + - "the generation and says own-replaced, never the value; answers both generations and the fingerprint of " + - "what it holds. Refused: another machine's secret, one that is not an own secret or not held, an empty " + - "value, a person's value, and a value the vault changed in the last 15 minutes. The same value is a no-op.", - Input: intakeInput(map[string]string{"module": "the module", "name": "its own secret's name", - "node": "the machine, when named: the caller's own, or refused", - "secret": "or the secret's whole name, own///", - "why": "why the value is handed, in words"}), - run: func(caller string, a map[string]any) (any, error) { - v, err := env.ready() - if err != nil { - return nil, err - } - if call := arg(a, "call"); call != "" { - return v.OwnTakeTake(caller, call, arg(a, "sealed")) - } - return v.OwnTakeOpen(caller, OwnTakeOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), - Secret: arg(a, "secret"), Why: arg(a, "why")}) - }}, {Name: "own_take_switch", - Description: "Switch own-take-running on or off for every machine, a person's act with why (on until " + + Description: "Switch move-in's replace_running on or off for every machine, a person's act with why (on until " + "switched off; kept over a restart). While off, every machine's ask is refused, saying who switched it " + "off and why. own_drift shows the switch.", Input: map[string]any{"type": "object", "required": []string{"on", "why"}, "properties": map[string]any{ @@ -240,7 +217,7 @@ func definitions(env *Env) []toolDef { Description: "Every own secret whose node's latest shadow answer says the vault holds another value than runs " + "there (the masked word \"another value\", issue 1126), for all nodes in one call: the secret, the " + "groups that show it, the vault's generation, origin and previous generation, and whether the node's " + - "own-take-running would take the running value now (or why not); and per node whose compositions it " + + "move-in with replace_running would take the running value now (or why not); and per node whose compositions it " + "applies and how many values differ per group. Never a value.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, run: func(_ string, _ map[string]any) (any, error) { @@ -256,7 +233,15 @@ func definitions(env *Env) []toolDef { "module and name for a module's own; provider, provision, consumer, consumer_module and local for a " + "pair credential), its origin and the controller's generation, and get a call and a key made for it; " + "then send the call, the value sealed to that key and read_back_to, a key of your own. Never replaces " + - "a value the vault holds; answers what it kept, read back from its store sealed to read_back_to.", + "a value the vault holds; answers what it kept, read back from its store sealed to read_back_to. " + + "With replace_running: true it does the one thing it otherwise never does (issue 1126, cause C4): it " + + "replaces one own secret of the asking machine (the machine as the bus proved the caller) with the " + + "value that runs there; first step module, name and why, second step call and sealed. It keeps the " + + "old value as the previous generation, raises the generation and says own-replaced, never the value; " + + "answers both generations and the fingerprint of what it holds. Refused: another machine's secret, " + + "one that is not an own secret or not held, an empty value, a person's value, a provider's but a " + + "covered bus account, a value the vault changed in the last 15 minutes, and everything while " + + "own_take_switch is off. The same value is a no-op.", Input: intakeInput(map[string]string{"node": "a module's own secret: its machine", "module": "its module", "name": "its name in the module", "provider": "a pair credential: the provider's machine", "provision": "the provision", "consumer": "the consumer's machine", @@ -265,6 +250,10 @@ func definitions(env *Env) []toolDef { "generation": "the controller's generation for the value, kept as it is", "made_at": "when the controller made or took it (RFC 3339)", "read_back_to": "the second step: a raw X25519 public key in base64 the read-back is sealed to", + ReplaceRunningArg: "true to replace an own secret of this machine with the value that runs here " + + "(the second step is known by its call)", + "secret": "with replace_running, or the secret's whole name, own///", + "why": "with replace_running: why the value is handed, in words", "sealing_key": "the first step, optional: this machine's sealing key, a raw X25519 public key in " + "base64, which the core's credentials are sealed to from then on (ADR 0349 §16)"}), run: func(caller string, a map[string]any) (any, error) { @@ -272,9 +261,17 @@ func definitions(env *Env) []toolDef { if err != nil { return nil, err } + replace := isTrue(a[ReplaceRunningArg]) if call := arg(a, "call"); call != "" { + if replace || v.callKind(call) == ReplaceRunning { + return v.OwnTakeTake(caller, call, arg(a, "sealed")) + } return v.MoveTake(caller, call, arg(a, "sealed"), arg(a, "read_back_to")) } + if replace { + return v.OwnTakeOpen(caller, OwnTakeOf{Node: arg(a, "node"), Module: arg(a, "module"), + Name: arg(a, "name"), Secret: arg(a, "secret"), Why: arg(a, "why")}) + } m := MoveOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), Provider: arg(a, "provider"), Provision: arg(a, "provision"), Consumer: arg(a, "consumer"), ConsumerModule: arg(a, "consumer_module"), Local: arg(a, "local"), Origin: arg(a, "origin"), @@ -610,6 +607,17 @@ func (e *Env) ready() (*Vault, error) { return nil, errors.New("the vault is not ready") } +// isTrue is a switch argument said true, as a boolean or as the word. +func isTrue(v any) bool { + switch t := v.(type) { + case bool: + return t + case string: + return strings.EqualFold(strings.TrimSpace(t), "true") + } + return false +} + func arg(a map[string]any, key string) string { s, _ := a[key].(string) return strings.TrimSpace(s) diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 00a61548..05155395 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -108,7 +108,7 @@ type keepOptions struct { neverReplace bool madeAt time.Time movedFrom *MovedFrom - // replaceOnly, keepPrevious and refuse are what own-take-running keeps (owntake.go): only a value held is replaced, + // replaceOnly, keepPrevious and refuse are what move-in with replace_running keeps (owntake.go): only a value held is replaced, // keeping its origin and the value it replaces, and refuse, under the store's lock, says why a held value is not. replaceOnly bool keepPrevious bool diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 12a50350..7b2e866f 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -181,8 +181,7 @@ "move-in", "node-key", "move-asks", - "own", - "own-take-running" + "own" ] } ] From 35ad58e6e2d6c449350630ec02c0432931dfbf6d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 09:51:48 +0100 Subject: [PATCH 4/6] mesh-vault pins the group's mesh-sdk head 1062b33 (issue 1126; go.sum is filled once that head is pushed) --- modules/mesh-vault/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mesh-vault/go.mod b/modules/mesh-vault/go.mod index f225847f..51db97af 100644 --- a/modules/mesh-vault/go.mod +++ b/modules/mesh-vault/go.mod @@ -3,7 +3,7 @@ module mesh-vault go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028084903-1062b33d32c5 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 From a17356a162a998fd5a7be405b29403e3469bdec5 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 09:53:34 +0100 Subject: [PATCH 5/6] Fill go.sum for mesh-sdk 1062b33, the group's head (issue 1126) --- modules/mesh-vault/go.sum | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/mesh-vault/go.sum b/modules/mesh-vault/go.sum index d4f12692..048d56ba 100644 --- a/modules/mesh-vault/go.sum +++ b/modules/mesh-vault/go.sum @@ -1,9 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865 h1:VLir9NZbyi1LdH1YwWDIVRuL+lDC+AYlm6FZPlvmlm4= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936 h1:HGBoWimZXYZp+Qn9VCstYDitOP84t9EUQYd1yM9j2eg= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 h1:by2zBASLb8h5jA1IiYJWykXwLnHYpuZh/zFNa4NsbOU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028084903-1062b33d32c5 h1:FRNW0kjmm2SYVbEAeG9IsMWALE/qRKt8IMKNnMn2p90= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028084903-1062b33d32c5/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= From 0974d1a57d998fb07308a5c5ff06e0e1a84683e3 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 10:02:46 +0100 Subject: [PATCH 6/6] mesh-vault's own_drift row carries the id as a node runs it, its module's name first, and the vault pins mesh-sdk 3028c7e (issue 1126, cause C4; review round 2) --- modules/mesh-vault/cmd/mesh-vault/move_test.go | 5 +++-- modules/mesh-vault/go.mod | 2 +- modules/mesh-vault/go.sum | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/mesh-vault/cmd/mesh-vault/move_test.go b/modules/mesh-vault/cmd/mesh-vault/move_test.go index 9ff90894..ab13d194 100644 --- a/modules/mesh-vault/cmd/mesh-vault/move_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/move_test.go @@ -435,9 +435,10 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) { t.Fatal(err) } later -= time.Hour - // own_drift reads it from the node's latest shadow answer, mesh-wide. + // own_drift reads it from the node's latest shadow answer, mesh-wide; the id as a node runs it, its module's name + // first (captured from novox's shadow answer, 2026-10-28: only-office.needs-jwt). answer := delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{ - Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "needs-superuser-password", + Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "postgres.needs-superuser-password", Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}} have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": answer}, Composes: delivery.ComposesController}) diff --git a/modules/mesh-vault/go.mod b/modules/mesh-vault/go.mod index 51db97af..27efc746 100644 --- a/modules/mesh-vault/go.mod +++ b/modules/mesh-vault/go.mod @@ -3,7 +3,7 @@ module mesh-vault go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028084903-1062b33d32c5 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2 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 diff --git a/modules/mesh-vault/go.sum b/modules/mesh-vault/go.sum index 048d56ba..a460d122 100644 --- a/modules/mesh-vault/go.sum +++ b/modules/mesh-vault/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028084903-1062b33d32c5 h1:FRNW0kjmm2SYVbEAeG9IsMWALE/qRKt8IMKNnMn2p90= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028084903-1062b33d32c5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2 h1:eSwObLdxs3jxPrlcwdaMo9hzIDTUz2ogWw8cjX8To7I= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2/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=