# Cluster source of truth and distribution

Audience: AI coding agents first. Status: DESIGN.

Relationship to `docs/specs/2026-08-06-execution-plane-insulation.md` (itself a proposal, not landed doctrine): this design **refines and constrains** it. That proposal's rule "No agent compute on the laptop" stays in force as the default. This design adds exactly one narrow exception — a controller-issued, expiring, revision-bound lease granted only during confirmed unavailability of all three builders. Every insulation invariant (cgroup confinement, bounded concurrency, observability, stoppability) still applies to leased local work.

## Rule

**Declare fleet desired state once in overdeck. Apply the same revision to workstation and debian1/2/3. NEVER sync one node from another node.**

Two planes stay separate:

1. **Desired-state plane (Git):** node roles, profiles, tool refs, home manifests, units, host config. MUST work with the controller down.
2. **Availability plane (controller):** health, draining, placement, fallback lease. MUST NEVER become desired-state storage.

Workstation policy:

- MUST stay synchronized and audited while execution-ineligible.
- MUST NOT accept agent or build work while any remote builder is available.
- MAY accept work only after all three remote builders are `unavailable`.
- MUST stop accepting new work after any builder recovers.
- MUST drain existing local work; NEVER kill an active job because a builder recovered.
- Busy, full, overloaded, one transiently-failed job, or controller uncertainty MUST NOT mean unavailable.
- Controller absent, unreachable, or ambiguous → workstation stays execution-ineligible.

**Roles select configuration only.** A node carrying `roles:["builder"]` or `roles:["agent-runtime"]` is thereby eligible for nothing. Workload placement eligibility comes solely from `execution` plus a live controller lease. Never derive placement from a role.

## Current problem

| Plane | Current source | Gap |
|---|---|---|
| Workstation install | `deckctl` + `modules/*/deck.module.json` | buildbox module has no descriptor; workstation is not modelled as a node |
| Agent homes | `modules/workstation/manifest/*.json` | canonical locally and for remote seats, but not fleet-level |
| Buildboxes | `modules/buildbox/bin/buildbox`, `lib/buildbox-checks.sh`, `devtools.json` | each artifact needs bespoke `push_*` + `EXPECT_*_SHA` plumbing |
| Work placement | `~/.claude/build-remote.json`, `remote-build.mjs` health cache, `controller/src/store.ts` | two health models; `unreachable-or-full` conflates down with full; local fallback is globally off rather than leased |

Adding one shared artifact today means edits in unrelated imperative paths. Host schema/defaults are duplicated across ~19 consumers of `build-remote.json`. The controller already models `builder|workstation` roles, draining, and spill leases — but its spill trigger is capacity-based and is not connected to any executor.

## Chosen architecture

**Git-declared push convergence.** No resident pull agents.

```text
overdeck landed revision
  └─ fleet declaration + profile declarations
       ├─ expand node → ordered desired items
       ├─ audit  (local | ssh transport)
       └─ converge (local | ssh transport)

runtime controller
  └─ builder health + placement
       ├─ any builder available → workstation denied
       └─ all builders unavailable → one bounded workstation lease
```

### 1. Canonical fleet declaration

Add one versioned declaration at `modules/fleet/fleet.json`, authoritative for node identity, transport reference, roles, profiles, and execution policy.

```json
{
  "schema_version": 1,
  "nodes": {
    "workstation": {
      "transport": "local",
      "roles": ["control", "agent-runtime"],
      "profiles": ["shared-agent-tools", "workstation"],
      "execution": "last-resort"
    },
    "debian1": {
      "transport": "ssh",
      "host_ref": "debian1",
      "roles": ["builder", "agent-runtime"],
      "profiles": ["shared-agent-tools", "buildbox"],
      "execution": "normal"
    }
  },
  "fallback": {
    "enabled": true,
    "node": "workstation",
    "requires_all_unavailable": ["debian1", "debian2", "debian3"],
    "max_concurrent_local_jobs": 1,
    "activation_windows": 3,
    "health_window_sec": 60,
    "lease_ttl_sec": 900
  }
}
```

Rules:

- debian2/debian3 are declared identically to debian1 except node identity and `host_ref`.
- `execution` is a closed enum: `normal | last-resort | none`. Absent → `none` (fail closed).
- Unknown keys, roles, profiles, transports, node references, or enum values MUST fail closed before any network or filesystem mutation.
- One parser/schema owns defaults. Shell consumers call its query interface; they MUST NOT reparse the JSON.
- Secret values stay out of Git. The declaration MAY name a required secret capability; enrollment supplies the value from existing protected storage.
- `fallback.requires_all_unavailable` is an **intentional explicit allow-list**, not a derived view of the builder nodes. Adding a builder node MUST NOT silently widen the outage condition that arms local execution. The loader MUST reject any name in it that is not a declared `builder`; a declared builder absent from it is legal and means that builder's state is ignored for activation.

#### Canonical transport/runtime config and migration direction

`~/.claude/build-remote.json` remains the **runtime transport + tuning** file (port, `ssh_user`, `identity_file`, `remote_root`, timeouts, `max_remote_jobs`, `rsync_only`, `ship_ignored`, `local_only`). It is out-of-repo by design: it carries host-local tuning and identity paths.

Migration direction is one-way and narrow:

- `hosts` moves **out** of `build-remote.json` and **into** `modules/fleet/fleet.json`. `build-remote.json` keeps per-host tuning keyed by the fleet's node names.
- `local_fallback` stays present and stays `false` forever. It is the legacy "remote unreachable or full → run locally" path and MUST remain dead. Nothing in this design reads it as permission.
- `fallback.enabled` in `fleet.json` is the only switch that arms leased fallback. The two keys are not aliases and MUST NOT be collapsed.
- During migration the fleet loader reads `hosts` from `fleet.json` and every consumer of the old key goes through the loader's query interface. A `hosts` key still present in `build-remote.json` after step 5 MUST fail the audit, not be silently preferred.

### 2. Profile expansion

Profiles compose existing declarations; they do not create a second content inventory.

| Profile | Reused source |
|---|---|
| `shared-agent-tools` | `modules/workstation/manifest/*.json`, generalized `modules/buildbox/devtools.json` |
| `workstation` | workstation module declarations and local units |
| `buildbox` | `modules/buildbox/user-config`, slice ceilings, scratch policy |
| `buildbox-root` | `modules/buildbox/host-config` — harden-only, never in fan-out converge |

Expansion emits ordered desired items:

```text
DesiredItem
  id: stable unique string
  source: repo path or pinned repository ref
  destination: node path
  ownership: user | root
  strategy: symlink | copy | git-checkout | unit
  mode: audit | converge | harden
  content_digest: deterministic digest
```

Rules:

- Existing runtime manifests stay canonical for agent-home contents.
- Generalize `devtools.json`; do NOT duplicate pinned refs in the fleet declaration.
- Preserve `source: repo|deploy`. Executed tools MUST resolve from the landed deploy revision, never a dirty development checkout.
- Duplicate destinations after expansion MUST abort before contacting any node.
- `ownership: root` items MUST NOT enter ordinary fleet-wide converge.

#### `system-units.sh` glob hazard — fix before adding a buildbox descriptor

`lib/deckctl/system-units.sh` installs every `modules/*/systemd/system` and `modules/*/*/systemd/system` tree into the workstation's `/etc/systemd/system`, and every `modules/*/system-bin` into `/usr/local/bin`. The depth-2 glob makes this a directory-name coincidence, not an opt-in: the moment `modules/buildbox` acquires a descriptor and any nested `systemd/system` directory, buildbox root units install onto the workstation.

Required fix, landed **before** any buildbox module descriptor exists:

- Replace both globs with an explicit per-module opt-in read from each `deck.module.json` (a `system_units` / `system_bin` path list). No path is installed unless its owning module declares it.
- A module declaring a path that does not exist MUST fail closed.
- `modules/buildbox/host-config` MUST NOT be declarable as a workstation system-unit source; it belongs to profile `buildbox-root`, reachable only through `hardenNode`.

### 3. Shared convergence engine

One planner/executor behind two transports.

```text
loadFleet(path) -> ValidatedFleet
expandNode(fleet, nodeName) -> DesiredItem[]
auditNode(nodeName, desiredItems, transport) -> AuditReport
convergeNode(nodeName, desiredItems, transport) -> ConvergeReport
hardenNode(nodeName, desiredItems, transport) -> HardenReport
```

Behavior:

- Audit compares declared digest, ownership, mode, link target, unit state, and pinned Git ref.
- Converge stages, validates, then atomically promotes where the destination supports atomic replacement.
- Non-root convergence is idempotent and safe to fan out in parallel.
- Per-node output is buffered and printed in declaration order. Exit nonzero if any node fails.
- One node's failure MUST NOT mutate or roll back another node.
- `hardenNode` accepts exactly one SSH buildbox node; it MUST reject workstation, multiple nodes, and implicit all-node selection.
- Preserve one-at-a-time reboot verification for disk, watchdog, SSH, kernel, and root systemd changes.
- Emit machine-readable reports plus concise terminal output.

Transports: `local` (direct filesystem/systemd on workstation) and `ssh` (existing port/identity/staging path). Transport changes how an operation reaches a node; it MUST NOT redefine desired-state behavior.

#### Remote agent-home ownership

`modules/workstation/claude/lib/remote-seat-provision.mjs` already owns remote agent-home deployment: manifest validation, revision/fingerprint generation, staged destinations, atomic finalization, remote seat audit. **It remains the sole owner of remote agent-home trees.** Two engines MUST NOT write that tree.

Therefore:

- The fleet engine's `shared-agent-tools` profile does NOT re-deploy agent homes. For nodes carrying that profile, `auditNode`/`convergeNode` **delegate** to `remote-seat-provision.mjs` for the agent-home item set and consume its report.
- The fleet engine keeps ownership of everything outside the agent-home tree: devtools, user-config, units, scratch policy, host config.
- If, after migration, delegation proves to be a thin pass-through with no distinct behavior, the correct resolution is to **absorb** `remote-seat-provision.mjs` into the engine and delete it — not to run both. That decision is made at migration step 3 with the parity report in hand, and recorded in this file.

### 4. Compatible commands

```text
deckctl fleet audit [node...]
deckctl fleet converge [node...]
deckctl fleet harden <one-buildbox-node>
deckctl fleet status
```

Adapters (argument translation only — no desired-state logic):

- `deckctl sync` → local workstation convergence.
- `buildbox audit` → fleet audit filtered to `role=builder`.
- `buildbox bootstrap` → non-root fleet convergence filtered to `role=builder`.
- `buildbox harden` → one-node fleet hardening, preserving its explicit-host guard.
- `deckctl agents` → unchanged behavior; node discovery moves to the fleet query interface.

### 5. Availability and last-resort workstation

#### 5a. Delete the existing capacity spill

`controller/src/scheduler.ts` currently spills to a hardcoded host `"laptop"` when `builders.every(isOverloaded) && queuedCount > builders.length`. That is **saturation-triggered laptop compute** and directly violates the policy above. It MUST be deleted, not reused, not gated behind a flag:

- Remove the overload/queue-depth spill branch from `reconcile()`.
- Remove `store.tryPlaceSpill()`, the `placement_kind = 'spill'` rows, the spill-lease TTL field, and the `"recall-spill"` transition — or, if the `placement_kind`/lease columns are reused by the new lease, they are re-specified from scratch with the new semantics and the old code paths deleted in the same change. No dormant capacity-spill code survives.
- The hardcoded `"laptop"` string disappears; the fallback node name comes only from `fleet.json`.

#### 5b. One builder-health evidence path

The controller does not currently probe builders — `watchdog.ts` probes controller/collector only, and `remote-build.mjs` keeps its own `remote-health-<host>.json` cache that conflates down with full under `unreachable-or-full`. There MUST be exactly one evidence path.

- The controller reconciler becomes the sole producer of builder availability. It probes each declared builder on a fixed `health_window_sec` cadence using the fleet's SSH transport.
- `HostStateSchema` gains `unavailable`. Full enum: `available | unavailable | draining | maintenance | restoring | degraded`.
- The probe classifies into exactly one of: reachable-and-admitting, reachable-but-full, reachable-but-degraded, unreachable. Only *unreachable* accumulates toward `unavailable`.
- `remote-build.mjs` stops classifying availability. Its health cache is reduced to a latency/affinity hint or deleted; the `unreachable-or-full` classification is removed. Executors read availability from the controller.
- Per-job pull/push/epoch failures remain transient job failures and MUST NOT feed host availability.
- `max_remote_jobs` and queue depth remain capacity signals and MUST NOT feed availability.

#### 5b-bis. How the controller learns the fleet

The controller reads `fleet.json` itself — it is not told the policy by callers. Pinned:

- Source: the same validated declaration in the overdeck checkout the controller runs from. It loads at startup and on an explicit `reload` signal; there is no polling and no partial reload.
- `fleetRevision` is the deterministic content digest of the validated declaration. Callers echo it in `POST /fallback/lease`; the controller compares and returns `revision-mismatch` on any difference.
- A caller may hold an older revision than the controller between landing a revision and the controller reloading it. That window is **deny**, not grant — the caller sees `revision-mismatch` and runs no local work.
- Load failure, schema-invalid declaration, or no declaration loaded → the controller serves `granted:false, reason:"fleet-unloaded"` and never falls back to defaults. This is distinct from `fallback-disabled`, which means the operator set `fallback.enabled=false`; one is a broken controller, the other is policy, and they need different operator responses.
- `fallback.enabled`, `activation_windows`, `health_window_sec`, `lease_ttl_sec`, `max_concurrent_local_jobs`, and the builder list come only from this load. Nothing in the runtime store overrides them.

#### 5c. Lease state machine

```text
builder availability   = available | unavailable | draining | maintenance | degraded | restoring
workstation eligibility = denied | leased | draining
```

1. Default `workstation=denied`.
2. Activate only after every builder in `requires_all_unavailable` has been `unavailable` for `activation_windows` (3) consecutive `health_window_sec` (60 s) windows — i.e. ≥180 s of confirmed whole-cluster outage.
   **Only `unavailable` counts toward activation.** `draining`, `maintenance`, `degraded`, and `restoring` do NOT. A builder in any of those states blocks activation exactly as `available` does, even though it accepts no work. Concrete case: debian1 `maintenance` + debian2/3 `unavailable` → **deny**. Zero builders can take work and the queue waits; a builder an operator deliberately took out of rotation is never evidence of an outage.
3. Grant at most `max_concurrent_local_jobs` (1) leases, each with `lease_ttl_sec` (900 s) expiry. Existing dedupe and local slot ceilings still apply.
4. On the first successful recovery probe from any builder, set `workstation=draining` and deny new local placements. Recovery needs one window, not three — recovery is allowed to be fast, activation is not.
5. When the local active count reaches zero, recall the lease and set `workstation=denied`.
6. Controller unreachable, request timeout, lease expired, fleet revision mismatch, `fallback.enabled=false`, or any ambiguous state → **deny**.
7. **Controller restart:** in-memory activation counters are lost. On restart the controller starts at `denied` with zero windows accumulated and re-earns activation from scratch. Leases persisted in the store are honored until expiry for *draining* purposes only — a restarted controller MUST NOT issue a new lease on the strength of a pre-restart counter.

#### 5d. Lease endpoint

Add a distinct endpoint. Do **not** relax `store.hostEligible()`, which returns `not-builder` for the workstation by design and also conflates `no-capacity` into the same answer.

```text
POST /fallback/lease          { node, job, fleetRevision }
  -> { granted: bool, reason: string, leaseId: string|null,
       expiresAt: iso8601|null, fleetRevision: string }
POST /fallback/lease/release  { leaseId }
```

Granting is a POST, never a GET: a GET that reserves a slot is retry-unsafe, and a proxy or client retry would double-grant against `max_concurrent_local_jobs`. Grants are idempotent per `job` — a repeated request for a job that already holds a live lease returns that same `leaseId`, never a second one.

- `reason` is a closed enum distinguishing at minimum: `granted`, `builders-available`, `activation-pending`, `draining`, `at-local-capacity`, `fallback-disabled`, `fleet-unloaded`, `revision-mismatch`. `fallback-disabled` means the operator set `fallback.enabled=false`; `fleet-unloaded` means the controller has no valid fleet declaration. They demand different operator responses and MUST NOT be collapsed.
- Any non-200, malformed body, or timeout is `granted:false`.

#### 5e. Every local launch door uses one lease library

`local-gate` is not the only door. All of these can start local build or agent work and MUST route through one shared fail-closed check:

- `modules/workstation/claude/bin/local-gate`
- cursor `ca.sh` dispatch
- `cdx` dispatch
- factory dispatch
- harnessd dispatch
- any `confine.sh`-launched agent/build scope

Contract:

```text
fallbackLeaseCheck(job) -> { allowed: bool, leaseId: string|null, reason: string }
```

- One implementation, one shared library, consumed by shell and JS callers alike. A caller that cannot reach the library MUST refuse to launch.
- The check runs at the launch boundary, before process spawn.
- `local_only` package-installation commands keep current behavior — they are workstation operations, not fallback compute placements, and do not consume a lease.
- Seat attestation and `local_only` semantics stay independent. A lease MUST NOT bypass seat-host mismatch or attestation failure.
- `local-gate` keeps reading `local_fallback` only to assert it is `false`. It never grants on it.
- `BUILD_REMOTE_LOCAL_FALLBACK=1` remains an explicit, incident-recorded manual override. Automatic fallback MUST NOT depend on it and MUST NOT set it.

## Data flow

### Desired-state convergence

1. Operator lands an overdeck revision.
2. `deckctl fleet` loads and validates `modules/fleet/fleet.json`.
3. Engine expands profiles and rejects conflicts.
4. Engine resolves every source from the same landed revision and pinned external refs.
5. Engine audits targets through the local/SSH transport; agent-home items delegate to `remote-seat-provision.mjs`.
6. Converge applies only drifted non-root items; harden stays separate and one-host-at-a-time.
7. Report records node, revision, changed item IDs, failures, final audit result.

### Work placement

1. Controller reconciler probes every builder and updates explicit availability.
2. Executor requests remote placement.
3. Available remote capacity wins; sticky mirror affinity is preserved.
4. Only confirmed whole-cluster unavailability can produce a workstation lease.
5. Every local launch door calls `fallbackLeaseCheck` before spawning.
6. Builder recovery recalls the lease for new work; current local work drains.

## Error handling

- Invalid fleet/profile declaration → abort before side effects.
- Missing source or pinned ref → abort the affected run before target mutation.
- Unreachable node → report unavailable, continue independent audits, never take configuration from another node.
- Partial staged upload → delete the staging path, leave previous active content intact.
- Root hardening failure → stop; do not continue to another node.
- Controller unavailable → remote execution continues on its own path; workstation fallback stays denied.
- Revision mismatch between lease and executor → deny and re-request placement.
- Leased local launch fails → record a lease incident; never reinterpret failure as permission for an unconfined launch.

## Observability

`deckctl fleet status` MUST show: desired revision per node; profile expansion digest; drift count and last successful audit; transport reachability; runtime role/state; workstation fallback state and lease expiry; local active/draining job count.

One edge-triggered notification per fallback activation, one per recovery. No polling notifications, cooldown gateways, or repeated healthy notices.

## Migration

Sequenced behind in-flight work. Steps 0a/0b are hard prerequisites — the fleet engine MUST NOT be built against a fleet whose nodes are still diverging.

0a. Land the second-disk rollout (`docs/specs/2026-08-06-buildbox-second-disk-design.md`, branch `wt/second-disk`) on all three builders. `/var/lib/buildbox` must exist and be identical before any node is declared converged.
0b. Finish debian3 parity (devtools, caches, projects, runner). Snapshot-equality of the three builders is the baseline the fleet audit is written against.
0c. Fix the `system-units.sh` glob hazard (§2). No buildbox module descriptor before this lands.

1. Add fleet schema/loader; model the four nodes with no behavior change.
2. Add profile expansion over existing manifests/devtools/config; prove output parity with current paths.
3. Add the shared audit/converge engine and both transports; resolve the `remote-seat-provision.mjs` delegate-or-absorb decision with the parity report and record it here.
4. Convert `deckctl sync` and `buildbox audit/bootstrap` to adapters; keep old acceptance tests until parity passes.
5. Move `hosts` from `build-remote.json` into `fleet.json`; route every consumer through the loader; make a leftover `hosts` key an audit failure.
6. Delete the capacity spill (§5a) and add controller builder probing plus the `unavailable` state (§5b, §5b-bis). `remote-build.mjs` keeps its health cache **reduced to a latency/affinity hint** in this step — remote host *selection* must keep working while nothing yet reads the controller. Only its availability authority is removed.
7. Add the lease endpoint and the shared `fallbackLeaseCheck` library; wire every launch door (§5d, §5e). Executors now read availability from the controller.
8. Delete superseded plumbing only after a clean live four-node audit — including the reduced `remote-build.mjs` health cache, whose last consumer disappears at step 7.

Each step lands independently. **Never combine desired-state migration with fallback activation in one release.**

### Named plumbing to delete

- `scheduler.ts` overload spill branch; `store.tryPlaceSpill`; `"recall-spill"` transition; hardcoded `"laptop"` host.
- `remote-build.mjs` `unreachable-or-full` classification and its availability role in `remote-health-<host>.json` (step 6), then the residual latency/affinity hint itself (step 8).
- `buildbox` bespoke pushers `push_agent_confine`, `push_scratch_prune`, `push_npmrc_token`, and the `EXPECT_GC_SHA`/`EXPECT_CONFINE_SHA`/`EXPECT_SCOPE_SHA`/`EXPECT_PRUNE_SHA` env plumbing — replaced by generic desired items.
- `hosts` key in `~/.claude/build-remote.json` once step 5 lands.
- Pre-existing and unrelated: `modules/workstation/claude/lib/remote-build.mjs.pre-phase2-1784601719` is a stale backup file. Flagged, not deleted by this work.

## Testing

### Static and unit

- Reject malformed schema, unknown profile, duplicate destination, missing node reference, root item in normal converge, and unknown `execution` value.
- Snapshot profile expansion for all four nodes; debian1/2/3 differ only in node identity/transport reference.
- Assert one parser supplies all defaults and no consumer parses source JSON directly.
- Assert a `roles`-only node with `execution` absent is placement-ineligible.
- State-machine tests over every availability combination: lease granted **iff** all three builders are `unavailable` for 3 consecutive windows.
- Assert busy/full/overloaded/transient-job-failure never grants a lease.
- Assert every non-`unavailable` state blocks activation: for each of `draining | maintenance | degraded | restoring | available` on one builder with the other two `unavailable`, expect **deny**.
- Assert a repeated grant request for the same `job` returns the same `leaseId` and never consumes a second slot.
- Assert an unloaded, unreadable, or schema-invalid `fleet.json` makes the controller deny with `fleet-unloaded` — distinct from the `fallback-disabled` returned for `fallback.enabled=false` — and never fall back to defaults.
- Assert recovery denies new local jobs without killing an active local job.
- Assert controller restart resets activation counters and cannot issue a lease from pre-restart state.
- Assert lease expiry, revision mismatch, `fallback.enabled=false`, malformed response, and request timeout each deny.
- Assert `local_fallback=true` in `build-remote.json` still grants nothing.
- Assert every launch door refuses to spawn when the lease library is unreachable.

### Transport and convergence

- Synthetic HOME plus a local SSH fixture.
- Local and SSH transports produce equivalent audit reports for the same desired items.
- Interrupted staging retains prior active content.
- Parallel audit output stays declaration-ordered; exit status aggregates failures.
- `hardenNode` rejects zero, multiple, and workstation targets.
- `system-units.sh` installs nothing from a module that does not declare it.

### Live rollout

1. Audit workstation without mutation.
2. Converge workstation; re-audit clean.
3. Converge debian1; reboot where root changes require it; audit clean.
4. Repeat for debian2, then debian3.
5. Confirm all four nodes report the same shared-profile digest.
6. Isolate all three builders; after activation hysteresis, run one confined canary locally.
7. Recover one builder; verify no new local placement, canary drains, lease recalls.
8. Saturate all builders while healthy; verify workstation stays denied.
9. Stop the controller; verify workstation stays denied.
10. Restart the controller during an active outage; verify no lease is issued from stale counters.

## Architecture decisions

- **Git desired state stays separate from live controller state.** A controller outage cannot block repair/audit; Git cannot represent ephemeral health.
- **Push convergence, not pull agents.** Four nodes do not justify resident agents, node Git credentials, rollout elections, or self-update supervision.
- **Reuse manifests and pinned tool declarations.** Profiles compose canonical content; they never clone it.
- **`remote-seat-provision.mjs` keeps sole ownership of remote agent homes.** Two writers on one tree is the failure this design exists to prevent; delegate now, absorb-and-delete only with a parity report.
- **One transport seam, two real implementations.** Passes the deletion and adapter tests; transport detail stays out of desired-state logic.
- **Command-specific engines collapse into adapters.** `deckctl` and `buildbox` stay familiar interfaces, not competing sources of truth.
- **Hardening stays outside fleet fan-out.** Root/watchdog/disk changes have different blast radius and need sequential proof.
- **Capacity never grants the laptop work.** The existing overload spill is deleted rather than tuned, because tuning leaves a saturation path alive.
- **`fallback.enabled` is a new key; `local_fallback` stays dead.** One switch means one audited path; reviving the legacy key would resurrect "remote full → run locally".
- **Lease fails closed everywhere.** Laptop protection wins whenever availability cannot be proved.
- **No package registry, artifact service, or orchestration platform.** Git, SSH, manifests, systemd, and the existing controller are sufficient.
