Closes the environment half of AMD1213-D defect D3. The executable half landed
in 585dac7a; this is the other thing the card asked for -- a capability-minimal
child environment that is measured rather than asserted.
MEASURED FIRST, THEN CHANGED. I ran the real `fleet launch` route with a shim in
place of the runtime binary and had the shim dump its own environment, so the
subject is what arrives at the far end of the chain -- after composition, after
the lease gate in launch-runtime.py -- and not the object the launcher believed
it was building. Those are different sets and only the first one matters.
What the measurement showed is that most of this defect was already closed by
construction and nobody knew, because nothing tested it. `minimalLaunchEnv`
builds from an empty object over a fixed name list, so BASH_ENV, ENV, PYTHON*,
NODE_*, NPM_CONFIG_*, LD_PRELOAD, LD_LIBRARY_PATH and every provider credential
in the operator's environment are already excluded, and they stay excluded
through the lease gate. I planted all sixteen and none reached the child. An
allowlist that no test names is one careless edit from being a denylist, which
is the actual defect here.
Two real gaps, one fixed and one not:
FIXED -- locale was inherited. A seat picked up the operator's LANG and LC_ALL,
so the same runtime doing the same work emitted different message language,
collation, and number and date formatting depending on who started it. Composed
launches now pin C.UTF-8. C.UTF-8 and not C: both are unambiguous, but plain C
is ASCII and would mangle non-ASCII output, trading one defect for another. A
seat that needs a different locale declares LANG or LC_ALL in its profile and
the declared value still wins -- covered by a test, so the escape hatch cannot
be removed silently. The operator path (no declared env) is untouched.
NOT FIXED, AND DELIBERATELY -- HOME is still the operator's. The card asks for
the seat config root instead, and it is right that this is the remaining leak:
the runtime is pointed at its own config directory, but anything it shells out
to (git, ssh, npm) still reads the operator's dotfiles and therefore the
operator's credentials. I am not changing it inside this amendment. A seat whose
HOME is a bare directory has no gitconfig and no ssh key, so it cannot commit or
push, and the fleet MVP's whole proof is a seat carrying a change to a pushed
branch. Moving HOME before the per-agent home is populated would improve the
isolation and break the deliverable. That population is what the harness-homes
design owns, and this is recorded as a residual there rather than half-done
here.
Eight tests. Each one falsified by inverting the property it claims to defend,
and each inversion hit exactly its own test and nothing else:
- added BASH_ENV to the inherited list -> permitted-set and loader-hook
killers both red, 2 failed
- reverted the locale pin -> locale killer red, 1 failed
- reused an ambient MOSAIC_LAUNCH_ID -> launch-id killer red, 1 failed
- recorded process.env into the ledger -> ledger-value killer red, 1 failed
The permitted-name list in the spec is written out by hand rather than derived
from the launcher. Deriving it would make the test agree with the code by
construction and detect nothing; the cost is that adding a variable means
editing the test, which is the point.
The launch-id test is worth naming separately. recordLaunch overwrites
MOSAIC_LAUNCH_ID in process.env before it is copied to the child, so a seat
launched from an operator session gets a fresh correlation id rather than
inheriting the operator's. That was already true and is now pinned, along with
the requirement that the child's id matches the one in the ledger -- correlation
is by this value and never by pid, because exec makes the runtime a different
process.
Verification: typecheck RC=0. eslint RC=0. prettier clean. Three consecutive
full-package runs under the sanitized lease environment, RC=0, 87 files / 1627
tests passed, 0 failed -- exactly one file and eight tests more than the 86/1619
baseline, so nothing else moved.
Commit-only per scrappy's controlling packet (comms 20260813T212447Z dc43de):
not pushed, PR #1213 not updated, nothing re-authored.
@mosaicstack/mosaic
CLI package for the Mosaic self-hosted AI agent platform.
Usage
mosaic wizard # First-run setup wizard
mosaic gateway install # Install the gateway daemon
mosaic config show # View current configuration
mosaic config hooks list # Manage Claude hooks
Headless / CI Installation
Set MOSAIC_ASSUME_YES=1 (or ensure stdin is not a TTY) to skip all interactive prompts. The following environment variables control the install:
Gateway configuration (mosaic gateway install)
| Variable | Default | Required |
|---|---|---|
MOSAIC_STORAGE_TIER |
local |
No |
MOSAIC_GATEWAY_PORT |
14242 |
No |
MOSAIC_DATABASE_URL |
(none) | Yes if tier=team |
MOSAIC_VALKEY_URL |
(none) | Yes if tier=team |
MOSAIC_ANTHROPIC_API_KEY |
(none) | No |
MOSAIC_CORS_ORIGIN |
http://localhost:3000 |
No |
Admin user bootstrap
| Variable | Default | Required |
|---|---|---|
MOSAIC_ADMIN_NAME |
(none) | Yes (headless) |
MOSAIC_ADMIN_EMAIL |
(none) | Yes (headless) |
MOSAIC_ADMIN_PASSWORD |
(none) | Yes (headless) |
MOSAIC_ADMIN_PASSWORD must be at least 8 characters. In headless mode a missing or too-short password causes a non-zero exit.
Example: Docker / CI install
export MOSAIC_ASSUME_YES=1
export MOSAIC_ADMIN_NAME="Admin"
export MOSAIC_ADMIN_EMAIL="[email protected]"
export MOSAIC_ADMIN_PASSWORD="securepass123"
mosaic gateway install
Runtime launchers
mosaic claude # Launch Claude Code with Mosaic injection
mosaic yolo claude # …with --dangerously-skip-permissions
mosaic codex | opencode | pi
mosaic claudex (EXPERIMENTAL)
Runs GPT models inside the Claude Code harness by pointing Claude Code at a
local claude-code-proxy that
translates the Anthropic Messages API to a ChatGPT-subscription (Codex OAuth)
backend. This is not Anthropic Claude — model behavior, tool use, and output
quality may differ. Intended for evaluation, not production delivery.
mosaic claudex # launch (prompts through the proxy readiness gate)
mosaic yolo claudex # …with --dangerously-skip-permissions
mosaic claudex --print "hello" # trailing args are forwarded to Claude Code
Prerequisite: the claude-code-proxy binary must be installed and
authenticated (claude-code-proxy codex auth …). mosaic claudex runs a
preflight that verifies the binary, the OAuth state (triggering a device re-auth
if needed), and a trusted local listener before launching; it fails closed
if the proxy cannot be brought up with a verified identity.
Isolation (never touches your real Claude state). claudex always launches
against an isolated CLAUDE_CONFIG_DIR (default ~/.config/mosaic/claudex/home).
The ambient CLAUDE_CONFIG_DIR is deliberately ignored, and a guard proves the
resolved dir can never be — or live under — the real ~/.claude. A claudex
session therefore cannot mutate your normal Claude Code config.
No token leakage. claudex never reads the proxy's credential file. Claude
Code is handed only ANTHROPIC_AUTH_TOKEN=unused pointed at the loopback proxy;
the entire credential-bearing env family (ANTHROPIC_*, AWS_*, GOOGLE_CLOUD_*,
GOOGLE_APPLICATION_CREDENTIALS, *_TOKEN, *_KEY, *_SECRET, …) is stripped
from the composed environment. The Bedrock/Vertex routing switches
(CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, and the _SKIP_*_AUTH
pair) are force-removed regardless of value — otherwise their mere presence
would route Claude Code to the real Anthropic API via AWS/GCP and bypass the
proxy. The proxy holds the real OAuth credential.
Model tiers (override via env).
| Tier | Env var | Default |
|---|---|---|
| primary (opus/sonnet) | ANTHROPIC_MODEL |
gpt-5.6-sol |
| small/fast (haiku) | ANTHROPIC_SMALL_FAST_MODEL |
gpt-5.6-luna |
Operator-provided values win over the defaults. Additional overrides:
MOSAIC_CLAUDEX_CONFIG_DIR (isolated config dir), ANTHROPIC_BASE_URL (proxy
endpoint).
Hooks management
After running mosaic wizard, Claude hooks are installed in ~/.claude/hooks-config.json.
mosaic config hooks list # Show all hooks and enabled/disabled status
mosaic config hooks disable PostToolUse # Disable a hook (reversible)
mosaic config hooks enable PostToolUse # Re-enable a disabled hook
Set CLAUDE_HOME to override the default ~/.claude directory.