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)

This commit is contained in:
2026-10-28 09:48:14 +01:00
parent 671a6a1f47
commit da5da4f6f7
8 changed files with 89 additions and 59 deletions
+11 -1
View File
@@ -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()
+10 -2
View File
@@ -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 {
+27 -23
View File
@@ -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.<machine>`), 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/<machine>/<module>/<name>): %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>.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 ""
+1 -1
View File
@@ -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"`
}
+34 -26
View File
@@ -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/<machine>/<module>/<name>, 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/<machine>/<module>/<name>",
"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/<machine>/<module>/<name>",
"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)
+1 -1
View File
@@ -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