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
}
// 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.
func (c *callKey) open(sealed string) ([]byte, error) {
defer func() { c.private = [32]byte{} }()
@@ -305,15 +315,33 @@ func (v *Vault) AcceptTake(caller, call, sealed string) (Changed, error) {
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.
// controller alone. A caller asking directly is named by the runtime's proof, never by this (ADR 0563 §8).
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
// 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 {
askedBy, err := v.askedByFor(caller, a)
if err != nil {
return Opened{}, err
}
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) {
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()
@@ -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
// 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 {
if _, err := v.askedByFor(caller, AskOf{}); err != nil {
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] {
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")
@@ -527,8 +558,8 @@ type Asked struct {
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
// asked it. The ask is recorded and its bounds held before any prompt opens; the vault asks the desk's prompt
// AskAtDesk is `secret-ask` (ADR 0349 clause 9, ADR 0277 §22–§27): asked by the controller, which names who asked it,
// 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
// 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) {
@@ -13,6 +13,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
stdio "git.novox.be/novox/mesh-sdk/go"
"os"
"path/filepath"
"strings"
@@ -173,6 +174,29 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) {
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)
}
// 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
+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},
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
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
}
@@ -283,3 +284,15 @@ func main() {
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",
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, " +
"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 " +
@@ -130,7 +131,7 @@ func definitions(env *Env) []toolDef {
Input: map[string]any{"type": "object", "properties": map[string]any{
"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"),
"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) {
v, err := env.ready()
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
// 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)
Now func() time.Time
Log func(format string, args ...any)
// AskCallers are the callers besides the controller that may ask secret-ask themselves, as the vault's manifest names
// 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 AccountStore
// 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_ROOT": "${dir:root}",
"MESH_VAULT_STORE": "${dir:store}",
"MESH_VAULT_STATE": "${dir:state}"
"MESH_VAULT_STATE": "${dir:state}",
"MESH_VAULT_SECRET_ASK_CALLERS": "node-tools"
}
}
]