Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/claude/intercept/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export interface StartClaudeInterceptOptions<T> {
loadPickerRoutes?: () => Promise<PickerRouteInput>;
/** Test seam: builds the picker runtime. */
createPicker?: (options: CreatePickerRuntimeOptions) => PickerRuntime;
/** Test seam: bind real CONNECT handlers on kernel-assigned ports without probe-and-release races. */
startProxy?: typeof startConnectProxy;
/** Test seams: the macOS `security` runner and platform for the picker runtime and controller. */
pickerSecurity?: SecurityRunner;
pickerPlatform?: NodeJS.Platform;
Expand All @@ -132,6 +134,7 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
if (options.requestedPort === 0 && !explicitPort) return null;
const configDir = options.configDir ?? getConfigDir();
const ca = await ensureLocalInterceptCaForStartup(configDir);
const startProxy = options.startProxy ?? startConnectProxy;
const authToken = ensureClaudeInterceptProxyToken(configDir);
const leaf = issueLocalInterceptLeaf(ca, CLAUDE_INTERCEPT_HOSTS);
// Refresh an env we already own (e.g. a pre-auth proxy URL left by an upgrade) before the
Expand All @@ -156,7 +159,7 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
});
let proxy: ConnectProxyHandle;
try {
proxy = await startConnectProxy(claudeInterceptProxyPort(options.config, options.publicPort), {
proxy = await startProxy(claudeInterceptProxyPort(options.config, options.publicPort), {
interceptPort: listener.port!,
// A real apply may recreate a missing token while this listener remains live.
// Read current validated authority per CONNECT; absent/invalid means deny, not mint.
Expand Down Expand Up @@ -187,7 +190,7 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
const runtime = picker;
const interceptPort = listener.port!;
try {
pickerProxy = await startConnectProxy(claudePickerProxyPort(options.config, options.publicPort), {
pickerProxy = await startProxy(claudePickerProxyPort(options.config, options.publicPort), {
interceptPort,
// No authToken: Desktop's egressProxyUrl cannot present proxy credentials, so this
// listener stays an unauthenticated loopback relay until the profile format can carry
Expand Down
4 changes: 4 additions & 0 deletions structure/clients/claude-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ Code, trusting only the intercept CA) gets the `api.anthropic.com` intercept and
blind, never the picker; a tunnel with Chromium's `Mozilla/` User-Agent (the app, trusting only the
login keychain) is asked of the picker runtime (`src/claude/intercept/picker-runtime.ts`), which
blind-tunnels every target except `claude.ai:443`.
Production always uses the configured adjacent ports. Lifecycle tests inject only the CONNECT
factory and bind the real handlers on kernel-assigned ports; this preserves request handling while
avoiding the false reservation created by probing and closing a port pair before the ephemeral TLS
listener starts. The injected factory does not change production port selection.
The User-Agent is a routing hint, not a trust boundary: a client that fakes it reaches only what
any local process already reaches (the `api.anthropic.com` intercept is on the Claude Code proxy
too; the `claude.ai` relay verifies upstream and adds no credential) and breaks only its own TLS,
Expand Down
41 changes: 20 additions & 21 deletions tests/claude-integration/claude-desktop-picker-routes.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { applyDesktopPickerProfile, inspectDesktopPickerProfile } from "../../src/claude/desktop-picker-profile";
import { startConnectProxy } from "../../src/claude/intercept/connect-proxy";
import { pickerCaCertPath, pickerCaFingerprints } from "../../src/claude/intercept/picker-ca";
import type { PickerListenerOptions } from "../../src/claude/intercept/picker-listener";
import { createPickerRuntime } from "../../src/claude/intercept/picker-runtime";
Expand All @@ -21,6 +21,7 @@ let handle: ClaudeInterceptHandle | null = null;
const previous: Record<string, string | undefined> = {};
const ENV_KEYS = ["OPENCODEX_HOME", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
const LISTENER_PORT = 45_679;
const REQUESTED_PROXY_PORT = 45_600;
const INTERCEPT = { kind: "intercept", port: LISTENER_PORT };
const BLIND = { kind: "blind" };

Expand Down Expand Up @@ -49,43 +50,41 @@ const security: SecurityRunner = async args => {
}
};

async function canBind(port: number): Promise<boolean> {
return new Promise(resolve => {
const server = createServer();
server.once("error", () => resolve(false));
server.listen({ port, host: "127.0.0.1", exclusive: true }, () => server.close(() => resolve(true)));
});
}

async function freePortPair(): Promise<number> {
for (let attempt = 0; attempt < 50; attempt += 1) {
const port = 20_000 + Math.floor(Math.random() * 30_000);
if (await canBind(port) && await canBind(port + 1)) return port;
}
throw new Error("no free port pair");
}

/** Start the intercept pair with picker mode wired, as the server lifecycle does. */
async function startPicker(saved: OcxConfig, onDispatch?: (req: Request) => Response): Promise<number> {
writeFileSync(join(root, "config.json"), JSON.stringify(saved));
const port = await freePortPair();
const requestedProxyPorts: number[] = [];
handle = await startClaudeIntercept({
config: config({ claudeCode: { intercept: { port } } }),
config: config({ claudeCode: { intercept: { port: REQUESTED_PROXY_PORT } } }),
publicPort: 10100,
configDir: root,
dispatch: async req => onDispatch?.(req) ?? new Response("unused"),
...(onDispatch ? { desiredClients: () => ({ desktop: true, cli: false }) } : {}),
loadPickerRoutes: async () => ({ nativeSlugs: [], routedModels: [{ provider: "xai", id: "grok-4.7", contextWindow: 256_000 }] }),
pickerSecurity: security,
pickerPlatform: "darwin",
// A probe that closes before startup does not reserve anything: the lifecycle's own
// ephemeral TLS listener or another process can take the observed pair. Bind the real proxy
// handlers directly on port 0 so the kernel owns both allocations until teardown.
startProxy: async (requestedPort, proxyOptions) => {
requestedProxyPorts.push(requestedPort);
return startConnectProxy(0, proxyOptions);
},
createPicker: options => createPickerRuntime({
...options,
startListener: (async (_: PickerListenerOptions) => ({ port: LISTENER_PORT, close: async () => {} })) as never,
trustTtlMs: 0,
refreshIntervalMs: 3_600_000,
}),
});
return port;
if (!handle || handle.pickerProxyPort === null || !getClaudePickerRuntime()) {
throw new Error("picker fixture did not start every runtime component");
}
expect(requestedProxyPorts).toEqual([REQUESTED_PROXY_PORT, REQUESTED_PROXY_PORT + 1]);
const boundPorts = [handle.listener.port, handle.proxyPort, handle.pickerProxyPort];
expect(boundPorts.every(port => typeof port === "number" && Number.isInteger(port) && port > 0)).toBe(true);
expect(new Set(boundPorts).size).toBe(boundPorts.length);
return handle.pickerProxyPort;
}

async function dispatch(path: string, init: RequestInit = {}, deps: Parameters<typeof handleManagementAPI>[3] = {}) {
Expand Down Expand Up @@ -146,7 +145,7 @@ describe("first-party turns picker mode on by default", () => {
expect(applied.status).toBe(200);
expect(applied.body.picker).toMatchObject({ effective: true, reason: "restart_required", trust: "trusted", profile: "applied" });
expect(decision()).toEqual(INTERCEPT);
expect(inspectDesktopPickerProfile()).toMatchObject({ kind: "applied", proxyUrl: `http://127.0.0.1:${port + 1}` });
expect(inspectDesktopPickerProfile()).toMatchObject({ kind: "applied", proxyUrl: `http://127.0.0.1:${port}` });
expect(keychain.trusted).toBe(true);
expect(persisted().claudeCode?.intercept?.picker).toBeUndefined();

Expand Down
Loading