mesh-vault: secret-ask answers the callers its manifest names, each named as the runtime proved it, within the same bounds; the controller's relay as before (issue 774, ADR 0563 §8)

This commit is contained in:
2026-10-28 21:54:36 +01:00
parent 2043200201
commit d3eb5cae80
6 changed files with 86 additions and 12 deletions
+37 -6
View File
@@ -177,6 +177,16 @@ func (v *Vault) takeCall(verb, id string) (*callKey, error) {
return c, nil return c, nil
} }
// openedBy is the caller that opened a secret-ask's call still open, as the ask's record names it; "" for none.
func (v *Vault) openedBy(id string) string {
v.callsMu.Lock()
defer v.callsMu.Unlock()
if c, ok := v.pending[id]; ok && c.verb == "secret-ask" && c.ask != nil {
return c.ask.Caller
}
return ""
}
// open opens what was sealed to the call's key, and forgets the key. // open opens what was sealed to the call's key, and forgets the key.
func (c *callKey) open(sealed string) ([]byte, error) { func (c *callKey) open(sealed string) ([]byte, error) {
defer func() { c.private = [32]byte{} }() defer func() { c.private = [32]byte{} }()
@@ -305,15 +315,33 @@ func (v *Vault) AcceptTake(caller, call, sealed string) (Changed, error) {
type AskOf struct { type AskOf struct {
Node, Module, Name, Desk string Node, Module, Name, Desk string
// AskedBy is who asked the controller, as the bus named its caller; the controller's word, taken from the // AskedBy is who asked the controller, as the bus named its caller; the controller's word, taken from the
// controller alone. // controller alone. A caller asking directly is named by the runtime's proof, never by this (ADR 0563 §8).
AskedBy string AskedBy string
} }
// askedByFor is who asked a secret-ask as the bus says it (novox/hq ADR 0563 §8): for a call relayed by the controller,
// the caller the bus named to the controller, which it takes from the controller alone, as before; for a caller the
// manifest names, that caller as the runtime proved it. Any other caller is refused: no caller names itself.
func (v *Vault) askedByFor(caller string, a AskOf) (string, error) {
switch {
case caller == ControllerCaller:
return askedByName(a.AskedBy), nil
case v.AskCallers.Allows(caller):
return askedByName(caller), nil
}
if caller == "" {
caller = stdio.Unproven
}
return "", fmt.Errorf("secret-ask answers the controller and the callers the vault's manifest names, and %s is "+
"neither; nobody was asked to type anything", caller)
}
// AskOpen records an ask for a module's own secret before any prompt opens, holding its bounds — one open ask // 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, // 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). // and who asked as the record says it, which the prompt names (§26).
func (v *Vault) AskOpen(caller string, a AskOf) (Opened, error) { func (v *Vault) AskOpen(caller string, a AskOf) (Opened, error) {
if err := fromTheController(caller, "secret-ask"); err != nil { askedBy, err := v.askedByFor(caller, a)
if err != nil {
return Opened{}, err return Opened{}, err
} }
secret, err := OwnSecret(a.Node, a.Module, a.Name) secret, err := OwnSecret(a.Node, a.Module, a.Name)
@@ -323,7 +351,6 @@ func (v *Vault) AskOpen(caller string, a AskOf) (Opened, error) {
if !namePart.MatchString(a.Desk) { 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) 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() v.Store.asksMu.Lock()
defer v.Store.asksMu.Unlock() defer v.Store.asksMu.Unlock()
now := v.Now().UTC() now := v.Now().UTC()
@@ -378,9 +405,13 @@ var askOutcomes = map[string]bool{"dismissed": true, "timed-out": true, "empty":
// AskEnd ends an ask: with what the desk sealed to its key, which the vault opens and keeps as typed, or with // 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. // the outcome the desk answered instead. How it ended is recorded whatever happens.
func (v *Vault) AskEnd(caller, call, sealed, outcome string) (Changed, error) { func (v *Vault) AskEnd(caller, call, sealed, outcome string) (Changed, error) {
if err := fromTheController(caller, "secret-ask"); err != nil { if _, err := v.askedByFor(caller, AskOf{}); err != nil {
return Changed{}, err return Changed{}, err
} }
// An ask is ended by the caller that opened it, and by no other.
if opener := v.openedBy(call); opener != "" && opener != caller {
return Changed{}, fmt.Errorf("the ask %s was opened by %s, and %s may not end it", call, opener, caller)
}
if sealed == "" && !askOutcomes[outcome] { if sealed == "" && !askOutcomes[outcome] {
return Changed{}, fmt.Errorf("an ask ends with what was sealed to its key, or with one of dismissed, " + 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") "timed-out, empty, refused or failed")
@@ -527,8 +558,8 @@ type Asked struct {
Unchanged bool `json:"unchanged,omitempty"` Unchanged bool `json:"unchanged,omitempty"`
} }
// AskAtDesk is `secret-ask` (ADR 0349 clause 9, ADR 0277 §22–§27): asked by the controller alone, which names who // AskAtDesk is `secret-ask` (ADR 0349 clause 9, ADR 0277 §22–§27): asked by the controller, which names who asked it,
// asked it. The ask is recorded and its bounds held before any prompt opens; the vault asks the desk's prompt // or by a caller the vault's manifest names, named by the runtime's proof (ADR 0563 §8). The ask is recorded and its bounds held before any prompt opens; the vault asks the desk's prompt
// itself, in its own name, naming the asker as recorded and giving a key made for this one call; what is typed // itself, in its own name, naming the asker as recorded and giving a key made for this one call; what is typed
// comes back sealed to that key and opens here. How the prompt ended is the answer and the record's. // comes back sealed to that key and opens here. How the prompt ended is the answer and the record's.
func (v *Vault) AskAtDesk(caller string, a AskOf) (Asked, error) { func (v *Vault) AskAtDesk(caller string, a AskOf) (Asked, error) {
@@ -13,6 +13,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
stdio "git.novox.be/novox/mesh-sdk/go"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -173,6 +174,29 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) {
if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil { 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) t.Errorf("an ask left open past its time still held the secret: %v", err)
} }
// 774 (ADR 0563 §8): a caller the manifest names asks directly, is named by its proven caller and never by the
// asked_by it gives, and is held to the same bounds; one it does not name is refused.
t.Run("a named caller asks itself, within the bounds", func(t *testing.T) {
x := newVault(t, nil)
x.AskCallers = stdio.Callers{"node-tools"}
if _, err := x.AskOpen("ace.claude-code", telegramAsk("x")); err == nil || !strings.Contains(err.Error(), "ace.claude-code") {
t.Errorf("a caller the manifest does not name opened an ask: %v", err)
}
opened, err := x.AskOpen("g14.node-tools", telegramAsk("person.operator"))
if err != nil || opened.AskedBy != "g14.node-tools" {
t.Fatalf("a named caller's ask: %+v, %v", opened, err)
}
if _, err := x.AskEnd(ControllerCaller, opened.Call, "", "dismissed"); err == nil {
t.Error("another caller ended a named caller's ask")
}
if _, err := x.AskOpen("ace.node-tools", telegramAsk("")); err == nil || !strings.Contains(err.Error(), "still open (asked by g14.node-tools)") {
t.Errorf("a second ask while a named caller's is open: %v", err)
}
recorded, _ := x.Store.Asks(10)
if len(recorded) == 0 || recorded[0].AskedBy != "g14.node-tools" || recorded[0].Caller != "g14.node-tools" {
t.Errorf("recorded as %+v", recorded)
}
})
} }
// Only the controller reaches a desk's prompt, so only it opens and ends an ask; a value not sealed to the // Only the controller reaches a desk's prompt, so only it opens and ends an ask; a value not sealed to the
+14 -1
View File
@@ -54,7 +54,8 @@ func build() *Env {
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: assigned.Read, Confirm: readAssignment, Store: store}, env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: assigned.Read, Confirm: readAssignment, Store: store},
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask, Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant, Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant,
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{}} HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{},
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS"))}
return env return env
} }
@@ -283,3 +284,15 @@ func main() {
os.Exit(1) os.Exit(1)
} }
} }
// askCallers are the callers the vault's manifest names for secret-ask besides the controller (novox/hq ADR 0563 §8):
// MESH_VAULT_SECRET_ASK_CALLERS, comma-separated, each a module (on any machine), <machine>.<module> or person.<name>.
func askCallers(list string) stdio.Callers {
var out stdio.Callers
for _, c := range strings.Split(list, ",") {
if c = strings.TrimSpace(c); c != "" {
out = append(out, c)
}
}
return out
}
+3 -2
View File
@@ -122,7 +122,8 @@ func definitions(env *Env) []toolDef {
}}, }},
{Name: Seat + ".secret-ask", {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), " + Description: "Ask the operator for a module's own secret at a desk (ADR 0349 clause 9, ADR 0277 §22–§27), " +
"asked by the controller alone: node, module, name, desk and asked_by. The ask is recorded before any " + "asked by the controller (naming asked_by) or by a caller the vault's manifest names, itself named by the " +
"bus (ADR 0563 §8): node, module, name and desk. The ask is recorded before any " +
"prompt opens — one open ask per secret and three an hour — and the vault asks the desk's prompt itself, " + "prompt opens — one open ask per secret and three an hour — and the vault asks the desk's prompt itself, " +
"in its own name, with a key made for the call; what is typed opens in the vault alone. Answers how the " + "in its own name, with a key made for the call; what is typed opens in the vault alone. Answers how the " +
"prompt ended (given, dismissed, timed-out, empty, refused or failed) and who asked as recorded; never " + "prompt ended (given, dismissed, timed-out, empty, refused or failed) and who asked as recorded; never " +
@@ -130,7 +131,7 @@ func definitions(env *Env) []toolDef {
Input: map[string]any{"type": "object", "properties": map[string]any{ Input: map[string]any{"type": "object", "properties": map[string]any{
"node": str("the machine the secret is for"), "module": str("the module"), "node": str("the machine the secret is for"), "module": str("the module"),
"name": str("the secret's name in the module"), "desk": str("the machine whose prompt asks"), "name": str("the secret's name in the module"), "desk": str("the machine whose prompt asks"),
"asked_by": str("who asked the controller, as the bus named its caller")}}, "asked_by": str("from the controller alone: who asked it, as the bus named its caller")}},
run: func(caller string, a map[string]any) (any, error) { run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready() v, err := env.ready()
if err != nil { if err != nil {
+6 -2
View File
@@ -40,8 +40,12 @@ type Vault struct {
// account (ADR 0259 §8's runs-as, issue 723): the desks' secret prompt and each issuing provider's tool, which // account (ADR 0259 §8's runs-as, issue 723): the desks' secret prompt and each issuing provider's tool, which
// the bus lets the vault's account alone call in its own name. Nil asks nobody. // the bus lets the vault's account alone call in its own name. Nil asks nobody.
Ask func(key string, body any) (json.RawMessage, error) Ask func(key string, body any) (json.RawMessage, error)
Now func() time.Time // AskCallers are the callers besides the controller that may ask secret-ask themselves, as the vault's manifest names
Log func(format string, args ...any) // them (MESH_VAULT_SECRET_ASK_CALLERS; novox/hq ADR 0563 §8): each is named by the runtime's proof. Empty: the
// controller alone.
AskCallers stdio.Callers
Now func() time.Time
Log func(format string, args ...any)
// Accounts is the vault's state of bus-account hashes (accounts.go, ADR 0547 clause 9): nil writes none. // Accounts is the vault's state of bus-account hashes (accounts.go, ADR 0547 clause 9): nil writes none.
Accounts AccountStore Accounts AccountStore
// HashCost is the bcrypt cost of those hashes: zero is the controller's, bcrypt's default. // HashCost is the bcrypt cost of those hashes: zero is the controller's, bcrypt's default.
+2 -1
View File
@@ -150,7 +150,8 @@
"MESH_VAULT_LEDGER": "${dir:ledger}", "MESH_VAULT_LEDGER": "${dir:ledger}",
"MESH_VAULT_ROOT": "${dir:root}", "MESH_VAULT_ROOT": "${dir:root}",
"MESH_VAULT_STORE": "${dir:store}", "MESH_VAULT_STORE": "${dir:store}",
"MESH_VAULT_STATE": "${dir:state}" "MESH_VAULT_STATE": "${dir:state}",
"MESH_VAULT_SECRET_ASK_CALLERS": "node-tools"
} }
} }
] ]