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" ] } ]