Serve the vault's intake verbs and say how they work (issue 675)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 19:10:55 +00:00
co-authored by Claude Opus 5.5
parent 2abe1f95e8
commit eba300794f
2 changed files with 202 additions and 5 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.
+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{}