rev-974 (#1085 review 130) found three blockers. This closes the first two. [1] SANDBOX ESCAPE. The test ran under `set -uo pipefail` with unchecked mkdir, calls-log redirect and `cd "$REPO_DIR"`, then prepended a possibly-nonexistent $MOCK_BIN to PATH -- while `git remote add origin` names the REAL repository. rev-974 forced setup failure with an unwritable AGENT_WORK_ROOT and the test continued past every error, ran `git init` in its CALLER's directory, added the real origin, and invoked its target: TARGET_REACHED args=-i 42 -c closing note With the committed target that is the real, provider-mutating issue-close.sh. ShellCheck flagged the unguarded cd as SC2164 independently. Now: `set -euo pipefail`, every setup step checked with a legible reason, and assert_mocked() proves BOTH `tea` and `curl` resolve inside $MOCK_BIN before any target invocation. Control: unwritable AGENT_WORK_ROOT -> rc=1 at mkdir, target never reached. [2] THE API/no-login BRANCH HAD NO DISCRIMINATING COVERAGE. The mock always returned a tea login, so `gitea_issue_comment_api || fail-closed` was never executed. rev-974 replaced the whole fallback contract with an unconditional close -- silently dropping the comment -- and the committed test still passed rc=0. Added three cases asserting the POSTCONDITION (which HTTP calls happened, in what order) rather than that a command ran: comment POST fails -> no PATCH and non-zero; comment succeeds -> strictly POST,PATCH; no comment requested -> PATCH only, never a POST. The curl mock now records method and URL. Control: replaying rev-974's contract destruction now fails with "API path: no comment POST attempted". Two self-inflicted traps hit while adding `set -e`, both the same family as the #1086 defect be-coder-08 found, and both silent: - `grep -q X "$CALLS" && fail "..."` -- the ABSENT case (grep rc=1, the PASSING case for a must-not-appear assertion) is the last command of an && list and terminates the script with no message. All four converted to if-blocks. - `run_target ...; rc=$?` -- the function's non-zero RETURN trips set -e in the CALLER before rc is read; run_target's internal `set +e` protects the target, not the caller. All five call sites now `rc=0; run_target ... || rc=$?`. Controls: unchanged main -> rc=1 "used 'tea issue comment'" fallback contract destroyed -> rc=1 "API path: no comment POST attempted" unwritable AGENT_WORK_ROOT -> rc=1 at setup, target never invoked fixed source -> rc=0 [3] remains open: with MOSAIC_GIT_IDENTITY=rev-974 the wrapper resolves GITEA_LOGIN_NAME=mosaicstack-mos, so one principal holds but the operation is attributed to Mos rather than the requested seat. That changes identity resolution shared by every wrapper in this directory and is not folded in here. Reported-by: rev-974
@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.