mesh-vault: tests of the bound, the retry's real path and a hand-out without its record; the README corrected (review 255)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head dadf824c3e33 merged into trunk 8aa37e8b835d — pass: the change touches no module of the mesh's graph
mesh/repo-check head dadf824c3e33 merged into trunk 8aa37e8b835d — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 01:39:49 +00:00
co-authored by Claude Opus 5.5
parent 8c5d70a186
commit dadf824c3e
2 changed files with 103 additions and 7 deletions
+23 -4
View File
@@ -96,7 +96,16 @@ the bus user the runtime proved — never an argument.
Every answer and every refusal is recorded in the vault's own store (`handed.jsonl`: who, which secret,
which generation, when, and why a refusal), synced before the answer is given — as ADR 0277 §25 records
asks. `secret_handed` reads it.
asks; a secret whose hand-out cannot be recorded is not handed out. A refusal is recorded only for a caller
of a bus user's shape and a well-formed secret name, so an arbitrary argument fills nothing. The record is
bounded: rolled at 1 MiB into `handed.jsonl.1` … `.3`, the oldest dropped, each roll said loudly in the
journal and counted in `vault_status`. `secret_handed` reads it.
`vault_status` says what needs a person: operator copies still sealed to an earlier key, an operator key in
the export the vault refused, and how often the record was rolled. **It is not yet a condition**: a health
check is declared on a long-running resource (ADR 0240), and the vault is a bundle the tool runner loads, so
nothing asks it on its own; until it runs as a resource of its own (V3's review) these reach a person
through the journal and this tool only.
**Who may ask, today: nobody.** The rule (ADR 0349 §11) is a node's provisioner client under a bus account
of its own, for what its node's declaration names, and a module under its own account (issue 569). Neither
@@ -126,9 +135,19 @@ ten minutes; more than three at once, or more than half of those held, waits for
only by `provisioner_delete`, a person's act with a why. `provisioner_retirement` shows it all. The vault is
mark-only: it holds no access to switch off.
What the TypeScript vault did and this one does not: it consumed its own events only to print an audit
line, which the pass now prints itself; and it withdrew a holder on the first pass that missed it, with its
history, and said `deprovisioned` — that is gone for ADR 0230's retirement.
The words are the controller's: the `provisioner.retirement` event is mesh-controller's `link.Retirement`
(`provider-node`, `consumers` as objects) and the `provisioner_retirement` answer its `link.RetirementState`
(`held`, `bound`, `retires_by: mark-only`), so `retire approve`, `retire reject` and `cleanup list` read the
vault and a set waiting for a person raises the controller's urgent condition. The acts take what the SDK's
harness takes: approve and reject repeat the set in `consumers` and refuse any other set; delete repeats the
name in `confirm`; each records the `by` and `via` the controller sends. `testdata/retirement-*.json` are
those types marshalled by the controller (feat/57 at dc36536), and the tests hold the vault's words to them.
What the TypeScript vault did and this one does not: it consumed its own events only to print an audit line,
which the pass now prints itself. Its retirement ran on the SDK's harness, which this one follows: mark-only,
and a holder's ledger file removed only through a person's delete, as here. The difference is where the mark
lives: the harness kept it in its process, so a restart forgot it (ADR 0230 §13); here it is in the ledger
file.
## What comes later
@@ -560,11 +560,12 @@ func TestANewOperatorKeyFailingPartwayIsFinished(t *testing.T) {
t.Fatalf("%d copies left on the earlier key, not 1", left)
}
v.Store.failWrite = nil
if n, err := v.Reseal(); err != nil || n != 1 {
t.Fatalf("the retry sealed %d again (%v), not the 1 left", n, err)
// The path the minute takes while the export holds the key: the same key again.
if n, err := v.SetOperatorKey(asPEM(t, pub), "the test"); err != nil || n != 1 {
t.Fatalf("the same key again sealed %d again (%v), not the 1 left", n, err)
}
if n, err := v.SetOperatorKey(asPEM(t, pub), "the test"); err != nil || n != 0 {
t.Fatalf("the same key again sealed %d (%v)", n, err)
t.Fatalf("the same key a third time sealed %d (%v)", n, err)
}
all, _ := v.Store.List()
for _, rec := range all {
@@ -651,3 +652,79 @@ func TestTheOperatorRecoversAVaultSecretFromItsExport(t *testing.T) {
t.Error("the export holds the value in the clear")
}
}
// With no key in the export the minute calls Reseal, which finishes what is pending too.
func TestResealFinishesWhatIsPending(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Mint("a/one", "the test"); err != nil {
t.Fatal(err)
}
pub, _ := operatorKey(t)
v.Store.failWrite = func(Record) error { return os.ErrPermission }
if _, err := v.SetOperatorKey(asPEM(t, pub), "the test"); err == nil {
t.Fatal("a failing write was not said")
}
v.Store.failWrite = nil
if n, err := v.Reseal(); err != nil || n != 1 {
t.Fatalf("%d %v", n, err)
}
}
// A secret is not handed out when who was handed it cannot be recorded.
func TestNoHandOutWithoutItsRecord(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(v.dir, "store", "handed.jsonl"), 0o700); err != nil {
t.Fatal(err)
}
asker, _ := seal.GenerateKeyPair()
if a, err := v.Get("g14.node-provisioner", "pair/postgres/gitea", asker.PublicKey); err == nil {
t.Fatalf("handed out with no record of it: %+v", a)
}
}
// What an asker cannot shape fills no record: a name that is no secret's name, a caller that is no bus user.
func TestAnArbitraryAskFillsNoRecord(t *testing.T) {
v := newVault(t, nil)
asker, _ := seal.GenerateKeyPair()
for _, ask := range [][2]string{{"g14.x", strings.Repeat("junk ", 1000)}, {"g14.x", "../../etc"},
{"not a bus user!", "pair/a"}, {strings.Repeat("a", 300), "pair/a"}} {
_, _ = v.Get(ask[0], ask[1], asker.PublicKey)
}
if h, _ := v.Store.Handed(10); len(h) != 0 {
t.Errorf("recorded %d arbitrary asks: %+v", len(h), h)
}
}
// The record of hand-outs is bounded: rolled at its size into a fixed number of files, the oldest dropped.
func TestTheRecordOfHandOutsIsBounded(t *testing.T) {
v := newVault(t, nil) // refuses everyone, as the running vault does
v.Store.HandedMax = 2000
asker, _ := seal.GenerateKeyPair()
for i := 0; i < 500; i++ {
_, _ = v.Get("g14.flood", "pair/postgres/gitea", asker.PublicKey)
}
var total int64
files, _ := filepath.Glob(filepath.Join(v.dir, "store", "handed.jsonl*"))
if len(files) != HandedKept+1 {
t.Errorf("%d files of the record, not %d: %v", len(files), HandedKept+1, files)
}
for _, f := range files {
info, _ := os.Stat(f)
if info.Size() > 2000 {
t.Errorf("%s is %d bytes, over its bound", f, info.Size())
}
total += info.Size()
}
if total > int64(HandedKept+1)*2000 {
t.Errorf("the record holds %d bytes", total)
}
if v.Store.Rolled() == 0 {
t.Error("rolling was not counted")
}
if h, _ := v.Store.Handed(5); len(h) != 5 || h[0].To != "g14.flood" {
t.Errorf("the newest are not read back: %+v", h)
}
}