feat(fleet): brain-home split — fleet state under ~/.mosaic, templates stay config-home
ci/woodpecker/pr/ci Pipeline was successful

Three-tree model per canon STRUCTURE-CANON §2 (first carried by the USC
estate brain): seat launch envs, roles.local overrides, and profile
working copies are user-owned fleet state and resolve from the brain home
(~/.mosaic) when one is active; roster.yaml, baseline roles, run/, and
services stay under the config home (~/.config/mosaic).

Resolution (packages/mosaic/src/fleet/brain-home.ts, mirrored in
tools/fleet/start-agent-session.sh):
1. MOSAIC_BRAIN_HOME env — explicit, always wins
2. canonical ~/.mosaic — adopted only when MOSAIC_HOME is the default
   ~/.config/mosaic AND ~/.mosaic/fleet/agents exists (custom
   --mosaic-home never adopts: tests/sandboxes stay hermetic)
3. else legacy single-tree behavior

Call sites wired: resolveFleetPaths, fleet regen, fleet-agent-crud,
fleet-migration, personas overrideDir, profiles dir, reconciler
projections, generated-env boundary (validate + ensure + reject split
state across trees).

Tests: brain-home.spec (8), generated-env-boundary brain case, bash
launcher brain + no-brain control cases; full fleet surface 733 green.
Pre-existing unrelated failure noted: uninstall.spec 'missing mosaicHome'
throws EACCES on this host with and without this change.
This commit is contained in:
Zane
2026-08-17 21:27:05 -05:00
parent 8199261caa
commit d04ff3b9f1
14 changed files with 362 additions and 17 deletions
+27
View File
@@ -12,6 +12,33 @@ The default tmux socket is `mosaic-fleet` so fleet commands do not touch the
default tmux server. The roster is the desired-state authority; generated environment files are
rebuildable projections, never a second source of configuration.
## Brain-home split (fleet state vs framework templates)
When a mosaic-brain clone is present, fleet **state** resolves from the brain
home while framework templates and dispatch state stay in the config home
(three-tree model, canon `docs/STRUCTURE-CANON.md` §2):
| Path | Without brain (legacy) | With brain |
| ------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------ |
| `fleet/agents/<seat>.env.*` | `~/.config/mosaic/fleet/agents/` | `~/.mosaic/fleet/agents/` |
| `fleet/roles.local/` (overrides) | `~/.config/mosaic/fleet/roles.local/` | `~/.mosaic/fleet/roles.local/` |
| `fleet/profiles/` (working copies) | `~/.config/mosaic/fleet/profiles/` | `~/.mosaic/fleet/profiles/` |
| `fleet/roster.yaml`, `fleet/roles/` (baseline), `fleet/run/`, `fleet/services/` | `~/.config/mosaic/fleet/…` | unchanged (config home) |
Activation (`packages/mosaic/src/fleet/brain-home.ts`, mirrored in
`tools/fleet/start-agent-session.sh`):
1. `MOSAIC_BRAIN_HOME` env var — explicit, always wins.
2. Canonical `~/.mosaic` — adopted only when `MOSAIC_HOME` is the default
`~/.config/mosaic` AND `~/.mosaic/fleet/agents` exists. Custom
`--mosaic-home` values (tests, sandboxes, canaries) never adopt, keeping
them hermetic.
3. Otherwise the config home (legacy single-tree behavior).
Seat env dirs under a brain are subject to the same privacy boundary (0700
dirs, 0600 files); `.env.generated` files are structure-valuable and tracked
in the brain repo, hand-maintained `.env`/`.env.local` stay ignored and private.
## Examples
- `examples/minimal.yaml` starts one local canary slot.
@@ -80,6 +80,26 @@ safe_path "$MOSAIC_HOME" || fail_env unsafe-path MOSAIC_HOME "$MOSAIC_HOME"
FLEET_DIR="$MOSAIC_HOME/fleet"
AGENT_ENV_DIR="$FLEET_DIR/agents"
# Brain-home split (canon docs/STRUCTURE-CANON.md §2): seat launch envs live
# under the brain home's fleet/agents when a brain is active; roster, roles
# baseline, and runtime state (fleet/run) stay under MOSAIC_HOME.
# Resolution mirrors packages/mosaic/src/fleet/brain-home.ts:
# 1. MOSAIC_BRAIN_HOME env (explicit, always wins)
# 2. ~/.mosaic — adopted only when MOSAIC_HOME is the default config home AND
# ~/.mosaic/fleet/agents exists
# 3. MOSAIC_HOME (legacy single-tree)
BRAIN_HOME="${MOSAIC_BRAIN_HOME:-}"
if [ -z "$BRAIN_HOME" ]; then
BRAIN_HOME="$MOSAIC_HOME"
if [ "$(cd "$MOSAIC_HOME" 2>/dev/null && pwd -P)" = "$HOME/.config/mosaic" ] \
&& [ -d "$HOME/.mosaic/fleet/agents" ]; then
BRAIN_HOME="$HOME/.mosaic"
fi
fi
if [ "$BRAIN_HOME" != "$MOSAIC_HOME" ]; then
AGENT_ENV_DIR="$BRAIN_HOME/fleet/agents"
fi
assert_managed_directory "$MOSAIC_HOME"
assert_managed_directory "$FLEET_DIR"
assert_private_directory "$AGENT_ENV_DIR"
@@ -167,6 +167,54 @@ if echo "$valid_args" | grep -qF 'bash -c'; then
fail "launcher constructed a shell command payload"
fi
# ── Brain-home split (canon §2) ─────────────────────────────────────────
# When MOSAIC_HOME is the default config home under $HOME and the host carries
# $HOME/.mosaic/fleet/agents, seat envs resolve from the brain tree; the config
# home still owns fleet/run (holder-owner) and remains a managed boundary.
: > "$TMUX_CALLS"
HOME_BRAIN="$ROOT/brain-home"
CONFIG_HOME="$HOME_BRAIN/.config/mosaic"
BRAIN="$HOME_BRAIN/.mosaic"
mkdir -p "$CONFIG_HOME/fleet/run" "$BRAIN/fleet/agents" "$HOME_BRAIN/work"
chmod 700 "$CONFIG_HOME" "$CONFIG_HOME/fleet" "$CONFIG_HOME/fleet/run" \
"$BRAIN/fleet/agents" "$HOME_BRAIN/work"
printf '123e4567-e89b-12d3-a456-426614174000\n' > "$CONFIG_HOME/fleet/run/holder-owner"
chmod 600 "$CONFIG_HOME/fleet/run/holder-owner"
cat > "$BRAIN/fleet/agents/coder-brain.env.generated" <<EOF
MOSAIC_AGENT_NAME=coder-brain
MOSAIC_AGENT_CLASS=code
MOSAIC_AGENT_RUNTIME=pi
MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol
MOSAIC_AGENT_REASONING=high
MOSAIC_AGENT_TOOL_POLICY=code
MOSAIC_AGENT_WORKDIR=$HOME_BRAIN/work
MOSAIC_TMUX_SOCKET=mosaic-test
EOF
chmod 600 "$BRAIN/fleet/agents/coder-brain.env.generated"
install_pane_binaries "$HOME_BRAIN"
HOME="$HOME_BRAIN" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_PANE_PID=$$ MOSAIC_TEST_HOME="$HOME_BRAIN" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$CONFIG_HOME" "$START" coder-brain
brain_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$brain_args" | grep -qF new-session || fail "brain-home generated projection did not reach tmux"
echo "$brain_args" | grep -qF 'coder-brain' || fail "brain-home agent env was not the launch source"
[ -f "$BRAIN/fleet/agents/coder-brain.env.generated" ] || fail "brain generated env vanished"
# Negative control: the SAME default-config-home shape but without
# ~/.mosaic/fleet/agents — the config-home env tree is used directly (legacy).
: > "$TMUX_CALLS"
HOME_NOBRAIN="$ROOT/brainless-home"
CONFIG_HOME_NOBRAIN="$HOME_NOBRAIN/.config/mosaic"
write_generated "$CONFIG_HOME_NOBRAIN" "coder-legacy"
install_pane_binaries "$HOME_NOBRAIN"
HOME="$HOME_NOBRAIN" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_PANE_PID=$$ MOSAIC_TEST_HOME="$HOME_NOBRAIN" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$CONFIG_HOME_NOBRAIN" "$START" coder-legacy
legacy_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$legacy_args" | grep -qF new-session || fail "legacy single-tree launch regressed"
# The pane must start through an absolute clean-environment boundary. Its
# runtime command remains an argv vector, but no holder/session environment
# control variable can pass through the pane command.