Sidecar isolation Task 9 (3/5): the agent arms the policy, keeps the spawn record and reports the inventory (D12, D14, D15, D16)
CARLOS_FLAT_EXEC and CARLOS_FLAT_EXEC_ALLOW are read once at agent start (unset is report, unknown is refuse, exact hosts only); the policy goes on the exec backend. Each status snapshot builds the inventory of what refuse would stop from its own registry read through the armed pass's Classify (R4-3), never from the layout pass's run; with no pass armed it is unknown, which never counts as empty or closed. Status also carries the effective intent word, the per-host spawn record, D15's closure check and the workers readiness word (nested only when the readiness, the intent, the gate and claim mode all allow it). A refused flat row on a nested box kicks the layout leg (D16); a foreign row is not nudged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5 files changed,
+779
−0
internal/host/agent.go+19 −0internal/host/flatexec.go+345 −0internal/host/flatexec_copy.go+9 −0internal/host/flatexec_test.go+396 −0internal/host/layoutleg.go+10 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go| index ebcfc3141..1c3d5057d 100644 |
| --- a/internal/host/agent.go |
| +++ b/internal/host/agent.go |
| @@ -248,6 +248,13 @@ func runAgent(args []string, buildClaims ClaimFactory) { |
| } else { |
| log.Printf("agent: workers readiness %s: %s", w.Word(), w.Reason()) |
| } |
| + // The flat-exec policy is read here too, once, for the life of this |
| + // agent (sidecar isolation D12, D15): refuse or report, and the exact |
| + // hosts allowed to run in the host namespace. It goes on the exec |
| + // backend below; a refusal of a managed row on a nested box kicks the |
| + // layout leg (D16) once fastConvergeLoop has built it. |
| + flatExec := armFlatExec(os.Getenv, nudgeLayoutLeg) |
| + log.Printf("agent: flat exec %s (%d hosts allowlisted)", armedFlatExec.Load().mode, len(flatExec.Allow)) |
| regPath := peekRegistryPath(args, dd) |
| // A second handle on the same registry file the edge opens — the registry |
| // is designed for concurrent handles (fresh-connection reads), and this |
| @@ -443,6 +450,9 @@ func runAgent(args []string, buildClaims ClaimFactory) { |
| UpdatesReader: updatesReader(reg, release.OptDir()), |
| // Asked inside Acquire, before the lease (amadan #213). |
| ConfigGate: configGate(), |
| + // The flat-exec policy (sidecar isolation D13), asked at both |
| + // spawn seams before any cgroup is touched. |
| + FlatExec: flatExec, |
| } |
| if claims.Writes.Enabled { |
| eb.Timers = schedule.ClaimTimers{TimerStore: reg, Writes: claims.Writes, Context: agentCtx} |
| @@ -3104,6 +3114,11 @@ func fastConvergeLoop(reg *registry.Registry, svc *activator.Service, sd *schedu |
| layoutBox := metricsBoxLabel(os.Getenv, readMachineID()) |
| layoutPasses := newLayoutLeg(ctx, func(ctx context.Context) bool { return runLayoutLeg(ctx, store, layoutBox) }) |
| d.layoutLeg = layoutPasses.kick |
| + // The flat-exec policy's D16 nudge: a refused managed row on a nested |
| + // box kicks this same leg, so the pass runs within seconds, finds the |
| + // host asleep and moves it. |
| + kickLayout := func() { layoutPasses.kick(ctx) } |
| + layoutNudge.Store(&kickLayout) |
| // The platform self-update leg (spec §4): the sandboxed half, which only |
| // ever fetches into /data and writes a request. It lives here rather than |
| // in a loop of its own because the spread deadline needs a seconds |
| @@ -4093,6 +4108,10 @@ func statusLoop(reg *registry.Registry, svc *activator.Service, buildStates func |
| // pass still says something rather than nothing. |
| Layout: layoutStatusReport(routes), |
| } |
| + // The flat-exec policy, its spawn record and the inventory of what |
| + // refuse would stop (sidecar isolation D14), from this same |
| + // registry read (R4-3). Private Metrics only: hosts. |
| + p.FlatExec = flatExecStatusReport(routes) |
| // The hosts this box is refusing because it cannot provide their |
| // config (amadan #213): private Metrics only, nothing public. |
| claimReport(&p) |
diff --git a/internal/host/flatexec.go b/internal/host/flatexec.go| new file mode 100644 |
| index 000000000..0fb7db9fe |
| --- /dev/null |
| +++ b/internal/host/flatexec.go |
| @@ -0,0 +1,345 @@ |
| +package host |
| + |
| +import ( |
| + "log" |
| + "os" |
| + "sort" |
| + "strings" |
| + "sync" |
| + "sync/atomic" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/activator" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/status" |
| +) |
| + |
| +// The flat-exec policy on the box (sidecar isolation spec rev 4, D12 to |
| +// D16): how the agent reads CARLOS_FLAT_EXEC and CARLOS_FLAT_EXEC_ALLOW |
| +// once at start, keeps the spawn record the policy reports into, builds |
| +// the inventory of what refuse would stop for each status snapshot, and |
| +// nudges the layout pass when a managed row is refused on a nested box. |
| +// Both settings live in root-owned host.env; no file under /data decides |
| +// any of this (D15). |
| + |
| +// The env names and the policy words (D12). |
| +const ( |
| + flatExecEnv = "CARLOS_FLAT_EXEC" |
| + flatExecAllowEnv = "CARLOS_FLAT_EXEC_ALLOW" |
| + flatExecRefuse = "refuse" |
| + flatExecReport = "report" |
| +) |
| + |
| +// The closed words for the effective layout intent with its source (D14): |
| +// nested; flat because the fleet or the deployment chose it (a recorded |
| +// flat intent, or CARLOS_LAYOUT_DEFAULT=flat); flat because the box fell |
| +// back to it (the gate failing with no intent recorded, an invalid default, |
| +// the agent still starting); unknown because the last intent read failed |
| +// or no evaluation has run yet. |
| +const ( |
| + intentNested = "nested" |
| + intentFlatChoice = "flat_choice" |
| + intentFlatFallback = "flat_fallback" |
| + intentUnknown = "unknown" |
| +) |
| + |
| +// workersBlocked is the readiness word status decides itself (D14): the |
| +// start-time readiness holds but the box's intent, gate or mode does not |
| +// let a worker go nested. |
| +const workersBlocked = "blocked" |
| + |
| +// flatExecFromEnv reads the setting the way CARLOS_LAYOUT_DEFAULT is read |
| +// (trimmed, lower-cased): refuse or report; unset is report (rev 4, D15: |
| +// no automatic change); an unknown value is refuse, because an operator |
| +// who typed the variable meant to choose and the reading that cannot |
| +// expose a tenant is the one to take. allow is CARLOS_FLAT_EXEC_ALLOW's |
| +// exact hosts, each a valid host shape (provision.ValidHostShape), sorted |
| +// and deduplicated; dropped is every entry that was not, kept so status |
| +// and the log can name the typo. No wildcards or suffixes: one entry |
| +// reopening *.sidecar.internal would undo the whole change, and a star |
| +// is not a valid host shape. |
| +func flatExecFromEnv(getenv func(string) string) (mode string, allow []string, dropped []string) { |
| + switch v := strings.ToLower(strings.TrimSpace(getenv(flatExecEnv))); v { |
| + case "", flatExecReport: |
| + mode = flatExecReport |
| + case flatExecRefuse: |
| + mode = flatExecRefuse |
| + default: |
| + mode = flatExecRefuse |
| + log.Printf("agent: %s=%q is not refuse or report; refusing flat spawns", flatExecEnv, v) |
| + } |
| + seen := map[string]bool{} |
| + for _, raw := range strings.Split(getenv(flatExecAllowEnv), ",") { |
| + h := strings.ToLower(strings.TrimSpace(raw)) |
| + if h == "" { |
| + continue |
| + } |
| + if !provision.ValidHostShape(h) { |
| + dropped = append(dropped, h) |
| + continue |
| + } |
| + if !seen[h] { |
| + seen[h] = true |
| + allow = append(allow, h) |
| + } |
| + } |
| + sort.Strings(allow) |
| + if len(dropped) > 0 { |
| + log.Printf("agent: %s: %d entries are not exact hosts and were dropped: %s", flatExecAllowEnv, len(dropped), strings.Join(dropped, ", ")) |
| + } |
| + return mode, allow, dropped |
| +} |
| + |
| +// flatExecRecord is one host's spawn record (D14), the shape ConfigRefusal |
| +// has: the last reason word, the first and last time, and a count. |
| +type flatExecRecord struct { |
| + reason string |
| + since time.Time |
| + last time.Time |
| + count int |
| +} |
| + |
| +// flatExecState is the policy as armed for the life of the agent, with the |
| +// spawn record the policy's Record fills in. One per process |
| +// (armedFlatExec); a test arms its own and resets it. |
| +type flatExecState struct { |
| + mode string |
| + allow map[string]bool |
| + allowed []string // sorted, for status |
| + dropped []string |
| + |
| + // now and nudge are seams: the clock for the record, and the layout |
| + // leg's kick for D16. Both set by armFlatExec; a test replaces them. |
| + now func() time.Time |
| + nudge func() |
| + |
| + mu sync.Mutex |
| + records map[string]*flatExecRecord |
| +} |
| + |
| +var armedFlatExec atomic.Pointer[flatExecState] |
| + |
| +// layoutNudge is the layout leg's kick, published by fastConvergeLoop once |
| +// the leg exists (the policy is armed before it, at agent start). Nil |
| +// until then: a refusal in the window before the leg is built is not |
| +// nudged, and the leg's own first kick covers it. |
| +var layoutNudge atomic.Pointer[func()] |
| + |
| +// nudgeLayoutLeg is the policy's D16 nudge: the published kick, if any. |
| +func nudgeLayoutLeg() { |
| + if f := layoutNudge.Load(); f != nil { |
| + (*f)() |
| + } |
| +} |
| + |
| +// flatExecRecordsCap bounds the spawn record. Hosts come from the registry, |
| +// which bounds them already; the cap is for a box whose registry churns |
| +// hosts faster than records age, so the record cannot grow without bound |
| +// over months of uptime. The oldest record goes first. |
| +const flatExecRecordsCap = 500 |
| + |
| +// armFlatExec reads the setting and arms the policy for the life of this |
| +// agent. nudge is the layout leg's kick (D16), asked when a managed row is |
| +// refused on a box whose effective intent is nested; nil is no nudge. |
| +// The returned policy goes on the exec backend; nothing else holds it. |
| +func armFlatExec(getenv func(string) string, nudge func()) *activator.FlatExecPolicy { |
| + mode, allow, dropped := flatExecFromEnv(getenv) |
| + s := &flatExecState{mode: mode, allow: map[string]bool{}, allowed: allow, dropped: dropped, now: time.Now, nudge: nudge, records: map[string]*flatExecRecord{}} |
| + for _, h := range allow { |
| + s.allow[h] = true |
| + } |
| + armedFlatExec.Store(s) |
| + return s.policy() |
| +} |
| + |
| +// policy is the activator's view of the state: refuse or report, the |
| +// allowlist, and the record hook. |
| +func (s *flatExecState) policy() *activator.FlatExecPolicy { |
| + return &activator.FlatExecPolicy{Refuse: s.mode == flatExecRefuse, Allow: s.allow, Record: s.record} |
| +} |
| + |
| +// record is the policy's Record: it files the host's refusal or would-be |
| +// refusal, and under a refusal of a flat row on a box whose effective |
| +// intent is nested it kicks the layout pass (D16), which finds the host |
| +// asleep and moves it, so the next wake is nested. A foreign row is not |
| +// nudged: the pass does not move foreign rows, and its repair is the |
| +// box-local carlos add. |
| +func (s *flatExecState) record(host, reason string) { |
| + now := s.now() |
| + s.mu.Lock() |
| + r := s.records[host] |
| + if r == nil { |
| + if len(s.records) >= flatExecRecordsCap { |
| + s.evictOldestLocked() |
| + } |
| + r = &flatExecRecord{since: now} |
| + s.records[host] = r |
| + } |
| + if r.reason != reason { |
| + // A new stretch: the reason changed (report became refuse at a |
| + // restart, or a row was repaired from foreign to flat). |
| + r.reason, r.since, r.count = reason, now, 0 |
| + } |
| + r.last = now |
| + r.count++ |
| + s.mu.Unlock() |
| + if reason == activator.FlatExecFlatRefused && s.nudge != nil && effectiveLayoutIntent(os.Getenv) == intentNested { |
| + s.nudge() |
| + } |
| +} |
| + |
| +func (s *flatExecState) evictOldestLocked() { |
| + var oldest string |
| + var at time.Time |
| + for h, r := range s.records { |
| + if oldest == "" || r.last.Before(at) { |
| + oldest, at = h, r.last |
| + } |
| + } |
| + delete(s.records, oldest) |
| +} |
| + |
| +// refusals is the spawn record for status, sorted by host. |
| +func (s *flatExecState) refusals() []status.FlatExecRefusal { |
| + s.mu.Lock() |
| + defer s.mu.Unlock() |
| + out := make([]status.FlatExecRefusal, 0, len(s.records)) |
| + for h, r := range s.records { |
| + out = append(out, status.FlatExecRefusal{Host: h, Reason: r.reason, Since: r.since, Last: r.last, Count: r.count}) |
| + } |
| + sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host }) |
| + return out |
| +} |
| + |
| +// InventoryRow is one row of what refuse would stop (D14). |
| +type InventoryRow = status.FlatExecRow |
| + |
| +// flatExecInventory is D14's inventory, from the registry rather than from |
| +// history: every row the agent could spawn through a seam (an |
| +// activator-managed instance row or a worker row, foreign ones included, |
| +// awake or asleep) that classify calls flat or foreign and that allow does |
| +// not name, by host and layout word, sorted by host. A dormant row that |
| +// has never woken is in it like any other. |
| +func flatExecInventory(routes []registry.Route, classify func(registry.Route) activator.Layout, allow map[string]bool) []InventoryRow { |
| + var rows []InventoryRow |
| + for _, rt := range routes { |
| + if !activator.ActivatorManaged(rt) && !activator.WorkerManaged(rt) { |
| + continue |
| + } |
| + if allow[rt.Host] { |
| + continue |
| + } |
| + l := classify(rt) |
| + if l != activator.LayoutFlat && l != activator.LayoutForeign { |
| + continue |
| + } |
| + rows = append(rows, InventoryRow{Host: rt.Host, Layout: string(l), Worker: rt.IsSidecar()}) |
| + } |
| + sort.Slice(rows, func(i, j int) bool { return rows[i].Host < rows[j].Host }) |
| + return rows |
| +} |
| + |
| +// flatExecStatusReport builds the box's flat-exec report for the status |
| +// snapshot from the routes the caller already listed (R4-3: the snapshot's |
| +// own registry read, never the layout pass's run, which returns early on a |
| +// flat default, an unreadable intent or a failed gate). nil when no policy |
| +// is armed, which is no agent at all. The inventory needs the armed pass's |
| +// Classify, the same classification a wake sees; with no pass armed it is |
| +// unknown, and an unknown inventory never counts as empty or closed. |
| +func flatExecStatusReport(routes []registry.Route) *status.FlatExecReport { |
| + s := armedFlatExec.Load() |
| + if s == nil { |
| + return nil |
| + } |
| + r := &status.FlatExecReport{ |
| + Mode: s.mode, |
| + Allow: s.allowed, |
| + Dropped: s.dropped, |
| + Intent: effectiveLayoutIntent(os.Getenv), |
| + Refusals: s.refusals(), |
| + } |
| + pass := armedLayoutPass.Load() |
| + if pass == nil { |
| + r.InventoryUnknown = "no layout pass is armed on this box, so its rows cannot be classified" |
| + return r |
| + } |
| + classify := func(rt registry.Route) activator.Layout { |
| + l, _ := pass.Classify(rt, routes) |
| + return l |
| + } |
| + r.Inventory = flatExecInventory(routes, classify, s.allow) |
| + workersListed := false |
| + for _, row := range r.Inventory { |
| + if row.Worker { |
| + workersListed = true |
| + } |
| + } |
| + for _, rt := range routes { |
| + if activator.WorkerManaged(rt) && classify(rt) == activator.LayoutFlat { |
| + r.FlatWorkers++ |
| + } |
| + } |
| + // D15's closure check: refuse in force, nothing allowlisted, the |
| + // inventory known (it is, here), and no worker row flat or foreign. |
| + r.Closed = s.mode == flatExecRefuse && len(s.allow) == 0 && !workersListed |
| + return r |
| +} |
| + |
| +// effectiveLayoutIntent is the box's layout intent with its source, as |
| +// the closed words of D14, read from what the layout leg last published |
| +// (layoutIntentSeen, layoutDefaultSeen) and the deployment's default. |
| +func effectiveLayoutIntent(getenv func(string) string) string { |
| + switch lastLayoutIntent() { |
| + case release.LayoutModeNested: |
| + return intentNested |
| + case release.LayoutModeFlat: |
| + return intentFlatChoice |
| + case release.LayoutModeNone: |
| + default: |
| + // IntentUnreadable, or a mode this binary does not know. |
| + return intentUnknown |
| + } |
| + // No intent recorded (or none read yet). The deployment's default |
| + // stands in: flat by choice when it says flat; nested when the leg |
| + // applied it (which publishes nested above); otherwise the box is flat |
| + // because the default could not take, or has not been asked yet. |
| + def, invalid := layoutDefault(getenv) |
| + if invalid != "" { |
| + return intentFlatFallback |
| + } |
| + if def == release.LayoutModeFlat { |
| + return intentFlatChoice |
| + } |
| + if lastLayoutDefault().Blocked != "" { |
| + return intentFlatFallback |
| + } |
| + return intentUnknown |
| +} |
| + |
| +// workersStatusWord is Layout.Workers and its reason (D14): the cached |
| +// start-time readiness word unless it holds, and then nested only when the |
| +// effective intent is nested, the nested gate answers and claim mode is |
| +// off; otherwise blocked, with the first of those that does not hold as |
| +// the private reason. |
| +func workersStatusWord(getenv func(string) string) (word, reason string) { |
| + w := agentWorkers() |
| + if !w.Ready() { |
| + return w.Word(), w.Reason() |
| + } |
| + if strings.TrimSpace(getenv("CARLOS_HOST_CLAIM")) != "" { |
| + return workersBlocked, "claim mode is on; the nested worker runs on the legacy path only" |
| + } |
| + if intent := effectiveLayoutIntent(getenv); intent != intentNested { |
| + return workersBlocked, "the effective layout intent is " + intent + ", not nested" |
| + } |
| + gate := layoutDefaultGate.Load() |
| + if gate == nil { |
| + return workersBlocked, "no activator is armed on this box, so nothing could wake nested" |
| + } |
| + if err := (*gate)(); err != nil { |
| + return workersBlocked, "isolation not ready: " + err.Error() |
| + } |
| + return w.Word(), "" |
| +} |
diff --git a/internal/host/flatexec_copy.go b/internal/host/flatexec_copy.go| new file mode 100644 |
| index 000000000..04de6e379 |
| --- /dev/null |
| +++ b/internal/host/flatexec_copy.go |
| @@ -0,0 +1,9 @@ |
| +package host |
| + |
| +// Copy for the flat-exec policy's operator surfaces (sidecar isolation spec |
| +// D12, D14), collected and reviewed in Task 12. A placeholder until then. |
| + |
| +// flatExecAllowlistedWarning is what `carlos status --layout` says beside |
| +// every allowlisted host: it runs as the agent in the host namespace and |
| +// is not isolated from its neighbours. |
| +const flatExecAllowlistedWarning = "unisolated: allowlisted in CARLOS_FLAT_EXEC_ALLOW, this host runs as the agent in the host namespace and can read every tenant on the box" // copy: placeholder, Task 12 |
diff --git a/internal/host/flatexec_test.go b/internal/host/flatexec_test.go| new file mode 100644 |
| index 000000000..b225906bd |
| --- /dev/null |
| +++ b/internal/host/flatexec_test.go |
| @@ -0,0 +1,396 @@ |
| +package host |
| + |
| +import ( |
| + "errors" |
| + "path/filepath" |
| + "reflect" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/activator" |
| + "github.com/carlosframework/platform/internal/layout" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// resetFlatExec puts the armed policy, the leg's nudge and the workers |
| +// readiness back the way a fresh process has them. |
| +func resetFlatExec(t *testing.T) { |
| + t.Helper() |
| + prevWorkers := startWorkers.Load() |
| + clear := func() { |
| + armedFlatExec.Store(nil) |
| + layoutNudge.Store(nil) |
| + } |
| + clear() |
| + t.Cleanup(func() { |
| + clear() |
| + startWorkers.Store(prevWorkers) |
| + }) |
| +} |
| + |
| +// TestFlatExecEnvTable pins D12's reading: refuse and report by name, unset |
| +// is report (rev 4, D15: nothing changes until an operator sets it), an |
| +// unknown value is refuse, the allowlist is exact hosts only (an invalid |
| +// entry or a wildcard is dropped and reported), sorted and deduplicated. |
| +func TestFlatExecEnvTable(t *testing.T) { |
| + for _, tc := range []struct { |
| + name string |
| + env map[string]string |
| + mode string |
| + allow []string |
| + dropped []string |
| + }{ |
| + {name: "unset is report", env: nil, mode: "report"}, |
| + {name: "report", env: map[string]string{"CARLOS_FLAT_EXEC": "report"}, mode: "report"}, |
| + {name: "refuse", env: map[string]string{"CARLOS_FLAT_EXEC": "refuse"}, mode: "refuse"}, |
| + {name: "trimmed and lower-cased", env: map[string]string{"CARLOS_FLAT_EXEC": " Refuse\n"}, mode: "refuse"}, |
| + {name: "unknown is refuse", env: map[string]string{"CARLOS_FLAT_EXEC": "allow"}, mode: "refuse"}, |
| + {name: "exact hosts", env: map[string]string{"CARLOS_FLAT_EXEC_ALLOW": "tools.example.com, shop.example.com,shop.example.com"}, mode: "report", allow: []string{"shop.example.com", "tools.example.com"}}, |
| + {name: "wildcards rejected", env: map[string]string{"CARLOS_FLAT_EXEC_ALLOW": "*.sidecar.internal,shop.example.com,.example.com"}, mode: "report", allow: []string{"shop.example.com"}, dropped: []string{"*.sidecar.internal", ".example.com"}}, |
| + {name: "invalid entry dropped and reported", env: map[string]string{"CARLOS_FLAT_EXEC": "refuse", "CARLOS_FLAT_EXEC_ALLOW": "shop.example.com,not a host,,"}, mode: "refuse", allow: []string{"shop.example.com"}, dropped: []string{"not a host"}}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + mode, allow, dropped := flatExecFromEnv(envOf(tc.env)) |
| + if mode != tc.mode || !reflect.DeepEqual(allow, tc.allow) || !reflect.DeepEqual(dropped, tc.dropped) { |
| + t.Fatalf("flatExecFromEnv = %q %v %v, want %q %v %v", mode, allow, dropped, tc.mode, tc.allow, tc.dropped) |
| + } |
| + }) |
| + } |
| + // Armed, the policy carries exactly the reading: refuse, the allowlist |
| + // as a set, and the record hook. |
| + resetFlatExec(t) |
| + p := armFlatExec(envOf(map[string]string{"CARLOS_FLAT_EXEC": "refuse", "CARLOS_FLAT_EXEC_ALLOW": "shop.example.com"}), nil) |
| + if !p.Refuse || !p.Allow["shop.example.com"] || len(p.Allow) != 1 || p.Record == nil { |
| + t.Fatalf("armed policy = %+v", p) |
| + } |
| + if err := p.Admit(registry.Route{Host: "shop.example.com"}, activator.LayoutFlat); err != nil { |
| + t.Fatalf("allowlisted host: %v", err) |
| + } |
| + if err := p.Admit(registry.Route{Host: "other.example.com"}, activator.LayoutFlat); !errors.Is(err, activator.ErrFlatExecRefused) { |
| + t.Fatalf("other host: %v", err) |
| + } |
| + s := armedFlatExec.Load() |
| + if got := s.refusals(); len(got) != 1 || got[0].Host != "other.example.com" || got[0].Reason != activator.FlatExecFlatRefused || got[0].Count != 1 { |
| + t.Fatalf("spawn record = %+v", got) |
| + } |
| +} |
| + |
| +// flatExecFixture arms a pass over a registry with one row of every shape |
| +// the inventory must judge: a dormant flat instance that has never woken, a |
| +// foreign instance (carlos add --db), a flat worker, a foreign worker, a |
| +// nested instance, a nested worker, an external socket row and a |
| +// unit-backed worker. It returns the routes as the status loop lists them. |
| +type flatExecFixtureRows struct { |
| + dormant, foreign, flatWorker, foreignWorker, nested, nestedWorker string |
| + routes []registry.Route |
| +} |
| + |
| +func flatExecFixture(t *testing.T) flatExecFixtureRows { |
| + t.Helper() |
| + resetLayoutGlobals(t) |
| + dd, sd := t.TempDir(), t.TempDir() |
| + reg := restartTestRegistry(t) |
| + armedLayoutPass.Store(&layout.Pass{Registry: reg, DataDir: dd, SocketDir: sd}) |
| + f := flatExecFixtureRows{ |
| + dormant: "dormant.example.com", |
| + foreign: "added.example.com", |
| + flatWorker: "flat.shop.abc123" + provision.SidecarSuffix, |
| + foreignWorker: "edited.shop.abc123" + provision.SidecarSuffix, |
| + nested: "nested.example.com", |
| + nestedWorker: "nested.shop.abc123" + provision.SidecarSuffix, |
| + } |
| + f.routes = []registry.Route{ |
| + // Never woken: no status beyond ready, no process ever started. |
| + {Host: f.dormant, Kind: "instance", Hibernate: true, Status: "ready", Socket: filepath.Join(sd, f.dormant+".sock"), DBPath: provision.DefaultDBPath(dd, f.dormant)}, |
| + {Host: f.foreign, Kind: "instance", Hibernate: true, Status: "ready", Socket: filepath.Join(sd, f.foreign+".sock"), DBPath: filepath.Join(dd, "handmade.db")}, |
| + {Host: f.flatWorker, Kind: "sidecar", Hibernate: true, Status: "ready", DBPath: provision.DefaultDBPath(dd, f.flatWorker)}, |
| + {Host: f.foreignWorker, Kind: "sidecar", Status: "ready", DBPath: filepath.Join(dd, "tenant-wrote-this.db")}, |
| + {Host: f.nested, Kind: "instance", Hibernate: true, Status: "ready", Socket: filepath.Join(sd, "instances", provision.InstanceID(f.nested), "app.sock"), DBPath: provision.NestedDBPath(dd, f.nested)}, |
| + {Host: f.nestedWorker, Kind: "sidecar", Status: "ready", DBPath: provision.NestedDBPath(dd, f.nestedWorker)}, |
| + {Host: "external.example.com", Kind: "instance", Socket: filepath.Join(sd, "external.example.com.sock"), DBPath: provision.DefaultDBPath(dd, "external.example.com")}, |
| + {Host: "unit.shop.abc123" + provision.SidecarSuffix, Kind: "sidecar", Backing: registry.BackingUnit, DBPath: provision.DefaultDBPath(dd, "unit.shop.abc123"+provision.SidecarSuffix)}, |
| + } |
| + for _, rt := range f.routes { |
| + if err := reg.Upsert(rt); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + return f |
| +} |
| + |
| +func inventoryHosts(rows []InventoryRow) map[string]string { |
| + out := map[string]string{} |
| + for _, r := range rows { |
| + out[r.Host] = r.Layout |
| + } |
| + return out |
| +} |
| + |
| +// A dormant flat row that has never woken is in the inventory on the first |
| +// snapshot (D14: from the registry, not from history), beside the flat and |
| +// foreign worker rows and the foreign instance row; nested, external and |
| +// unit-backed rows are not. |
| +func TestInventoryIncludesDormantRow(t *testing.T) { |
| + resetFlatExec(t) |
| + f := flatExecFixture(t) |
| + armFlatExec(envOf(nil), nil) |
| + r := flatExecStatusReport(f.routes) |
| + if r == nil || r.InventoryUnknown != "" { |
| + t.Fatalf("report = %+v", r) |
| + } |
| + want := map[string]string{f.dormant: "flat", f.foreign: "foreign", f.flatWorker: "flat", f.foreignWorker: "foreign"} |
| + if got := inventoryHosts(r.Inventory); !reflect.DeepEqual(got, want) { |
| + t.Fatalf("inventory = %v, want %v", got, want) |
| + } |
| + for _, row := range r.Inventory { |
| + if row.Worker != strings.HasSuffix(row.Host, provision.SidecarSuffix) { |
| + t.Errorf("%s worker = %t", row.Host, row.Worker) |
| + } |
| + } |
| + if r.FlatWorkers != 1 { |
| + t.Errorf("flat workers = %d, want 1", r.FlatWorkers) |
| + } |
| + if r.Mode != "report" || r.Closed { |
| + t.Errorf("mode %q closed %t under an unset setting", r.Mode, r.Closed) |
| + } |
| +} |
| + |
| +// Review Focus 2: a box with a foreign row (carlos add --db) before refuse. |
| +// Status lists it in the inventory with its layout word, and once a spawn |
| +// of it has been recorded, the spawn record names it with the reported |
| +// reason, so the operator sees it before setting refuse. |
| +func TestStatusListsForeignRowInInventory(t *testing.T) { |
| + resetFlatExec(t) |
| + f := flatExecFixture(t) |
| + p := armFlatExec(envOf(map[string]string{"CARLOS_FLAT_EXEC": "report"}), nil) |
| + foreign := f.routes[1] |
| + if err := p.Admit(foreign, activator.LayoutForeign); err != nil { |
| + t.Fatalf("report admitted nothing: %v", err) |
| + } |
| + r := flatExecStatusReport(f.routes) |
| + if got := inventoryHosts(r.Inventory)[f.foreign]; got != "foreign" { |
| + t.Fatalf("foreign row in inventory = %q, want foreign (inventory %v)", got, r.Inventory) |
| + } |
| + if len(r.Refusals) != 1 || r.Refusals[0].Host != f.foreign || r.Refusals[0].Reason != activator.FlatExecForeignReported || r.Refusals[0].Count != 1 { |
| + t.Fatalf("spawn record = %+v", r.Refusals) |
| + } |
| + // The allowlist takes a row out of the inventory, and the allowlist is |
| + // reported beside it. |
| + resetFlatExec(t) |
| + armFlatExec(envOf(map[string]string{"CARLOS_FLAT_EXEC_ALLOW": f.foreign}), nil) |
| + r = flatExecStatusReport(f.routes) |
| + if _, listed := inventoryHosts(r.Inventory)[f.foreign]; listed || !reflect.DeepEqual(r.Allow, []string{f.foreign}) { |
| + t.Fatalf("allowlisted foreign row: inventory %v allow %v", r.Inventory, r.Allow) |
| + } |
| +} |
| + |
| +// D15's closure check is true only with refuse in force, an empty |
| +// allowlist, a known inventory and no worker row flat or foreign. An |
| +// allowlisted worker does not close it; a flat instance row alone does. |
| +func TestClosureCheck(t *testing.T) { |
| + resetFlatExec(t) |
| + f := flatExecFixture(t) |
| + nestedOnly := []registry.Route{f.routes[4], f.routes[5], f.routes[0]} // nested instance, nested worker, a flat instance row |
| + for _, tc := range []struct { |
| + name string |
| + env map[string]string |
| + routes []registry.Route |
| + closed bool |
| + }{ |
| + {name: "report with no flat worker", env: map[string]string{"CARLOS_FLAT_EXEC": "report"}, routes: nestedOnly}, |
| + {name: "refuse with a flat worker", env: map[string]string{"CARLOS_FLAT_EXEC": "refuse"}, routes: f.routes}, |
| + {name: "refuse with only a foreign worker", env: map[string]string{"CARLOS_FLAT_EXEC": "refuse"}, routes: append(nestedOnly, f.routes[3])}, |
| + {name: "refuse with the flat worker allowlisted", env: map[string]string{"CARLOS_FLAT_EXEC": "refuse", "CARLOS_FLAT_EXEC_ALLOW": f.flatWorker}, routes: append(nestedOnly, f.routes[2])}, |
| + {name: "refuse, empty allowlist, no flat or foreign worker", env: map[string]string{"CARLOS_FLAT_EXEC": "refuse"}, routes: nestedOnly, closed: true}, |
| + {name: "refuse with no rows at all", env: map[string]string{"CARLOS_FLAT_EXEC": "refuse"}, routes: nil, closed: true}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + armedFlatExec.Store(nil) |
| + armFlatExec(envOf(tc.env), nil) |
| + r := flatExecStatusReport(tc.routes) |
| + if r.Closed != tc.closed { |
| + t.Fatalf("closed = %t, want %t (report %+v)", r.Closed, tc.closed, r) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// R4-3: the inventory is computed from the snapshot's registry read under |
| +// every intent state the layout pass returns early on: a flat default, an |
| +// unreadable intent, a failed gate. Each is reported with its intent word. |
| +func TestInventoryComputedUnderFlatDefault(t *testing.T) { |
| + resetFlatExec(t) |
| + f := flatExecFixture(t) |
| + t.Setenv(layoutDefaultEnv, "flat") |
| + armFlatExec(envOf(nil), nil) |
| + r := flatExecStatusReport(f.routes) |
| + if r.Intent != intentFlatChoice || len(r.Inventory) != 4 || r.InventoryUnknown != "" { |
| + t.Fatalf("under a flat default: %+v", r) |
| + } |
| +} |
| + |
| +func TestInventoryComputedUnderUnreadableIntent(t *testing.T) { |
| + resetFlatExec(t) |
| + f := flatExecFixture(t) |
| + noteLayoutIntent(string(layout.IntentUnreadable)) |
| + armFlatExec(envOf(nil), nil) |
| + r := flatExecStatusReport(f.routes) |
| + if r.Intent != intentUnknown || len(r.Inventory) != 4 || r.InventoryUnknown != "" { |
| + t.Fatalf("under an unreadable intent: %+v", r) |
| + } |
| +} |
| + |
| +func TestInventoryComputedUnderFailedGate(t *testing.T) { |
| + resetFlatExec(t) |
| + f := flatExecFixture(t) |
| + noteLayoutDefault("", "isolation not ready: the probe has not run") |
| + armFlatExec(envOf(nil), nil) |
| + r := flatExecStatusReport(f.routes) |
| + if r.Intent != intentFlatFallback || len(r.Inventory) != 4 || r.InventoryUnknown != "" { |
| + t.Fatalf("under a failed gate: %+v", r) |
| + } |
| + // And the two intents that are a choice or nested. |
| + noteLayoutIntent(release.LayoutModeNested) |
| + if r := flatExecStatusReport(f.routes); r.Intent != intentNested || len(r.Inventory) != 4 { |
| + t.Fatalf("under a nested intent: %+v", r) |
| + } |
| + noteLayoutIntent(release.LayoutModeFlat) |
| + if r := flatExecStatusReport(f.routes); r.Intent != intentFlatChoice { |
| + t.Fatalf("under a recorded flat intent: %+v", r) |
| + } |
| +} |
| + |
| +// An inventory that could not be computed (no pass armed, so no |
| +// classification) is unknown, and unknown never counts as empty or closed, |
| +// however the policy is set. |
| +func TestInventoryUnknownIsNotClosed(t *testing.T) { |
| + resetFlatExec(t) |
| + resetLayoutGlobals(t) |
| + armFlatExec(envOf(map[string]string{"CARLOS_FLAT_EXEC": "refuse"}), nil) |
| + r := flatExecStatusReport(nil) |
| + if r == nil || r.InventoryUnknown == "" || r.Closed || len(r.Inventory) != 0 || r.Mode != "refuse" { |
| + t.Fatalf("with no pass armed: %+v", r) |
| + } |
| + // No policy armed at all (no agent): nothing to report. |
| + armedFlatExec.Store(nil) |
| + if r := flatExecStatusReport(nil); r != nil { |
| + t.Fatalf("unarmed policy reported %+v", r) |
| + } |
| +} |
| + |
| +// D16: a refusal of a flat (managed) row on a box whose effective intent is |
| +// nested kicks the layout leg; a foreign row's refusal does not, nor does a |
| +// would-be refusal under report, nor any refusal on a box whose intent is |
| +// not nested. |
| +func TestRefusedManagedRowNudgesPass(t *testing.T) { |
| + resetFlatExec(t) |
| + resetLayoutGlobals(t) |
| + kicks := 0 |
| + armFlatExec(envOf(map[string]string{"CARLOS_FLAT_EXEC": "refuse"}), func() { kicks++ }) |
| + s := armedFlatExec.Load() |
| + noteLayoutIntent(release.LayoutModeNested) |
| + s.record("shop.example.com", activator.FlatExecFlatRefused) |
| + if kicks != 1 { |
| + t.Fatalf("flat refusal under nested: kicks = %d, want 1", kicks) |
| + } |
| + s.record("added.example.com", activator.FlatExecForeignRefused) |
| + s.record("shop.example.com", activator.FlatExecFlatReported) |
| + if kicks != 1 { |
| + t.Fatalf("foreign refusal or a report nudged: kicks = %d", kicks) |
| + } |
| + noteLayoutIntent(release.LayoutModeFlat) |
| + s.record("shop.example.com", activator.FlatExecFlatRefused) |
| + if kicks != 1 { |
| + t.Fatalf("flat refusal under a flat intent nudged: kicks = %d", kicks) |
| + } |
| + // The published kick is what the agent's nudge reaches; nil before the |
| + // leg exists is a quiet no-op. |
| + nudgeLayoutLeg() |
| + published := 0 |
| + f := func() { published++ } |
| + layoutNudge.Store(&f) |
| + nudgeLayoutLeg() |
| + if published != 1 { |
| + t.Fatalf("published kick ran %d times", published) |
| + } |
| +} |
| + |
| +// The spawn record keeps one stretch per host: a count and the first and |
| +// last time, restarted when the reason changes. |
| +func TestFlatExecRecordStretches(t *testing.T) { |
| + resetFlatExec(t) |
| + armFlatExec(envOf(map[string]string{"CARLOS_FLAT_EXEC": "refuse"}), nil) |
| + s := armedFlatExec.Load() |
| + clock := time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC) |
| + s.now = func() time.Time { return clock } |
| + s.record("a.example.com", activator.FlatExecFlatRefused) |
| + clock = clock.Add(time.Minute) |
| + s.record("a.example.com", activator.FlatExecFlatRefused) |
| + got := s.refusals() |
| + if len(got) != 1 || got[0].Count != 2 || !got[0].Since.Equal(clock.Add(-time.Minute)) || !got[0].Last.Equal(clock) { |
| + t.Fatalf("two refusals: %+v", got) |
| + } |
| + clock = clock.Add(time.Minute) |
| + s.record("a.example.com", activator.FlatExecForeignRefused) |
| + got = s.refusals() |
| + if got[0].Reason != activator.FlatExecForeignRefused || got[0].Count != 1 || !got[0].Since.Equal(clock) { |
| + t.Fatalf("a changed reason did not start a new stretch: %+v", got) |
| + } |
| + // The cap evicts the oldest record, never the newest. |
| + for i := 0; i < flatExecRecordsCap+5; i++ { |
| + clock = clock.Add(time.Second) |
| + s.record(strings.Repeat("h", 1+i%3)+string(rune('a'+i%26))+".example.com"+strings.Repeat("x", i/26), activator.FlatExecFlatRefused) |
| + } |
| + if n := len(s.refusals()); n > flatExecRecordsCap { |
| + t.Fatalf("records = %d, over the cap %d", n, flatExecRecordsCap) |
| + } |
| +} |
| + |
| +// Layout.Workers (D14): the cached readiness word unless it holds, and then |
| +// nested only with a nested intent, an answering gate and claim mode off; |
| +// blocked, with the reason, otherwise. |
| +func TestWorkersStatusWord(t *testing.T) { |
| + resetFlatExec(t) |
| + resetLayoutGlobals(t) |
| + noEnv := envOf(nil) |
| + startWorkers.Store(nil) |
| + if w, _ := workersStatusWord(noEnv); w != "off" { |
| + t.Fatalf("unarmed = %q, want off", w) |
| + } |
| + startWorkers.Store(&WorkersReadiness{word: workersWaitingForPrev, reason: "prev below"}) |
| + if w, r := workersStatusWord(noEnv); w != "waiting_for_prev" || r != "prev below" { |
| + t.Fatalf("waiting = %q %q", w, r) |
| + } |
| + startWorkers.Store(&WorkersReadiness{ready: true, word: workersNested}) |
| + if w, r := workersStatusWord(noEnv); w != "blocked" || !strings.Contains(r, "unknown") { |
| + t.Fatalf("ready with no intent = %q %q, want blocked on the intent", w, r) |
| + } |
| + noteLayoutIntent(release.LayoutModeNested) |
| + if w, r := workersStatusWord(noEnv); w != "blocked" || !strings.Contains(r, "no activator") { |
| + t.Fatalf("ready, nested, no gate = %q %q", w, r) |
| + } |
| + gateErr := errors.New("mounts missing") |
| + gate := func() error { return gateErr } |
| + layoutDefaultGate.Store(&gate) |
| + if w, r := workersStatusWord(noEnv); w != "blocked" || !strings.Contains(r, "mounts missing") { |
| + t.Fatalf("ready, nested, failing gate = %q %q", w, r) |
| + } |
| + gateErr = nil |
| + if w, r := workersStatusWord(envOf(map[string]string{"CARLOS_HOST_CLAIM": "on"})); w != "blocked" || !strings.Contains(r, "claim") { |
| + t.Fatalf("claim mode = %q %q", w, r) |
| + } |
| + if w, r := workersStatusWord(noEnv); w != "nested" || r != "" { |
| + t.Fatalf("everything holds = %q %q, want nested", w, r) |
| + } |
| + // And it rides on the layout report, whatever the pass state. |
| + if rep := layoutStatusReport(nil); rep.Workers != "nested" { |
| + t.Fatalf("layout report workers = %q", rep.Workers) |
| + } |
| + noteLayoutIntent(release.LayoutModeFlat) |
| + if rep := layoutStatusReport(nil); rep.Workers != "blocked" || !strings.Contains(rep.WorkersReason, "flat_choice") { |
| + t.Fatalf("layout report under flat = %q %q", rep.Workers, rep.WorkersReason) |
| + } |
| +} |
diff --git a/internal/host/layoutleg.go b/internal/host/layoutleg.go| index 8ccc4606c..a021b48fe 100644 |
| --- a/internal/host/layoutleg.go |
| +++ b/internal/host/layoutleg.go |
| @@ -190,6 +190,16 @@ func noteLayoutDefault(mode, reason string) { |
| // this never lists the registry itself, mirroring every other probe in |
| // this file. |
| func layoutStatusReport(routes []registry.Route) *status.LayoutReport { |
| + r := layoutRowsReport(routes) |
| + // The workers readiness word (sidecar isolation D11, D14) rides on |
| + // the layout report whatever its mode: it is about this agent, not |
| + // about any row. |
| + r.Workers, r.WorkersReason = workersStatusWord(os.Getenv) |
| + return r |
| +} |
| + |
| +// layoutRowsReport is layoutStatusReport without the workers word. |
| +func layoutRowsReport(routes []registry.Route) *status.LayoutReport { |
| pass := armedLayoutPass.Load() |
| if pass == nil { |
| return unmanagedLayoutReport(routes) |