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) } } }