Review found a chain through the command verb: set the operator's key to one the caller holds, rotate secrets so they are sealed to it too, read the sealed copies, open them. Whoever may call a verb includes agents (hq ADR 0266), so command now runs an allow list of reading forms, and operator, identity, token, broker, api, licence and every secret command but rotate are refused through any verb.
200 lines
8.4 KiB
Go
200 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
snapshot "github.com/novox/mesh-controller/internal/facts"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The agent account's judgement (novox/hq ADR 0266): confined only on a healthy account verdict judged for
|
|
// root, on the very account; every verdict not given — no statement, an older engine, no verdict on it, a
|
|
// verdict of unknown — is "not judged" and fails, never a pass.
|
|
func TestAnAgentAccountIsConfinedOnlyOnAHealthyVerdictJudgedForRoot(t *testing.T) {
|
|
at := time.Date(2026, 10, 8, 19, 21, 0, 0, time.UTC)
|
|
verdict := func(target, root, state, reason string) inventory.ResourceHealth {
|
|
return inventory.ResourceHealth{Module: "claude-code", Resource: "claude-code.agent-account",
|
|
Kind: link.KindAccount, Target: target, State: state, Reason: reason, Root: root, Account: target}
|
|
}
|
|
statement := func(contract int, rs ...inventory.ResourceHealth) inventory.NodeHealth {
|
|
return inventory.NodeHealth{Node: "anchor", Contract: contract, SaidAt: at, HeardAt: at, Resources: rs}
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
h inventory.NodeHealth
|
|
had bool
|
|
confined bool
|
|
says string
|
|
}{
|
|
{"judged and unable", statement(link.RootContract, verdict("agent", link.RootNever, link.StateHealthy, "")),
|
|
true, true, "cannot become root without a person"},
|
|
{"judged and able", statement(link.RootContract, verdict("agent", link.RootNever, link.StateUnhealthy,
|
|
link.ReasonRoot+": in the group docker, which grants root")), true, false, "in the group docker"},
|
|
{"a verdict of unknown", statement(link.RootContract, verdict("agent", link.RootNever, link.StateUnknown,
|
|
"sudo could not be read")), true, false, "not judged"},
|
|
{"no statement", inventory.NodeHealth{}, false, false, "not judged"},
|
|
{"an older engine", statement(link.ReadinessContract, verdict("agent", "", link.StateHealthy, "")),
|
|
true, false, "older than the judging"},
|
|
{"a verdict on groups only", statement(link.RootContract, verdict("agent", "", link.StateHealthy, "")),
|
|
true, false, "no verdict on it"},
|
|
{"a verdict on another account", statement(link.RootContract, verdict("ops", link.RootNever, link.StateHealthy, "")),
|
|
true, false, "no verdict on it"},
|
|
} {
|
|
confined, why := judgedConfined("agent", c.h, c.had, at.Add(time.Minute))
|
|
if confined != c.confined || !strings.Contains(why, c.says) {
|
|
t.Errorf("%s: confined %v, %q; want %v saying %q", c.name, confined, why, c.confined, c.says)
|
|
}
|
|
}
|
|
// A verdict heard longer ago than the bound is no verdict: an agent that stopped the node-engine must not
|
|
// leave "healthy" standing.
|
|
fresh := statement(link.RootContract, verdict("agent", link.RootNever, link.StateHealthy, ""))
|
|
if ok, _ := judgedConfined("agent", fresh, true, at.Add(verdictFreshFor)); !ok {
|
|
t.Error("a verdict exactly at the bound is still one")
|
|
}
|
|
if ok, why := judgedConfined("agent", fresh, true, at.Add(verdictFreshFor+time.Second)); ok ||
|
|
!strings.Contains(why, "not judged") {
|
|
t.Errorf("a stale healthy verdict passed: %q", why)
|
|
}
|
|
}
|
|
|
|
// DA raises an urgent condition, with plain words, on a machine whose agent account is not judged unable to
|
|
// become root; a machine that names none is not its to judge.
|
|
func TestTheSelfCheckSaysAnAgentAccountThatCanBecomeRoot(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
for _, n := range []string{"anchor", "laptop"} {
|
|
if _, err := inv.NodeByName(ctx, n); err != nil {
|
|
if _, err := inv.AddNode(ctx, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.SetAccount(ctx, n, "ops", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.SetAgentAccount(ctx, "anchor", "agent", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := &doctor{open: open}
|
|
found, err := probeAgentAccounts(ctx, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found = onlyMachine(found, "anchor")
|
|
if len(found) != 1 || found[0].Machine != "anchor" || found[0].Severity != conditions.Urgent ||
|
|
!strings.Contains(found[0].Said, "not judged") {
|
|
t.Fatalf("a named agent account with no verdict: %+v", found)
|
|
}
|
|
w := plainWordings[kindAgentCanBecomeRoot](conditions.Observation{Kind: kindAgentCanBecomeRoot, Machine: "anchor"})
|
|
if w.Headline == "" || w.Needs == "" || w.Resolved == "" {
|
|
t.Errorf("the condition has no plain words: %+v", w)
|
|
}
|
|
|
|
healthy := inventory.ResourceHealth{Module: "claude-code", Resource: "claude-code.agent-account",
|
|
Kind: link.KindAccount, Target: "agent", State: link.StateHealthy, Root: link.RootNever, Account: "agent"}
|
|
if _, err := inv.RecordHealth(ctx, inventory.NodeHealth{Node: "anchor", Contract: link.RootContract,
|
|
SaidAt: time.Now(), HeardAt: time.Now(), Resources: []inventory.ResourceHealth{healthy}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if found, err = probeAgentAccounts(ctx, d); err != nil || len(onlyMachine(found, "anchor")) != 0 {
|
|
t.Fatalf("a judged agent account still fails: %+v %v", found, err)
|
|
}
|
|
if named, confined, why, err := agentConfined(ctx, inv, "anchor"); err != nil || !named || !confined {
|
|
t.Fatalf("agentConfined on anchor: %v %v %q %v", named, confined, why, err)
|
|
}
|
|
if named, _, why, err := agentConfined(ctx, inv, "laptop"); err != nil || named ||
|
|
!strings.Contains(why, "operator account") {
|
|
t.Fatalf("agentConfined on a machine naming none: %v %q %v", named, why, err)
|
|
}
|
|
}
|
|
|
|
func TestTheAgentRootWordsArePlain(t *testing.T) {
|
|
w := plainWordings[kindAgentCanBecomeRoot](conditions.Observation{Kind: kindAgentCanBecomeRoot, Machine: "anchor"})
|
|
if why, ok := conditions.PlainWords(w, "anchor"); !ok {
|
|
t.Fatalf("not plain: %s: %+v", why, w)
|
|
}
|
|
}
|
|
|
|
func onlyMachine(obs []conditions.Observation, machine string) []conditions.Observation {
|
|
var out []conditions.Observation
|
|
for _, o := range obs {
|
|
if o.Machine == machine {
|
|
out = append(out, o)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// The snapshot a merge check composes from carries the agent account as a pseudonym (novox/hq ADR 0266),
|
|
// so a change is judged against machines that name one, and the name never leaves.
|
|
func TestTheFactsCarryTheAgentAccountAsAPseudonym(t *testing.T) {
|
|
open, _ := aMeshWithSecrets(t)
|
|
ctx := t.Context()
|
|
if err := open.inventory.SetAccount(ctx, "anchor", "keeper", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.SetAgentAccount(ctx, "anchor", "warden", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := gatherFacts(ctx, open, "2.11.17")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := f.Encode()
|
|
if strings.Contains(string(body), "warden") {
|
|
t.Error("the agent account's name is in the snapshot")
|
|
}
|
|
m, ok := f.Machine(snapshot.Pseudonym("machine", "anchor"))
|
|
if !ok || m.AgentAccount != snapshot.Pseudonym("account", "warden") || m.Account == m.AgentAccount {
|
|
t.Fatalf("the anchor's agent account reads as %q (operator %q)", m.AgentAccount, m.Account)
|
|
}
|
|
}
|
|
|
|
// No verb runs a `node` command that sets something: through the generic `command` verb, `node account`,
|
|
// `node agent-account` and every other `node` subcommand but list and show are refused, naming the terminal.
|
|
func TestNoVerbSetsANodesAccounts(t *testing.T) {
|
|
for _, line := range []string{
|
|
"node agent-account novox --clear",
|
|
"node agent-account novox ops",
|
|
"node account novox agent",
|
|
"node account novox",
|
|
"node add intruder",
|
|
"node public-domain novox --clear",
|
|
"node",
|
|
"node frobnicate",
|
|
} {
|
|
argv, err := argvFor("command", map[string]any{"command": line})
|
|
if err == nil || !strings.Contains(err.Error(), "controller's terminal") ||
|
|
!strings.Contains(err.Error(), "ADR 0266") {
|
|
t.Errorf("%q ran as %v (%v); want a refusal naming the terminal", line, argv, err)
|
|
}
|
|
}
|
|
for _, line := range []string{"node show novox", "node list --json", "status --json"} {
|
|
if _, err := argvFor("command", map[string]any{"command": line}); err != nil {
|
|
t.Errorf("%q, a read, was refused: %v", line, err)
|
|
}
|
|
}
|
|
if err := terminalOnly([]string{"node", "account", "a", "b"}); err == nil {
|
|
t.Error("the refusal is not only the command verb's")
|
|
}
|
|
}
|
|
|
|
// Naming the agent account is the controller's terminal's alone: the `node` verb only shows.
|
|
func TestTheNodeVerbOnlyShows(t *testing.T) {
|
|
argv, err := argvFor("node", map[string]any{"node": "anchor"})
|
|
if err != nil || strings.Join(argv, " ") != "node show anchor" {
|
|
t.Fatalf("the node verb runs %v (%v)", argv, err)
|
|
}
|
|
for _, v := range catalogue.ControllerVerbs {
|
|
if strings.Contains(v.Name, "agent") {
|
|
t.Errorf("a verb %q may name the agent account", v.Name)
|
|
}
|
|
}
|
|
}
|