//go:build integration package main import ( "strings" "testing" "time" "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" "github.com/novox/mesh-host/rootsearch" ) 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", time.Now()); 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", time.Now()); err != nil || named || !strings.Contains(why, "operator account") { t.Fatalf("agentConfined on a machine naming none: %v %q %v", named, why, err) } } 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) } } func TestASearchStillRunningAfterARestartIsNotUrgent(t *testing.T) { if searchQuietFor != rootsearch.Quiet { t.Fatalf("the quiet is %s and the node-engine's %s: they are one value", searchQuietFor, rootsearch.Quiet) } // A daily search that finishes within the quiet never leaves the account unjudged between two of them. if rootsearch.FreshFor < rootsearch.Every+rootsearch.Quiet { t.Fatalf("a complete search judges for %s, less than a day's search (%s) and the quiet (%s)", rootsearch.FreshFor, rootsearch.Every, rootsearch.Quiet) } open := aMesh(t) ctx := t.Context() inv := open.inventory if _, err := inv.NodeByName(ctx, "anchor"); err != nil { if _, err := inv.AddNode(ctx, "anchor"); err != nil { t.Fatal(err) } } if err := inv.SetAgentAccount(ctx, "anchor", "agent", ""); err != nil { t.Fatal(err) } d := &doctor{open: open} say := func(state, reason string) []conditions.Observation { t.Helper() // The engine's since is always now: it was just restarted. v := inventory.ResourceHealth{Module: "claude-code", Resource: "claude-code.agent-account", Kind: link.KindAccount, Target: "agent", State: state, Reason: reason, Root: link.RootNever, Account: "agent", Since: time.Now()} if _, err := inv.RecordHealth(ctx, inventory.NodeHealth{Node: "anchor", Contract: link.RootContract, SaidAt: time.Now(), HeardAt: time.Now(), Resources: []inventory.ResourceHealth{v}}); err != nil { t.Fatal(err) } found, err := probeAgentAccounts(ctx, d) if err != nil { t.Fatal(err) } return onlyMachine(found, "anchor") } running := link.ReasonRootPending + ": the search for setuid programs, started at 19:21, has not finished yet" healthy := func() { t.Helper() if found := say(link.StateHealthy, ""); len(found) != 0 { t.Fatalf("a healthy verdict raised: %+v", found) } } if found := say(link.StateUnknown, running); len(found) != 0 { t.Fatalf("a search first seen now was raised: %+v", found) } if _, confined, why, _ := agentConfined(ctx, inv, "anchor", time.Now()); confined || !strings.Contains(why, "not judged") { t.Fatalf("an account whose search runs was read as confined: %q", why) } // The engine restarted again and again, each statement's own since fresh: the controller's clock runs on. if _, err := inv.RootSearchPending(ctx, "anchor", time.Now().Add(-searchQuietFor-time.Minute)); err != nil { t.Fatal(err) } healthy() // a complete verdict forgets when the waiting began … if _, err := inv.RootSearchPending(ctx, "anchor", time.Now().Add(-searchQuietFor-time.Minute)); err != nil { t.Fatal(err) // … and this restart loop began past the bound } if found := say(link.StateUnknown, running); len(found) != 1 || found[0].Severity != conditions.Urgent { t.Fatalf("a search pending past the bound, by the controller's clock, was not urgent: %+v", found) } healthy() incomplete := rootsearch.ReasonIncomplete + ": the search for setuid programs did not finish (last tried at " + "19:23: timeout); it is tried again later" if found := say(link.StateUnknown, incomplete); len(found) != 1 || found[0].Severity != conditions.Urgent { t.Fatalf("a search that did not finish was not urgent: %+v", found) } if found := say(link.StateUnhealthy, link.ReasonRoot+": in the group docker; "+running); len(found) != 1 || found[0].Severity != conditions.Urgent { t.Fatalf("a way to root found while the search runs was not urgent: %+v", found) } }