Repository navigation
feat(claude-desktop): land the local intercept chain on dev - #5319
Conversation
…ver wiring Claude Code honours HTTPS_PROXY/NODE_EXTRA_CA_CERTS from its settings env, so a loopback CONNECT proxy plus a locally-signed TLS listener for api.anthropic.com lets the router see Claude Code's Messages traffic without any ANTHROPIC_BASE_URL rewrite and without touching the Desktop app's own (first-party) configuration. - src/claude/intercept/local-ca.ts: ECDSA P-256 CA + leaf issuance with a hand-rolled DER encoder (node:crypto only); CA persisted under <OPENCODEX_HOME>/claude-intercept with a 0600 key, never installed in an OS store. - src/claude/intercept/connect-proxy.ts: CONNECT-only loopback proxy; splices api.anthropic.com:443 onto the TLS listener, relays other targets blind, refuses plain HTTP, loopback targets and oversized heads. - src/claude/intercept/listener.ts: TLS listener; POST /v1/messages and /v1/messages/count_tokens are rewritten onto a loopback origin and dispatched to the route table under the new claude-intercept ingress (loopback policy); every other path is relayed verbatim to the configured Anthropic upstream. - src/claude/intercept/settings.ts: ownership-aware apply/inspect/remove of the two env keys in Claude Code settings.json (anchored on the CA path). - src/claude/intercept/runtime.ts + server wiring: enabled by default on a hub, proxy port = public port + 100 unless claudeCode.intercept.port is set; bind failure degrades to a warning; stop joins both sockets. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…t wiring out of index.ts startServer(0) has no stable port to derive the CONNECT proxy from, so the intercept pair now stays off unless claudeCode.intercept.port is explicit; this also keeps in-process test fixtures at their expected listener count. The wiring moves into src/server/index/claude-intercept-lifecycle.ts and the inbound-body-limit warning into startup-warnings.ts so src/server/index.ts stays under its file-size cap. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…gets, share the intercepted-path predicate isLoopbackTarget now checks 127/8, ::1, 0.0.0.0 and :: through a BlockList (which also matches IPv4-mapped IPv6), *.localhost, and numeric resolver shorthands like 127.1. serve-options.ts reuses isClaudeInterceptedPath from listener.ts so the two route lists cannot drift apart. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… explicit opt-in Add claudeCode.desktopMode (first-party | gateway). First-party keeps Claude Desktop on claude.ai and writes only HTTPS_PROXY/NODE_EXTRA_CA_CERTS into Claude Code's settings.json so the Code tab, subagents and the claude CLI go through the local intercept pair. Gateway keeps the existing 3P profile writer. - resolveClaudeDesktopMode: explicit > applied gateway fingerprint > first-party, so existing gateway installs do not flip on update while new installs get 1P - resolveClaudeDesktopApplyMode: implied 1P falls back to gateway where the intercept pair cannot run (client role / intercept disabled) - CLI: ocx claude desktop apply [--first-party|--gateway]; legacy shape flags imply --gateway; connected clients default to gateway - API: /api/claude-desktop/apply accepts first-party|gateway (+ legacy shapes), status reports mode + firstParty block; native toggle applies resolved mode and disable removes both gateway profile and 1P env - ocx ensure: refresh stale 1P env when ON, remove it when OFF - modes are mutually exclusive; foreign proxy/CA env is never overwritten Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…file Switching gateway -> first-party from the GUI/CLI/API previously refused with gateway_profile_active and required turning the integration off first, while the docs and the GUI switch note promise a direct replacement in both directions. The first-party branch now pivots the owned gateway profile back to standard (removeDesktop3pStandardPivot with replaceWhileEnabled, since the durable switch stays ON) before writing the env, and fails without writing when the pivot cannot complete. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…for first-party - The native ON toggle pivots an owned gateway profile to standard (replaceWhileEnabled) before writing the first-party env, and persists the desktopMode marker, matching POST /api/claude-desktop/apply. - Gateway apply now reports a failed mode-marker save as saved:false plus a warning instead of dropping the result. - ensure/update warns when an explicit first-party marker contradicts a gateway profile still on disk rather than returning silently. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… record gateway mode from the native toggle All three mode-marker writers (CLI apply, /api/claude-desktop/apply, native toggle) now share recordClaudeDesktopMode. Switching to first-party clears desktopProfile.appliedFingerprint/appliedAt so a lost explicit marker can no longer resolve back to gateway while first-party env is on disk; the profile assignments stay for a later gateway apply. The native toggle's gateway enable branch saves desktopMode="gateway" like the apply route does. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…er from an unmarked config Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ty docs Dashboard Desktop tab gains a Connection mode picker (first-party default, gateway opt-in) that sends the chosen mode with /api/claude-desktop/apply and shows the intercept proxy state in first-party mode. Strings added to every locale. Docs describe both modes, the settings.json env the first-party apply writes, intercepted routes, the local CA trust boundary, update behavior for existing gateway installs, and Claude Code CLI compatibility/limitations. Translated guides get a summary section pointing at the canonical English text. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
gui/src/styles.css sits at its file-size cap; the picker rules move byte-for-byte into gui/src/styles/claude-desktop-mode-picker.css, imported from main.tsx. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The picker no longer pre-checks the first-party default while the status request is in flight, so a gateway install does not see the wrong radio and badge flash. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ilure A failed /status with no cached status left the picker disabled forever. It now unlocks on the first-party default once the error is shown, while the current-mode badge and switch note stay hidden because the real mode is still unknown. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ction mode Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Consolidates the three-part Claude Desktop chain into one branch against the current dev: the local intercept proxy with its CA and CONNECT handling, the first-party mode default with the gateway profile, and the connection-mode selector with its documentation. Both sides register new test files in the layout maps; the resolution is the union of the two entry sets. Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
✅ Deterministic PR hygiene checks passed. |
⏳ DRAFT
What to do
Automatic draft conversion failed (token cannot change draft status). Please convert this pull request to a draft manually. The required |
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: Repository: lidge-jun/opencodex/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (48)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 44755a4b99
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}\n`, "utf8"); | ||
| renameSync(tmp, path); |
There was a problem hiding this comment.
Preserve private permissions when rewriting settings
When an existing ~/.claude/settings.json is mode 0600, this replacement file is created with the process umask (commonly 0644) and then renamed over it. Because the entire document is serialized, arbitrary env values such as API keys can become readable by other local users after applying or removing first-party mode; preserve the original mode or create/chmod the replacement to 0600 before renaming it.
AGENTS.md reference: AGENTS.md:L420-L426
Useful? React with 👍 / 👎.
| ): DesktopFirstPartyApplyResult { | ||
| const opencodexConfigDir = options.opencodexConfigDir ?? getConfigDir(); | ||
| const target = desktopFirstPartyTarget(config, opencodexConfigDir); | ||
| if (!claudeInterceptEnabled(config)) return { ok: false, reason: "intercept_disabled", path: "" }; |
There was a problem hiding this comment.
Refuse apply when the intercept proxy failed to bind
When the derived proxy port is occupied or another intercept startup step fails, createClaudeInterceptLifecycle catches the error and leaves no live proxy, but this check only verifies that interception is enabled in configuration. A dashboard apply can consequently report success and write a global HTTPS_PROXY pointing at a dead port, breaking Claude Desktop and standalone Claude Code HTTPS traffic; the management apply path should require the live intercept state before changing settings.
AGENTS.md reference: src/AGENTS.md:L15-L20
Useful? React with 👍 / 👎.
| if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") { | ||
| const removed = (deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint, replaceWhileEnabled: true }); |
There was a problem hiding this comment.
Validate first-party settings before removing gateway mode
When switching a working gateway installation to first-party mode, the gateway profile is removed before applyDesktopFirstParty can refuse a foreign/unreadable settings file or fail to create the CA. In those expected failure cases the endpoint returns an error only after disabling the previously working mode, and desired state has already been set to ON; preflight or stage the first-party write before performing the destructive pivot, with rollback if the final write fails. The CLI and native-toggle paths use the same unsafe ordering.
AGENTS.md reference: src/AGENTS.md:L15-L20
Useful? React with 👍 / 👎.
| if (!certPem.includes("BEGIN CERTIFICATE")) return null; | ||
| return { certPem, keyPem, publicKey, privateKey }; |
There was a problem hiding this comment.
Verify the persisted certificate matches its private key
A persisted certificate is accepted solely because its text contains a PEM marker, without checking that its public key matches ca.key. The two files are replaced separately, so a crash or two concurrent processes interleaving between the key and certificate renames can leave a valid certificate from one authority beside another authority's key; subsequent starts then issue leaves with the key while clients trust the mismatched certificate, permanently failing TLS until the state directory is deleted. Parse and validate the certificate/key pair before returning it, regenerating mismatched pairs.
Useful? React with 👍 / 👎.
Summary
devas one branch: the local intercept proxy with its CA and CONNECT handling (feat(claude): local intercept proxy — CA, CONNECT proxy, TLS listener, server wiring #5301), first-party mode as the default with the gateway profile (feat(claude-desktop): first-party mode as default, gateway profile as explicit opt-in #5302), and the connection-mode selector with its documentation (feat(gui,docs): Claude Desktop connection-mode selector and first-party docs #5303).dev, so this is a straight consolidation rather than a reconstruction. Attribution is in the branch commit so it survives the squash.Verification
Checklist