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:
@@ -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.
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user