Swaps three import paths for mesh-sdk's go/catalogue, go/overlay and go/validate (the same packages, copied from this repository at 6c2a8d62) and lets gofmt order the groups. The six swap commits compile together once the pin commit (go.mod, modules.txt) is on the branch; each alone does not, because a file on the SDK's types cannot call one still on the old package.
135 lines
6.2 KiB
Go
135 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"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 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.
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Until a complete search for setuid programs judges — none since the node-engine's state was kept, or the last
|
|
// older than the engine lets one judge — the agent account is not judged, and the search runs to its end with no
|
|
// bound (novox/hq issue 361). DA does not raise that as urgent within searchQuietFor, counted from when this
|
|
// controller first saw it waiting — never from the engine's own "since", which a restart resets, so an engine
|
|
// restarted in a loop does not keep it quiet. The account is still not confined, and `node show` still says not
|
|
// judged; a search that failed, or a way to root found, is urgent at once.
|