feat(mosaic): claudex isolated config + env-inject + yolo wiring (P2–P4 of #790) #806

Merged
jason.woltje merged 2 commits from feat/790-mosaic-yolo-claudex-pr2 into main 2026-07-16 22:10:34 +00:00
Owner

PR-2 — claudex isolated config + env-inject + yolo wiring (P2–P4 of #790)

Second and final PR for mosaic [yolo] claudex — running GPT models inside the Claude Code harness via raine/claude-code-proxy (ChatGPT-subscription OAuth). Builds on the PR-1 preflight/lifecycle library (#793, merged 59f5f51f). This PR makes the command reachable.

Branch is off main at exactly 59f5f51f (PR-1 merge). Diff vs origin/main = only the 5 files below.

Scope

  • NEW packages/mosaic/src/commands/claudex.ts — pure / dependency-injected composition (mirrors PR-1 style; every side-effecting boundary injected).
  • EDIT launch.tsexecRuntime env param; claudex + yolo claudex dispatch; thin launchClaudexProduction glue.
  • EDIT launch.spec.ts — commander wiring tests (claudex handler spy; yolo claudex #454 arg-slice).
  • NEW claudex.spec.ts — 45 specs. DOCS README.md.

HARD SECURITY REQ 1 — provable isolation (assertIsolatedConfigDir / resolveClaudexConfigDir)

The isolated CLAUDE_CONFIG_DIR seam is provably incapable of resolving to the real ~/.claude:

  • Default dir ~/.config/mosaic/claudex/home — structurally under mosaic home. Ambient CLAUDE_CONFIG_DIR is never inherited; only MOSAIC_CLAUDEX_CONFIG_DIR overrides.
  • Guard canonicalizes BOTH sides (either may be a symlink — same realpath-both-sides rule that fixed PR-1's F2a) and rejects the candidate if it equals OR is a descendant of ~/.claude. Empty/relative → throw.
  • TOCTOU: after mkdir -p (0700) it re-checks — rejects a symlinked target and re-runs the guard on the now-existing path (catches a symlinked ancestor introduced between pre-check and create). Fails closed on any post-create failure.
  • Test asserts the guard rejects literal ~/.claude, a descendant, a symlink-canonical match, and canonicalizes the ~/.claude side too; a claudex launch can never emit ~/.claude.

HARD SECURITY REQ 2 — zero token leakage (buildClaudexEnv)

  • This module never reads the proxy's auth.json / any credential file.
  • The composed env strips the entire credential-bearing family by name pattern (^ANTHROPIC_, ^CLAUDE_CODE_OAUTH, *_API_KEY, *_TOKEN, *_SECRET) — not just two vars — then re-sets only the safe claudex vars. Claude Code sees only ANTHROPIC_AUTH_TOKEN=unused; the proxy holds the real credential. ANTHROPIC_API_KEY is dropped by the pattern.
  • Returns a fresh object (no mutation of the base env). Tests assert: only auth var emitted is literal unused; a pre-set real token is overwritten; no composed value matches token shapes (sk-, eyJ, access_token, refresh_token); and no credential-named key survives; harmless env (PATH/HOME/XDG) is preserved.

Env-sweep design note (for secrev): denylist-by-name-pattern was chosen over a strict allowlist because the harness/MCP/hooks legitimately need arbitrary non-credential env (PATH, HOME, XDG, terminal, proxies…). The pattern is comprehensive and name-pattern coverage is asserted directly, so a specific var a denylist might forget is still caught by shape+name tests.

P3 — model-tier map

primary (opus/sonnet) → ANTHROPIC_MODEL=gpt-5.6-sol; small/fast (haiku) → ANTHROPIC_SMALL_FAST_MODEL=gpt-5.6-luna. Operator-provided env values win; blank ignored.

P4 — EXPERIMENTAL classification

Console banner + a composed-contract note appended after composeContract('claude') so the model knows it is GPT-in-Claude-harness via claude-code-proxy (not Anthropic). No token material in either by construction.

Fail-closed orchestration (launchClaudex)

harnessPreflight → proxyGate → compose env → exec. exec is injected, so ordering + abort paths are unit-tested: gate-not-ok → abort before exec; config-dir guard throw → abort before exec; reauth invoked only when OAuth state needs it. Proxy endpoint imported from PR-1's claudex-proxy.ts (single source of truth — no re-hardcoded 18765/127.0.0.1).

Tests / gates

  • 45 claudex specs + 5 launch wiring specs, TDD red-first (spec RED before claudex.ts existed; wiring RED before dispatch).
  • New-module coverage: 99.03% stmts/lines, 93.33% branch (≥85% gate; the isolation-guard and env-sweep branches are the ones hit — the only uncovered lines are a defensive canonicalize root-walk unreachable on a real FS).
  • Full @mosaicstack/mosaic suite: 1164 passing. pnpm typecheck / lint / format:check: green. No --no-verify.

Review asks

  • Independent reviewer (author ≠ reviewer), exact-head review + secrev on REQ 1 / REQ 2.
  • No self-merge — merge authorized by coordinator (MS-LEAD) on APPROVE + green CI (queue guard first).

Closes #790 once merged (P1–P4 all land).

## PR-2 — claudex isolated config + env-inject + `yolo` wiring (P2–P4 of #790) Second and final PR for `mosaic [yolo] claudex` — running GPT models **inside the Claude Code harness** via `raine/claude-code-proxy` (ChatGPT-subscription OAuth). Builds on the PR-1 preflight/lifecycle library (#793, merged `59f5f51f`). This PR makes the command reachable. Branch is off `main` at exactly `59f5f51f` (PR-1 merge). Diff vs `origin/main` = only the 5 files below. ### Scope - **NEW** `packages/mosaic/src/commands/claudex.ts` — pure / dependency-injected composition (mirrors PR-1 style; every side-effecting boundary injected). - **EDIT** `launch.ts` — `execRuntime` env param; `claudex` + `yolo claudex` dispatch; thin `launchClaudexProduction` glue. - **EDIT** `launch.spec.ts` — commander wiring tests (claudex handler spy; `yolo claudex` #454 arg-slice). - **NEW** `claudex.spec.ts` — 45 specs. **DOCS** `README.md`. ### HARD SECURITY REQ 1 — provable isolation (`assertIsolatedConfigDir` / `resolveClaudexConfigDir`) The isolated `CLAUDE_CONFIG_DIR` seam is provably incapable of resolving to the real `~/.claude`: - Default dir `~/.config/mosaic/claudex/home` — structurally under mosaic home. Ambient `CLAUDE_CONFIG_DIR` is **never** inherited; only `MOSAIC_CLAUDEX_CONFIG_DIR` overrides. - Guard **canonicalizes BOTH sides** (either may be a symlink — same realpath-both-sides rule that fixed PR-1's F2a) and rejects the candidate if it **equals OR is a descendant of** `~/.claude`. Empty/relative → throw. - **TOCTOU:** after `mkdir -p` (0700) it re-checks — rejects a symlinked target and **re-runs the guard** on the now-existing path (catches a symlinked ancestor introduced between pre-check and create). Fails **closed** on any post-create failure. - Test asserts the guard rejects literal `~/.claude`, a descendant, a symlink-canonical match, and canonicalizes the `~/.claude` side too; a claudex launch can never emit `~/.claude`. ### HARD SECURITY REQ 2 — zero token leakage (`buildClaudexEnv`) - This module **never reads** the proxy's `auth.json` / any credential file. - The composed env **strips the entire credential-bearing family** by name pattern (`^ANTHROPIC_`, `^CLAUDE_CODE_OAUTH`, `*_API_KEY`, `*_TOKEN`, `*_SECRET`) — not just two vars — then re-sets only the safe claudex vars. Claude Code sees only `ANTHROPIC_AUTH_TOKEN=unused`; the proxy holds the real credential. `ANTHROPIC_API_KEY` is dropped by the pattern. - Returns a **fresh object** (no mutation of the base env). Tests assert: only auth var emitted is literal `unused`; a pre-set real token is overwritten; no composed value matches token shapes (`sk-`, `eyJ`, `access_token`, `refresh_token`); **and** no credential-*named* key survives; harmless env (PATH/HOME/XDG) is preserved. > **Env-sweep design note (for secrev):** denylist-by-name-pattern was chosen over a strict allowlist because the harness/MCP/hooks legitimately need arbitrary non-credential env (PATH, HOME, XDG, terminal, proxies…). The pattern is comprehensive and name-pattern coverage is asserted directly, so a specific var a denylist might forget is still caught by shape+name tests. ### P3 — model-tier map primary (opus/sonnet) → `ANTHROPIC_MODEL=gpt-5.6-sol`; small/fast (haiku) → `ANTHROPIC_SMALL_FAST_MODEL=gpt-5.6-luna`. Operator-provided env values **win**; blank ignored. ### P4 — EXPERIMENTAL classification Console banner + a composed-contract note appended after `composeContract('claude')` so the model knows it is GPT-in-Claude-harness via claude-code-proxy (not Anthropic). No token material in either by construction. ### Fail-closed orchestration (`launchClaudex`) `harnessPreflight → proxyGate → compose env → exec`. `exec` is injected, so ordering + abort paths are unit-tested: gate-not-ok → abort **before** exec; config-dir guard throw → abort before exec; reauth invoked only when OAuth state needs it. Proxy endpoint imported from PR-1's `claudex-proxy.ts` (single source of truth — no re-hardcoded `18765`/`127.0.0.1`). ### Tests / gates - 45 `claudex` specs + 5 `launch` wiring specs, **TDD red-first** (spec RED before `claudex.ts` existed; wiring RED before dispatch). - New-module coverage: **99.03% stmts/lines, 93.33% branch** (≥85% gate; the isolation-guard and env-sweep branches are the ones hit — the only uncovered lines are a defensive canonicalize root-walk unreachable on a real FS). - Full `@mosaicstack/mosaic` suite: **1164 passing**. `pnpm typecheck` / `lint` / `format:check`: green. No `--no-verify`. ### Review asks - Independent reviewer (author ≠ reviewer), **exact-head** review + secrev on REQ 1 / REQ 2. - No self-merge — merge authorized by coordinator (MS-LEAD) on APPROVE + green CI (queue guard first). Closes #790 once merged (P1–P4 all land).
jason.woltje added 1 commit 2026-07-16 21:44:09 +00:00
feat(mosaic): claudex isolated config + env-inject + yolo wiring (P2–P4 of #790)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ab8c9a2d4b
Second and final PR for `mosaic [yolo] claudex` — runs GPT models inside the
Claude Code harness via claude-code-proxy (ChatGPT-subscription OAuth). Builds
on the PR-1 preflight/lifecycle library (#793).

New `claudex.ts` (pure / dependency-injected composition):
- resolveClaudexConfigDir / assertIsolatedConfigDir — the isolated
  CLAUDE_CONFIG_DIR seam. HARD REQ 1 (provable isolation): canonicalizes both
  sides, rejects `~/.claude` and any descendant, ensures the dir 0700, then
  re-checks post-create and rejects a symlinked target (TOCTOU). Ambient
  CLAUDE_CONFIG_DIR is never inherited. Fails closed on any uncertainty.
- buildClaudexEnv — HARD REQ 2 (zero token leakage): strips the entire
  credential-bearing env family (ANTHROPIC_*, *_TOKEN, *_API_KEY, *_SECRET,
  CLAUDE_CODE_OAUTH*) and hands Claude Code only ANTHROPIC_AUTH_TOKEN=unused.
  Never reads the proxy credential file. Returns a fresh object (no mutation).
- resolveClaudexModels — P3 tier map: primary→gpt-5.6-sol,
  small/fast→gpt-5.6-luna; operator env values win.
- buildClaudexBanner / buildClaudexContractNote — P4 EXPERIMENTAL classification
  (no token material by construction).
- runClaudexProxyGate — preflight → device reauth (only when needed) → ensure a
  trusted-live listener → re-preflight; surfaces non-sensitive problems only.
- launchClaudex — fail-closed DI orchestration: preflight → gate → compose env →
  exec; never reaches exec on a gate failure or guard throw. Proxy endpoint
  imported from claudex-proxy.ts (single source of truth).

launch.ts wiring:
- execRuntime gains an env parameter.
- `claudex` + `yolo claudex` commander dispatch (keeps the #454 arg-slice fix);
  claudexHandler is an injectable seam for the wiring tests.
- launchClaudexProduction glue assembles the real harness adapter.

Tests: 45 claudex specs + 5 launch wiring specs, TDD red-first. New-module
coverage 99% stmts/lines, 93% branch (≥85% gate). Full mosaic suite 1164 green;
typecheck / lint / format:check green.

Refs #790

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jason.woltje added 1 commit 2026-07-16 22:00:48 +00:00
fix(mosaic): close claudex isolation gaps S1 (Bedrock/Vertex bypass) + S2 (fail-open catches)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
79e2fa35d5
Addresses MS-LEAD secrev REQUEST CHANGES on #806.

S1 (CRITICAL — REQ 2 defeated by provider switches + cloud creds): the env sweep
did not neutralize AWS Bedrock / GCP Vertex routing. CLAUDE_CODE_USE_BEDROCK /
CLAUDE_CODE_USE_VERTEX are routing switches whose mere presence makes Claude Code
talk to the real Anthropic API via the ambient cloud credential chain, bypassing
ANTHROPIC_BASE_URL (the loopback proxy) entirely — and the cloud credentials
(AWS_SECRET_ACCESS_KEY, AWS_BEARER_TOKEN_BEDROCK, GOOGLE_APPLICATION_CREDENTIALS,
…) passed straight through.
- buildClaudexEnv now force-deletes the routing switches by exact name
  (CLAUDEX_FORCED_UNSET_ENV) regardless of value — a name pattern is the wrong
  model for a boolean switch.
- CLAUDEX_CREDENTIAL_ENV_RE extended to the cloud-cred families (^AWS_,
  ^GOOGLE_APPLICATION_CREDENTIALS, ^GOOGLE_CLOUD_, ^GCP_) and the mid-string
  _KEY$ / _SECRET gap (STRIPE_SECRET_KEY, SSH_PRIVATE_KEY, AWS_SECRET_ACCESS_KEY).
- Doc-comment updated to match reality.

S2 (HIGH — fail-open contradicts documented fail-closed): defaultCanonicalizeIntended
and defaultIsSymlink swallowed ANY fs error. Now they distinguish ENOENT
(genuinely absent → safe to continue) from every other errno (ELOOP, EACCES,
ENOTDIR, …) which rethrows and fails CLOSED.

Tests (TDD red-first, 4 new specs red before the fix): Bedrock/Vertex switches +
AWS/GCP creds swept while the loopback proxy stays the only route; mid-string
_KEY/_SECRET closed; real-FS ELOOP (canonicalize) and ENOTDIR (isSymlink) fail
closed; injected EACCES not swallowed.

New-module coverage 100% stmts/lines, 93.75% branch. Full mosaic suite 1169 green;
typecheck / lint / format:check green. No --no-verify.

Refs #790

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jason.woltje merged commit 3be443c96d into main 2026-07-16 22:10:34 +00:00
Author
Owner

Exact-head independent review + security review — APPROVED @ 79e2fa35d547274bc877a8e51b0d8c59ea302025 (reviewer: mosaic-100; author: ms-claudex-790 — author≠reviewer). Merged clean+green under coordinated-lane self-merge authority. Publishing the exact-head record for provenance (prior REQUEST CHANGES @ ab8c9a2d did not transfer; this is a fresh review of the exact SHA).

Provenance: head 79e2fa35 = exactly one fix commit on ab8c9a2d, no rebase/amend; diff vs origin/main = 5 files, all under packages/mosaic/. CI pipeline 1873 terminal-green at this exact head.

S1 (was CRITICAL — REQ 2 Bedrock/Vertex bypass) — RESOLVED by construction. buildClaudexEnv now force-deletes the routing switches CLAUDE_CODE_USE_BEDROCK / USE_VERTEX / SKIP_BEDROCK_AUTH / SKIP_VERTEX_AUTH by exact name regardless of value (presence, not credential-ness, is the defeat — correct model), applied after the credential sweep and before the ANTHROPIC_* re-sets. CLAUDEX_CREDENTIAL_ENV_RE extended to ^AWS_, ^GOOGLE_APPLICATION_CREDENTIALS$, ^GOOGLE_CLOUD_, ^GCP_, plus _KEY$ and de-anchored _SECRET (closes mid-string AWS_SECRET_ACCESS_KEY / STRIPE_SECRET_KEY / SSH_PRIVATE_KEY). Independent adversarial secrev swept the full Bedrock/Vertex/direct provider surface and could NOT defeat REQ 2: every credential stripped, every routing switch force-deleted, surviving vars (CLOUD_ML_REGION/VERTEX_REGION) inert without the force-unset switches, ANTHROPIC_BASE_URL pinned to the loopback proxy, opts.baseUrl not env-derived, isolated CLAUDE_CONFIG_DIR freshly 0700.

S2 (was HIGH — fail-open errno swallow) — RESOLVED. defaultCanonicalizeIntended and defaultIsSymlink now distinguish ENOENT (genuinely absent → walk up / not-a-symlink) from every other errno (ELOOP/EACCES/ENOTDIR → rethrow → fail CLOSED). Rethrow propagates to launchClaudex's catch → fail(1).

Tests: 4 new red-first specs, non-tautological — S1 asserts both switch/cred removal AND proxy-URL + unused + PATH preservation; S2 uses real-FS ELOOP/ENOTDIR + injected EACCES asserting toThrow. Red against ab8c9a2d, green after fix; no test edited to pass. New-module coverage 100% stmts/lines, 93.75% branch; full mosaic suite 1169 passing; typecheck/lint/format ✓; no --no-verify; queue guard run before push.

Non-blocking follow-up: CLAUDEX_FORCED_UNSET_ENV is a name-denylist with no pattern safety-net — add a guard/test if Claude Code introduces a new provider-enabling switch. Not gating.

Closes #790.

**Exact-head independent review + security review — APPROVED @ `79e2fa35d547274bc877a8e51b0d8c59ea302025`** (reviewer: mosaic-100; author: ms-claudex-790 — author≠reviewer). Merged clean+green under coordinated-lane self-merge authority. Publishing the exact-head record for provenance (prior REQUEST CHANGES @ `ab8c9a2d` did not transfer; this is a fresh review of the exact SHA). **Provenance:** head `79e2fa35` = exactly one fix commit on `ab8c9a2d`, no rebase/amend; diff vs origin/main = 5 files, all under `packages/mosaic/`. CI pipeline 1873 terminal-green at this exact head. **S1 (was CRITICAL — REQ 2 Bedrock/Vertex bypass) — RESOLVED by construction.** `buildClaudexEnv` now force-deletes the routing switches `CLAUDE_CODE_USE_BEDROCK` / `USE_VERTEX` / `SKIP_BEDROCK_AUTH` / `SKIP_VERTEX_AUTH` by exact name regardless of value (presence, not credential-ness, is the defeat — correct model), applied after the credential sweep and before the ANTHROPIC_* re-sets. `CLAUDEX_CREDENTIAL_ENV_RE` extended to `^AWS_`, `^GOOGLE_APPLICATION_CREDENTIALS$`, `^GOOGLE_CLOUD_`, `^GCP_`, plus `_KEY$` and de-anchored `_SECRET` (closes mid-string `AWS_SECRET_ACCESS_KEY` / `STRIPE_SECRET_KEY` / `SSH_PRIVATE_KEY`). Independent adversarial secrev swept the full Bedrock/Vertex/direct provider surface and could NOT defeat REQ 2: every credential stripped, every routing switch force-deleted, surviving vars (CLOUD_ML_REGION/VERTEX_REGION) inert without the force-unset switches, ANTHROPIC_BASE_URL pinned to the loopback proxy, opts.baseUrl not env-derived, isolated CLAUDE_CONFIG_DIR freshly 0700. **S2 (was HIGH — fail-open errno swallow) — RESOLVED.** `defaultCanonicalizeIntended` and `defaultIsSymlink` now distinguish `ENOENT` (genuinely absent → walk up / not-a-symlink) from every other errno (`ELOOP`/`EACCES`/`ENOTDIR` → rethrow → fail CLOSED). Rethrow propagates to `launchClaudex`'s catch → `fail(1)`. **Tests:** 4 new red-first specs, non-tautological — S1 asserts both switch/cred removal AND proxy-URL + `unused` + PATH preservation; S2 uses real-FS ELOOP/ENOTDIR + injected EACCES asserting `toThrow`. Red against `ab8c9a2d`, green after fix; no test edited to pass. New-module coverage 100% stmts/lines, 93.75% branch; full mosaic suite 1169 passing; typecheck/lint/format ✓; no `--no-verify`; queue guard run before push. **Non-blocking follow-up:** `CLAUDEX_FORCED_UNSET_ENV` is a name-denylist with no pattern safety-net — add a guard/test if Claude Code introduces a new provider-enabling switch. Not gating. Closes #790.
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#806