Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
9.8 kB · 285 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286/** * CCR upstreamproxy — container-side wiring. * * When running inside a CCR session container with upstreamproxy configured, * this module: * 1. Reads the session token from /run/ccr/session_token * 2. Sets prctl(PR_SET_DUMPABLE, 0) to block same-UID ptrace of the heap * 3. Downloads the upstreamproxy CA cert and concatenates it with the * system bundle so curl/gh/python trust the MITM proxy * 4. Starts a local CONNECT→WebSocket relay (see relay.ts) * 5. Unlinks the token file (token stays heap-only; file is gone before * the agent loop can see it, but only after the relay is confirmed up * so a supervisor restart can retry) * 6. Exposes HTTPS_PROXY / SSL_CERT_FILE env vars for all agent subprocesses * * Every step fails open: any error logs a warning and disables the proxy. * A broken proxy setup must never break an otherwise-working session. * * Design doc: api-go/ccr/docs/plans/CCR_AUTH_DESIGN.md § "Week-1 pilot scope". */
import { mkdir, readFile, unlink, writeFile } from 'fs/promises'import { homedir } from 'os'import { join } from 'path'import { registerCleanup } from '../utils/cleanupRegistry.js'import { logForDebugging } from '../utils/debug.js'import { isEnvTruthy } from '../utils/envUtils.js'import { isENOENT } from '../utils/errors.js'import { startUpstreamProxyRelay } from './relay.js'
export const SESSION_TOKEN_PATH = '/run/ccr/session_token'const SYSTEM_CA_BUNDLE = '/etc/ssl/certs/ca-certificates.crt'
// Hosts the proxy must NOT intercept. Covers loopback, RFC1918, the IMDS// range, and the package registries + GitHub that CCR containers already// reach directly. Mirrors airlock/scripts/sandbox-shell-ccr.sh.const NO_PROXY_LIST = [ 'localhost', '127.0.0.1', '::1', '169.254.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', // Anthropic API: no upstream route will ever match, and the MITM breaks // non-Bun runtimes (Python httpx/certifi doesn't trust the forged CA). // Three forms because NO_PROXY parsing differs across runtimes: // *.anthropic.com — Bun, curl, Go (glob match) // .anthropic.com — Python urllib/httpx (suffix match, strips leading dot) // anthropic.com — apex domain fallback 'anthropic.com', '.anthropic.com', '*.anthropic.com', 'github.com', 'api.github.com', '*.github.com', '*.githubusercontent.com', 'registry.npmjs.org', 'pypi.org', 'files.pythonhosted.org', 'index.crates.io', 'proxy.golang.org',].join(',')
type UpstreamProxyState = { enabled: boolean port?: number caBundlePath?: string}
let state: UpstreamProxyState = { enabled: false }
/** * Initialize upstreamproxy. Called once from init.ts. Safe to call when the * feature is off or the token file is absent — returns {enabled: false}. * * Overridable paths are for tests; production uses the defaults. */export async function initUpstreamProxy(opts?: { tokenPath?: string systemCaPath?: string caBundlePath?: string ccrBaseUrl?: string}): Promise<UpstreamProxyState> { if (!isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) { return state } // CCR evaluates ccr_upstream_proxy_enabled server-side (where GrowthBook is // warm) and injects this env var via StartupContext.EnvironmentVariables. // Every CCR session is a fresh container with no GB cache, so a client-side // GB check here always returned the default (false). if (!isEnvTruthy(process.env.CCR_UPSTREAM_PROXY_ENABLED)) { return state }
const sessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID if (!sessionId) { logForDebugging( '[upstreamproxy] CLAUDE_CODE_REMOTE_SESSION_ID unset; proxy disabled', { level: 'warn' }, ) return state }
const tokenPath = opts?.tokenPath ?? SESSION_TOKEN_PATH const token = await readToken(tokenPath) if (!token) { logForDebugging('[upstreamproxy] no session token file; proxy disabled') return state }
setNonDumpable()
// CCR injects ANTHROPIC_BASE_URL via StartupContext (sessionExecutor.ts / // sessionHandler.ts). getOauthConfig() is wrong here: it keys off // USER_TYPE + USE_{LOCAL,STAGING}_OAUTH, none of which the container sets, // so it always returned the prod URL and the CA fetch 404'd. const baseUrl = opts?.ccrBaseUrl ?? process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com' const caBundlePath = opts?.caBundlePath ?? join(homedir(), '.ccr', 'ca-bundle.crt')
const caOk = await downloadCaBundle( baseUrl, opts?.systemCaPath ?? SYSTEM_CA_BUNDLE, caBundlePath, ) if (!caOk) return state
try { const wsUrl = baseUrl.replace(/^http/, 'ws') + '/v1/code/upstreamproxy/ws' const relay = await startUpstreamProxyRelay({ wsUrl, sessionId, token }) registerCleanup(async () => relay.stop()) state = { enabled: true, port: relay.port, caBundlePath } logForDebugging(`[upstreamproxy] enabled on 127.0.0.1:${relay.port}`) // Only unlink after the listener is up: if CA download or listen() // fails, a supervisor restart can retry with the token still on disk. await unlink(tokenPath).catch(() => { logForDebugging('[upstreamproxy] token file unlink failed', { level: 'warn', }) }) } catch (err) { logForDebugging( `[upstreamproxy] relay start failed: ${err instanceof Error ? err.message : String(err)}; proxy disabled`, { level: 'warn' }, ) }
return state}
/** * Env vars to merge into every agent subprocess. Empty when the proxy is * disabled. Called from subprocessEnv() so Bash/MCP/LSP/hooks all inherit * the same recipe. */export function getUpstreamProxyEnv(): Record<string, string> { if (!state.enabled || !state.port || !state.caBundlePath) { // Child CLI processes can't re-initialize the relay (token file was // unlinked by the parent), but the parent's relay is still running and // reachable at 127.0.0.1:<port>. If we inherited proxy vars from the // parent (HTTPS_PROXY + SSL_CERT_FILE both set), pass them through so // our subprocesses also route through the parent's relay. if (process.env.HTTPS_PROXY && process.env.SSL_CERT_FILE) { const inherited: Record<string, string> = {} for (const key of [ 'HTTPS_PROXY', 'https_proxy', 'NO_PROXY', 'no_proxy', 'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', ]) { if (process.env[key]) inherited[key] = process.env[key] } return inherited } return {} } const proxyUrl = `http://127.0.0.1:${state.port}` // HTTPS only: the relay handles CONNECT and nothing else. Plain HTTP has // no credentials to inject, so routing it through the relay would just // break the request with a 405. return { HTTPS_PROXY: proxyUrl, https_proxy: proxyUrl, NO_PROXY: NO_PROXY_LIST, no_proxy: NO_PROXY_LIST, SSL_CERT_FILE: state.caBundlePath, NODE_EXTRA_CA_CERTS: state.caBundlePath, REQUESTS_CA_BUNDLE: state.caBundlePath, CURL_CA_BUNDLE: state.caBundlePath, }}
/** Test-only: reset module state between test cases. */export function resetUpstreamProxyForTests(): void { state = { enabled: false }}
async function readToken(path: string): Promise<string | null> { try { const raw = await readFile(path, 'utf8') return raw.trim() || null } catch (err) { if (isENOENT(err)) return null logForDebugging( `[upstreamproxy] token read failed: ${err instanceof Error ? err.message : String(err)}`, { level: 'warn' }, ) return null }}
/** * prctl(PR_SET_DUMPABLE, 0) via libc FFI. Blocks same-UID ptrace of this * process, so a prompt-injected `gdb -p $PPID` can't scrape the token from * the heap. Linux-only; silently no-ops elsewhere. */function setNonDumpable(): void { if (process.platform !== 'linux' || typeof Bun === 'undefined') return try { // eslint-disable-next-line @typescript-eslint/no-require-imports const ffi = require('bun:ffi') as typeof import('bun:ffi') const lib = ffi.dlopen('libc.so.6', { prctl: { args: ['int', 'u64', 'u64', 'u64', 'u64'], returns: 'int', }, } as const) const PR_SET_DUMPABLE = 4 const rc = lib.symbols.prctl(PR_SET_DUMPABLE, 0n, 0n, 0n, 0n) if (rc !== 0) { logForDebugging( '[upstreamproxy] prctl(PR_SET_DUMPABLE,0) returned nonzero', { level: 'warn', }, ) } } catch (err) { logForDebugging( `[upstreamproxy] prctl unavailable: ${err instanceof Error ? err.message : String(err)}`, { level: 'warn' }, ) }}
async function downloadCaBundle( baseUrl: string, systemCaPath: string, outPath: string,): Promise<boolean> { try { // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins const resp = await fetch(`${baseUrl}/v1/code/upstreamproxy/ca-cert`, { // Bun has no default fetch timeout — a hung endpoint would block CLI // startup forever. 5s is generous for a small PEM. signal: AbortSignal.timeout(5000), }) if (!resp.ok) { logForDebugging( `[upstreamproxy] ca-cert fetch ${resp.status}; proxy disabled`, { level: 'warn' }, ) return false } const ccrCa = await resp.text() const systemCa = await readFile(systemCaPath, 'utf8').catch(() => '') await mkdir(join(outPath, '..'), { recursive: true }) await writeFile(outPath, systemCa + '\n' + ccrCa, 'utf8') return true } catch (err) { logForDebugging( `[upstreamproxy] ca-cert download failed: ${err instanceof Error ? err.message : String(err)}; proxy disabled`, { level: 'warn' }, ) return false }}