feat(mosaic): claudex proxy preflight + lifecycle (P1 of #790) #793
Merged
jason.woltje
merged 4 commits from 2026-07-16 21:15:34 +00:00
feat/790-mosaic-yolo-claudex into main
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1f61d16f16 |
fix(mosaic): close two CWE-345 identity bypasses in claudex preflight (re-review #3, #790)
ci/woodpecker/pr/ci Pipeline was successful
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 <[email protected]> |
||
|
|
6bef94ea0d |
fix(mosaic): close dup-proxy race + verify listener identity (re-review #2, #790)
ci/woodpecker/pr/ci Pipeline was successful
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 <[email protected]> |
||
|
|
7f987f52fc |
fix(mosaic): harden claudex proxy preflight per review (P1 of #790)
ci/woodpecker/pr/ci Pipeline was successful
Addresses the four findings from the independent review of #793 (exact head
|
||
|
|
0e41a5c264 |
feat(mosaic): claudex proxy preflight + lifecycle helpers (P1 of #790)
ci/woodpecker/pr/ci Pipeline was successful
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 <[email protected]> |