feat(doctor): brain-home fleet-state check (#1298 follow-up, canon §6)
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
mosaic-doctor now reports which tree fleet state resolves from and warns on the drift a launch would hit at runtime: - stale MOSAIC_BRAIN_HOME (no fleet/agents under it) - symlinked brain/agents paths (managed-directory boundary) - group/world-readable agents dir (0700 projection boundary) - split state: env files in BOTH config-home and brain trees Resolution mirrors brain-home.ts (#1298): MOSAIC_BRAIN_HOME wins; canonical ~/.mosaic adopted only at the default config home; else legacy. test-brain-home-check.sh: extraction-style harness (test-fleet-transport discipline — functions pulled from the shipped script, never copied), 8 cases covering every branch; wired into test:framework-shell and covered by the enumeration guard (population 53, all present). Live-run verified on dragon-lin: caught two real drifts — agents dir 755 (fixed locally) and pre-cutover split state (expected, tracked in mosaic-brain fleet/SEAT-CUTOVER.md).
This commit is contained in:
@@ -255,6 +255,68 @@ fleet_declared_transport() {
|
|||||||
printf '%s\n' "${declared:-tmux}"
|
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() {
|
check_fleet_transport() {
|
||||||
local transport
|
local transport
|
||||||
transport="$(fleet_declared_transport)"
|
transport="$(fleet_declared_transport)"
|
||||||
@@ -273,6 +335,8 @@ check_fleet_transport() {
|
|||||||
|
|
||||||
check_fleet_transport
|
check_fleet_transport
|
||||||
|
|
||||||
|
check_brain_home
|
||||||
|
|
||||||
# Legacy migration surfaces should no longer contain symlink trees.
|
# Legacy migration surfaces should no longer contain symlink trees.
|
||||||
legacy_paths=(
|
legacy_paths=(
|
||||||
"$HOME/.claude/agent-guides"
|
"$HOME/.claude/agent-guides"
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
"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": {
|
"dependencies": {
|
||||||
"@mosaicstack/brain": "workspace:*",
|
"@mosaicstack/brain": "workspace:*",
|
||||||
|
|||||||
Reference in New Issue
Block a user