From 8c5d70a18677ea17a0fdacbc05a537b34b308347 Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 01:39:15 +0000 Subject: [PATCH] mesh-vault: the retirement tools take the controller's consumers, confirm, by and via; vault_status; the export's fingerprint (review 255) Co-Authored-By: Claude Opus 5.5 --- modules/mesh-vault/cmd/mesh-vault/tools.go | 155 ++++++++++++++++----- 1 file changed, 121 insertions(+), 34 deletions(-) diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index 06a1babf..1bd56770 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -3,15 +3,19 @@ package main // What the vault serves: the seat's verb `get`, and the three tools the TypeScript vault offered — // secret_export, secret_holders and secret_verify — answering as they did; beside them secret_handed and // secret_vault_export over the vault's own store, and ADR 0230's four retirement tools over the custody -// ledger. None returns a value: `get` returns one sealed to the asker's key, the others fingerprints and -// ciphertext only. +// ledger, and vault_status. None returns a value: `get` returns one sealed to the asker's key, the others +// fingerprints and ciphertext only. import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" + "fmt" "os" "path/filepath" "strings" + "sync" stdio "git.novox.be/novox/mesh-sdk/go" ) @@ -24,10 +28,25 @@ type Env struct { Receives string Root string Custody *Custody + // refused is why the operator key in the mesh's export was not taken, or empty. + mu sync.Mutex + refused string // Broken is why the vault could not be built, said by `get`. Broken error } +func (e *Env) keyRefused() string { + e.mu.Lock() + defer e.mu.Unlock() + return e.refused +} + +func (e *Env) refuseKey(why string) { + e.mu.Lock() + defer e.mu.Unlock() + e.refused = why +} + // toolDef is a tool with its caller given apart from its arguments, so a test can be the runtime. type toolDef struct { Name string @@ -135,8 +154,9 @@ func definitions(env *Env) []toolDef { return vaultExport(env.Vault) }}, {Name: "provisioner_retirement", - Description: "The custody ledger's holders: active, retired (with when and why), the set counted toward " + - "retirement, and any set waiting for a person or rejected (novox/hq ADR 0230).", + Description: "This provider's retirement (novox/hq ADR 0230): the holders it keeps active, a retirement waiting " + + "for a person, one a person rejected, and every holder retired with when and why. Mark-only: a retired " + + "holder keeps what it has until a person deletes it.", run: func(string, map[string]any) (any, error) { if env.Custody == nil { return nil, errors.New("the vault has no custody ledger") @@ -144,45 +164,102 @@ func definitions(env *Env) []toolDef { return env.Custody.Retirement(), nil }}, {Name: "provisioner_retire_approve", - Description: "Retire exactly the set of holders waiting for a person (novox/hq ADR 0230). Marks them retired; deletes nothing.", - Input: map[string]any{"why": str("why they are retired")}, + Description: "Retire exactly the set waiting for a person (or rejected earlier). Repeat the set in consumers; why is required.", + Input: actInput(true, false), run: func(caller string, a map[string]any) (any, error) { if env.Custody == nil { return nil, errors.New("the vault has no custody ledger") } - why, _ := a["why"].(string) - set, err := env.Custody.Approve(caller, why) - return map[string]any{"retired": set}, err - }}, - {Name: "provisioner_retire_reject", - Description: "Keep the set of holders waiting for a person active; not asked about again while it stays the same set (novox/hq ADR 0230).", - Input: map[string]any{"why": str("why they are kept")}, - run: func(caller string, a map[string]any) (any, error) { - if env.Custody == nil { - return nil, errors.New("the vault has no custody ledger") - } - why, _ := a["why"].(string) - set, err := env.Custody.Reject(caller, why) - return map[string]any{"kept": set}, err - }}, - {Name: "provisioner_delete", - Description: "Delete one retired holder from the custody ledger, its rotation history with it — a person's act " + - "(novox/hq ADR 0230). Refused for a holder that is active or asked for, and without a why.", - Input: map[string]any{"consumer": str("the holder's login"), "why": str("why it is deleted")}, - run: func(caller string, a map[string]any) (any, error) { - if env.Custody == nil { - return nil, errors.New("the vault has no custody ledger") - } - as, _ := a["consumer"].(string) - why, _ := a["why"].(string) - if err := env.Custody.Delete(as, caller, why); err != nil { + done, err := env.Custody.Approve(stringsOf(a["consumers"]), actOf(caller, a)) + if err != nil { return nil, err } - return map[string]any{"deleted": as}, nil + return map[string]any{"retired": done}, nil + }}, + {Name: "provisioner_retire_reject", + Description: "Keep the set waiting for a person active, though the mesh no longer asks for it. Repeat the set in consumers; why is required.", + Input: actInput(true, false), + run: func(caller string, a map[string]any) (any, error) { + if env.Custody == nil { + return nil, errors.New("the vault has no custody ledger") + } + kept, err := env.Custody.Reject(stringsOf(a["consumers"]), actOf(caller, a)) + if err != nil { + return nil, err + } + return map[string]any{"kept": kept}, nil + }}, + {Name: "provisioner_delete", + Description: "Delete one RETIRED holder from the custody ledger, its rotation history with it — never an active one. " + + "Repeat its name in confirm; why is required.", + Input: actInput(false, true), + run: func(caller string, a map[string]any) (any, error) { + if env.Custody == nil { + return nil, errors.New("the vault has no custody ledger") + } + consumer, _ := a["consumer"].(string) + confirm, _ := a["confirm"].(string) + if err := env.Custody.Delete(consumer, confirm, actOf(caller, a)); err != nil { + return nil, err + } + return map[string]any{"deleted": consumer, "freed_bytes": -1}, nil + }}, + {Name: "vault_status", + Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " + + "key the mesh's export holds if the vault refused it, and how often the record of hand-outs was rolled.", + run: func(string, map[string]any) (any, error) { + if env.Vault == nil { + return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil + } + left, err := env.Vault.StillSealedToAnother() + if err != nil { + return nil, err + } + key, _ := env.Vault.Store.OperatorKey() + return map[string]any{"ready": true, "operator_key": key, "copies_on_an_earlier_key": left, + "operator_key_refused": env.keyRefused(), "handed_rolled": env.Vault.Store.Rolled(), + "ok": left == 0 && env.keyRefused() == "" && key != ""}, nil }}, } } +// actInput is the arguments of an act on retirement, as the SDK's harness takes them. +func actInput(set, one bool) map[string]any { + text := map[string]any{"type": "string"} + in := map[string]any{"why": text, "by": text, "via": text} + if set { + in["consumers"] = map[string]any{"type": "array", "items": text} + } + if one { + in["consumer"], in["confirm"] = text, text + } + return in +} + +// actOf is an act's why, by and via. `by` is the person the controller names; with none, the caller the +// runtime proved. +func actOf(caller string, a map[string]any) Act { + act := Act{} + act.Why, _ = a["why"].(string) + act.By, _ = a["by"].(string) + act.Via, _ = a["via"].(string) + if act.By == "" { + act.By = caller + } + return act +} + +func stringsOf(v any) []string { + list, _ := v.([]any) + out := []string{} + for _, x := range list { + if s, ok := x.(string); ok { + out = append(out, s) + } + } + return out +} + func limitOf(a map[string]any) int { if v, ok := a["limit"].(float64); ok && v > 0 { return int(v) @@ -190,6 +267,16 @@ func limitOf(a map[string]any) int { return 50 } +// keyFingerprint is a key's fingerprint as the controller's secrets.Fingerprint says it: sha256 of the key, +// its first eight bytes in hex. +func keyFingerprint(key string) string { + if key == "" { + return "" + } + sum := sha256.Sum256([]byte(strings.TrimSpace(key))) + return "sha256:" + hex.EncodeToString(sum[:8]) +} + // vaultExport is the vault's operator copies in the shape `secret export` writes and `secret recover // --from-export` reads: each as node "vault", module "mesh-vault" and the secret's name. func vaultExport(v *Vault) (map[string]any, error) { @@ -211,7 +298,7 @@ func vaultExport(v *Vault) (map[string]any, error) { earlier = append(earlier, k) } } - return map[string]any{"export": 1, "operator-key": key, "fingerprint": "", "kept": kept, + return map[string]any{"export": 1, "operator-key": key, "fingerprint": keyFingerprint(key), "kept": kept, "sealed-to-earlier-key": earlier}, nil }