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