CENSUS CORRECTION: FOUR suites, not the three my own #1007 audit named. The fourth (test-pr-metadata-gitea.sh) was outside the candidate set that audit worked from and was found only by sweeping the discriminator across all 16 tools/git/test-*.sh suites. Recording that as a correction to my finding, not as part of the original claim. THE DEFECT. get_gitea_token() (detect-platform.sh:502-599) resolves a per-agent identity at STEP 0, from `git config --get mosaic.gitIdentity`, BEFORE both the Mosaic credential loader (step 1) and the GITEA_TOKEN env check (step 2). On a provisioned agent seat that value is set GLOBALLY in ~/.gitconfig and is inherited by any freshly-`git init`ed repo, so step 0 reads a REAL per-slot token out of $HOME and returns it without ever consulting the suite's own MOSAIC_CREDENTIALS_FILE / GITEA_TOKEN fixtures. The suites were running against production credentials, and the fixture credential each one carefully constructs was inert. THE FIX: an empty repo-local `mosaic.gitIdentity`. An empty local value shadows the global one and reads back empty at rc=0, so step 0 declines. The env route does NOT work: detect-platform.sh reads "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset. OPERATIVE vs CONTAINMENT — the two mechanisms are not interchangeable and the comment in each suite says so. The pin is operative: it prevents the resolution. The sandboxed HOME each suite now also gets is containment: it bounds a failure the pin should already have prevented. Conflating them is how this class stays invisible, because a decoy HOME REMOVES the trigger (~/.gitconfig is where the global identity lives), so any suite audited under one reads clean however vulnerable it is. To MEASURE, replicate a seat: a decoy HOME whose .gitconfig sets mosaic.gitIdentity with no per-slot token, so step 0 reaches its fail-loud branch. That note is in each file for the next auditor. SECOND, INDEPENDENT DEFECT in test-pr-metadata-gitea.sh. Applying the pin alone turned that suite RED — and a control at baseline826a8b3under a plain HOME reproduced the same failure, so it is pre-existing, not introduced. Its `GITEA_TOKEN="stub-token"` / `GITEA_URL="https://git.example.test"` pair can never satisfy step 2, because step 2 accepts GITEA_TOKEN only when GITEA_URL matches the remote host and this repo's origin is git.uscllc.com. The suite had therefore only ever passed by resolving a REAL credential — step 0 on a seat, or step 1 from the operator's own credentials.json. A MOSAIC_CREDENTIALS_FILE fixture is added rather than leaning on the sandboxed HOME making step 1 find nothing: a test that passes because production configuration is ABSENT fails the moment it is present. Shipping the pin without this would have moved the failure rather than removed it. NO CI ARM. .woodpecker/ci.yml does not run these suites; packages/mosaic/ package.json:28 (test:framework-shell) runs an ENUMERATED list that excludes all four. They run only by hand — i.e. exclusively on a provisioned seat, the one environment where the defect is live. "Passes in CI, fails on a seat" does not apply here; there is no CI observation at all. VERIFICATION (seat replica = decoy HOME with mosaic.gitIdentity set, no per-slot token; canary = same plus a marked non-credential at both per-slot paths; plain = empty HOME; real = ordinary invocation): - bash -n clean on all four. - Sweep of all 16 suites at baseline826a8b3under the seat replica: test-gitea-login-resolution rc=1 REACHES-STEP0; test-issue-create- interactive-auth rc=1 REACHES-STEP0; test-pr-merge-gitea-empty-uid rc=1 REACHES-STEP0; test-pr-metadata-gitea rc=1 REACHES-STEP0. - Same sweep after: every row rc=0 with step0 absent. - test-gitea-token-identity flags REACHES-STEP0 in BOTH arms and is NOT a defect: it runs under `env -i HOME="$FAKE_HOME"` (line 77) and its hit is its own deliberate assert_failloud fixtures (lines 158-171). The fail-loud grep matches the intended behaviour as well as the defect, so it needs the second discriminator; recorded here so the next sweep does not re-file it. - Durable-argv assay (a PATH shim that tees argv out of each suite's own mock curl, because test-pr-merge-gitea-empty-uid truncates its log between phases and its EXIT trap removes the sandbox — a post-hoc read of that suite is a non-measurement, and "no trace" there is not a clearance): test-pr-merge-gitea-empty-uid before: canary token in argv, fixture never used. after: fixture token in argv, canary absent. 5 curl calls both arms. test-pr-metadata-gitea before: canary in argv. after: both calls carry the fixture token against git.uscllc.com. - test-pr-metadata-gitea across seat/canary/plain HOMEs after the fix: rc=0, rc=0, rc=0. - All four under the real HOME: rc=0. No regression to ordinary invocation. The comment block is duplicated across the four files rather than pointing at a shared note. Deliberate, and matching the merged #1006 precedent (test-pr-review-gitea-comment.sh:87-95): the reader who needs it is auditing one file. TWO FINDINGS DELIBERATELY NOT FIXED HERE (out of this branch's scope, to be filed): 1. pr-metadata.sh:89-92 — the anonymous curl fallback does not check ^2, so an HTTP 200 carrying valid JSON is reported as "unknown API error" at rc=1. 2. test-issue-comment-readback.sh exits 1 with ZERO bytes on stdout AND stderr, dying at its first seed_state python3 heredoc. Reproduces at baseline826a8b3under both a seat replica and the real HOME. Silently red at main for everyone; unrelated to #1007. Refs #1007
@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.