Commit Graph
13 Commits
Author SHA1 Message Date
terraandClaude Opus 5 309a99a600 fleet: fix four defects that made no seat launchable on a clean install
Found by rehearsing the full install on a greenfield Debian 13 VM
(mosaic-sbx-dev) rather than on a host that already had a working Mosaic
tree. Each one is invisible on a developer machine and fatal on a new host.

1. Required system settings layer. The framework ships runtime/<harness>/
   for claude, codex, opencode and pi but a settings.json only for claude,
   so requiring the file made every pi, codex and opencode seat refuse to
   compose. The system layer is now optional; what must exist is the
   harness runtime directory, which is the thing that actually proves the
   framework is installed and carries that harness.

2. Required mcpServers in canonical Claude settings. The shipped
   settings.json has no such key, so `fleet agent new` refused to scaffold
   any Claude seat. Absent now means the same as empty. A present but
   wrong-typed value is still an error.

3. Never-enrolled hosts were told their auth directory "must be a real,
   non-symlink directory", which reads as a tampering report when the real
   situation is that nobody has logged in yet. Absent and wrong-shaped are
   now separate messages, and the absent one names `mosaic auth enroll`.

4. A fleet seat whose host had no system SOUL.md reached checkSoul(),
   which spawns the interactive `mosaic wizard` with inherited stdio. On a
   detached tmux seat that parks the pane on a menu with nobody at it: the
   session is live, the systemd unit reports fine, and no agent ever
   starts. A seat's identity is its own SOUL.md, written by `fleet agent
   new`, so the fleet path checks that and fails loudly instead.

Each fix has a regression test verified red against the unfixed source.
The launch.spec.ts seat fixtures gained a SOUL.md they always should have
had -- without it those tests were satisfied by whatever SOUL.md the
developer's real ~/.config/mosaic happened to contain.

Full suite before and after: the same 5 pre-existing failures in
mutator-gate.acceptance.spec.ts and install-ordering-guard.spec.ts,
1585 -> 1591 passing. typecheck and eslint clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
2026-08-14 19:03:30 -05:00
terra c1a42cdb81 fleet: start a roster pane through its seat when one is scaffolded
The roster lane and the harness-homes lane did not touch. start-agent-session.sh
ran `mosaic yolo "$RUNTIME"` with HOME set to the operator's home, so every fleet
seat on a host shared the operator's harness home and, for Claude, the operator's
own ~/.claude credentials. Nothing in framework/ called `mosaic fleet launch` at
all, which meant ~/.mosaic was a directory nothing read.

The pane now runs `mosaic fleet launch "$AGENT_NAME"` when a scaffolded seat
exists at $PANE_HOME/.mosaic/fleet/agents/<name>/profile.json, and the historical
command otherwise. Detection uses $PANE_HOME/.mosaic rather than MOSAIC_DATA_HOME
because the pane environment is cleared with env -i; the composition resolves the
same root from HOME, so the two cannot disagree.

Additive by construction: a host with no scaffolded seats launches exactly as
before, so this can land ahead of any seat being enrolled.

- fleet launch gains --dangerous, threaded to launchFleetRuntime. Without it a
  seat launched from the roster would drop the permissions footing `mosaic yolo`
  gave it and prompt at a pane with nobody at it. The roster launcher asks for it
  explicitly so it stays visible in the process table instead of becoming a
  profile default.
- A caller's --model replaces the profile's instead of being appended after it.
  The roster carries a model per seat and is the surface operators edit; emitting
  both flags would leave the choice to each harness's argument parser.
- Claude workdir trust is written into the seat's .claude.json when the pane will
  run in a seat home. It previously always went to the operator's ~/.claude.json,
  which would leave the seat prompting on its first turn.

Covers Jason's scope amendment for web1: without this seam, "multiple
authentication accounts and agent pegging to auth" cannot be demonstrated on a
roster-managed seat.
2026-08-14 18:35:11 -05:00
terra a12eeb4786 fleet: share Claude credentials by directory env, not a seat symlink
Claude Code saves credentials by writing a sibling temp file and rename()-ing
it over the target. rename(2) replaces a symlink rather than following it, so
the managed link W-F1/W-F2 planted at <seat>/.claude/.credentials.json is
destroyed by the first token refresh and the seat silently forks its
credentials. The in-place fallback arm opens with O_NOFOLLOW and would refuse
the link anyway. Evidence, quoting the 2.1.232 binary:
docs/reports/harness/claude-credential-write-path-2026-08-14.md (jarvis-brain).

CLAUDE_SECURESTORAGE_CONFIG_DIR resolves the credential directory
independently of CLAUDE_CONFIG_DIR, so the temp file and the rename both land
inside the bundle. That is the property the design wanted -- share the
credential, never the transcripts -- with no symlink and no privileges.

- new fleet/credential-sharing.ts owns the harness -> credential-file and
  harness -> credential-directory-variable maps, so scaffold and launch cannot
  disagree about the mechanism. It also removes the duplicate credential-file
  name table the two already carried.
- launch composes CLAUDE_SECURESTORAGE_CONFIG_DIR from the resolved bundle
  directory and plans no credential link for Claude. The value is always the
  absolute bundle path: Claude reads an empty value as ~/.claude, which is the
  operator's own account.
- scaffold stops emitting the credential symlink and its manifest entry for
  Claude, and tolerates one left by an earlier scaffold rather than reporting
  it as a foreign file or rewriting it.
- FIRST_AUTH_REFUSAL still fires when a real file occupies the seat path.
- Harnesses absent from the map (pi, codex, opencode) keep managed links; the
  containment specs now exercise them on pi.

Answers promotion gate #1 negatively for the frozen mechanism and positively
for the replacement. E3.3 (two seats refreshing one bundle at once) is still
open.
2026-08-14 18:20:54 -05:00
terra 326a1a58b5 fix(fleet): harden managed launch composition
AMD1213-C: repair stale array consumer, fail closed on foreign link provenance, validate manifests before mutation, and exercise the fleet MCP preflight call path.
2026-08-13 15:37:32 -05:00
terra 2755f86f7b fix(fleet): seed seat MCP preflight config
AMD1213-B5: derive Claude seat MCP configuration from the active installed runtime base and inspect the isolated seat during fleet launch.
2026-08-13 14:38:25 -05:00
terra fe2cf19461 fix(fleet): preserve managed link provenance
AMD1213-B3: record Mosaic-owned links and refuse foreign or retargeted symlink mutations. Out-of-scope review follow-up: settings output/snapshot apply-time TOCTOU remains reported, not patched.
2026-08-13 14:38:25 -05:00
terra 4fde3f622d fix(fleet): contain credential trust roots
AMD1213-B4: reject symlinked auth ancestry and group/world-readable credential artifacts before composition can write.
2026-08-13 14:38:25 -05:00
terra 9de9ffa56b fix(lease): restore uniform settings array replacement
AMD1213-B1: preserve the gated Claude hook composition explicitly in the lease overlay while restoring last-layer-wins arrays and null tombstones.
2026-08-13 14:38:19 -05:00
Jason WoltjeandClaude Fable 5 5e15431027 fix(fleet): tolerate harness metadata files in the managed install root
ci/woodpecker/pr/ci Pipeline was canceled
Claude Code writes installed_plugins.json and other metadata files into
the seat's plugins directory during a session, so refusing every real
entry made composition fail on each seat's second launch. Only a real
directory is an unmanaged entry the pruner would orphan; plain files are
harness state and pass through untouched. Found by the in-box hour-gate
relaunch of the probe seat.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:59:31 -05:00
Jason WoltjeandClaude Fable 5 c16256d48c fix(fleet): compose system settings from the installed flattened home layout
The installed ~/.config/mosaic home flattens the repo's
packages/mosaic/framework/ prefix: the real file is
<home>/runtime/<harness>/settings.json, exactly as launch.ts already
resolves it everywhere. The fleet launch composition leaked the repo
layout (framework/runtime/...) into the system layer path, so a real
installed home failed with COMPOSITION_FAILED while the temp-fixture
specs (which mirrored the same wrong prefix) stayed green. Found by the
in-box hour-gate dry-run against the installed mos-dev-stage home.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:52:00 -05:00
Jason WoltjeandClaude Fable 5 92e790ae9d fix(fleet): additive hook-event merge and gated-composition acceptance reads
Integration adjudication (fred, W-F1): the general arrays-replace merge rule
conflicts with the gap-7 base/overlay split — base and lease overlay share
the PreToolUse and Stop events, so replace semantics would silently drop the
base QA hooks from every gated seat. Ruling: hook event arrays directly
under the top-level hooks key concatenate (base first); all other arrays
keep replace semantics; null tombstones still delete an event.

- mutator-gate acceptance now asserts lease wiring against the COMPOSED
  gated settings (base + lease-overlay via the launcher's own merge),
  matching the post-split contract.
- fleet subcommand canary gains the intended new 'agent' surface from T3.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:44:58 -05:00
Jason WoltjeandClaude Fable 5 0fdcfa0ff4 fix(fleet): unify user data-home seam and add actionable unscaffolded-agent error
Integration reconciliation of T2/T3 seams on feat/wf-fleet-mvp:
- fleet launch now resolves the user root through defaultFleetDataHome()
  (MOSAIC_DATA_HOME), the same seam fleet agent new uses, instead of a
  divergent MOSAIC_USER_HOME variable.
- Launching an unscaffolded name raises AGENT_NOT_SCAFFOLDED with the
  actionable message pointing at 'mosaic fleet agent new <name>' (acceptance
  carried over from the T3 card after the roster-v2 reconciliation moved it
  onto the launch path).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dtdjx4Gxude9fwyLezCrhh
2026-08-13 11:39:44 -05:00
Jason Woltje 378c227cbb feat(fleet): compose and launch profile-backed seats 2026-08-13 11:30:53 -05:00