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