The terminal path's order is one function, keepGiven, so the test fails when the announcement before a trusted party's secret is removed, and when a failed announcement still keeps it. give also refuses a machine the mesh does not know, as the secret's or as the desk, before the prompt (the final review).
362 lines
16 KiB
Go
362 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/secrets"
|
|
)
|
|
|
|
const typed = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g"
|
|
|
|
// aDesk is the desk path with a prompt the test answers as the operator would, and what it was asked kept.
|
|
func aDesk(t *testing.T, answer func(args map[string]any) (json.RawMessage, error)) (deskGive, *[]string, *[]link.HandAct, *[]map[string]any) {
|
|
t.Helper()
|
|
var accepted []string
|
|
var acts []link.HandAct
|
|
var asked []map[string]any
|
|
return deskGive{
|
|
declares: func(module, name string) error {
|
|
if module != "telegram" || name != "telegram-token" {
|
|
return errors.New(module + " does not declare " + name + " as an own secret")
|
|
}
|
|
return nil
|
|
},
|
|
ask: func(machine string, args map[string]any) (json.RawMessage, error) {
|
|
asked = append(asked, args)
|
|
return answer(args)
|
|
},
|
|
accept: func(value string) (bool, error) { accepted = append(accepted, value); return false, nil },
|
|
record: func(a link.HandAct) error { acts = append(acts, a); return nil },
|
|
}, &accepted, &acts, &asked
|
|
}
|
|
|
|
func sealedTo(t *testing.T, value string) func(args map[string]any) (json.RawMessage, error) {
|
|
return func(args map[string]any) (json.RawMessage, error) {
|
|
sealed, err := secrets.Seal(args["seal_to"].(string), []byte(value+"\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"sealed": sealed})
|
|
return raw, nil
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0259 §10: the value typed at the desk is sealed as `secret accept` seals it, and is in no
|
|
// answer, no prompt argument and no act recorded.
|
|
func TestASecretGivenAtTheDeskIsSealedAndSaidNowhere(t *testing.T) {
|
|
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
|
|
words, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*accepted) != 1 || (*accepted)[0] != typed {
|
|
t.Fatalf("the value sealed is not what was typed, its line ending taken off")
|
|
}
|
|
if len(*acts) != 1 || (*acts)[0].Verb != "secret accept" || (*acts)[0].Cause != "given-at-the-desk" ||
|
|
!strings.Contains((*acts)[0].Why, "at the desk on laptop") {
|
|
t.Errorf("the act: %+v", *acts)
|
|
}
|
|
raw, _ := json.Marshal(struct {
|
|
Words string
|
|
Acts []link.HandAct
|
|
Asked []map[string]any
|
|
}{words, *acts, *asked})
|
|
if strings.Contains(string(raw), typed) || strings.Contains(string(raw), "AAEhBP0") {
|
|
t.Fatal("the value appears in what was said, asked or recorded")
|
|
}
|
|
if !strings.Contains(words, "push anchor") || !strings.Contains(words, "given at the desk on laptop") {
|
|
t.Errorf("%q", words)
|
|
}
|
|
if p := (*asked)[0]; p["seal_to"] == "" || p["timeout_seconds"] != deskPromptWithin {
|
|
t.Errorf("the prompt was asked %v", p)
|
|
}
|
|
}
|
|
|
|
func TestNothingIsAskedForASecretTheMeshWouldRefuse(t *testing.T) {
|
|
for _, c := range [][2]string{{"telegram", "chat-id"}, {"nobody", "telegram-token"}} {
|
|
d, accepted, _, asked := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d.give("anchor", c[0], c[1], "laptop"); err == nil || !strings.Contains(err.Error(), "nobody was asked") {
|
|
t.Errorf("%v: %v", c, err)
|
|
}
|
|
if len(*asked) != 0 || len(*accepted) != 0 {
|
|
t.Errorf("%v: the operator was asked anyway", c)
|
|
}
|
|
}
|
|
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", ""); err == nil {
|
|
t.Error("no desk was refused nowhere")
|
|
}
|
|
}
|
|
|
|
func TestADismissedEmptyLateOrForeignAnswerTakesNothing(t *testing.T) {
|
|
for want, answer := range map[string]func(map[string]any) (json.RawMessage, error){
|
|
"not answered within 25 seconds": func(map[string]any) (json.RawMessage, error) {
|
|
return json.RawMessage(`{"cancelled":true,"timed_out":true}`), nil
|
|
},
|
|
"was dismissed": func(map[string]any) (json.RawMessage, error) { return json.RawMessage(`{"cancelled":true}`), nil },
|
|
"answered empty": sealedTo(t, " "),
|
|
"not sealed to this call": func(map[string]any) (json.RawMessage, error) {
|
|
other, _, _ := secrets.Keypair()
|
|
sealed, _ := secrets.Seal(other, []byte(typed))
|
|
raw, _ := json.Marshal(map[string]any{"sealed": sealed})
|
|
return raw, nil
|
|
},
|
|
"could not be asked": func(map[string]any) (json.RawMessage, error) { return nil, errors.New("no session answers") },
|
|
} {
|
|
d, accepted, acts, _ := aDesk(t, answer)
|
|
_, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err == nil || !strings.Contains(err.Error(), want) || strings.Contains(err.Error(), typed) {
|
|
t.Errorf("want %q, got %v", want, err)
|
|
}
|
|
if len(*accepted) != 0 || len(*acts) != 0 {
|
|
t.Errorf("%s: something was taken or recorded", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheGiveVerbRunsTheDeskPathAndTheControllerMayAskTheDesk(t *testing.T) {
|
|
argv, err := argvFor("give", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token", "at": "laptop"})
|
|
if err != nil || strings.Join(argv, " ") != "secret accept anchor telegram telegram-token --at-desk laptop" {
|
|
t.Fatalf("%v %v", argv, err)
|
|
}
|
|
if _, err := argvFor("give", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token"}); err == nil {
|
|
t.Error("give without a desk was taken")
|
|
}
|
|
perms, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindController})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, p := range perms.Publish {
|
|
found = found || p == "mesh.seat.node-launcher.tool.secret.*"
|
|
}
|
|
if !found {
|
|
t.Error("the controller may not ask the desk's prompt")
|
|
}
|
|
}
|
|
|
|
// The review of 2026-10-09 (M4): the desk's prompt says who asks in words the caller does not choose — the
|
|
// controller, which the bus alone lets ask it — and what for, from names the controller checked; the prompt
|
|
// carries no free text of the caller's.
|
|
func TestThePromptIsAskedByNameNeverByWordsTheCallerChose(t *testing.T) {
|
|
d, _, _, asked := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := (*asked)[0]
|
|
if p["module"] != "telegram" || p["secret"] != "telegram-token" || p["node"] != "anchor" {
|
|
t.Errorf("the prompt was not asked by name: %v", p)
|
|
}
|
|
for _, free := range []string{"prompt", "message"} {
|
|
if _, there := p[free]; there {
|
|
t.Errorf("the prompt carries the caller's %s: %v", free, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every value given for a module's own secret is announced as a condition, on every channel (the review of
|
|
// 2026-10-09, M4): a bot token changed by somebody else is a channel that now answers for them.
|
|
func TestAValueGivenAtTheDeskIsAnnounced(t *testing.T) {
|
|
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
|
var said []string
|
|
d.announce = func(node, module, name, how string) error {
|
|
said = append(said, node+" "+module+" "+name+" "+how)
|
|
return nil
|
|
}
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(said) != 1 || !strings.Contains(said[0], "anchor telegram telegram-token") || !strings.Contains(said[0], "laptop") {
|
|
t.Fatalf("announced %v", said)
|
|
}
|
|
o := secretGivenObservation("anchor", "telegram", "telegram-token", "at the desk on laptop", time.Date(2026, 10, 9, 12, 3, 0, 0, time.UTC))
|
|
if o.Severity != conditions.Urgent || !strings.Contains(o.Explanation, "telegram-token") ||
|
|
len(o.Actions) == 0 || o.Key() == "" {
|
|
t.Errorf("the announcement %+v", o)
|
|
}
|
|
if strings.Contains(o.Summary+o.Explanation+o.Said, typed) {
|
|
t.Error("the announcement carries the value")
|
|
}
|
|
}
|
|
|
|
// The bus lets the controller alone ask the desk's prompt (the review of 2026-10-09, M4): the runtime, which
|
|
// carries every agent's calls, and a person granted every tool are denied it, however wide their grant.
|
|
func TestOnlyTheControllerMayAskTheDesksPrompt(t *testing.T) {
|
|
for _, p := range []broker.Principal{
|
|
{Kind: broker.KindNodeTools, Node: "laptop"},
|
|
{Kind: broker.KindPerson, Module: "operator", Invokes: []string{"*"}},
|
|
{Kind: broker.KindModule, Node: "laptop", Module: "lab", Invokes: []string{"seat:node-launcher.secret"}},
|
|
} {
|
|
perms, err := broker.PermissionsFor(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, subject := range []string{"mesh.seat.node-launcher.tool.secret.laptop", "mesh.seat.node-launcher.tool.secret",
|
|
"mesh.mod.rofi.tool.node-launcher.secret", "mesh.mod.rofi.tool.node-launcher.secret.laptop"} {
|
|
if broker.MayPublish(perms, subject) {
|
|
t.Errorf("%s may publish %s", p.Username(), subject)
|
|
}
|
|
}
|
|
}
|
|
perms, _ := broker.PermissionsFor(broker.Principal{Kind: broker.KindController})
|
|
if !broker.MayPublish(perms, "mesh.seat.node-launcher.tool.secret.laptop") {
|
|
t.Error("the controller may not ask the desk's prompt")
|
|
}
|
|
}
|
|
|
|
// A value for a secret comes from the terminal or the desk, never through a verb (the review of 2026-10-09,
|
|
// M4): `secret accept` with a value, run for a verb, is refused before anything is read.
|
|
func TestASecretValueIsNeverAcceptedThroughAVerb(t *testing.T) {
|
|
t.Setenv(verbVar, "mesh-controller.command")
|
|
for _, args := range [][]string{
|
|
{"accept", "anchor", "telegram", "telegram-token", "--from", "/dev/null"},
|
|
{"accept", "anchor", "app", "db", "--from", "/dev/null", "--provider", "store"},
|
|
} {
|
|
err := secretCommand(context.Background(), args)
|
|
if err == nil || !strings.Contains(err.Error(), "never through a verb") {
|
|
t.Errorf("%v: %v", args, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The `give` verb's own line passes the terminal-only rule of ADR 0266, and no other `secret accept` does: a
|
|
// value, a file, a provider or an extra word is still the terminal's alone.
|
|
func TestOnlyTheGiveLinePassesTheTerminalRuleForSecrets(t *testing.T) {
|
|
if err := terminalOnly([]string{"secret", "accept", "anchor", "telegram", "telegram-token", "--at-desk", "laptop"}); err != nil {
|
|
t.Errorf("give's line refused: %v", err)
|
|
}
|
|
for _, argv := range [][]string{
|
|
{"secret", "accept", "anchor", "telegram", "telegram-token"},
|
|
{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "/tmp/x"},
|
|
{"secret", "accept", "anchor", "telegram", "telegram-token", "--at-desk", "laptop", "--local"},
|
|
{"secret", "accept", "anchor", "telegram", "--provider", "--at-desk", "laptop"},
|
|
{"secret", "export", "anchor", "telegram", "telegram-token", "--at-desk", "laptop"},
|
|
} {
|
|
if err := terminalOnly(argv); err == nil {
|
|
t.Errorf("%v passed the terminal rule", argv)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The confirmation review of 2026-10-09, N1-give: a desk's prompt is answered over the desk machine's bus, and
|
|
// on a desk machine agents run as the operator, who holds its runtime's credential — so a trusted party's
|
|
// secret (a module running as an account of its own: the Telegram bot's token) is never taken at a desk.
|
|
// Refused before anybody is asked to type, whoever called, naming the terminal's line.
|
|
func TestATrustedPartysSecretIsNeverTakenAtADesk(t *testing.T) {
|
|
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
|
|
d.trusted = func(module string) (bool, error) { return module == "telegram", nil }
|
|
_, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err == nil || !strings.Contains(err.Error(), "controller's terminal alone") ||
|
|
!strings.Contains(err.Error(), "secret accept anchor telegram telegram-token") {
|
|
t.Fatalf("a trusted party's secret was taken at the desk, or refused without the line: %v", err)
|
|
}
|
|
if len(*asked)+len(*accepted)+len(*acts) != 0 {
|
|
t.Errorf("asked %v, accepted %d, recorded %v", *asked, len(*accepted), *acts)
|
|
}
|
|
d.trusted = func(string) (bool, error) { return false, errors.New("the store did not answer") }
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err == nil || len(*asked) != 0 {
|
|
t.Errorf("a module not known to be untrusted was asked at the desk: %v", err)
|
|
}
|
|
}
|
|
|
|
// And who may answer the desk's prompt at all: only the runtime of the machine it is asked on, carrying the
|
|
// launcher that holds the seat there — never the controller, another machine's runtime, or a module's own
|
|
// account (the confirmation review of 2026-10-09, N1-give).
|
|
func TestOnlyTheDeskMachinesLauncherMayAnswerItsPrompt(t *testing.T) {
|
|
launcher := broker.Declared{Module: "rofi", Holds: []broker.Seat{{Name: "node-launcher", Scope: "node",
|
|
Serves: []string{"run", "secret"}}}}
|
|
subject := "mesh.seat.node-launcher.tool.secret.laptop"
|
|
for _, c := range []struct {
|
|
p broker.Principal
|
|
answers bool
|
|
}{
|
|
{broker.Principal{Kind: broker.KindNodeTools, Node: "laptop", Module: broker.RuntimeModule, Carries: []broker.Declared{launcher}}, true},
|
|
{broker.Principal{Kind: broker.KindNodeTools, Node: "anchor", Module: broker.RuntimeModule, Carries: []broker.Declared{launcher}}, false},
|
|
{broker.Principal{Kind: broker.KindController}, false},
|
|
{broker.Principal{Kind: broker.KindModule, Node: "laptop", Module: "lab"}, false},
|
|
{broker.Principal{Kind: broker.KindNode, Node: "laptop"}, false},
|
|
} {
|
|
perms, err := broker.PermissionsFor(c.p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := broker.MaySubscribe(perms, subject); got != c.answers {
|
|
t.Errorf("%s may answer %s: %v, want %v", c.p.Username(), subject, got, c.answers)
|
|
}
|
|
}
|
|
}
|
|
|
|
// N1-give at the controller's terminal (the confirmation review of 2026-10-09): a trusted party's secret is
|
|
// announced before it is kept, and not kept when the announcement fails; another module's is kept first and
|
|
// a failed announcement is said, not undone.
|
|
func TestATrustedPartysSecretGivenAtTheTerminalIsAnnouncedBeforeItIsKept(t *testing.T) {
|
|
var order []string
|
|
announce := func(fail bool) func() error {
|
|
return func() error {
|
|
order = append(order, "announce")
|
|
if fail {
|
|
return errors.New("no channel")
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
keep := func() (bool, error) { order = append(order, "keep"); return false, nil }
|
|
|
|
order = nil
|
|
if _, unannounced, err := keepGiven(true, announce(false), keep); err != nil || unannounced != nil ||
|
|
strings.Join(order, ",") != "announce,keep" {
|
|
t.Errorf("trusted: %v %v, order %v; want announced, then kept", unannounced, err, order)
|
|
}
|
|
order = nil
|
|
if _, _, err := keepGiven(true, announce(true), keep); err == nil || strings.Join(order, ",") != "announce" {
|
|
t.Errorf("trusted, announcement failed: %v, order %v; want refused and nothing kept", err, order)
|
|
}
|
|
order = nil
|
|
if _, unannounced, err := keepGiven(false, announce(true), keep); err != nil || unannounced == nil ||
|
|
strings.Join(order, ",") != "keep,announce" {
|
|
t.Errorf("not trusted: %v %v, order %v; want kept, then the failed announcement said", unannounced, err, order)
|
|
}
|
|
order = nil
|
|
failing := func() (bool, error) { order = append(order, "keep"); return false, errors.New("store away") }
|
|
if _, _, err := keepGiven(false, announce(false), failing); err == nil || strings.Join(order, ",") != "keep" {
|
|
t.Errorf("not trusted, keep failed: %v, order %v; want refused and nothing announced", err, order)
|
|
}
|
|
}
|
|
|
|
// A machine the mesh does not know, as the secret's or as the desk, is refused before anybody is asked to type.
|
|
func TestAGiveNamingAMachineTheMeshDoesNotKnowAsksNobody(t *testing.T) {
|
|
for _, unknown := range []string{"elsewhere", "nodesk"} {
|
|
d, accepted, acts, asked := aDesk(t, func(map[string]any) (json.RawMessage, error) {
|
|
t.Fatal("the desk was asked")
|
|
return nil, nil
|
|
})
|
|
d.known = func(machine string) error {
|
|
if machine == unknown {
|
|
return errors.New("no node " + machine)
|
|
}
|
|
return nil
|
|
}
|
|
node, desk := "anchor", "laptop"
|
|
if unknown == "elsewhere" {
|
|
node = unknown
|
|
} else {
|
|
desk = unknown
|
|
}
|
|
_, err := d.give(node, "telegram", "telegram-token", desk)
|
|
if err == nil || !strings.Contains(err.Error(), "nobody was asked") || !strings.Contains(err.Error(), unknown) {
|
|
t.Errorf("%s: %v", unknown, err)
|
|
}
|
|
if len(*accepted)+len(*acts)+len(*asked) != 0 {
|
|
t.Errorf("%s: something happened: %v %v %v", unknown, *accepted, *acts, *asked)
|
|
}
|
|
}
|
|
}
|