Compare commits

...
Author SHA1 Message Date
mesh-adminandClaude Opus 5.5 eb958f668e Prove the vault's secret-ask test and the issuing provider's test with a stand-in provider (issue 675)
mesh/merge-gate head eb958f668ecd merged into trunk 1a2c917abfdb — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] t…
mesh/repo-check head eb958f668ecd merged into trunk 1a2c917abfdb — fail: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] i…
mesh/delivery-group group pkg/57-v2-vault-verbs rejected: a member's own check failed
mesh/delivery stopped: the pull request closed unmerged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:14:34 +00:00
mesh-adminandClaude Opus 5.5 63b7aec065 Prove V1's review follow-ups (issue 675)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:13:09 +00:00
mesh-adminandClaude Opus 5.5 efddb634e5 Open a value only in the vault, sealed to a key made for its call (issue 675)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:12:21 +00:00
mesh-adminandClaude Opus 5.5 eba300794f Serve the vault's intake verbs and say how they work (issue 675)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:10:55 +00:00
mesh-adminandClaude Opus 5.5 2abe1f95e8 Take accept, secret-ask, issued and rotate into the vault (issue 675)
ADR 0349 clauses 8 to 10: a value given at the controller's terminal, typed
at a desk or issued by a provider's software enters the vault and opens
there alone, and rotation is the vault's. Each comes in two steps from the
controller, which reaches the desk and the provider: the vault makes a key
for the one call and opens what was sealed to it. An ask is recorded in the
vault's store before the prompt opens and bounded to one open per secret
and three an hour.

Also V1's review follow-ups: the tool layer passes an act through, a
rolled record is said, the export's fingerprint and vault_status are
tested, and an unproven by is kept beside the proven caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:10:03 +00:00
8 changed files with 1455 additions and 8 deletions
+56 -4
View File
@@ -112,6 +112,54 @@ of its own, for what its node's declaration names, and a module under its own ac
exists yet, so the running vault refuses every asker; the tests set a stand-in list. The real rule goes
behind the `Askers` interface in `askers.go` (design 57's V4 and P3).
## Taking values in: `accept`, `secret-ask`, `issued`, `rotate`
Design 57's V2 (issue 675; ADR 0349 clauses 8 to 10, ADR 0277 §22–§27 and ADR 0305 with the vault in the
controller's place). `intake.go`.
**Two steps of one verb, from the controller alone.** The first step names the secret and answers a `call` and
`seal_to`, the public half of an X25519 key the vault made for that one call (raw, base64 — the NaCl anonymous box
the desk's prompt and a provider's tool already seal to). The second step sends `call` and `sealed`, and the vault
opens it with the private half, which lives in this process only and is forgotten once used or after two minutes.
A value sealed to any other key is refused, and a call is answered once.
**Why the controller carries it.** A desk's prompt (`node-launcher.secret`) and a provider's tool answer the
controller alone: the bus lets only the controller ask them, on the subject that names its caller (issue 365). The
vault cannot call in a name of its own while it runs in its machine's tool runner, whose one bus account carries
that machine's agents' calls too (issue 569, ADR 0305 §3). So the controller asks them with the vault's key and
passes the sealed answer on; it cannot open it. The value opens in the vault and nowhere else. The vault refuses
these steps from any caller but `controller`, as the runtime proved it.
- `accept` — node, module, name (a module's own secret) or provider, provision (a provision's one credential),
and `by`. The controller's terminal seals the value it read to the call's key before it leaves the terminal.
Kept with origin `accepted`; an accepted provision credential is never remade (`rotate` refuses it); a value a
provider's software issued is not taken by `accept`.
- `secret-ask` — node, module, name, desk and `asked_by` (who asked the controller, as the bus named its caller).
The ask is recorded in `asks.jsonl` before anything else, and refused while another ask for the same secret is
open (two minutes at most) or when three were made in the last hour. The answer names who asked as the record
has it, which the prompt shows. The second step carries what the prompt sealed, or `outcome` (dismissed,
timed-out, empty, refused, failed); how each ask ended is recorded. `secret_asks` reads the record. The hand-act
log stays the controller's: it records the act once the vault has taken the value.
- `issued` — provision, provider (machine), module. The controller asks the provider's tool with the call's key,
as it asks it today; the value is kept as the provision's one credential with origin `issued`. The same value
again writes nothing, says nothing and keeps the generation (ADR 0305 §5).
- `rotate` — a module's own secret or a provision's credential, made anew. Refused for an `issued` value (the
software rotates it, ADR 0305 §9) and an accepted provision credential (clause 8). Who asked is the proven caller,
or the `by` the controller names when it carries the act; both are kept (`changed-by`, `changed-via`).
Names in the store: `own/<node>/<module>/<name>` and `provision/<provider node>/<provision>`. V3 copies the
controller's rows in under these names.
**Which modules reach the vault this way.** The controller sends a module's verbs here only when its secrets are
filled the new way (its machine asks the vault for them, V4); every other module keeps the controller's path, so
no value lands where its machine never looks (ADR 0349 clause 17; the release lead's decision on issue 675). Until
V4 that is no module: these verbs are served and tested, and nothing calls them yet.
**What the issuing provider's test is here.** `TestAnIssuingProviderAnswersOnlyTheControllersCallSealedToTheVault`
uses a stand-in provider that refuses every caller but the controller and answers only sealed to the call's key.
plex's own tool already answers the controller alone; whether it moves with the media catalogue is for that move,
after release 57.
## Events
- `secret-changed` — `name`, `by`, `at`, `generation`: which secret, who changed it, when. Never a value
@@ -151,9 +199,9 @@ file.
## What comes later
Taking values in (`secret accept`, `secret-ask`, `issued`, `rotate`) is V2; the controller's secrets move
in with V3; nodes ask with V4; pair credentials are minted here with V5. Until V3 the vault's store is
empty and the controller still serves every secret.
Taking values in is built (above); the controller's secrets move in with V3; nodes ask with V4, which also turns
the controller's switch on for each module it moves; pair credentials are minted here with V5. Until V3 the
vault's store is empty and the controller still serves every secret.
## Tests
@@ -161,4 +209,8 @@ empty and the controller still serves every secret.
`vault_test.go` is ADR 0349's vault test list as far as this package goes: no bucket holds a secret; no
private key is accepted; every secret has an operator-sealed copy; `get` answers only an allowed asker,
sealed to its key; `secret-changed` carries no value.
sealed to its key; `secret-changed` carries no value. `intake_test.go` is ADR 0349's `secret-ask` test as far as
the vault answers it (recorded before the prompt, one open ask per secret and three an hour, the asker named as
the bus named it, the typed value opened only here), the issuing provider's test with a stand-in provider (ADR 0305
§13's checks that belong to the vault: nothing rewritten for a value held, `rotate` refused, no other way in), and
accept and rotate. `followups_test.go` holds V1's review follow-ups.
@@ -0,0 +1,162 @@
package main
// The four follow-ups V1's third review (review 263 on mesh-catalog#286) left for this package: the tool layer
// passes an act's words through; a rolled record of hand-outs is said in the journal; the export's fingerprint
// and vault_status are what they say; and an act's unproven `by` is kept beside the proven caller.
import (
"fmt"
"sort"
"strings"
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/seal"
)
func anyOf(list []string) []any {
out := make([]any, len(list))
for i, s := range list {
out[i] = s
}
return out
}
// (1) and (4): the retirement tools pass consumers, why, by, via and confirm through to the custody ledger, and
// a `by` naming somebody other than the proven caller is kept beside it.
func TestTheRetirementToolsPassTheActThrough(t *testing.T) {
c := newCustody(t)
c.ask(apps(5))
c.passes(1)
c.ask(keepOnly(apps(5), 1))
c.stable()
set := waitingNames(c.Retirement())
sort.Strings(set)
defs := definitions(&Env{Custody: c.Custody})
approve := toolNamed(t, defs, "provisioner_retire_approve")
if _, err := approve.runAs(ControllerCaller, map[string]any{"consumers": anyOf(set[:2]), "why": "x"}); err == nil {
t.Error("the tool took a set other than the one waiting")
}
got, err := approve.runAs(ControllerCaller, map[string]any{"consumers": anyOf(set), "why": "the apps are gone",
"by": "person.jochen", "via": "telegram"})
if err != nil {
t.Fatal(err)
}
if done := got.(map[string]any)["retired"].([]string); len(done) != 4 {
t.Errorf("retired %v", done)
}
last := c.bus.events[len(c.bus.events)-5]["body"].(map[string]any)
for _, e := range c.bus.events {
if b := e["body"].(map[string]any); b["change"] == "approved" {
last = b
}
}
if last["why"] != "the apps are gone" || last["by"] != "person.jochen" || last["via"] != "telegram, called by controller" {
t.Errorf("the approval said %v", last)
}
// Delete: consumer and confirm through, the act's words with them.
r := retiredC(t)
del := toolNamed(t, definitions(&Env{Custody: r.Custody}), "provisioner_delete")
if _, err := del.runAs("person.jochen", map[string]any{"consumer": "d-app", "confirm": "c-app", "why": "x"}); err == nil {
t.Error("a confirm naming another was taken through the tool")
}
if _, err := del.runAs("person.jochen", map[string]any{"consumer": "d-app", "confirm": "d-app", "why": "gone"}); err != nil {
t.Fatal(err)
}
body := r.bus.events[len(r.bus.events)-1]["body"].(map[string]any)
if body["change"] != "deleted" || body["by"] != "person.jochen" || body["via"] != nil || body["why"] != "gone" {
t.Errorf("the deletion said %v", body)
}
// Reject, by the proven caller alone: nothing more is added to via.
k := newCustody(t)
k.ask(apps(5))
k.passes(1)
k.ask(keepOnly(apps(5), 1))
k.stable()
waiting := waitingNames(k.Retirement())
if _, err := toolNamed(t, definitions(&Env{Custody: k.Custody}), "provisioner_retire_reject").runAs("person.jochen",
map[string]any{"consumers": anyOf(waiting), "why": "they come back"}); err != nil {
t.Fatal(err)
}
if body := k.bus.events[len(k.bus.events)-1]["body"].(map[string]any); body["by"] != "person.jochen" || body["via"] != nil {
t.Errorf("the rejection said %v", body)
}
}
// (2): a roll of the record of hand-outs is said in the journal, loudly.
func TestARolledRecordOfHandOutsIsSaid(t *testing.T) {
v := newVault(t, nil)
var mu sync.Mutex
var said []string
v.Log = func(format string, args ...any) {
mu.Lock()
defer mu.Unlock()
said = append(said, fmt.Sprintf(format, args...))
}
v.Store.HandedMax = 600
asker, _ := seal.GenerateKeyPair()
for i := 0; i < 20; i++ {
_, _ = v.Get("g14.flood", "pair/postgres/gitea", asker.PublicKey)
}
rolled := 0
for _, s := range said {
if strings.Contains(s, "THE RECORD OF HAND-OUTS WAS ROLLED") {
rolled++
}
}
if rolled == 0 || rolled != v.Store.Rolled() {
t.Errorf("rolled %d times, said %d times", v.Store.Rolled(), rolled)
}
}
// (3): the vault's export names the operator key's fingerprint as the controller's secrets.Fingerprint says it,
// and vault_status says what needs a person.
func TestTheExportsFingerprintAndVaultStatus(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
t.Fatal(err)
}
answer, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_vault_export").Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
doc := answer.(map[string]any)
// sha256 of the key, its first eight bytes in hex: the controller's secrets.Fingerprint.
if fp := doc["fingerprint"].(string); !strings.HasPrefix(fp, "sha256:") || len(fp) != len("sha256:")+16 ||
fp != keyFingerprint(v.operatorPublic) || doc["operator-key"] != v.operatorPublic {
t.Errorf("the export says %v", doc)
}
if keyFingerprint("") != "" {
t.Error("no key has a fingerprint")
}
env := &Env{Vault: v.Vault}
status := func() map[string]any {
got, err := toolNamed(t, definitions(env), "vault_status").Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
return got.(map[string]any)
}
if s := status(); s["ok"] != true || s["copies_on_an_earlier_key"] != 0 || s["operator_key"] != v.operatorPublic {
t.Errorf("a vault in order says %v", s)
}
env.refuseKey("a private key was offered")
if s := status(); s["ok"] != false || s["operator_key_refused"] != "a private key was offered" {
t.Errorf("a refused key says %v", s)
}
env.refuseKey("")
// A new operator key whose resealing fails leaves copies on the earlier key, and the status says so.
pub, _ := operatorKey(t)
v.Store.failWrite = func(Record) error { return fmt.Errorf("disk full") }
_, _ = v.SetOperatorKey(asPEM(t, pub), "the test")
v.Store.failWrite = nil
if s := status(); s["ok"] != false || s["copies_on_an_earlier_key"] != 1 {
t.Errorf("a copy on an earlier key says %v", s)
}
if s, _ := toolNamed(t, definitions(&Env{}), "vault_status").Run(map[string]any{}); s.(map[string]any)["ready"] != false {
t.Errorf("no vault says %v", s)
}
}
+472
View File
@@ -0,0 +1,472 @@
package main
// What the vault takes in, and how it rotates (novox/hq ADR 0349 clauses 8 to 10; ADR 0277 §22–§27 and ADR 0305
// with the vault in the controller's place; design 57's V2, issue 675).
//
// Four verbs of the seat: `accept` (a value given at the controller's terminal), `secret-ask` (a value the
// operator types at a desk), `issued` (a value a provider's software issued) and `rotate`.
//
// **A value comes in sealed to a key the vault made for that one call, and opens only here.** The first three
// are two steps of one verb. The first names what is taken and answers a call and the public half of a key made
// for it (`seal_to`, a raw X25519 key in base64, the NaCl anonymous box the desk's prompt and a provider's tool
// already seal to); the second hands back what was sealed to that key, and the vault opens it with the private
// half, which never leaves this process and is forgotten once used or after two minutes.
//
// **The controller carries the call, and cannot open what it carries.** The desk's prompt and a provider's tool
// answer the controller alone: the bus lets only the controller ask them, on the subject that names its caller
// (issue 365). The vault cannot call in a name of its own while it runs in its machine's tool runner, whose one
// bus account carries that machine's agents' calls too (issue 569, ADR 0305 §3). So the controller asks them on
// the vault's behalf with the vault's key, and passes on the sealed answer: the value is opened in the vault and
// nowhere else (ADR 0349 clause 9, ADR 0277 §23), and the vault takes these steps from the controller alone.
//
// Which secret each one is, by name in the vault's store:
//
// own/<node>/<module>/<name> a module's own secret on one machine
// provision/<provider node>/<provision> the one credential of a provider assignment and provision (clause 8)
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"golang.org/x/crypto/nacl/box"
)
// ControllerCaller is the controller's bus user: the one caller that carries a value in (broker.ControllerName).
const ControllerCaller = "controller"
// Where a value came from.
const (
OriginMade = "made"
OriginAccepted = "accepted"
OriginTyped = "typed"
OriginIssued = "issued"
)
// CallOpenFor is how long a call's key waits for its sealed value: longer than any prompt or tool waits.
const CallOpenFor = 2 * time.Minute
// The bounds of asking for one secret (ADR 0277 §24, the controller's inventory.SecretAsk* until now).
const (
// SecretAskOpenFor is how long an ask that has not ended counts as open.
SecretAskOpenFor = 2 * time.Minute
// SecretAsksPerHour is how many asks for one secret an hour takes.
SecretAsksPerHour = 3
)
var namePart = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,62}$`)
// OwnSecret is the vault's name for a module's own secret on one machine.
func OwnSecret(node, module, name string) (string, error) {
for what, v := range map[string]string{"the machine": node, "the module": module, "the secret": name} {
if !namePart.MatchString(v) {
return "", fmt.Errorf("%s %q is not a name the mesh writes", what, v)
}
}
return "own/" + node + "/" + module + "/" + name, nil
}
// ProvisionSecret is the vault's name for the one credential of a provider assignment and provision.
func ProvisionSecret(provider, provision string) (string, error) {
for what, v := range map[string]string{"the provider's machine": provider, "the provision": provision} {
if !namePart.MatchString(v) {
return "", fmt.Errorf("%s %q is not a name the mesh writes", what, v)
}
}
return "provision/" + provider + "/" + provision, nil
}
// callKey is one call's key and what the call takes in. Never written anywhere.
type callKey struct {
verb, secret, by string
// ask is the ask's record for `secret-ask`, which the call's id is.
ask *AskRecord
public [32]byte
private [32]byte
until time.Time
}
// Opened is a first step's answer: the call, and the key to seal its value to. Never a value.
type Opened struct {
Call string `json:"call"`
SealTo string `json:"seal_to"`
Secret string `json:"secret"`
AskedBy string `json:"asked_by,omitempty"`
}
// fromTheController refuses every caller but the controller: only it reaches a desk's prompt and a provider's
// tool, and only it carries a value given at its terminal.
func fromTheController(caller, verb string) error {
if caller != ControllerCaller {
if caller == "" {
caller = stdio.Unproven
}
return fmt.Errorf("%s takes a value from the controller alone, which carries it sealed to this vault's key "+
"for one call; %s is not the controller", verb, caller)
}
return nil
}
// openCall makes a key for one call and keeps its private half here until the call is taken or two minutes
// have passed.
func (v *Vault) openCall(verb, secret, by string, ask *AskRecord) (string, string, error) {
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
return "", "", err
}
idRaw := make([]byte, 12)
if _, err := rand.Read(idRaw); err != nil {
return "", "", err
}
id := hex.EncodeToString(idRaw)
if ask != nil {
id = ask.ID
}
v.callsMu.Lock()
defer v.callsMu.Unlock()
if v.pending == nil {
v.pending = map[string]*callKey{}
}
now := v.Now()
for k, c := range v.pending {
if now.After(c.until) {
delete(v.pending, k)
}
}
v.pending[id] = &callKey{verb: verb, secret: secret, by: by, ask: ask, public: *pub, private: *priv,
until: now.Add(CallOpenFor)}
return id, base64.StdEncoding.EncodeToString(pub[:]), nil
}
// takeCall ends a call: its key is forgotten whatever follows.
func (v *Vault) takeCall(verb, id string) (*callKey, error) {
v.callsMu.Lock()
defer v.callsMu.Unlock()
c, ok := v.pending[id]
if !ok || c.verb != verb {
return nil, fmt.Errorf("no %s call %q is open in the vault: it ended, it was never opened, or the vault "+
"started again; open it anew", verb, id)
}
delete(v.pending, id)
if v.Now().After(c.until) {
return nil, fmt.Errorf("the %s call %q waited longer than %s and is closed; open it anew", verb, id, CallOpenFor)
}
return c, nil
}
// open opens what was sealed to the call's key, and forgets the key.
func (c *callKey) open(sealed string) ([]byte, error) {
defer func() { c.private = [32]byte{} }()
blob, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sealed))
if err != nil {
return nil, errors.New("the value is not sealed to this call; nothing was taken")
}
out, ok := box.OpenAnonymous(nil, blob, &c.public, &c.private)
if !ok {
// Never the value, never what failed to open: only that it was not sealed to this call.
return nil, errors.New("the value is not sealed to this call; nothing was taken")
}
return out, nil
}
func zero(b []byte) {
for i := range b {
b[i] = 0
}
}
// ---- accept -------------------------------------------------------------------------------------------------
// AcceptOpen opens a call to take a value given at the controller's terminal for secret, which the terminal
// seals to the call's key before it leaves it (ADR 0349 clause 8: the vault alone opens an accepted value).
func (v *Vault) AcceptOpen(caller, secret, by string) (Opened, error) {
if err := fromTheController(caller, "accept"); err != nil {
return Opened{}, err
}
if err := validName(secret); err != nil {
return Opened{}, err
}
if rec, held, err := v.Store.Read(secret); err != nil {
return Opened{}, err
} else if held && rec.Origin == OriginIssued {
return Opened{}, fmt.Errorf("%s is issued by its provider's software, and `secret issued` is the only way "+
"it enters the vault (ADR 0305 §10); nothing is taken", secret)
}
id, key, err := v.openCall("accept", secret, by, nil)
if err != nil {
return Opened{}, err
}
return Opened{Call: id, SealTo: key, Secret: secret}, nil
}
// AcceptTake opens the value sealed to the call's key and keeps it, accepted: never remade by the vault.
func (v *Vault) AcceptTake(caller, call, sealed string) (Changed, error) {
if err := fromTheController(caller, "accept"); err != nil {
return Changed{}, err
}
c, err := v.takeCall("accept", call)
if err != nil {
return Changed{}, err
}
value, err := c.open(sealed)
if err != nil {
return Changed{}, err
}
defer zero(value)
if strings.TrimSpace(string(value)) == "" {
return Changed{}, errors.New("the value given is empty; nothing was taken")
}
return v.keep(c.secret, string(value), someoneOr(c.by, caller), caller, OriginAccepted)
}
// ---- secret-ask ---------------------------------------------------------------------------------------------
// AskOf is what an ask is for, and where.
type AskOf struct {
Node, Module, Name, Desk string
// AskedBy is who asked the controller, as the bus named its caller; the controller's word, taken from the
// controller alone.
AskedBy string
}
// AskOpen records an ask for a module's own secret before any prompt opens, holding its bounds — one open ask
// per secret, three an hour (ADR 0277 §24, §25) — and answers the key the desk's prompt seals what is typed to,
// and who asked as the record says it, which the prompt names (§26).
func (v *Vault) AskOpen(caller string, a AskOf) (Opened, error) {
if err := fromTheController(caller, "secret-ask"); err != nil {
return Opened{}, err
}
secret, err := OwnSecret(a.Node, a.Module, a.Name)
if err != nil {
return Opened{}, fmt.Errorf("nobody was asked to type anything: %w", err)
}
if !namePart.MatchString(a.Desk) {
return Opened{}, fmt.Errorf("nobody was asked to type anything: the desk %q is not a machine's name", a.Desk)
}
askedBy := askedByName(a.AskedBy)
v.Store.asksMu.Lock()
defer v.Store.asksMu.Unlock()
now := v.Now().UTC()
recent, err := v.Store.AsksSince(now.Add(-time.Hour))
if err != nil {
return Opened{}, fmt.Errorf("nobody was asked to type anything: the record of asks cannot be read: %w", err)
}
ended := map[string]bool{}
for _, r := range recent {
if r.Event == AskEnded {
ended[r.ID] = true
}
}
lastHour, openBy := 0, ""
for _, r := range recent {
if r.Event != AskOpened || r.Secret != secret {
continue
}
lastHour++
if !ended[r.ID] && r.At.After(now.Add(-SecretAskOpenFor)) {
openBy = r.AskedBy
}
}
if openBy != "" {
return Opened{}, fmt.Errorf("an ask for %s of %s on %s is still open (asked by %s): one prompt at a time for a "+
"secret, and this one ends within %s", a.Name, a.Module, a.Node, openBy, SecretAskOpenFor)
}
if lastHour >= SecretAsksPerHour {
return Opened{}, fmt.Errorf("%s of %s on %s was asked for %d times in the last hour, and an hour takes %d asks "+
"for one secret: the operator is not kept at a prompt", a.Name, a.Module, a.Node, lastHour, SecretAsksPerHour)
}
idRaw := make([]byte, 12)
if _, err := rand.Read(idRaw); err != nil {
return Opened{}, err
}
rec := AskRecord{ID: hex.EncodeToString(idRaw), At: now, Event: AskOpened, Secret: secret, Node: a.Node,
Module: a.Module, Name: a.Name, Desk: a.Desk, AskedBy: askedBy, Caller: caller}
if err := v.recordAsk(rec); err != nil {
return Opened{}, fmt.Errorf("nobody was asked to type anything: the ask could not be recorded: %w", err)
}
id, key, err := v.openCall("secret-ask", secret, askedBy, &rec)
if err != nil {
_ = v.recordAsk(AskRecord{ID: rec.ID, At: v.Now().UTC(), Event: AskEnded, Secret: secret, Outcome: "failed"})
return Opened{}, err
}
return Opened{Call: id, SealTo: key, Secret: secret, AskedBy: askedBy}, nil
}
// The outcomes an ask ends with, as the controller's record had them.
var askOutcomes = map[string]bool{"dismissed": true, "timed-out": true, "empty": true, "refused": true, "failed": true}
// AskEnd ends an ask: with what the desk sealed to its key, which the vault opens and keeps as typed, or with
// the outcome the desk answered instead. How it ended is recorded whatever happens.
func (v *Vault) AskEnd(caller, call, sealed, outcome string) (Changed, error) {
if err := fromTheController(caller, "secret-ask"); err != nil {
return Changed{}, err
}
if sealed == "" && !askOutcomes[outcome] {
return Changed{}, fmt.Errorf("an ask ends with what was sealed to its key, or with one of dismissed, " +
"timed-out, empty, refused or failed")
}
c, err := v.takeCall("secret-ask", call)
if err != nil {
return Changed{}, err
}
end := func(how string) {
if err := v.recordAsk(AskRecord{ID: c.ask.ID, At: v.Now().UTC(), Event: AskEnded, Secret: c.secret,
Outcome: how}); err != nil {
v.log("the end of ask %s (%s) could not be recorded: %v", c.ask.ID, how, err)
}
}
if sealed == "" {
end(outcome)
return Changed{Name: c.secret, Unchanged: true}, nil
}
opened, err := c.open(sealed)
if err != nil {
end("failed")
return Changed{}, err
}
value := strings.TrimRight(string(opened), "\r\n")
zero(opened)
if strings.TrimSpace(value) == "" {
end("empty")
return Changed{}, fmt.Errorf("nothing was given: the prompt on %s was answered empty", c.ask.Desk)
}
changed, err := v.keep(c.secret, value, c.ask.AskedBy, caller, OriginTyped)
value = ""
if err != nil {
end("failed")
return Changed{}, err
}
end("given")
return changed, nil
}
func (v *Vault) recordAsk(r AskRecord) error {
rolled, err := v.Store.RecordAsk(r)
if rolled {
v.log("THE RECORD OF ASKS WAS ROLLED: it reached its bound of %d bytes; the oldest of %d kept files is "+
"dropped. Many asks in little time is worth a look: secret_asks", v.Store.HandedBound(), HandedKept)
}
return err
}
// askedByName is who asked as the prompt and the record name it: the first clause of what the bus said, in the
// characters a name has, at most 80 (the controller's own rule, ADR 0277 §26), so no words of the caller's own
// reach the prompt.
func askedByName(caller string) string {
first, _, _ := strings.Cut(caller, ",")
var b strings.Builder
for _, r := range strings.TrimSpace(first) {
if b.Len() >= 80 {
break
}
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '_', r == '/', r == '@',
r == '-', r == ' ':
b.WriteRune(r)
default:
b.WriteRune('-')
}
}
name := strings.TrimSpace(b.String())
if name == "" || strings.HasPrefix(name, "-") {
return "an unnamed caller"
}
return name
}
// ---- issued -------------------------------------------------------------------------------------------------
// IssuedOpen opens a call to take the credential a provider's software issued for a provision (ADR 0349 clause
// 10, ADR 0305 with the vault in the controller's place): the controller asks the provider's tool, on the
// provider's machine, with the call's key, and passes on what it answered.
func (v *Vault) IssuedOpen(caller, provision, provider, module string) (Opened, error) {
if err := fromTheController(caller, "issued"); err != nil {
return Opened{}, err
}
secret, err := ProvisionSecret(provider, provision)
if err != nil {
return Opened{}, err
}
if !namePart.MatchString(module) {
return Opened{}, fmt.Errorf("the provider's module %q is not a name the mesh writes", module)
}
id, key, err := v.openCall("issued", secret, module+" on "+provider, nil)
if err != nil {
return Opened{}, err
}
return Opened{Call: id, SealTo: key, Secret: secret}, nil
}
// IssuedTake opens what the provider's tool sealed to the call's key and keeps it as the provision's one
// credential. The same value again writes nothing (ADR 0305 §5): every holder that has it is left as it is.
func (v *Vault) IssuedTake(caller, call, sealed string) (Changed, error) {
if err := fromTheController(caller, "issued"); err != nil {
return Changed{}, err
}
c, err := v.takeCall("issued", call)
if err != nil {
return Changed{}, err
}
opened, err := c.open(sealed)
if err != nil {
return Changed{}, err
}
value := strings.TrimSpace(string(opened))
zero(opened)
if value == "" {
return Changed{}, fmt.Errorf("%s answered an empty value; nothing was taken", c.by)
}
return v.keep(c.secret, value, c.by, caller, OriginIssued)
}
// ---- rotate -------------------------------------------------------------------------------------------------
// Rotate makes a secret anew (ADR 0114, ADR 0228). Refused: a secret the vault does not hold; a value a
// provider's software issued, which the software rotates (ADR 0305 §9); and a provision's accepted credential,
// which the vault does not remake (ADR 0349 clause 8, ADR 0158 §8). A module's own value given by hand is
// rotated like a made one (ADR 0228). Who asked is the caller the runtime proved, or the person the controller
// names when the controller carries the act — both are kept.
func (v *Vault) Rotate(caller, secret, by string) (Changed, error) {
if caller == "" || caller == stdio.Unproven || !busUser.MatchString(caller) {
return Changed{}, errors.New("the vault rotates only for a caller the runtime proved")
}
if err := validName(secret); err != nil {
return Changed{}, err
}
rec, held, err := v.Store.Read(secret)
if err != nil {
return Changed{}, err
}
switch {
case !held:
return Changed{}, fmt.Errorf("not rotated: the vault holds no secret named %s", secret)
case rec.Origin == OriginIssued:
return Changed{}, fmt.Errorf("not rotated: %s is issued by its provider's software, which the mesh cannot "+
"make or rotate (ADR 0305 §9): change it in that software, and `secret issued` takes the value it holds now",
secret)
case rec.Origin == OriginAccepted && strings.HasPrefix(secret, "provision/"):
return Changed{}, fmt.Errorf("not rotated: %s was accepted, and an accepted credential is not remade by the "+
"vault (ADR 0349 clause 8): accept its new value instead", secret)
}
if caller != ControllerCaller || by == "" {
by = caller
}
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return Changed{}, err
}
return v.keep(secret, base64.RawURLEncoding.EncodeToString(raw), by, caller, OriginMade)
}
func someoneOr(by, caller string) string {
if strings.TrimSpace(by) == "" {
return caller
}
return by
}
@@ -0,0 +1,495 @@
package main
// ADR 0349's tests for what the vault takes in (design 57's V2, issue 675): the vault's `secret-ask` test, the
// issuing provider's test with a stand-in provider (plex's own tool moves with the media catalogue, after
// release 57), an accepted value opened in the vault alone and never remade, and rotate.
//
// In each, "the controller" is a test function that holds no private key: it only carries what the desk or the
// provider sealed to the key the vault made for the call, as the controller does on the bus.
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/seal"
"golang.org/x/crypto/nacl/box"
)
// sealToCall seals value to a call's key as the desk's prompt and a provider's tool do: the NaCl anonymous box.
func sealToCall(t *testing.T, sealTo, value string) string {
t.Helper()
raw, err := base64.StdEncoding.DecodeString(sealTo)
if err != nil || len(raw) != 32 {
t.Fatalf("seal_to %q is not a key", sealTo)
}
var key [32]byte
copy(key[:], raw)
sealed, err := box.SealAnonymous(nil, []byte(value), &key, rand.Reader)
if err != nil {
t.Fatal(err)
}
return base64.StdEncoding.EncodeToString(sealed)
}
// handedTo opens what `get` hands an allowed asker: the value the vault holds.
func handedTo(t *testing.T, v testVault, asker, name string) string {
t.Helper()
key, _ := seal.GenerateKeyPair()
a, err := v.Get(asker, name, key.PublicKey)
if err != nil {
t.Fatal(err)
}
value, err := seal.Open(a.Sealed, key.PrivateKey)
if err != nil {
t.Fatal(err)
}
return value
}
// everything is all the vault said and wrote during a test, as text.
func everything(t *testing.T, v testVault, more ...any) string {
t.Helper()
var b strings.Builder
for _, said := range v.bus.raw {
b.WriteString(said)
}
for _, content := range filesUnder(t, v.dir) {
b.WriteString(content)
}
for _, m := range more {
raw, _ := json.Marshal(m)
b.Write(raw)
}
return b.String()
}
const typedAtTheDesk = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g"
func telegramAsk(by string) AskOf {
return AskOf{Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: by}
}
// ---- the vault's secret-ask test (ADR 0349 "How it is checked") ---------------------------------------------
// An ask is recorded in the vault's store before the prompt opens; its prompt names the asker as the bus named
// it; the typed value opens only in the vault; the controller carries ciphertext only.
func TestAnAskIsRecordedBeforeThePromptAndTheTypedValueOpensOnlyInTheVault(t *testing.T) {
v := newVault(t, AllowList{"anchor.node-provisioner": {"own/anchor/telegram/telegram-token"}})
opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14/claude-code, via the console"))
if err != nil {
t.Fatal(err)
}
// Recorded before any prompt opens: the record holds the ask now, with no end.
asks, _ := v.Store.Asks(10)
if len(asks) != 1 || asks[0].Event != AskOpened || asks[0].ID != opened.Call || asks[0].AskedBy != "g14/claude-code" ||
asks[0].Caller != ControllerCaller || asks[0].Desk != "laptop" || asks[0].Secret != "own/anchor/telegram/telegram-token" {
t.Fatalf("the record before the prompt: %+v", asks)
}
// The prompt names the asker as the bus named it, cut to a name: what the vault answers is what it recorded.
if opened.AskedBy != "g14/claude-code" {
t.Errorf("the prompt would name %q", opened.AskedBy)
}
// The desk seals what is typed to the call's key; the controller passes it on.
changed, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, typedAtTheDesk+"\n"), "")
if err != nil {
t.Fatal(err)
}
if changed.Generation != 1 || changed.By != "g14/claude-code" {
t.Errorf("kept as %+v", changed)
}
if got := handedTo(t, v, "anchor.node-provisioner", "own/anchor/telegram/telegram-token"); got != typedAtTheDesk {
t.Errorf("the vault holds %q, not what was typed with its line ending taken off", got)
}
rec, _, _ := v.Store.Read("own/anchor/telegram/telegram-token")
if rec.Origin != OriginTyped || rec.ChangedBy != "g14/claude-code" || rec.ChangedVia != ControllerCaller {
t.Errorf("the record says origin %q, by %q, via %q", rec.Origin, rec.ChangedBy, rec.ChangedVia)
}
asks, _ = v.Store.Asks(10)
if len(asks) != 2 || asks[0].Event != AskEnded || asks[0].Outcome != "given" || asks[0].ID != opened.Call {
t.Errorf("how it ended: %+v", asks)
}
if strings.Contains(everything(t, v, opened, changed, asks), typedAtTheDesk) {
t.Fatal("the typed value is in something the vault said, answered or wrote in the clear")
}
}
// One open ask per secret, and three an hour (ADR 0277 §24), read from the vault's own record.
func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) {
v := newVault(t, nil)
first, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
if err != nil {
t.Fatal(err)
}
if _, err := v.AskOpen(ControllerCaller, telegramAsk("ace.claude-code")); err == nil ||
!strings.Contains(err.Error(), "still open (asked by g14.claude-code)") {
t.Fatalf("a second ask while one is open: %v", err)
}
// Another secret is not held back by it.
other := telegramAsk("ace.claude-code")
other.Name = "webhook-secret"
if _, err := v.AskOpen(ControllerCaller, other); err != nil {
t.Fatalf("another secret's ask was held back: %v", err)
}
if _, err := v.AskEnd(ControllerCaller, first.Call, "", "dismissed"); err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
o, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
if err != nil {
t.Fatalf("ask %d of the hour: %v", i+2, err)
}
if _, err := v.AskEnd(ControllerCaller, o.Call, "", "timed-out"); err != nil {
t.Fatal(err)
}
}
if _, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err == nil ||
!strings.Contains(err.Error(), "3 times in the last hour") {
t.Fatalf("a fourth ask in the hour: %v", err)
}
asks, _ := v.Store.Asks(50)
outcomes := map[string]int{}
for _, a := range asks {
outcomes[a.Outcome]++
}
if outcomes["dismissed"] != 1 || outcomes["timed-out"] != 2 {
t.Errorf("the record's outcomes: %v", outcomes)
}
// An ask left open counts as open for SecretAskOpenFor only: a prompt that died does not hold the secret.
w := newVault(t, nil)
clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC)
w.Now = func() time.Time { return clock }
if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil {
t.Fatal(err)
}
clock = clock.Add(SecretAskOpenFor + time.Second)
if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil {
t.Errorf("an ask left open past its time still held the secret: %v", err)
}
}
// Only the controller reaches a desk's prompt, so only it opens and ends an ask; a value not sealed to the
// call's key, or a call that waited too long, keeps nothing.
func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) {
v := newVault(t, nil)
for _, caller := range []string{"g14.node-tools", "unproven", "", "person.jochen"} {
if _, err := v.AskOpen(caller, telegramAsk("x")); err == nil {
t.Errorf("%q opened an ask", caller)
}
}
if asks, _ := v.Store.Asks(10); len(asks) != 0 {
t.Fatalf("a refused caller filled the record: %+v", asks)
}
opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
if err != nil {
t.Fatal(err)
}
if _, err := v.AskEnd("g14.node-tools", opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil {
t.Error("another caller ended the ask")
}
other, _, _ := box.GenerateKey(rand.Reader)
if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(other[:]), "x"), ""); err == nil ||
!strings.Contains(err.Error(), "not sealed to this call") {
t.Errorf("a value sealed to another key: %v", err)
}
if _, held, _ := v.Store.Read("own/anchor/telegram/telegram-token"); held {
t.Error("a value not sealed to the call was kept")
}
// The call's key is forgotten once used: the same call cannot be answered twice.
if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil {
t.Error("a call was answered twice")
}
asks, _ := v.Store.Asks(1)
if asks[0].Outcome != "failed" {
t.Errorf("the ask that failed ended %+v", asks[0])
}
// A call that waited longer than its time is closed.
clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC)
v.Now = func() time.Time { return clock }
late, err := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex")
if err != nil {
t.Fatal(err)
}
clock = clock.Add(CallOpenFor + time.Second)
if _, err := v.IssuedTake(ControllerCaller, late.Call, sealToCall(t, late.SealTo, "x")); err == nil {
t.Error("a call that waited too long was taken")
}
// An empty answer gives nothing and says so.
clock = clock.Add(time.Hour)
empty, _ := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
if _, err := v.AskEnd(ControllerCaller, empty.Call, sealToCall(t, empty.SealTo, " \n"), ""); err == nil {
t.Error("an empty answer was kept")
}
if asks, _ := v.Store.Asks(1); asks[0].Outcome != "empty" {
t.Errorf("an empty answer ended %+v", asks[0])
}
}
// ---- the issuing provider's test, with a stand-in provider -------------------------------------------------
// standInProvider is a provider's tool for an `issued-by: software` offer, as plex's must answer (ADR 0305 §2
// with the vault in the controller's place): it refuses every caller but the controller — the one caller the
// bus lets ask it, carrying the vault's call — and answers only the software's value sealed to the call's key.
func standInProvider(software *string) func(caller string, args map[string]any) (map[string]any, error) {
return func(caller string, args map[string]any) (map[string]any, error) {
if caller != ControllerCaller {
return nil, errors.New("this tool answers the controller alone")
}
key, _ := args["seal_to"].(string)
raw, err := base64.StdEncoding.DecodeString(key)
if err != nil || len(raw) != 32 {
return nil, errors.New("seal_to is not a sealing key")
}
var to [32]byte
copy(to[:], raw)
sealed, err := box.SealAnonymous(nil, []byte(*software), &to, rand.Reader)
if err != nil {
return nil, err
}
return map[string]any{"sealed": base64.StdEncoding.EncodeToString(sealed)}, nil
}
}
// issue is the controller's `secret issued` for one provider, carried as the controller carries it: open the
// vault's call, ask the provider's tool with the call's key, pass on what it sealed.
func issue(t *testing.T, v testVault, provider func(string, map[string]any) (map[string]any, error)) (Changed, error) {
t.Helper()
opened, err := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex")
if err != nil {
return Changed{}, err
}
answer, err := provider(ControllerCaller, map[string]any{"provision": "plex-api", "seal_to": opened.SealTo})
if err != nil {
return Changed{}, err
}
return v.IssuedTake(ControllerCaller, opened.Call, answer["sealed"].(string))
}
func TestAnIssuingProviderAnswersOnlyTheControllersCallSealedToTheVault(t *testing.T) {
v := newVault(t, AllowList{"anchor.node-provisioner": {"provision/anchor/plex-api"}})
software := "plex-token-from-the-vendor"
provider := standInProvider(&software)
// The provider's tool refuses every caller but the controller, whatever it asks.
for _, caller := range []string{"anchor.node-tools", "g14.claude-code", "unproven", ""} {
if _, err := provider(caller, map[string]any{"provision": "plex-api", "seal_to": "x"}); err == nil {
t.Errorf("the provider's tool answered %q", caller)
}
}
// The vault takes the steps from the controller alone.
if _, err := v.IssuedOpen("anchor.node-tools", "plex-api", "anchor", "plex"); err == nil {
t.Error("the vault opened an issued call for another caller")
}
changed, err := issue(t, v, provider)
if err != nil {
t.Fatal(err)
}
if changed.Name != "provision/anchor/plex-api" || changed.Generation != 1 || changed.By != "plex on anchor" {
t.Errorf("kept as %+v", changed)
}
if got := handedTo(t, v, "anchor.node-provisioner", "provision/anchor/plex-api"); got != software {
t.Errorf("the vault holds %q", got)
}
rec, _, _ := v.Store.Read("provision/anchor/plex-api")
if rec.Origin != OriginIssued || rec.ChangedVia != ControllerCaller {
t.Errorf("the record: origin %q via %q", rec.Origin, rec.ChangedVia)
}
// Nothing rewritten for a value every holder has (ADR 0305 §5): no new generation, no write, nothing said.
path := filepath.Join(v.dir, "store", "secrets", "provision%2Fanchor%2Fplex-api.json")
before, _ := os.ReadFile(path)
said := len(v.bus.raw)
again, err := issue(t, v, provider)
if err != nil {
t.Fatal(err)
}
after, _ := os.ReadFile(path)
if !again.Unchanged || again.Generation != 1 || string(before) != string(after) || len(v.bus.raw) != said {
t.Errorf("the same value again rewrote something: %+v", again)
}
// The software changes its value: the next ask keeps it as a new generation, and says so without it.
software = "plex-token-renewed"
renewed, err := issue(t, v, provider)
if err != nil || renewed.Generation != 2 {
t.Fatalf("%+v %v", renewed, err)
}
if got := handedTo(t, v, "anchor.node-provisioner", "provision/anchor/plex-api"); got != software {
t.Errorf("after the change the vault holds %q", got)
}
// A value sealed to any other key is refused, and nothing changes.
opened, _ := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex")
stranger, _, _ := box.GenerateKey(rand.Reader)
if _, err := v.IssuedTake(ControllerCaller, opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(stranger[:]), "forged")); err == nil {
t.Error("a value not sealed to the call was taken")
}
// The software rotates it, not the mesh (ADR 0305 §9); and no other path puts it into the vault (§10).
if _, err := v.Rotate(ControllerCaller, "provision/anchor/plex-api", "person.jochen"); err == nil ||
!strings.Contains(err.Error(), "software") {
t.Errorf("rotate of an issued value: %v", err)
}
if _, err := v.AcceptOpen(ControllerCaller, "provision/anchor/plex-api", "person.jochen"); err == nil {
t.Error("an issued value was taken by accept")
}
if strings.Contains(everything(t, v), "plex-token") {
t.Fatal("the issued value is in something the vault said or wrote in the clear")
}
}
// ---- accept and rotate -------------------------------------------------------------------------------------
// A value given at the controller's terminal is sealed there to the vault's call and opened here alone; an
// accepted provision's credential is never remade; a module's own given value rotates as a made one (ADR 0228).
func TestAnAcceptedValueOpensInTheVaultAloneAndIsNotRemade(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"provision/novox/postgres", "own/g14/dunst/token"}})
if _, err := v.AcceptOpen("g14.claude-code", "provision/novox/postgres", ""); err == nil {
t.Error("accept was opened for a caller that is not the controller")
}
for name, value := range map[string]string{"provision/novox/postgres": "pw-of-last-year", "own/g14/dunst/token": "tok"} {
opened, err := v.AcceptOpen(ControllerCaller, name, "jochen")
if err != nil {
t.Fatal(err)
}
changed, err := v.AcceptTake(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, value))
if err != nil {
t.Fatal(err)
}
if changed.By != "jochen" {
t.Errorf("%s accepted by %q", name, changed.By)
}
if got := handedTo(t, v, "g14.node-provisioner", name); got != value {
t.Errorf("%s holds %q", name, got)
}
rec, _, _ := v.Store.Read(name)
if rec.Origin != OriginAccepted || rec.ChangedVia != ControllerCaller {
t.Errorf("%s: origin %q via %q", name, rec.Origin, rec.ChangedVia)
}
}
if _, err := v.Rotate(ControllerCaller, "provision/novox/postgres", "jochen"); err == nil ||
!strings.Contains(err.Error(), "not remade") {
t.Errorf("rotate of an accepted credential: %v", err)
}
rotated, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen")
if err != nil || rotated.Generation != 2 {
t.Fatalf("rotate of a module's own given value: %+v %v", rotated, err)
}
if got := handedTo(t, v, "g14.node-provisioner", "own/g14/dunst/token"); got == "tok" || len(got) != 40 {
t.Errorf("rotated to %q", got)
}
if strings.Contains(everything(t, v), "pw-of-last-year") {
t.Fatal("an accepted value is in something the vault said or wrote in the clear")
}
}
// Rotate: refused for a secret not held and for an unproven caller; who asked is the proven caller, or the
// person the controller names when it carries the act — both kept.
func TestRotateKeepsWhoAskedAndWhoCarriedIt(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen"); err == nil {
t.Error("a secret not held was rotated")
}
if _, err := v.Mint("own/g14/dunst/token", "the test"); err != nil {
t.Fatal(err)
}
if _, err := v.Rotate("unproven", "own/g14/dunst/token", ""); err == nil {
t.Error("an unproven caller rotated")
}
if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen"); err != nil {
t.Fatal(err)
}
rec, _, _ := v.Store.Read("own/g14/dunst/token")
if rec.ChangedBy != "jochen" || rec.ChangedVia != ControllerCaller || rec.Generation != 2 {
t.Errorf("by %q via %q generation %d", rec.ChangedBy, rec.ChangedVia, rec.Generation)
}
// A `by` from any caller but the controller names nobody: the proven caller is who asked.
if _, err := v.Rotate("g14.claude-code", "own/g14/dunst/token", "jochen"); err != nil {
t.Fatal(err)
}
rec, _, _ = v.Store.Read("own/g14/dunst/token")
if rec.ChangedBy != "g14.claude-code" || rec.ChangedVia != "" {
t.Errorf("by %q via %q", rec.ChangedBy, rec.ChangedVia)
}
}
// The names the vault keeps them under are names, never paths.
func TestTheNamesOfWhatIsTakenIn(t *testing.T) {
if n, err := OwnSecret("g14", "dunst", "token"); err != nil || n != "own/g14/dunst/token" {
t.Errorf("%q %v", n, err)
}
if n, err := ProvisionSecret("novox", "postgres"); err != nil || n != "provision/novox/postgres" {
t.Errorf("%q %v", n, err)
}
for _, bad := range [][3]string{{"..", "x", "y"}, {"g14", "a/b", "y"}, {"g14", "x", ""}, {"G14", "x", "y"}} {
if _, err := OwnSecret(bad[0], bad[1], bad[2]); err == nil {
t.Errorf("%v was taken", bad)
}
}
}
// ---- the verbs as the runtime calls them ------------------------------------------------------------------
// Each verb takes its caller from the runtime, never from its arguments, and names its secret one way only.
func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) {
v := newVault(t, nil)
env := &Env{Vault: v.Vault}
defs := definitions(env)
ask := toolNamed(t, defs, Seat+".secret-ask")
args := map[string]any{"node": "anchor", "module": "telegram", "name": "telegram-token", "desk": "laptop",
"asked_by": "g14.claude-code", "caller": ControllerCaller}
if _, err := ask.Run(args); err == nil {
t.Error("an ask opened for a caller named in the arguments")
}
answer, err := ask.runAs(ControllerCaller, args)
if err != nil {
t.Fatal(err)
}
opened := answer.(Opened)
if _, err := ask.runAs(ControllerCaller, map[string]any{"call": opened.Call, "outcome": "dismissed"}); err != nil {
t.Fatal(err)
}
listed, err := toolNamed(t, defs, "secret_asks").Run(map[string]any{})
if err != nil || len(listed.([]AskRecord)) != 2 {
t.Fatalf("secret_asks: %v %v", listed, err)
}
accept := toolNamed(t, defs, Seat+".accept")
if _, err := accept.runAs(ControllerCaller, map[string]any{"node": "g14", "provision": "postgres"}); err == nil {
t.Error("accept took a name both ways")
}
answer, err = accept.runAs(ControllerCaller, map[string]any{"provider": "novox", "provision": "postgres", "by": "jochen"})
if err != nil {
t.Fatal(err)
}
o := answer.(Opened)
if _, err := accept.runAs(ControllerCaller, map[string]any{"call": o.Call, "sealed": sealToCall(t, o.SealTo, "pw")}); err != nil {
t.Fatal(err)
}
rotate := toolNamed(t, defs, Seat+".rotate")
if _, err := rotate.runAs("g14.claude-code", map[string]any{"node": "g14", "module": "dunst", "name": "token"}); err == nil {
t.Error("a secret not held was rotated through the verb")
}
issued := toolNamed(t, defs, Seat+".issued")
answer, err = issued.runAs(ControllerCaller, map[string]any{"provision": "plex-api", "provider": "anchor", "module": "plex"})
if err != nil {
t.Fatal(err)
}
o = answer.(Opened)
if _, err := issued.runAs(ControllerCaller, map[string]any{"call": o.Call, "sealed": sealToCall(t, o.SealTo, "tok")}); err != nil {
t.Fatal(err)
}
broken := &Env{Broken: errors.New("no store")}
if _, err := toolNamed(t, definitions(broken), Seat+".rotate").runAs(ControllerCaller, map[string]any{}); err == nil ||
err.Error() != "no store" {
t.Errorf("a broken vault said %v", err)
}
}
+105 -1
View File
@@ -39,6 +39,12 @@ type Record struct {
MadeAt time.Time `json:"made-at"`
ChangedAt time.Time `json:"changed-at"`
ChangedBy string `json:"changed-by"`
// ChangedVia is the caller the runtime proved, when it is not who ChangedBy names: the controller
// carrying a person's act (design 57's V2). Empty when they are the same.
ChangedVia string `json:"changed-via,omitempty"`
// Origin is where the value came from: made by the vault, accepted at the controller's terminal, typed at
// a desk, or issued by a provider's software (ADR 0349 clauses 8 to 10). Empty is made, as V1 wrote it.
Origin string `json:"origin,omitempty"`
}
// OperatorCopy is a value sealed to the operator key named in Key.
@@ -58,6 +64,8 @@ type Store struct {
handedMu sync.Mutex
rolled int
// asksMu serialises the record of asks, so two asks for one secret at once do not both pass its bounds.
asksMu sync.Mutex
}
// The record of hand-outs is bounded: rolled at HandedMaxDefault into handed.jsonl.1 … .HandedKept, the
@@ -160,6 +168,7 @@ func (s *Store) RecordHanding(h Handing) (rolled bool, err error) {
if err := s.roll(path); err != nil {
return false, err
}
s.rolled++
rolled = true
}
return rolled, appendSynced(path, append(raw, '\n'))
@@ -176,7 +185,6 @@ func (s *Store) roll(path string) error {
if err := os.Rename(path, path+".1"); err != nil {
return err
}
s.rolled++
return syncDir(s.dir)
}
@@ -334,3 +342,99 @@ func syncDir(dir string) error {
defer d.Close()
return d.Sync()
}
// The two lines an ask leaves in the record: when it opened, and how it ended.
const (
AskOpened = "opened"
AskEnded = "ended"
)
// AskRecord is one line of the record of asks for a module's own secret at a desk (ADR 0277 §25, in the vault's
// store since ADR 0349 clause 9): who asked, as the bus named them, for what, at which desk, and how it ended.
// Never a value.
type AskRecord struct {
ID string `json:"id"`
At time.Time `json:"at"`
Event string `json:"event"`
Secret string `json:"secret"`
Node string `json:"node,omitempty"`
Module string `json:"module,omitempty"`
Name string `json:"name,omitempty"`
Desk string `json:"desk,omitempty"`
AskedBy string `json:"asked_by,omitempty"`
Caller string `json:"caller,omitempty"`
Outcome string `json:"outcome,omitempty"`
}
// RecordAsk appends one line to the record of asks, synced before it returns; rolled as the record of
// hand-outs is, and it says whether it was.
func (s *Store) RecordAsk(r AskRecord) (rolled bool, err error) {
raw, err := json.Marshal(r)
if err != nil {
return false, err
}
path := filepath.Join(s.dir, "asks.jsonl")
if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() && info.Size()+int64(len(raw))+1 > s.HandedBound() {
if err := s.roll(path); err != nil {
return false, err
}
rolled = true
}
return rolled, appendSynced(path, append(raw, '\n'))
}
// asks is every line of the record of asks, oldest first, the rolled files before the current one.
func (s *Store) asks() ([]AskRecord, error) {
path := filepath.Join(s.dir, "asks.jsonl")
var out []AskRecord
for i := HandedKept; i >= 0; i-- {
file := path
if i > 0 {
file = fmt.Sprintf("%s.%d", path, i)
}
raw, err := os.ReadFile(file)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return nil, err
}
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
var r AskRecord
if line != "" && json.Unmarshal([]byte(line), &r) == nil {
out = append(out, r)
}
}
}
return out, nil
}
// AsksSince is every line of the record of asks at or after since, oldest first.
func (s *Store) AsksSince(since time.Time) ([]AskRecord, error) {
all, err := s.asks()
if err != nil {
return nil, err
}
var out []AskRecord
for _, r := range all {
if !r.At.Before(since) {
out = append(out, r)
}
}
return out, nil
}
// Asks is the latest lines of the record of asks, newest first, at most limit.
func (s *Store) Asks(limit int) ([]AskRecord, error) {
s.asksMu.Lock()
defer s.asksMu.Unlock()
all, err := s.asks()
if err != nil {
return nil, err
}
out := []AskRecord{}
for i := len(all) - 1; i >= 0 && len(out) < limit; i-- {
out = append(out, all[i])
}
return out, nil
}
+146 -1
View File
@@ -83,6 +83,100 @@ func definitions(env *Env) []toolDef {
key, _ := a["public_key"].(string)
return env.Vault.Get(caller, strings.TrimSpace(name), key)
}},
{Name: Seat + ".accept",
Description: "Take a value given at the controller's terminal (ADR 0349 clause 8), in two steps from the " +
"controller alone: name the secret (node, module and name for a module's own; provider and provision " +
"for a provision's one credential) and get a call and a key made for it; then send the call and the " +
"value sealed to that key. The vault alone opens it, and never remakes an accepted credential.",
Input: intakeInput(map[string]string{"node": "the machine, for a module's own secret",
"module": "the module, for a module's own secret", "name": "the secret's name in the module",
"provider": "the provider's machine, for a provision's credential", "provision": "the provision",
"by": "who gave it at the terminal, as the controller names them"}),
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.AcceptTake(caller, call, arg(a, "sealed"))
}
secret, err := secretNamed(a)
if err != nil {
return nil, err
}
return v.AcceptOpen(caller, secret, arg(a, "by"))
}},
{Name: Seat + ".secret-ask",
Description: "Ask the operator for a module's own secret at a desk (ADR 0349 clause 9, ADR 0277 §22–§27), in " +
"two steps from the controller alone, which reaches the desk's prompt: node, module, name, desk and " +
"asked_by open the ask — recorded before the prompt opens, one open ask per secret and three an " +
"hour — and answer the key the prompt seals to and who asked as the record names them; then the call " +
"with what the prompt sealed, or with how it ended (dismissed, timed-out, empty, refused, failed). " +
"What is typed opens in the vault alone.",
Input: intakeInput(map[string]string{"node": "the machine the secret is for", "module": "the module",
"name": "the secret's name in the module", "desk": "the machine whose prompt asks",
"asked_by": "who asked the controller, as the bus named its caller",
"outcome": "instead of sealed: how the prompt ended — dismissed, timed-out, empty, refused or failed"}),
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.AskEnd(caller, call, arg(a, "sealed"), arg(a, "outcome"))
}
return v.AskOpen(caller, AskOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"),
Desk: arg(a, "desk"), AskedBy: arg(a, "asked_by")})
}},
{Name: Seat + ".issued",
Description: "Take the credential a provider's software issued for a provision (ADR 0349 clause 10, ADR 0305), " +
"in two steps from the controller alone, which asks the provider's tool: provision, provider and module " +
"open a call and answer the key the tool seals to; then the call with what the tool sealed. Kept as the " +
"provision's one credential; the same value again writes nothing.",
Input: intakeInput(map[string]string{"provision": "the provision", "provider": "the provider's machine",
"module": "the provider's module, whose software issued it"}),
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.IssuedTake(caller, call, arg(a, "sealed"))
}
return v.IssuedOpen(caller, arg(a, "provision"), arg(a, "provider"), arg(a, "module"))
}},
{Name: Seat + ".rotate",
Description: "Make a secret anew (ADR 0114, ADR 0228): node, module and name for a module's own, or provider " +
"and provision for a provision's credential. Refused for a value a provider's software issued, which " +
"the software rotates, and for an accepted credential, which the vault does not remake. Never answers " +
"the value.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"node": str("the machine, for a module's own secret"), "module": str("the module"),
"name": str("the secret's name in the module"), "provider": str("the provider's machine"),
"provision": str("the provision"),
"by": str("who asked, as the controller names them when it carries the act")}},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
secret, err := secretNamed(a)
if err != nil {
return nil, err
}
return v.Rotate(caller, secret, arg(a, "by"))
}},
{Name: "secret_asks",
Description: "The record of asks for a module's own secret at a desk, newest first: who asked as the bus named " +
"them, for what, at which desk, when, and how each ended. Never a value.",
Input: map[string]any{"limit": map[string]any{"type": "number", "description": "how many lines (default 50)"}},
run: func(_ string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
return v.Store.Asks(limitOf(a))
}},
{Name: "secret_export",
Description: "The mesh's root secrets as this vault keeps them: every secret a module holds for itself, " +
"sealed to the operator's key (novox/hq ADR 0085, amended). Ciphertext — nothing here can open a line " +
@@ -237,7 +331,8 @@ func actInput(set, one bool) map[string]any {
}
// actOf is an act's why, by and via. `by` is the person the controller names; with none, the caller the
// runtime proved.
// runtime proved. `by` is an argument, so it is not proven: when it names somebody other than the caller the
// runtime proved, the proven caller is kept beside it in via, so the record says both.
func actOf(caller string, a map[string]any) Act {
act := Act{}
act.Why, _ = a["why"].(string)
@@ -246,9 +341,59 @@ func actOf(caller string, a map[string]any) Act {
if act.By == "" {
act.By = caller
}
if act.By != caller {
proven := "called by " + caller
if act.Via == "" {
act.Via = proven
} else {
act.Via += ", " + proven
}
}
return act
}
// ready is the vault, or why there is none.
func (e *Env) ready() (*Vault, error) {
if e.Vault != nil {
return e.Vault, nil
}
if e.Broken != nil {
return nil, e.Broken
}
return nil, errors.New("the vault is not ready")
}
func arg(a map[string]any, key string) string {
s, _ := a[key].(string)
return strings.TrimSpace(s)
}
// secretNamed is the secret a call names: a module's own (node, module, name), or a provision's one credential
// (provider, provision) — one or the other.
func secretNamed(a map[string]any) (string, error) {
own := arg(a, "node") != "" || arg(a, "module") != "" || arg(a, "name") != ""
pair := arg(a, "provider") != "" || arg(a, "provision") != ""
switch {
case own && !pair:
return OwnSecret(arg(a, "node"), arg(a, "module"), arg(a, "name"))
case pair && !own:
return ProvisionSecret(arg(a, "provider"), arg(a, "provision"))
}
return "", errors.New("name a module's own secret (node, module, name) or a provision's credential " +
"(provider, provision), one of the two")
}
// intakeInput is a two-step verb's arguments: what its first step names, then the call and what was sealed.
func intakeInput(first map[string]string) map[string]any {
props := map[string]any{}
for k, d := range first {
props[k] = str(d)
}
props["call"] = str("the second step: the call the first step answered")
props["sealed"] = str("the second step: the value sealed to the call's key (NaCl anonymous box, base64) — never the value")
return map[string]any{"type": "object", "properties": props}
}
func stringsOf(v any) []string {
list, _ := v.([]any)
out := []string{}
+13 -1
View File
@@ -35,6 +35,9 @@ type Vault struct {
Log func(format string, args ...any)
mu sync.Mutex
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
callsMu sync.Mutex
pending map[string]*callKey
}
// Changed is what one change to a secret was. Never its value.
@@ -65,6 +68,12 @@ func (v *Vault) log(format string, args ...any) {
// Refused: a private key (it stays where it was made), and any value while the vault has no operator key —
// a secret only the vault could open is what ADR 0113 §22 rules out.
func (v *Vault) Put(name, value, by string) (Changed, error) {
return v.keep(name, value, by, "", OriginMade)
}
// keep is Put with where the value came from (origin) and the caller the runtime proved when it is not who
// by names (via).
func (v *Vault) keep(name, value, by, via, origin string) (Changed, error) {
if err := validName(name); err != nil {
return Changed{}, err
}
@@ -104,9 +113,12 @@ func (v *Vault) Put(name, value, by string) (Changed, error) {
return Changed{}, err
}
now := v.Now().UTC()
if via == by {
via = ""
}
rec := Record{Name: name, Generation: 1, AtRest: atRest,
Operator: OperatorCopy{Box: OperatorBox, Key: operator, Sealed: forOperator},
MadeAt: now, ChangedAt: now, ChangedBy: by}
MadeAt: now, ChangedAt: now, ChangedBy: by, ChangedVia: via, Origin: origin}
if held {
rec.Generation, rec.MadeAt = before.Generation+1, before.MadeAt
}
+6 -1
View File
@@ -112,6 +112,7 @@
"secret_holders",
"secret_verify",
"secret_handed",
"secret_asks",
"secret_vault_export",
"provisioner_retirement",
"provisioner_retire_approve",
@@ -124,7 +125,11 @@
"name": "mesh-vault",
"scope": "mesh",
"serves": [
"get"
"get",
"accept",
"secret-ask",
"issued",
"rotate"
]
}
]