Privilege no caller named controller in the vault; match a hand act's verb exactly
The controller is not placed (ADR 0590), yet the vault still answered a caller named controller alone (accept), took its relay of who asked (secret-ask, rotate's by) and sealed it the core's credentials. Those branches and accept go with their tests; a node's account alone is never minted or rotated here. The sensitive-verb judge now needs the act's verb to be the vault's own, mesh-vault.<verb>, not any verb ending so (issue 1126 review 1). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -136,7 +136,7 @@ the declaration sealed by the vault to their machine, which the controller carri
|
||||
machine's sealing key as its own node-engine stated it in its own name, in `move-in`'s first step (`sealing_key`).
|
||||
A machine whose engine stated none, any other key, any other module and any other name are refused and recorded.
|
||||
|
||||
## Taking values in: `accept`, `secret-ask`, `issued`, `rotate`
|
||||
## Taking values in: `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`.
|
||||
@@ -156,12 +156,8 @@ controller still asks them itself for a module filled the old way, until R1 (iss
|
||||
answer both until then. Everything the vault keeps is its account's: its directories, the export it keeps, the
|
||||
grant files the controller still writes for it.
|
||||
|
||||
- `accept` — two steps: node, module, name (a module's own secret) or provider, provision (a provision's one
|
||||
credential), and `by`; then `call` and `sealed`. 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` — one step: 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-ask` — one step: node, module, name and desk, asked by a caller the vault's manifest names (`accept`, and
|
||||
the controller's relay of who asked it, went with the controller, ADR 0590). 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 vault then asks the desk's
|
||||
prompt itself, naming who asked as the record has it, with `timeout_seconds` 20. The answer is how it ended
|
||||
(`given`, dismissed, timed-out, empty, refused, failed) and who asked; how each ask ended is recorded.
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -103,40 +102,3 @@ func (v *Vault) recordNodeKey(node, key, caller string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// coreRefusal says why the controller may not have name sealed to key, or "" when it may: name is a core module's
|
||||
// own secret and key is its machine's sealing key as that machine's engine stated it.
|
||||
func (v *Vault) coreRefusal(name, key string) string {
|
||||
parts := strings.Split(name, "/")
|
||||
if len(parts) != 4 || parts[0] != "own" {
|
||||
return fmt.Sprintf("the controller is handed a core module's own secret alone, own/<node>/<%s>/<name>, sealed "+
|
||||
"to its machine (ADR 0349 §16); %s is not one", strings.Join(CoreModules, "|"), name)
|
||||
}
|
||||
node, module := parts[1], parts[2]
|
||||
if !slices.Contains(CoreModules, module) {
|
||||
return fmt.Sprintf("%s is not one of the core's modules (%s): its secrets reach its machine through the "+
|
||||
"provisioner or its declaration, never through the controller's ask (ADR 0349 §16)", module,
|
||||
strings.Join(CoreModules, ", "))
|
||||
}
|
||||
stated, held, err := v.Store.NodeKey(node)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s's sealing key cannot be read: %v", node, err)
|
||||
}
|
||||
if !held {
|
||||
return fmt.Sprintf("%s's node-engine has stated no sealing key to the vault (it does so at the move), so "+
|
||||
"there is no key of that machine's own to seal %s to", node, name)
|
||||
}
|
||||
want, err := publicPEM(stated)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s's sealing key as recorded is not usable: %v", node, err)
|
||||
}
|
||||
named, err := askerKey(key)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if named != want {
|
||||
return fmt.Sprintf("%s is sealed only to %s's sealing key as its node-engine stated it, and the key named "+
|
||||
"is another", name, node)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
@@ -205,74 +204,3 @@ authorization {
|
||||
type errString string
|
||||
|
||||
func (e errString) Error() string { return string(e) }
|
||||
|
||||
func TestOnARealBusTheControllerGetsACoreCredentialSealedOnlyToItsNodesKey(t *testing.T) {
|
||||
ask, anchorPub, anchorPriv, heard := coreOnABus(t)
|
||||
|
||||
got, err := ask("own/anchor/mesh-provisioner-client/provisioner-broker", pemOf(t, anchorPub))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Name != "own/anchor/mesh-provisioner-client/provisioner-broker" || got.Generation != 1 {
|
||||
t.Errorf("answered %+v", got)
|
||||
}
|
||||
opened, err := seal.Open(got.Sealed, anchorPriv)
|
||||
if err != nil || opened != brokerValue {
|
||||
t.Fatalf("anchor's key opens %q, %v", opened, err)
|
||||
}
|
||||
|
||||
// The controller's own key, or any key but anchor's, is refused: it carries the box and never opens it.
|
||||
mine, err := seal.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := ask("own/anchor/mesh-provisioner-client/provisioner-broker", mine.PublicKey); err == nil {
|
||||
t.Errorf("the controller was handed anchor's core credential sealed to its own key: %+v", got)
|
||||
} else if !strings.Contains(err.Error(), "sealing key") {
|
||||
t.Errorf("refused for another reason: %v", err)
|
||||
}
|
||||
// A module that is not the core's is refused, though anchor's key is the one named.
|
||||
if got, err := ask("own/anchor/postgres/superuser-password", pemOf(t, anchorPub)); err == nil {
|
||||
t.Errorf("the controller was handed a leaf module's secret: %+v", got)
|
||||
} else if !strings.Contains(err.Error(), "core") {
|
||||
t.Errorf("refused for another reason: %v", err)
|
||||
}
|
||||
// A machine whose engine stated no key: nothing to seal to, so nothing is handed.
|
||||
laptopPub, _ := aSealingKey(t)
|
||||
if _, err := ask("own/laptop/nats/broker", pemOf(t, laptopPub)); err == nil {
|
||||
t.Error("the controller was handed a core credential for a machine whose engine stated no key")
|
||||
}
|
||||
|
||||
all := heard()
|
||||
if !strings.Contains(all, "mesh.seat.mesh-vault.call.get.controller") {
|
||||
t.Fatalf("the listener heard nothing of the asks:\n%s", all)
|
||||
}
|
||||
for _, v := range []string{brokerValue, movedValue} {
|
||||
for _, form := range []string{v, base64.StdEncoding.EncodeToString([]byte(v))} {
|
||||
if strings.Contains(all, form) {
|
||||
t.Fatal("a value crossed the bus in the clear")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only the controller is answered by this rule: a machine's client asking for a core credential, which no plan
|
||||
// names, is still refused by the running rule, and a node-engine is not an asker.
|
||||
func TestTheCoreRuleAnswersTheControllerAlone(t *testing.T) {
|
||||
v := newVault(t, Declared{Read: assignments{}.read})
|
||||
pub, _ := aSealingKey(t)
|
||||
if _, err := moveIn(t, v, "node.anchor", clientsBroker(pub), brokerValue); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, caller := range []string{"provisioner.anchor", "node.anchor", "anchor.mesh-provisioner-client"} {
|
||||
if _, err := v.Get(caller, "own/anchor/mesh-provisioner-client/provisioner-broker", pemOf(t, pub)); err == nil {
|
||||
t.Errorf("%s was handed a core credential", caller)
|
||||
}
|
||||
}
|
||||
// The controller is answered for nothing else: not a pair credential, not a provision's.
|
||||
for _, name := range []string{"pair/anchor/postgres/anchor/gitea", "provision/anchor/postgres"} {
|
||||
if _, err := v.Get(ControllerCaller, name, pemOf(t, pub)); err == nil {
|
||||
t.Errorf("the controller was handed %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ func ProvisionSecret(provider, provision string) (string, error) {
|
||||
// callKey is one call's key and what the call takes in. Never written anywhere.
|
||||
type callKey struct {
|
||||
verb, secret, by string
|
||||
// origin is what an `accept` call keeps the value as: accepted, or issued for a covered account (issue 787).
|
||||
origin string
|
||||
// ask is the ask's record for `secret-ask`, which the call's id is.
|
||||
ask *AskRecord
|
||||
// move is what `move-in` takes: the row of the controller's store (move.go).
|
||||
@@ -107,19 +105,6 @@ type Opened struct {
|
||||
AskedBy string `json:"asked_by,omitempty"`
|
||||
}
|
||||
|
||||
// fromTheController refuses every caller but the controller: only it asks the vault to take a value in, and only
|
||||
// it carries a value given at its terminal.
|
||||
func fromTheController(caller, verb string) error {
|
||||
if caller != ControllerCaller {
|
||||
if caller == "" {
|
||||
caller = stdio.Unproven
|
||||
}
|
||||
return fmt.Errorf("%s takes a value from the controller alone, which carries it sealed to this vault's key "+
|
||||
"for one call; %s is not the controller", verb, caller)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fromAGrantedCaller refuses a caller the runtime did not prove: `rotate` and `issued` answer every caller whose
|
||||
// manifest invokes them, which the bus grants it (novox/hq ADR 0570, Consequences), no longer the controller alone.
|
||||
func fromAGrantedCaller(caller, verb string) error {
|
||||
@@ -258,80 +243,6 @@ func NamedAccount(secret string) bool {
|
||||
return len(parts) == 4 && parts[0] == "own" && (parts[3] == "broker" || parts[3] == "provisioner-broker")
|
||||
}
|
||||
|
||||
// AcceptOpen opens a call to take a value given at the controller's terminal for secret, which the terminal
|
||||
// seals to the call's key before it leaves it (ADR 0349 clause 8: the vault alone opens an accepted value).
|
||||
//
|
||||
// **With origin issued it is a covered account the controller minted** (ADR 0374, issue 787): the controller alone
|
||||
// hands it, for a covered secret alone, and a re-issue replaces the value in place. Without origin it is a value
|
||||
// given at the terminal, as before.
|
||||
func (v *Vault) AcceptOpen(caller, secret, by, origin string) (Opened, error) {
|
||||
if err := fromTheController(caller, "accept"); err != nil {
|
||||
return Opened{}, err
|
||||
}
|
||||
if err := validName(secret); err != nil {
|
||||
return Opened{}, err
|
||||
}
|
||||
kept := OriginAccepted
|
||||
switch origin {
|
||||
case "":
|
||||
if rec, held, err := v.Store.Read(secret); err != nil {
|
||||
return Opened{}, err
|
||||
} else if held && rec.Origin == OriginIssued && !Covered(secret) {
|
||||
return Opened{}, fmt.Errorf("%s is issued by its provider's software, and `secret issued` is the only way "+
|
||||
"it enters the vault (ADR 0305 §10); nothing is taken", secret)
|
||||
}
|
||||
case OriginIssued:
|
||||
if !Covered(secret) {
|
||||
return Opened{}, fmt.Errorf("%s is not a bus account the controller mints for a core or moved module "+
|
||||
"(%s), so it is not handed with origin issued; nothing is taken", secret, strings.Join(CoveredSecrets, ", "))
|
||||
}
|
||||
kept = OriginIssued
|
||||
default:
|
||||
return Opened{}, fmt.Errorf("the origin %q is not one accept takes (issued, or none for a value given at the "+
|
||||
"terminal); nothing is taken", origin)
|
||||
}
|
||||
id, key, err := v.openCall("accept", secret, by, nil)
|
||||
if err != nil {
|
||||
return Opened{}, err
|
||||
}
|
||||
v.callsMu.Lock()
|
||||
if c := v.pending[id]; c != nil {
|
||||
c.origin = kept
|
||||
}
|
||||
v.callsMu.Unlock()
|
||||
return Opened{Call: id, SealTo: key, Secret: secret}, nil
|
||||
}
|
||||
|
||||
// AcceptTake opens the value sealed to the call's key and keeps it, accepted: never remade by the vault.
|
||||
func (v *Vault) AcceptTake(caller, call, sealed string) (Changed, error) {
|
||||
if err := fromTheController(caller, "accept"); err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
c, err := v.takeCall("accept", call)
|
||||
if err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
value, err := c.open(sealed)
|
||||
if err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
defer zero(value)
|
||||
if strings.TrimSpace(string(value)) == "" {
|
||||
return Changed{}, errors.New("the value given is empty; nothing was taken")
|
||||
}
|
||||
origin := c.origin
|
||||
if origin == "" {
|
||||
origin = OriginAccepted
|
||||
}
|
||||
if origin == OriginIssued {
|
||||
// **The vault's mint wins** (issue 774): during the delivery a controller older than the hand-over may still mint
|
||||
// a covered account and hand it here; it never replaces an account the vault minted, judged under the store's
|
||||
// lock, so the value and the hash the bus loaded stay the vault's.
|
||||
return v.keepWith(c.secret, string(value), someoneOr(c.by, caller), caller, origin, keepOptions{refuse: v.vaultMinted})
|
||||
}
|
||||
return v.keep(c.secret, string(value), someoneOr(c.by, caller), caller, origin)
|
||||
}
|
||||
|
||||
// vaultMinted refuses a value handed for a secret holding an account the vault minted itself (mint.go): one is never
|
||||
// replaced but by the vault's own rotation.
|
||||
func (v *Vault) vaultMinted(before Record) string {
|
||||
@@ -351,26 +262,19 @@ func (v *Vault) vaultMinted(before Record) string {
|
||||
// AskOf is what an ask is for, and where.
|
||||
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. 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.
|
||||
// askedByFor is who asked a secret-ask as the bus says it (novox/hq ADR 0563 §8): a caller the manifest names, as the
|
||||
// runtime proved it. Any other caller is refused: no caller names itself, and none relays another's word.
|
||||
func (v *Vault) askedByFor(caller string, a AskOf) (string, error) {
|
||||
switch {
|
||||
case caller == ControllerCaller:
|
||||
return askedByName(a.AskedBy), nil
|
||||
case v.AskCallers.Allows(caller):
|
||||
if 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)
|
||||
return "", fmt.Errorf("secret-ask answers the callers the vault's manifest names, and %s is not one; 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
|
||||
@@ -787,9 +691,7 @@ func (v *Vault) RotateFor(caller, secret string, one Narrow, by, why string) (Ch
|
||||
if err := fromAGrantedCaller(caller, "rotate"); err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
if caller != ControllerCaller {
|
||||
by = "" // only the controller relays who asked at its terminal; any other caller is who asked
|
||||
}
|
||||
by = "" // who asked is the caller the runtime proved: no caller relays another's word (novox/hq ADR 0590)
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return Changed{}, errors.New("not rotated: a rotation says why it was asked (`why`)")
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func everything(t *testing.T, v testVault, more ...any) string {
|
||||
const typedAtTheDesk = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g"
|
||||
|
||||
func telegramAsk(by string) AskOf {
|
||||
return AskOf{Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: by}
|
||||
return AskOf{Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop"}
|
||||
}
|
||||
|
||||
// ---- the vault's secret-ask test (ADR 0349 "How it is checked") ---------------------------------------------
|
||||
@@ -84,33 +84,34 @@ func telegramAsk(by string) AskOf {
|
||||
// it; the typed value opens only in the vault; the controller carries ciphertext only.
|
||||
func TestAnAskIsRecordedBeforeThePromptAndTheTypedValueOpensOnlyInTheVault(t *testing.T) {
|
||||
v := newVault(t, AllowList{"anchor.node-provisioner": {"own/anchor/telegram/telegram-token"}})
|
||||
opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14/claude-code, via the console"))
|
||||
v.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
opened, err := v.AskOpen("g14.claude-code", telegramAsk("g14/claude-code, via the console"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Recorded before any prompt opens: the record holds the ask now, with no end.
|
||||
asks, _ := v.Store.Asks(10)
|
||||
if len(asks) != 1 || asks[0].Event != AskOpened || asks[0].ID != opened.Call || asks[0].AskedBy != "g14/claude-code" ||
|
||||
asks[0].Caller != ControllerCaller || asks[0].Desk != "laptop" || asks[0].Secret != "own/anchor/telegram/telegram-token" {
|
||||
if len(asks) != 1 || asks[0].Event != AskOpened || asks[0].ID != opened.Call || asks[0].AskedBy != "g14.claude-code" ||
|
||||
asks[0].Caller != "g14.claude-code" || asks[0].Desk != "laptop" || asks[0].Secret != "own/anchor/telegram/telegram-token" {
|
||||
t.Fatalf("the record before the prompt: %+v", asks)
|
||||
}
|
||||
// The prompt names the asker as the bus named it, cut to a name: what the vault answers is what it recorded.
|
||||
if opened.AskedBy != "g14/claude-code" {
|
||||
if opened.AskedBy != "g14.claude-code" {
|
||||
t.Errorf("the prompt would name %q", opened.AskedBy)
|
||||
}
|
||||
// The desk seals what is typed to the call's key; the controller passes it on.
|
||||
changed, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, typedAtTheDesk+"\n"), "")
|
||||
changed, err := v.AskEnd("g14.claude-code", opened.Call, sealToCall(t, opened.SealTo, typedAtTheDesk+"\n"), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changed.Generation != 1 || changed.By != "g14/claude-code" {
|
||||
if changed.Generation != 1 || changed.By != "g14.claude-code" {
|
||||
t.Errorf("kept as %+v", changed)
|
||||
}
|
||||
if got := handedTo(t, v, "anchor.node-provisioner", "own/anchor/telegram/telegram-token"); got != typedAtTheDesk {
|
||||
t.Errorf("the vault holds %q, not what was typed with its line ending taken off", got)
|
||||
}
|
||||
rec, _, _ := v.Store.Read("own/anchor/telegram/telegram-token")
|
||||
if rec.Origin != OriginTyped || rec.ChangedBy != "g14/claude-code" || rec.ChangedVia != ControllerCaller {
|
||||
if rec.Origin != OriginTyped || rec.ChangedBy != "g14.claude-code" || rec.ChangedVia != "" {
|
||||
t.Errorf("the record says origin %q, by %q, via %q", rec.Origin, rec.ChangedBy, rec.ChangedVia)
|
||||
}
|
||||
asks, _ = v.Store.Asks(10)
|
||||
@@ -125,33 +126,34 @@ func TestAnAskIsRecordedBeforeThePromptAndTheTypedValueOpensOnlyInTheVault(t *te
|
||||
// One open ask per secret, and three an hour (ADR 0277 §24), read from the vault's own record.
|
||||
func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
first, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
|
||||
v.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
first, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.AskOpen(ControllerCaller, telegramAsk("ace.claude-code")); err == nil ||
|
||||
if _, err := v.AskOpen("g14.claude-code", telegramAsk("ace.claude-code")); err == nil ||
|
||||
!strings.Contains(err.Error(), "still open (asked by g14.claude-code)") {
|
||||
t.Fatalf("a second ask while one is open: %v", err)
|
||||
}
|
||||
// Another secret is not held back by it.
|
||||
other := telegramAsk("ace.claude-code")
|
||||
other.Name = "webhook-secret"
|
||||
if _, err := v.AskOpen(ControllerCaller, other); err != nil {
|
||||
if _, err := v.AskOpen("g14.claude-code", other); err != nil {
|
||||
t.Fatalf("another secret's ask was held back: %v", err)
|
||||
}
|
||||
if _, err := v.AskEnd(ControllerCaller, first.Call, "", "dismissed"); err != nil {
|
||||
if _, err := v.AskEnd("g14.claude-code", first.Call, "", "dismissed"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
o, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
|
||||
o, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code"))
|
||||
if err != nil {
|
||||
t.Fatalf("ask %d of the hour: %v", i+2, err)
|
||||
}
|
||||
if _, err := v.AskEnd(ControllerCaller, o.Call, "", "timed-out"); err != nil {
|
||||
if _, err := v.AskEnd("g14.claude-code", o.Call, "", "timed-out"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err == nil ||
|
||||
if _, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")); err == nil ||
|
||||
!strings.Contains(err.Error(), "3 times in the last hour") {
|
||||
t.Fatalf("a fourth ask in the hour: %v", err)
|
||||
}
|
||||
@@ -165,13 +167,14 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) {
|
||||
}
|
||||
// An ask left open counts as open for SecretAskOpenFor only: a prompt that died does not hold the secret.
|
||||
w := newVault(t, nil)
|
||||
w.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC)
|
||||
w.Now = func() time.Time { return clock }
|
||||
if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil {
|
||||
if _, err := w.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clock = clock.Add(SecretAskOpenFor + time.Second)
|
||||
if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil {
|
||||
if _, err := w.AskOpen("g14.claude-code", 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
|
||||
@@ -186,7 +189,7 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) {
|
||||
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 {
|
||||
if _, err := x.AskEnd("g14.claude-code", 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)") {
|
||||
@@ -199,10 +202,11 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Only the controller reaches a desk's prompt, so only it opens and ends an ask; a value not sealed to the
|
||||
// call's key, or a call that waited too long, keeps nothing.
|
||||
func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) {
|
||||
// Only a caller the vault's manifest names opens an ask, and only it ends it; a value not sealed to the call's key, or
|
||||
// a call that waited too long, keeps nothing.
|
||||
func TestAnAskIsTakenFromANamedCallerAloneAndOnlySealedToItsCall(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
v.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
for _, caller := range []string{"g14.node-tools", "unproven", "", "person.jochen"} {
|
||||
if _, err := v.AskOpen(caller, telegramAsk("x")); err == nil {
|
||||
t.Errorf("%q opened an ask", caller)
|
||||
@@ -211,7 +215,7 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T)
|
||||
if asks, _ := v.Store.Asks(10); len(asks) != 0 {
|
||||
t.Fatalf("a refused caller filled the record: %+v", asks)
|
||||
}
|
||||
opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
|
||||
opened, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -219,7 +223,7 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T)
|
||||
t.Error("another caller ended the ask")
|
||||
}
|
||||
other, _, _ := box.GenerateKey(rand.Reader)
|
||||
if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(other[:]), "x"), ""); err == nil ||
|
||||
if _, err := v.AskEnd("g14.claude-code", opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(other[:]), "x"), ""); err == nil ||
|
||||
!strings.Contains(err.Error(), "not sealed to this call") {
|
||||
t.Errorf("a value sealed to another key: %v", err)
|
||||
}
|
||||
@@ -227,7 +231,7 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T)
|
||||
t.Error("a value not sealed to the call was kept")
|
||||
}
|
||||
// The call's key is forgotten once used: the same call cannot be answered twice.
|
||||
if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil {
|
||||
if _, err := v.AskEnd("g14.claude-code", opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil {
|
||||
t.Error("a call was answered twice")
|
||||
}
|
||||
asks, _ := v.Store.Asks(1)
|
||||
@@ -237,18 +241,18 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T)
|
||||
// A call that waited longer than its time is closed.
|
||||
clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC)
|
||||
v.Now = func() time.Time { return clock }
|
||||
late, err := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex")
|
||||
late, err := v.IssuedOpen("g14.claude-code", "plex-api", "anchor", "plex")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clock = clock.Add(CallOpenFor + time.Second)
|
||||
if _, err := v.IssuedTake(ControllerCaller, late.Call, sealToCall(t, late.SealTo, "x")); err == nil {
|
||||
if _, err := v.IssuedTake("g14.claude-code", late.Call, sealToCall(t, late.SealTo, "x")); err == nil {
|
||||
t.Error("a call that waited too long was taken")
|
||||
}
|
||||
// An empty answer gives nothing and says so.
|
||||
clock = clock.Add(time.Hour)
|
||||
empty, _ := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
|
||||
if _, err := v.AskEnd(ControllerCaller, empty.Call, sealToCall(t, empty.SealTo, " \n"), ""); err == nil {
|
||||
empty, _ := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code"))
|
||||
if _, err := v.AskEnd("g14.claude-code", empty.Call, sealToCall(t, empty.SealTo, " \n"), ""); err == nil {
|
||||
t.Error("an empty answer was kept")
|
||||
}
|
||||
if asks, _ := v.Store.Asks(1); asks[0].Outcome != "empty" {
|
||||
@@ -365,62 +369,14 @@ func TestAnIssuingProviderAnswersOnlyTheVaultsCallSealedToTheVault(t *testing.T)
|
||||
!strings.Contains(err.Error(), "software") {
|
||||
t.Errorf("rotate of an issued value: %v", err)
|
||||
}
|
||||
if _, err := v.AcceptOpen(ControllerCaller, "provision/anchor/plex-api", "person.jochen", ""); err == nil {
|
||||
t.Error("an issued value was taken by accept")
|
||||
}
|
||||
if strings.Contains(everything(t, v), "plex-token") {
|
||||
t.Fatal("the issued value is in something the vault said or wrote in the clear")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- accept and rotate -------------------------------------------------------------------------------------
|
||||
|
||||
// A value given at the controller's terminal is sealed there to the vault's call and opened here alone; an
|
||||
// accepted provision's credential is never remade; a module's own given value rotates as a made one (ADR 0228).
|
||||
func TestAnAcceptedValueOpensInTheVaultAloneAndIsNotRemade(t *testing.T) {
|
||||
v := newVault(t, AllowList{"g14.node-provisioner": {"provision/novox/postgres", "own/g14/dunst/token"}})
|
||||
if _, err := v.AcceptOpen("g14.claude-code", "provision/novox/postgres", "", ""); err == nil {
|
||||
t.Error("accept was opened for a caller that is not the controller")
|
||||
}
|
||||
for name, value := range map[string]string{"provision/novox/postgres": "pw-of-last-year", "own/g14/dunst/token": "tok"} {
|
||||
opened, err := v.AcceptOpen(ControllerCaller, name, "jochen", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changed, err := v.AcceptTake(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, value))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changed.By != "jochen" {
|
||||
t.Errorf("%s accepted by %q", name, changed.By)
|
||||
}
|
||||
if got := handedTo(t, v, "g14.node-provisioner", name); got != value {
|
||||
t.Errorf("%s holds %q", name, got)
|
||||
}
|
||||
rec, _, _ := v.Store.Read(name)
|
||||
if rec.Origin != OriginAccepted || rec.ChangedVia != ControllerCaller {
|
||||
t.Errorf("%s: origin %q via %q", name, rec.Origin, rec.ChangedVia)
|
||||
}
|
||||
}
|
||||
if _, err := v.Rotate(ControllerCaller, "provision/novox/postgres", "jochen", "leaked"); err == nil ||
|
||||
!strings.Contains(err.Error(), "not remade") {
|
||||
t.Errorf("rotate of an accepted credential: %v", err)
|
||||
}
|
||||
rotated, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen", "leaked")
|
||||
if err != nil || rotated.Generation != 2 {
|
||||
t.Fatalf("rotate of a module's own given value: %+v %v", rotated, err)
|
||||
}
|
||||
if got := handedTo(t, v, "g14.node-provisioner", "own/g14/dunst/token"); got == "tok" || len(got) != 40 {
|
||||
t.Errorf("rotated to %q", got)
|
||||
}
|
||||
if strings.Contains(everything(t, v), "pw-of-last-year") {
|
||||
t.Fatal("an accepted value is in something the vault said or wrote in the clear")
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate: refused for a secret not held and for an unproven caller; who asked is the proven caller, or the
|
||||
// person the controller names when it carries the act — both kept.
|
||||
func TestRotateKeepsWhoAskedAndWhoCarriedIt(t *testing.T) {
|
||||
func TestRotateKeepsTheProvenCallerAsWhoAsked(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen", "leaked"); err == nil {
|
||||
t.Error("a secret not held was rotated")
|
||||
@@ -431,11 +387,12 @@ func TestRotateKeepsWhoAskedAndWhoCarriedIt(t *testing.T) {
|
||||
if _, err := v.Rotate("unproven", "own/g14/dunst/token", "", "leaked"); err == nil {
|
||||
t.Error("an unproven caller rotated")
|
||||
}
|
||||
if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen", "leaked"); err != nil {
|
||||
// A `by` is a word, never who asked: who asked is the caller the runtime proved (no caller relays another's, ADR 0590).
|
||||
if _, err := v.Rotate("person.jochen", "own/g14/dunst/token", "someone else", "leaked"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec, _, _ := v.Store.Read("own/g14/dunst/token")
|
||||
if rec.ChangedBy != "jochen" || rec.ChangedVia != ControllerCaller || rec.Generation != 2 {
|
||||
if rec.ChangedBy != "person.jochen" || rec.ChangedVia != "" || rec.Generation != 2 {
|
||||
t.Errorf("by %q via %q generation %d", rec.ChangedBy, rec.ChangedVia, rec.Generation)
|
||||
}
|
||||
}
|
||||
@@ -491,6 +448,7 @@ func TestTheNamesOfWhatIsTakenIn(t *testing.T) {
|
||||
// Each verb takes its caller from the runtime, never from its arguments, and names its secret one way only.
|
||||
func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
v.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
env := &Env{Vault: v.Vault}
|
||||
defs := definitions(env)
|
||||
var asked []map[string]any
|
||||
@@ -499,11 +457,11 @@ func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) {
|
||||
"plex.plex_issued_credential@anchor": standInProvider(&software)}}).ask
|
||||
ask := toolNamed(t, defs, Seat+".secret-ask")
|
||||
args := map[string]any{"node": "anchor", "module": "telegram", "name": "telegram-token", "desk": "laptop",
|
||||
"asked_by": "g14.claude-code", "caller": ControllerCaller}
|
||||
"caller": "ace.claude-code"}
|
||||
if _, err := ask.Run(args); err == nil {
|
||||
t.Error("an ask opened for a caller named in the arguments")
|
||||
}
|
||||
answer, err := ask.runAs(ControllerCaller, args)
|
||||
answer, err := ask.runAs("g14.claude-code", args)
|
||||
if err != nil || answer.(Asked).Outcome != "given" || len(asked) != 1 {
|
||||
t.Fatalf("secret-ask: %+v %v", answer, err)
|
||||
}
|
||||
@@ -512,18 +470,6 @@ func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) {
|
||||
t.Fatalf("secret_asks: %v %v", listed, err)
|
||||
}
|
||||
|
||||
accept := toolNamed(t, defs, Seat+".accept")
|
||||
if _, err := accept.runAs(ControllerCaller, map[string]any{"node": "g14", "provision": "postgres"}); err == nil {
|
||||
t.Error("accept took a name both ways")
|
||||
}
|
||||
answer, err = accept.runAs(ControllerCaller, map[string]any{"provider": "novox", "provision": "postgres", "by": "jochen"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
o := answer.(Opened)
|
||||
if _, err := accept.runAs(ControllerCaller, map[string]any{"call": o.Call, "sealed": sealToCall(t, o.SealTo, "pw")}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rotate := toolNamed(t, defs, Seat+".rotate")
|
||||
if _, err := rotate.runAs("g14.claude-code", map[string]any{"node": "g14", "module": "dunst", "name": "token"}); err == nil {
|
||||
t.Error("a secret not held was rotated through the verb")
|
||||
@@ -547,7 +493,8 @@ func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) {
|
||||
// With the lock held elsewhere, AskEnd waits, and writes nothing until it is let go.
|
||||
func TestTheEndOfAnAskIsWrittenUnderTheRecordsLock(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code"))
|
||||
v.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
opened, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -562,7 +509,7 @@ func TestTheEndOfAnAskIsWrittenUnderTheRecordsLock(t *testing.T) {
|
||||
v.Store.asksMu.Lock()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := v.AskEnd(ControllerCaller, opened.Call, "", "dismissed")
|
||||
_, err := v.AskEnd("g14.claude-code", opened.Call, "", "dismissed")
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
|
||||
@@ -87,13 +87,11 @@ func eventsNamed(v testVault, from int, name string) int {
|
||||
// ADR 0374, issue 787): kept as that module's own secret on that machine, served to the controller's core get and to
|
||||
// the moved module's client, replaced in place on a re-issue with `secret-changed`, and never made anew by the vault.
|
||||
|
||||
// handIssued keeps a covered account as the controller's accept kept one with origin issued (accept went with the
|
||||
// controller, ADR 0590): what the vault holds of it is served, hashed and never made anew.
|
||||
func handIssued(t *testing.T, v testVault, caller, secret, value string) (Changed, error) {
|
||||
t.Helper()
|
||||
opened, err := v.AcceptOpen(caller, secret, "module issue", OriginIssued)
|
||||
if err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
return v.AcceptTake(caller, opened.Call, sealToCall(t, opened.SealTo, value))
|
||||
return v.keepWith(secret, value, "module issue", caller, OriginIssued, keepOptions{refuse: v.vaultMinted})
|
||||
}
|
||||
|
||||
func TestAHandedBusAccountIsKeptServedReplacedAndNeverMadeByTheVault(t *testing.T) {
|
||||
@@ -142,19 +140,6 @@ func TestAHandedBusAccountIsKeptServedReplacedAndNeverMadeByTheVault(t *testing.
|
||||
t.Errorf("a refused rotation changed the value to %q", got)
|
||||
}
|
||||
|
||||
// A secret that is not a covered account is not handed with origin issued; and only the controller hands one.
|
||||
if _, err := v.AcceptOpen(ControllerCaller, "own/g14/moved-bot/token", "x", OriginIssued); err == nil {
|
||||
t.Error("origin issued was taken for a secret that is not a bus account")
|
||||
}
|
||||
if _, err := v.AcceptOpen(ControllerCaller, "provision/anchor/postgres", "x", OriginIssued); err == nil {
|
||||
t.Error("origin issued was taken for a provision's credential")
|
||||
}
|
||||
if _, err := v.AcceptOpen("g14.moved-bot", moved, "x", OriginIssued); err == nil {
|
||||
t.Error("a caller that is not the controller handed a covered account")
|
||||
}
|
||||
if _, err := v.AcceptOpen(ControllerCaller, moved, "x", "made"); err == nil {
|
||||
t.Error("an origin accept does not take was taken")
|
||||
}
|
||||
if strings.Contains(everything(t, v), "first-pw") || strings.Contains(everything(t, v), "second-pw") {
|
||||
t.Fatal("a handed account is in something the vault said or wrote in the clear")
|
||||
}
|
||||
|
||||
@@ -372,9 +372,8 @@ func (v *Vault) IssueAccount(caller, user string) (any, error) {
|
||||
if user == "" {
|
||||
return nil, errors.New("issue-account names a bus user (`user`), e.g. ace.mesh-build-go")
|
||||
}
|
||||
if nodeOrController(user) {
|
||||
return nil, fmt.Errorf("%s is never minted by the vault: a node's account and the controller's keep the hash they "+
|
||||
"have (ADR 0561 §4)", user)
|
||||
if nodeAccount(user) {
|
||||
return nil, fmt.Errorf("%s is never minted by the vault: a node's account keeps the hash it has (ADR 0561 §4)", user)
|
||||
}
|
||||
raw, found, err := []byte(nil), false, error(nil)
|
||||
if v.WantRead != nil {
|
||||
@@ -417,7 +416,7 @@ func (v *Vault) wanted(user string) (buscompose.WantedAccount, bool) {
|
||||
return w, ok
|
||||
}
|
||||
|
||||
// nodeOrController says user is a node's account or the controller's: never minted here (ADR 0561 §4, §9).
|
||||
func nodeOrController(user string) bool {
|
||||
return user == ControllerCaller || strings.HasPrefix(user, "node.")
|
||||
// nodeAccount says user is a node's account: never minted here (ADR 0561 §4, §9).
|
||||
func nodeAccount(user string) bool {
|
||||
return strings.HasPrefix(user, "node.")
|
||||
}
|
||||
|
||||
@@ -161,8 +161,7 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
|
||||
return nil, false, nil
|
||||
}
|
||||
for user, says := range map[string]string{
|
||||
"node.ace": "never minted",
|
||||
"controller": "never minted",
|
||||
"node.ace": "never minted",
|
||||
} {
|
||||
if _, err := v.IssueAccount("person.jochen", user); err == nil || !strings.Contains(err.Error(), says) {
|
||||
t.Errorf("%s: %v, want a refusal saying %q", user, err, says)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -93,15 +94,16 @@ func aDesk(typed string, asked *[]map[string]any) func(string, map[string]any) (
|
||||
// is typed opened here alone; what crossed is a key and a sealed box.
|
||||
func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T) {
|
||||
v := newVault(t, AllowList{"anchor.node-provisioner": {"own/anchor/telegram/telegram-token"}})
|
||||
v.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
var asked []map[string]any
|
||||
mesh := &aMesh{desk: aDesk(typedAtTheDesk, &asked)}
|
||||
v.Ask = mesh.ask
|
||||
|
||||
got, err := v.AskAtDesk(ControllerCaller, telegramAsk("g14/claude-code, via the console"))
|
||||
got, err := v.AskAtDesk("g14.claude-code", telegramAsk("g14/claude-code, via the console"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Outcome != "given" || got.Generation != 1 || got.AskedBy != "g14/claude-code" ||
|
||||
if got.Outcome != "given" || got.Generation != 1 || got.AskedBy != "g14.claude-code" ||
|
||||
got.Secret != "own/anchor/telegram/telegram-token" {
|
||||
t.Errorf("answered %+v", got)
|
||||
}
|
||||
@@ -109,7 +111,7 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T
|
||||
t.Fatalf("the vault asked %v", mesh.keys)
|
||||
}
|
||||
p := asked[0]
|
||||
if p["asked_by"] != "g14/claude-code" || p["module"] != "telegram" || p["secret"] != "telegram-token" ||
|
||||
if p["asked_by"] != "g14.claude-code" || p["module"] != "telegram" || p["secret"] != "telegram-token" ||
|
||||
p["node"] != "anchor" || p["seal_to"] == "" || p["timeout_seconds"] != float64(DeskPromptFor) {
|
||||
t.Errorf("the prompt was asked %v", p)
|
||||
}
|
||||
@@ -117,7 +119,7 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T
|
||||
t.Errorf("the vault holds %q", v)
|
||||
}
|
||||
rec, _, _ := v.Store.Read("own/anchor/telegram/telegram-token")
|
||||
if rec.Origin != OriginTyped || rec.ChangedBy != "g14/claude-code" || rec.ChangedVia != ControllerCaller {
|
||||
if rec.Origin != OriginTyped || rec.ChangedBy != "g14.claude-code" || rec.ChangedVia != "" {
|
||||
t.Errorf("the record: origin %q by %q via %q", rec.Origin, rec.ChangedBy, rec.ChangedVia)
|
||||
}
|
||||
asks, _ := v.Store.Asks(10)
|
||||
@@ -131,9 +133,10 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T
|
||||
// Dismissed and timed out end the ask so, and keep nothing.
|
||||
for typed, outcome := range map[string]string{"": "dismissed", "\x00timeout": "timed-out"} {
|
||||
w := newVault(t, nil)
|
||||
w.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
var a []map[string]any
|
||||
w.Ask = (&aMesh{desk: aDesk(typed, &a)}).ask
|
||||
got, err := w.AskAtDesk(ControllerCaller, telegramAsk("g14.claude-code"))
|
||||
got, err := w.AskAtDesk("g14.claude-code", telegramAsk("g14.claude-code"))
|
||||
if err != nil || got.Outcome != outcome {
|
||||
t.Errorf("%s: %+v %v", outcome, got, err)
|
||||
}
|
||||
@@ -146,15 +149,17 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T
|
||||
}
|
||||
// Answered empty: the operator's answer, said so, nothing kept.
|
||||
e := newVault(t, nil)
|
||||
e.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
var ea []map[string]any
|
||||
e.Ask = (&aMesh{desk: aDesk(" ", &ea)}).ask
|
||||
if got, err := e.AskAtDesk(ControllerCaller, telegramAsk("g14.claude-code")); err != nil || got.Outcome != "empty" {
|
||||
if got, err := e.AskAtDesk("g14.claude-code", telegramAsk("g14.claude-code")); err != nil || got.Outcome != "empty" {
|
||||
t.Errorf("empty: %+v %v", got, err)
|
||||
}
|
||||
// The desk cannot be asked: refused, recorded so.
|
||||
r := newVault(t, nil)
|
||||
r.AskCallers = stdio.Callers{"g14.claude-code"}
|
||||
r.Ask = (&aMesh{}).ask
|
||||
if _, err := r.AskAtDesk(ControllerCaller, telegramAsk("g14.claude-code")); err == nil {
|
||||
if _, err := r.AskAtDesk("g14.claude-code", telegramAsk("g14.claude-code")); err == nil {
|
||||
t.Error("an ask nobody answered was said to be done")
|
||||
}
|
||||
if asks, _ := r.Store.Asks(1); asks[0].Outcome != "refused" {
|
||||
|
||||
@@ -153,13 +153,10 @@ func TestAProviderIssuesAValueForEachHolderAndEachHolderGetsOnlyItsOwn(t *testin
|
||||
moved.Holders[1].Generation != 2 {
|
||||
t.Errorf("renewed: %+v %v", moved, err)
|
||||
}
|
||||
// A holder's value is issued by the software: never rotated, never accepted.
|
||||
// A holder's value is issued by the software: never rotated.
|
||||
if _, err := v.Rotate(ControllerCaller, laptopsToken, "jochen", "test"); err == nil {
|
||||
t.Error("a holder's issued value was rotated")
|
||||
}
|
||||
if _, err := v.AcceptOpen(ControllerCaller, laptopsToken, "jochen", ""); err == nil {
|
||||
t.Error("a holder's issued value was opened for accept")
|
||||
}
|
||||
}
|
||||
|
||||
// A holder the tool was not asked about, a holder twice, a value not sealed to the call, or an answer of both
|
||||
|
||||
@@ -35,11 +35,11 @@ func (v *Vault) LoadedListHeard(revision uint64) {
|
||||
}
|
||||
|
||||
// BeginRotation records a rotation of user's account at the loaded list's revision, under the one rule: none for a
|
||||
// node's account or the controller's, none while minting is off, none of an account the vault did not make or was not
|
||||
// node's account, none while minting is off, none of an account the vault did not make or was not
|
||||
// handed, none twice for one account, and none when another began at this revision.
|
||||
func (v *Vault) BeginRotation(user string) (Rotating, error) {
|
||||
if nodeOrController(user) {
|
||||
return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user)
|
||||
if nodeAccount(user) {
|
||||
return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account keeps the hash it has (ADR 0561 §4)", user)
|
||||
}
|
||||
v.minting.mu.Lock()
|
||||
revision := v.minting.loadedRevision
|
||||
|
||||
@@ -62,7 +62,7 @@ func JudgeSensitive(read ActReader, now func() time.Time, caller, verb, act stri
|
||||
return fmt.Errorf("%s: the hand act %s was recorded by %s, not by %s who asks", verb, act, a.By, caller)
|
||||
case now().Sub(a.At) > HandActWithin:
|
||||
return fmt.Errorf("%s: the hand act %s is older than %s: record a new one", verb, act, HandActWithin)
|
||||
case a.Verb != verb && !strings.HasSuffix(a.Verb, "."+verb):
|
||||
case a.Verb != Seat+"."+verb:
|
||||
return fmt.Errorf("%s: the hand act %s is %q: it warrants no %s", verb, act, a.Verb, verb)
|
||||
}
|
||||
// mesh-hand-acts records one what; an act for several names lists them space-separated, so each word is a name.
|
||||
|
||||
@@ -13,9 +13,12 @@ func TestEachSensitiveVerbJudgesItsOwnCaller(t *testing.T) {
|
||||
now := time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)
|
||||
acts := map[string]HandActSeen{
|
||||
"mine": {ID: "mine", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}},
|
||||
"old": {ID: "old", By: "novox.mesh-healers", At: now.Add(-25 * time.Hour), Verb: "rotate", Args: []string{"own/g14/app/key"}},
|
||||
"other": {ID: "other", By: "novox.mesh-config", At: now.Add(-time.Hour), Verb: "rotate", Args: []string{"own/g14/app/key"}},
|
||||
"del": {ID: "del", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "provisioner_delete", Args: []string{"d-app"}},
|
||||
"old": {ID: "old", By: "novox.mesh-healers", At: now.Add(-25 * time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}},
|
||||
"other": {ID: "other", By: "novox.mesh-config", At: now.Add(-time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}},
|
||||
"del": {ID: "del", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "mesh-vault.provisioner_delete", Args: []string{"d-app"}},
|
||||
// Review 1: a verb of another seat or module that ends in the vault verb's name warrants nothing.
|
||||
"alike": {ID: "alike", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "other-module.rotate", Args: []string{"own/g14/app/key"}},
|
||||
"bare": {ID: "bare", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "rotate", Args: []string{"own/g14/app/key"}},
|
||||
// As mesh-hand-acts records nox's act: one what, the consumers space-separated (review 1 of issue 1126).
|
||||
"two": {ID: "two", By: "shanks.node-tools", At: now.Add(-time.Minute), Verb: "mesh-vault.provisioner_retire_approve", Args: []string{"c1 c2"}},
|
||||
}
|
||||
@@ -43,6 +46,8 @@ func TestEachSensitiveVerbJudgesItsOwnCaller(t *testing.T) {
|
||||
{"an act too old", "novox.mesh-healers", SensitiveRotate, "old", []string{"own/g14/app/key"}, "older than"},
|
||||
{"an act not recorded", "novox.mesh-healers", SensitiveDelete, "none", []string{"d-app"}, "holds no hand act"},
|
||||
{"a delete's own act", "novox.mesh-healers", SensitiveDelete, "del", []string{"d-app"}, ""},
|
||||
{"an act of another seat's rotate", "novox.mesh-healers", SensitiveRotate, "alike", []string{"own/g14/app/key"}, "warrants no rotate"},
|
||||
{"an act naming the verb without its seat", "novox.mesh-healers", SensitiveRotate, "bare", []string{"own/g14/app/key"}, "warrants no rotate"},
|
||||
{"one act for two consumers", "shanks.node-tools", SensitiveRetireApprove, "two", []string{"c1", "c2"}, ""},
|
||||
{"one act naming two, asked for a third", "shanks.node-tools", SensitiveRetireApprove, "two", []string{"c1", "c3"}, "not c3"},
|
||||
} {
|
||||
|
||||
@@ -93,36 +93,9 @@ 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. With " +
|
||||
"origin issued it keeps a covered bus account the controller minted, replacing it in place.",
|
||||
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",
|
||||
"origin": "issued for a bus account the controller minted for a core or moved module and hands at " +
|
||||
"once (ADR 0374, issue 787), kept as that module's own secret and replaced in place on a " +
|
||||
"re-issue; left out for a value given at the terminal"}),
|
||||
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"), arg(a, "origin"))
|
||||
}},
|
||||
{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 (naming asked_by) or by a caller the vault's manifest names, itself named by the " +
|
||||
"asked 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 " +
|
||||
@@ -130,15 +103,14 @@ func definitions(env *Env) []toolDef {
|
||||
"the value.",
|
||||
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("from the controller alone: who asked it, as the bus named its caller")}},
|
||||
"name": str("the secret's name in the module"), "desk": str("the machine whose prompt asks")}},
|
||||
run: func(caller string, a map[string]any) (any, error) {
|
||||
v, err := env.ready()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.AskAtDesk(caller, AskOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"),
|
||||
Desk: arg(a, "desk"), AskedBy: arg(a, "asked_by")})
|
||||
Desk: arg(a, "desk")})
|
||||
}},
|
||||
{Name: Seat + ".issued",
|
||||
Description: "Take the credential a provider's software issued for a provision (ADR 0349 clause 10, ADR 0305), " +
|
||||
|
||||
@@ -370,15 +370,8 @@ func (v *Vault) Get(caller, name, publicKey string) (Answer, error) {
|
||||
_ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why})
|
||||
return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why)
|
||||
}
|
||||
// The core's own credentials, to the controller alone, sealed only to their machine's key (ADR 0349 §16).
|
||||
if isLogin {
|
||||
// Judged above: the node's own engine, sealed to its own key.
|
||||
} else if caller == ControllerCaller {
|
||||
if why := v.coreRefusal(name, publicKey); why != "" {
|
||||
v.log("refused %s to %s: %s", name, caller, why)
|
||||
_ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why})
|
||||
return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why)
|
||||
}
|
||||
} else if ok, why := askers.May(caller, name); !ok {
|
||||
if reason, notYet := strings.CutPrefix(why, notYetPrefix); notYet {
|
||||
// Not a refusal (issue 928): the client waits and asks again, and nothing is recorded against it.
|
||||
|
||||
@@ -198,7 +198,6 @@
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"get",
|
||||
"accept",
|
||||
"secret-ask",
|
||||
"issued",
|
||||
"rotate",
|
||||
|
||||
Reference in New Issue
Block a user