forked from mosaicstack/stack
Mosaic wrote into the operator's harness base installs — ~/.claude, ~/.pi/agent, ~/.codex, ~/.config/opencode — for settings, instructions, and a 102-symlink skill farm per harness. Any experiment with hooks or gating therefore mutated the operator's own tooling, and a broken framework change could take out the very harness needed to repair it. Harness home isolation ---------------------- Each runtime now reads config from a dedicated mosaic-owned home via the harness's own config-dir variable: claude CLAUDE_CONFIG_DIR ~/.config/mosaic/.claude pi PI_CODING_AGENT_DIR ~/.config/mosaic/.pi (replaces ~/.pi/agent) codex CODEX_HOME ~/.config/mosaic/.codex opencode XDG_CONFIG_HOME ~/.config/mosaic/.opencode These paths are manifest-UNKNOWN, so rule 3 (#791) resolves them to operator ownership and a keep-mode upgrade can neither overwrite nor prune them. A bare `claude` / `pi` keeps its own config AND auth, making it a structural break-glass rather than one depending on restoring a file under pressure. opencode is blunter than the rest: it has no dedicated variable and follows XDG, so isolation also relocates XDG lookups for anything it spawns. Documented in place. mosaic-sync-skills now links into those homes and cleans the legacy farms it previously planted in base installs. Ownership is proven by RESOLUTION, not by name — only symlinks resolving inside the canonical/local skills dirs are removed, mirroring the refusal already in commands/skill.js. Verified against a real install: codex's own .system directory survived while its 102 mosaic links were removed. Both resolution prefixes are length-checked first; an empty prefix would make "$resolved" == "$prefix/"* match every absolute path and delete foreign symlinks. Immutable launch record ----------------------- Every launch now appends one record to fleet/run/sessions/events.ndjson before exec. Mandatory, mechanical, no model involvement. pi rewrites its own argv to a bare `pi`, so /proc/<pid>/cmdline destroys the launch evidence — that has already produced a confident wrong diagnosis ("this agent bypassed the launcher"), disproved only by the parent's argv and only because the parent had not yet exited. A record written before exec is the only place this survives. The path is the #797 Runtime Session Ledger, already operator-classified and already covered by test-upgrade-manifest-guard.sh, which seeds it and proves a populated ledger survives keep-mode upgrades — but nothing shipped ever wrote it. This implements it in the shape that guard already asserts (0600 files under a 0700 dir). `mosaic` writes session.launch; launch-runtime.py appends lease.register with the broker session id and activation capability. They correlate by an explicit MOSAIC_LAUNCH_ID, never by pid: execRuntime uses spawnSync, so the runtime is a child with a different pid. Records normative fragment digests (CONSTITUTION/AGENTS/SOUL/USER/STANDARDS/ TOOLS/RUNTIME) — the same set the broker hashes for promotion, so drift is mechanically detectable rather than a matter of judgement. Credential-safe: env is captured as PRESENT NAMES ONLY, and argv values over 256 bytes become a sha256 + length rather than being inlined. Also fixes CLI_VERSION resolution: '@mosaicstack/mosaic/package.json' is not in the package exports map and always throws ERR_PACKAGE_PATH_NOT_EXPORTED. resolveTool() uses that same failing specifier, which is why its documented preference for bundled tools over the deployed ~/.config/mosaic copy has never once applied — noted in place, not fixed here. Verified on sb-it-1-dt: isolated homes written and base installs byte-identical for all four harnesses; 408 legacy symlinks removed with 1 foreign entry preserved; launch records paired across the spawn boundary. typecheck shows zero errors in launch.ts (the @mosaicstack/types failures are pre-existing and reproduce on a pristine origin/main worktree).