Completes the promotion path: the client landed in the previous commit, but nothing drove step 2 — the model emitting the receipt. This wires it. LAZY, not at session start. Promotion costs an entire model turn, because the receipt must be the whole message (hmac.compare_digest, "not a transcript substring"). Minting at session start would collide with the Constitution's first-response mode declaration — the two cannot share a message, so requiring both would be unsatisfiable. Deferring to the first DENIED MUTATOR means the mode declaration happens first and the receipt gets its own later turn, so no governance change is needed. A read-only session never pays for promotion at all. Mechanism: on a MUTATOR_UNVERIFIED denial the tool_call hook mints a challenge and returns the receipt in the block `reason`, which pi feeds back to the model as the tool result — the existing injection path already used by lease-lifecycle.ts. The model emits the receipt as its next message, message_end ships it to the observer, and the extension then calls observe_receipt + promote_lease. Only MUTATOR_UNVERIFIED triggers minting. Other denials (GATE_UNAVAILABLE, STALE_GENERATION, LEASE_EXPIRED, ANCESTRY_MISMATCH) describe conditions a receipt cannot fix, and begin_verification revokes before it mints, so minting there would thrash the broker. Completion is gated on an EXACT text match against the minted receipt. This is load-bearing, not defensive: message_end also fires for the message that CONTAINED the blocked tool call — one turn BEFORE the model answers. An earlier version completed there, so observe_receipt compared against the wrong text, failed, and burned the challenge before the model ever emitted it. Matching the text mirrors the broker's own compare_digest semantics and waits for the right turn. Confirmed by instrumenting message_end and watching it fire with pending=yes one message too early. It never posts the receipt itself. receipt-observer-client.py accepts any string, so self-posting would satisfy the broker while proving nothing — the whole point is that a live model echoes a challenge it was given. Bounded by MAX_PROMOTION_ATTEMPTS: model compliance is not guaranteed (observed a run where the model retried the command instead of emitting the receipt), so a non-complying model degrades to today's behaviour — denied mutators — rather than looping. Verified with a live model on sb-it-1-dt: receipt emitted verbatim as a whole message, and the broker token was minted AND consumed, i.e. observe_receipt and promote_lease both succeeded and the lease reached VERIFIED. Known limitation: under `pi -p`, the receipt is a text-only turn, which ends the one-shot loop — so promotion completes but the blocked tool is not retried in that same invocation. Interactive and durable fleet sessions continue and retry normally.
@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.