feat(mosaic): claudex proxy preflight + lifecycle (P1 of #790) #793

Merged
jason.woltje merged 4 commits from feat/790-mosaic-yolo-claudex into main 2026-07-16 21:15:34 +00:00
Owner

PR-1 — claudex proxy preflight + lifecycle (P1 of #790)

First of two sequential PRs for mosaic yolo claudex (GPT models in the Claude Code harness via raine/claude-code-proxy, ChatGPT OAuth). This PR is P1 only — proxy preflight + lifecycle helpers. It ships no session launch; the isolated CLAUDE_CONFIG_DIR composition, env injection, model-tier map, and EXPERIMENTAL banner are PR-2 (branches off main after this merges).

Scope

New module packages/mosaic/src/commands/claudex-proxy.ts (+ co-located .spec.ts). Pure / dependency-injected — every side-effecting boundary (spawn, fetch, which, fs) is an injected dep so the logic is unit-testable without touching a real proxy or the network. Nothing wired into cli.ts/launch.ts yet, so no runtime behavior change for existing commands.

What it adds

  • checkProxyBinary — resolves claude-code-proxy on PATH.
  • parseAuthStatus / checkAuthStatus — coarse auth state (valid|expired|unauthenticated|unknown) from claude-code-proxy codex auth status. Carries no token material by construction.
  • runDeviceReauth — device-code re-auth via stdio: 'inherit'; the launcher never captures or logs the device code or token.
  • probeLivenessgotcha #1: ANY HTTP response (incl. non-2xx) means alive; never curl -f. Bounded by an explicit Promise.race timeout so a hung socket can't wedge preflight.
  • buildSystemdUnitContent / systemdUnitPath / installSystemdUnit — optional user unit claude-code-proxy.service; unit text contains no credentials.
  • runProxyPreflight — structured report (binary/auth/liveness/needsReauth/problems).
  • ensureProxyRunning — systemd-preferred start with nohup fallback; re-probes before/after each start so it never spawns a duplicate proxy.

Security invariants (light secrev surface, charter #4)

  • No curl -fprobeLiveness treats non-2xx as alive (tested: 404/500 → alive; reject/timeout → dead).
  • Re-auth never logs device code/tokenrunDeviceReauth uses stdio:'inherit'; test asserts inherit.
  • No duplicate proxyensureProxyRunning re-probes around every start.
  • parseAuthStatus drops token-shaped output — test asserts access_token/refresh_token/sk- never retained.

Tests / gates

  • 39 unit tests, all green. Full @mosaicstack/mosaic suite: 1080 passing.
  • pnpm typecheck, pnpm lint, pnpm format:check: green.
  • Coverage on new module: 89.6% statements/lines, 87.1% branch (≥85% gate met). Uncovered lines are only the real process-spawn wrappers (systemd/nohup/wait), unsuitable for unit spawning.
  • TDD: spec authored and run red before implementation.

Review asks

  • Independent reviewer (author ≠ reviewer), exact-head review of 0e41a5c2.
  • Light secrev on the four invariants above; route verdict to MS-LEAD (mosaic-100).
  • No self-merge — merge authorized by coordinator on APPROVE + green CI.

Refs #790 (partial — issue stays open until P2–P4 land).

## PR-1 — claudex proxy preflight + lifecycle (P1 of #790) First of two sequential PRs for `mosaic yolo claudex` (GPT models in the Claude Code harness via `raine/claude-code-proxy`, ChatGPT OAuth). **This PR is P1 only** — proxy preflight + lifecycle helpers. It ships **no session launch**; the isolated `CLAUDE_CONFIG_DIR` composition, env injection, model-tier map, and EXPERIMENTAL banner are **PR-2** (branches off `main` after this merges). ### Scope New module `packages/mosaic/src/commands/claudex-proxy.ts` (+ co-located `.spec.ts`). Pure / dependency-injected — every side-effecting boundary (spawn, fetch, which, fs) is an injected dep so the logic is unit-testable without touching a real proxy or the network. Nothing wired into `cli.ts`/`launch.ts` yet, so no runtime behavior change for existing commands. ### What it adds - `checkProxyBinary` — resolves `claude-code-proxy` on PATH. - `parseAuthStatus` / `checkAuthStatus` — coarse auth state (`valid|expired|unauthenticated|unknown`) from `claude-code-proxy codex auth status`. Carries **no token material** by construction. - `runDeviceReauth` — device-code re-auth via `stdio: 'inherit'`; the launcher never captures or logs the device code or token. - `probeLiveness` — **gotcha #1**: ANY HTTP response (incl. non-2xx) means alive; never `curl -f`. Bounded by an explicit `Promise.race` timeout so a hung socket can't wedge preflight. - `buildSystemdUnitContent` / `systemdUnitPath` / `installSystemdUnit` — optional user unit `claude-code-proxy.service`; unit text contains no credentials. - `runProxyPreflight` — structured report (binary/auth/liveness/needsReauth/problems). - `ensureProxyRunning` — systemd-preferred start with nohup fallback; re-probes before/after each start so it never spawns a duplicate proxy. ### Security invariants (light secrev surface, charter #4) - **No `curl -f`** — `probeLiveness` treats non-2xx as alive (tested: 404/500 → alive; reject/timeout → dead). - **Re-auth never logs device code/token** — `runDeviceReauth` uses `stdio:'inherit'`; test asserts inherit. - **No duplicate proxy** — `ensureProxyRunning` re-probes around every start. - **`parseAuthStatus` drops token-shaped output** — test asserts `access_token`/`refresh_token`/`sk-` never retained. ### Tests / gates - 39 unit tests, all green. Full `@mosaicstack/mosaic` suite: 1080 passing. - `pnpm typecheck`, `pnpm lint`, `pnpm format:check`: green. - Coverage on new module: **89.6% statements/lines, 87.1% branch** (≥85% gate met). Uncovered lines are only the real process-spawn wrappers (systemd/nohup/wait), unsuitable for unit spawning. - TDD: spec authored and run **red before** implementation. ### Review asks - Independent reviewer (author ≠ reviewer), **exact-head** review of `0e41a5c2`. - Light secrev on the four invariants above; route verdict to MS-LEAD (`mosaic-100`). - No self-merge — merge authorized by coordinator on APPROVE + green CI. Refs #790 (partial — issue stays open until P2–P4 land).
jason.woltje added 1 commit 2026-07-16 19:41:12 +00:00
feat(mosaic): claudex proxy preflight + lifecycle helpers (P1 of #790)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
0e41a5c264
P1 of the `mosaic yolo claudex` launcher (#790): pure, dependency-injected
preflight and lifecycle helpers for `raine/claude-code-proxy` (the local
Anthropic->Codex translation proxy on 127.0.0.1:18765). No session launch yet;
the isolated CLAUDE_CONFIG_DIR composition + env-injection land in PR-2.

Authored test-first (spec written and run red before implementation, then
green). 39 unit tests, 89.6% statement/line coverage on the new module; the
only uncovered lines are the process-spawning system wrappers
(systemd/nohup/wait), which are integration glue unsuitable for unit spawning.

Helpers:
- checkProxyBinary — binary presence via an injectable `which` resolver.
- parseAuthStatus / checkAuthStatus — coarse OAuth state from
  `codex auth status`. Carries NO token material (state + optional expiry only)
  so token-shaped output can never leak downstream.
- runDeviceReauth — `codex auth device` with stdio:'inherit' so the device code
  streams to the user's TTY; the launcher never captures/logs it or sees the
  resulting token.
- probeLiveness — gotcha #1: ANY HTTP response (incl. non-2xx) = alive; only a
  transport failure/timeout = dead. NEVER `curl -f` (that spawned duplicate
  proxies). Bounded by an explicit timeout race so a hung socket can't wedge.
- buildSystemdUnitContent / systemdUnitPath / installSystemdUnit — systemd
  --user unit management; unit carries no credential material.
- runProxyPreflight — structured binary+auth+liveness report.
- ensureProxyRunning — no-op when live, else systemd-preferred with nohup
  fallback, re-probing after each attempt so it never spawns a duplicate.

Refs #790. Not part of the #758 fleet DAG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jason.woltje added 1 commit 2026-07-16 20:14:25 +00:00
fix(mosaic): harden claudex proxy preflight per review (P1 of #790)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
7f987f52fc
Addresses the four findings from the independent review of #793 (exact head
0e41a5c2). TDD: each fix's failing test was added first (18 new tests), then
the implementation; full mosaic suite 1098 green, three gates green.

1. nohup fallback async spawn error (correctness): spawn() reports ENOENT/EACCES
   asynchronously via the child 'error' event, which the old try/catch could not
   see -> an unhandled 'error' would crash the launcher, and it returned status 0
   before the child had started. Extract startNohupProxy(): attach the 'error'
   listener BEFORE unref(), resolve non-zero on async error/sync throw, and
   resolve success only after a confirmed 'spawn'. startNohup dep is now async.

2. systemd start not socket-bound (correctness): `systemctl --user start` exit 0
   means the job was accepted, not bound within one probe. ensureProxyRunning now
   polls liveness to a bounded startupDeadlineMs after a start before falling
   back, so a slow systemd bind can't trigger a second, contending proxy.

3. liveness trust (security, CWE-345): probeLiveness hit the root and trusted ANY
   HTTP response, so a local port-squatter could be taken for the proxy and MITM
   Claude traffic. Now probe the proxy-specific GET /healthz and require a 2xx.
   This also resolves gotcha #1 (root returns non-2xx): /healthz returns 2xx when
   healthy, so a live proxy is never mistaken for dead. (Upstream offers no unix
   socket or shared-secret handshake; /healthz is the in-scope identity ceiling.)

4. systemd ExecStart injection (security, CWE-74): buildSystemdUnitContent
   interpolated binaryPath raw, so a CR/LF could inject unit directives. Validate
   the path (absolute, reject control chars) and systemd-quote it when it carries
   whitespace/quotes; installSystemdUnit now refuses to write a poisoned unit.

Coverage on claudex-proxy.ts: 96.3% stmts/lines, 87.1% branch.

Refs #790
jason.woltje added 1 commit 2026-07-16 20:44:55 +00:00
fix(mosaic): close dup-proxy race + verify listener identity (re-review #2, #790)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
6bef94ea0d
Addresses the three items from PR #793 re-review #2, all TDD red-first on this
branch:

F1 (BLOCKER, dup-proxy race) — ensureProxyRunning no longer falls back to nohup
after `systemctl start` is ACCEPTED. An accepted-but-not-yet-healthy systemd
unit may bind late or be restarted; spawning nohup then would create a second
proxy contending for :18765. Once systemd accepts the job we poll to the startup
deadline and, on a miss, report a managed-service startup failure. nohup is now
reachable only when systemd never accepted the job. New test: "does NOT fall back
to nohup after systemd accepts but never binds".

F2 (BLOCKER, CWE-345) — a 2xx on /healthz is liveness, not identity. Added
verifyListenerIdentity(): an OS-level check (via `ss` + /proc) that the process
owning :18765 is the current uid running the expected proxy executable, failing
CLOSED (`unknown`) when identity can't be established — needs no upstream
shared-secret/unix-socket support. ensureProxyRunning now gates EVERY trust point
on it: an already-present responder that fails identity returns `untrusted`
without starting anything; a started proxy is trusted only once it is both live
AND identity-verified. probeLiveness doc-comment updated to record the residual
CWE-345 risk and point at the identity check (multi-user warning deferred).

F3 (SHOULD-FIX) — parseAuthStatus no longer treats a signal-terminated check
(status null) with an auth-looking line as valid; a clean exit 0 is required.
Regression test: { status: null, stdout: 'Authenticated' } → unknown.

Gates green: typecheck, lint, format:check; full mosaic suite 1110 passing;
claudex-proxy.ts coverage 92.3% stmts/lines, 88.13% branch (>= 85%).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jason.woltje added 1 commit 2026-07-16 21:05:45 +00:00
fix(mosaic): close two CWE-345 identity bypasses in claudex preflight (re-review #3, #790)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
1f61d16f16
Addresses PR #793 re-review #3 (F1/F3 already confirmed closed). Both fixes TDD
red-first on this branch (7 tests red against the prior head, then green).

F2a (BLOCKER) — verifyListenerIdentity no longer accepts a listener by BASENAME.
The old code ran `if (basename(exePath) === CLAUDEX_PROXY_BINARY) return 'ok'`
unconditionally, so a same-uid process running `/tmp/claude-code-proxy` (right
name, wrong path) was trusted. On a shared-uid host that is the squatter vector,
not hypothetical — uid is NOT a trust boundary there, the executable path is.
Now: when the expected proxy path resolves we require an EXACT canonical-path
match (both sides canonicalized via realpath for symlinks); there is NO basename
fallback. If the expected path can't be resolved, or either path can't be
canonicalized, we fail closed (`unknown`). New tests: wrong-path/right-basename →
wrong-exe; unresolved expected → unknown; uncanonicalizable → unknown; and a
symlink-resolution case that canonically matches → ok.

F2b (BLOCKER) — runProxyPreflight now verifies listener identity too. Previously
`ok` was `bin.present && auth==='valid' && live` where `live` is /healthz-2xx
only, so a squatter answering 2xx yielded `ok:true` and any consumer of the
report (the phase-2 launch path) would route Claude traffic to it. Added
`verifyListener` to PreflightDeps + a `listenerVerdict` field; `ok` now requires
a trusted verdict; a live-but-unverified responder emits a non-sensitive problem
(port + verdict only — no listener command line or token). Identity is not
checked when the port is dead (no listener to trust). New tests: each non-ok
verdict fails preflight and leaks no token material; dead proxy skips verify.

Gates green: typecheck, lint, format:check; full mosaic suite 1115 passing;
claudex-proxy.ts coverage 90.57% stmts/lines, 89.06% branch (>= 85%).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jason.woltje merged commit 59f5f51ffd into main 2026-07-16 21:15:34 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#793