With no --login, both wrappers resolved a login by GUESSING it from the repo host (get_gitea_login_for_host / get_gitea_login), then looked that name up in ~/.config/tea/config.yml. On git.mosaicstack.dev the guess resolves to mosaicstack-mos-dt-0, a SHARED account. get_gitea_token_for_login returns the matching token string with no authentication check anywhere in its body, so it returns rc=0 for a dead credential and the `|| get_gitea_token` fallback never fires. The identity-aware resolver was unreachable on this path. Measured on git.mosaicstack.dev with MOSAIC_GIT_IDENTITY set: get_gitea_token_for_login <guessed> rc=0, token authenticates HTTP 401 get_gitea_token <host> rc=0, token authenticates HTTP 200 Control: the same endpoint with no credential returns 401. The dead token is what made this visible; it is not the defect. Had the shared token been alive, every seat's reviews and comments would have been authored by the shared account, making Gate-16 author-is-not-reviewer unenforceable across the fleet. A caller that passes no --login is asking to act as ITSELF, and the guess answered a question nobody asked. This removes the guess and its tea lookup from the no---login path in both wrappers. That path now resolves the acting identity's own credential via get_gitea_token, which fails loud on a fleet host when no identity resolves. That refusal is the correct outcome and is deliberately not fallen back from. Unchanged on purpose: - get_gitea_token_for_login keeps its behaviour and its 53 assert_token pins in test-gitea-login-resolution.sh. pr-edit.sh and the explicit --login branches still use it; --login remains the only way to reach the tea store. - No in-function verification was added. These wrappers verify every write by id-plus-author read-back against the credential-derived login, so a revoked token fails at the write with no misattribution. A GET /user pre-check would also hard-fail a live token scoped write:repository without read:user, which returns 403 while being fully comment-capable. Tests: 28 pass. test-issue-close-fail-closed.sh fails identically on pristine upstream (byte-identical output) and touches issue-close.sh, which this does not modify. The wrapper test harness inherits an ambient MOSAIC_GIT_IDENTITY into its sandbox HOME; tests were run with it unset. Adversarial review by fargo, who found that reordering alone is a no-op wherever MOSAIC_GIT_IDENTITY is unset, and that a 401/403 fail-closed pre-check would reject correctly-scoped live tokens.
@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.