From e4ee1acf244d555ce2cd4aa512169a9fb66e093a Mon Sep 17 00:00:00 2001 From: Jason Woltje <2+jason.woltje@noreply.git.mosaicstack.dev> Date: Tue, 18 Aug 2026 05:26:01 +0000 Subject: [PATCH] feat(doctor): brain-home fleet-state check (#1298 follow-up) (#1301) Co-authored-by: Jason Woltje <2+jason.woltje@noreply.git.mosaicstack.dev> --- .../framework/tools/_scripts/mosaic-doctor | 64 +++++++++++ .../tools/_scripts/test-brain-home-check.sh | 108 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100755 packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh diff --git a/packages/mosaic/framework/tools/_scripts/mosaic-doctor b/packages/mosaic/framework/tools/_scripts/mosaic-doctor index 9a1ddd01..b281e056 100755 --- a/packages/mosaic/framework/tools/_scripts/mosaic-doctor +++ b/packages/mosaic/framework/tools/_scripts/mosaic-doctor @@ -255,6 +255,68 @@ fleet_declared_transport() { printf '%s\n' "${declared:-tmux}" } +# Brain-home fleet-state resolution (#1298; canon STRUCTURE-CANON §2). +# +# Seat launch envs, roles.local overrides, and profile working copies resolve +# from the brain home when one is active; roster, baseline roles, run/, and +# services stay under MOSAIC_HOME. This check surfaces which tree fleet state +# resolves from and the drift a launch would otherwise hit at runtime: +# +# - a stale MOSAIC_BRAIN_HOME pointing at a directory with no fleet/agents is a +# misconfiguration the resolver honors (explicit wins) — warn, don't pass; +# - a symlinked brain or agents dir defeats the managed-directory boundary; +# - a group/world-readable agents dir violates the 0700 projection boundary; +# - env files left in the config-home tree while a brain is active are split +# state — the write path rejects NEW split writes, but nothing would ever +# tell the operator the old files are stranded. +resolve_brain_home() { + local explicit="${MOSAIC_BRAIN_HOME:-}" + if [[ -n "$(printf '%s' "$explicit" | tr -d '[:space:]')" ]]; then + printf '%s' "$explicit" + return + fi + if [[ "$(cd "$MOSAIC_HOME" 2>/dev/null && pwd -P)" == "$HOME/.config/mosaic" \ + && -d "$HOME/.mosaic/fleet/agents" ]]; then + printf '%s' "$HOME/.mosaic" + return + fi + printf '%s' "$MOSAIC_HOME" +} + +check_brain_home() { + local brain agents mode + brain="$(resolve_brain_home)" + + if [[ "$brain" == "$MOSAIC_HOME" ]]; then + pass "Fleet state home: $MOSAIC_HOME (legacy single-tree; no brain adopted)" + return + fi + + agents="$brain/fleet/agents" + if [[ ! -d "$agents" ]]; then + warn "Brain home '$brain' has no fleet/agents — seat envs will not resolve from it. Point MOSAIC_BRAIN_HOME at a brain carrying fleet/agents, or unset it." + return + fi + if [[ -L "$brain" || -L "$agents" ]]; then + warn "Brain fleet-state path resolves through a symlink ($brain) — the managed-directory boundary requires regular directories." + return + fi + + mode="$(stat -c '%a' -- "$agents" 2>/dev/null)" || mode="" + if [[ -n "$mode" ]] && (( (8#$mode & 8#077) != 0 )); then + warn "Brain agents dir '$agents' is group/world-accessible (mode $mode) — the projection boundary requires 0700." + return + fi + + if [[ -d "$MOSAIC_HOME/fleet/agents" ]] \ + && ls "$MOSAIC_HOME/fleet/agents/"*.env* >/dev/null 2>&1; then + warn "Fleet env files exist in BOTH trees — brain '$brain' is active but '$MOSAIC_HOME/fleet/agents' still carries env files (split state). Migrate them (mosaic fleet regen) and remove the config-home copies." + return + fi + + pass "Fleet state home: $brain (brain active); roster + templates: $MOSAIC_HOME" +} + check_fleet_transport() { local transport transport="$(fleet_declared_transport)" @@ -273,6 +335,8 @@ check_fleet_transport() { check_fleet_transport +check_brain_home + # Legacy migration surfaces should no longer contain symlink trees. legacy_paths=( "$HOME/.claude/agent-guides" diff --git a/packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh b/packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh new file mode 100755 index 00000000..20119800 --- /dev/null +++ b/packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Covers the brain-home fleet-state check in `mosaic-doctor` (#1298 follow-up). +# +# The functions are extracted from the shipped script rather than copied here +# (same discipline as test-fleet-transport-check.sh): a test that carries its +# own copy of the logic keeps passing after the shipped copy changes. +# Extraction is by exact function header and a closing brace in column one. +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname "$0")" && pwd) +DOCTOR="$SCRIPT_DIR/mosaic-doctor" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +[ -f "$DOCTOR" ] || fail "missing mosaic-doctor at $DOCTOR" + +extract_function() { + local name="$1" + local extracted + extracted=$(sed -n "/^${name}() {/,/^}/p" "$DOCTOR") + [ -n "$extracted" ] || fail "could not extract ${name}() from mosaic-doctor — script reshaped?" + printf '%s\n' "$extracted" +} + +for fn in resolve_brain_home check_brain_home; do + extract_function "$fn" >/dev/null +done + +warn_count=0 +warn() { warn_count=$((warn_count + 1)); echo "[WARN] $*"; } +pass() { echo "[OK] $*"; return 0; } + +eval "$(extract_function resolve_brain_home)" +eval "$(extract_function check_brain_home)" + +ROOT=$(mktemp -d) +trap 'rm -rf "$ROOT"' EXIT + +run_case() { + # label, expect (ok|warn), then env assignments as arguments. + # The check runs under `env` in a subshell, so its warn() also prints a + # sentinel the parent counts — a subshell counter would never be visible. + local label="$1" expect="$2" + shift 2 + local out warns + out=$(env "$@" bash -c "warn() { echo \"[WARN] \$*\"; }; pass() { echo \"[OK] \$*\"; return 0; }; $(extract_function resolve_brain_home); $(extract_function check_brain_home); check_brain_home" 2>&1) + warns=$(printf '%s\n' "$out" | grep -c '^\[WARN\]' || true) + if [[ "$expect" == ok && "$warns" -eq 0 ]]; then + echo "ok - $label" + elif [[ "$expect" == warn && "$warns" -gt 0 ]]; then + echo "ok - $label (warned)" + else + echo "output: $out" >&2 + fail "$label: expected $expect (warns=$warns)" + fi +} + +# ── legacy: no brain, custom home never adopts ───────────────────────────── +mkdir -p "$ROOT/legacy-mosaic/fleet/agents" +run_case "custom home without brain stays legacy" ok \ + MOSAIC_HOME="$ROOT/legacy-mosaic" HOME="$ROOT" + +# ── healthy brain at the default config home ─────────────────────────────── +mkdir -p "$ROOT/home/.config/mosaic" "$ROOT/home/.mosaic/fleet/agents" +chmod 700 "$ROOT/home/.mosaic/fleet/agents" +run_case "default home adopts healthy brain" ok \ + MOSAIC_HOME="$ROOT/home/.config/mosaic" HOME="$ROOT/home" + +# ── explicit MOSAIC_BRAIN_HOME to a brain without fleet/agents → warn ────── +mkdir -p "$ROOT/brain-noagents/fleet" "$ROOT/config" +run_case "explicit brain without agents warns" warn \ + MOSAIC_HOME="$ROOT/config" HOME="$ROOT" MOSAIC_BRAIN_HOME="$ROOT/brain-noagents" + +# ── explicit MOSAIC_BRAIN_HOME to a healthy brain → ok ───────────────────── +mkdir -p "$ROOT/brain-ok/fleet/agents" "$ROOT/config2" +chmod 700 "$ROOT/brain-ok/fleet/agents" +run_case "explicit healthy brain passes" ok \ + MOSAIC_HOME="$ROOT/config2" HOME="$ROOT" MOSAIC_BRAIN_HOME="$ROOT/brain-ok" + +# ── group-readable agents dir → warn (0700 boundary) ─────────────────────── +mkdir -p "$ROOT/brain-loose/fleet/agents" "$ROOT/config3" +chmod 750 "$ROOT/brain-loose/fleet/agents" +run_case "group-readable brain agents warns" warn \ + MOSAIC_HOME="$ROOT/config3" HOME="$ROOT" MOSAIC_BRAIN_HOME="$ROOT/brain-loose" + +# ── symlinked agents dir → warn (managed-directory boundary) ─────────────── +mkdir -p "$ROOT/brain-link/real-agents" "$ROOT/brain-link/fleet" "$ROOT/config4" +ln -s "$ROOT/brain-link/real-agents" "$ROOT/brain-link/fleet/agents" +run_case "symlinked brain agents warns" warn \ + MOSAIC_HOME="$ROOT/config4" HOME="$ROOT" MOSAIC_BRAIN_HOME="$ROOT/brain-link" + +# ── split state: envs in BOTH trees → warn ───────────────────────────────── +mkdir -p "$ROOT/brain-split/fleet/agents" "$ROOT/config5/fleet/agents" +chmod 700 "$ROOT/brain-split/fleet/agents" "$ROOT/config5/fleet/agents" +touch "$ROOT/config5/fleet/agents/coder0.env.generated" +run_case "env files in both trees warns (split state)" warn \ + MOSAIC_HOME="$ROOT/config5" HOME="$ROOT" MOSAIC_BRAIN_HOME="$ROOT/brain-split" + +# ── config-home agents dir WITHOUT env files alongside a brain → ok ──────── +mkdir -p "$ROOT/brain-clean/fleet/agents" "$ROOT/config6/fleet/agents" +chmod 700 "$ROOT/brain-clean/fleet/agents" "$ROOT/config6/fleet/agents" +run_case "empty config-home agents dir alongside brain passes" ok \ + MOSAIC_HOME="$ROOT/config6" HOME="$ROOT" MOSAIC_BRAIN_HOME="$ROOT/brain-clean" + +echo "ok - mosaic-doctor brain-home check" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 21ce7cf2..5164571e 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*",