// ─── remote.ts — Matches original rust/crates/runtime/src/remote.rs ────────── // Upstream proxy, WebSocket bridge, subprocess environment, remote session context import * as fs from "fs"; import * as path from "path"; // ─── Types ─────────────────────────────────────────────────────────────────── export interface RemoteSessionContext { enabled: boolean; sessionId: string | undefined; baseUrl: string; } export interface UpstreamProxyBootstrap { enabled: boolean; token: string | undefined; sessionId: string | undefined; baseUrl: string; caBundlePath: string | undefined; wsUrl: () => string; shouldEnable: () => boolean; stateForPort: (port: number) => UpstreamProxyState; } export interface UpstreamProxyState { enabled: boolean; proxyUrl: string | undefined; caBundlePath: string | undefined; token: string | undefined; subprocessEnv: () => Record; } // ─── Remote session context (from env) ─────────────────────────────────────── export function remoteSessionContextFromEnv(): RemoteSessionContext { return remoteSessionContextFromEnvMap(process.env as Record); } export function remoteSessionContextFromEnvMap( env: Record ): RemoteSessionContext { const remoteFlag = env["CLAW_CODE_REMOTE"]; const enabled = remoteFlag === "true" || remoteFlag === "1" || remoteFlag === "yes"; return { enabled, sessionId: env["CLAW_CODE_REMOTE_SESSION_ID"], baseUrl: env["ANTHROPIC_BASE_URL"] || "https://api.anthropic.com", }; } // ─── Token reader ──────────────────────────────────────────────────────────── export function readToken(tokenPath: string): string | undefined { try { const content = fs.readFileSync(tokenPath, "utf-8"); const trimmed = content.trim(); return trimmed.length > 0 ? trimmed : undefined; } catch { return undefined; } } // ─── Upstream proxy URL ────────────────────────────────────────────────────── export function upstreamProxyWsUrl(baseUrl: string): string { // Convert http(s) to ws(s) let wsUrl = baseUrl.replace(/\/$/, ""); if (wsUrl.startsWith("https://")) { wsUrl = "wss://" + wsUrl.slice(8); } else if (wsUrl.startsWith("http://")) { wsUrl = "ws://" + wsUrl.slice(7); } return `${wsUrl}/v1/code/upstreamproxy/ws`; } // ─── No-proxy list ─────────────────────────────────────────────────────────── export function noProxyList(): string { return [ "localhost", "127.0.0.1", "::1", "anthropic.com", "api.anthropic.com", "github.com", "api.github.com", "*.github.com", "*.githubusercontent.com", ].join(","); } // ─── Inherited upstream proxy env (matches original remote.rs EXACTLY) ────── // All 8 env keys from original Rust UPSTREAM_PROXY_ENV_KEYS const UPSTREAM_PROXY_ENV_KEYS = [ "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy", "SSL_CERT_FILE", "NODE_EXTRA_CA_CERTS", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", ]; export function inheritedUpstreamProxyEnv( env: Record ): Record { // Both HTTPS_PROXY and SSL_CERT_FILE must be present (matches original guard) if (!env["HTTPS_PROXY"] || !env["SSL_CERT_FILE"]) { return {}; } const result: Record = {}; for (const key of UPSTREAM_PROXY_ENV_KEYS) { if (env[key]) { result[key] = env[key]; } } return result; } // ─── Upstream proxy bootstrap ──────────────────────────────────────────────── export function upstreamProxyBootstrapFromEnv(): UpstreamProxyBootstrap { return upstreamProxyBootstrapFromEnvMap( process.env as Record ); } export function upstreamProxyBootstrapFromEnvMap( env: Record ): UpstreamProxyBootstrap { const remoteFlag = env["CLAW_CODE_REMOTE"]; const proxyEnabled = env["CCR_UPSTREAM_PROXY_ENABLED"]; const sessionId = env["CLAW_CODE_REMOTE_SESSION_ID"]; const baseUrl = env["ANTHROPIC_BASE_URL"] || "https://api.anthropic.com"; const tokenPath = env["CCR_SESSION_TOKEN_PATH"]; const caBundlePath = env["CCR_CA_BUNDLE_PATH"]; const isRemote = remoteFlag === "true" || remoteFlag === "1" || remoteFlag === "yes"; const isProxyEnabled = proxyEnabled === "true" || proxyEnabled === "1" || proxyEnabled === "yes"; // Read token from file const token = tokenPath ? readToken(tokenPath) : undefined; const bootstrap: UpstreamProxyBootstrap = { enabled: isRemote && isProxyEnabled, token, sessionId, baseUrl, caBundlePath, wsUrl(): string { return upstreamProxyWsUrl(baseUrl); }, shouldEnable(): boolean { return isRemote && isProxyEnabled && !!token && !!sessionId; }, stateForPort(port: number): UpstreamProxyState { if (!bootstrap.shouldEnable()) { return { enabled: false, proxyUrl: undefined, caBundlePath: undefined, token: undefined, subprocessEnv: () => ({}), }; } const proxyUrl = `http://127.0.0.1:${port}`; const capturedToken = token; const capturedCaBundle = caBundlePath; return { enabled: true, proxyUrl, caBundlePath: capturedCaBundle, token: capturedToken, subprocessEnv(): Record { const noProxy = noProxyList(); const result: Record = { HTTPS_PROXY: proxyUrl, https_proxy: proxyUrl, HTTP_PROXY: proxyUrl, http_proxy: proxyUrl, NO_PROXY: noProxy, no_proxy: noProxy, }; if (capturedCaBundle) { result["SSL_CERT_FILE"] = capturedCaBundle; result["NODE_EXTRA_CA_CERTS"] = capturedCaBundle; result["REQUESTS_CA_BUNDLE"] = capturedCaBundle; result["CURL_CA_BUNDLE"] = capturedCaBundle; } return result; }, }; }, }; return bootstrap; } // ─── Remote session management ─────────────────────────────────────────────── export interface RemoteSession { id: string; baseUrl: string; token: string | undefined; proxyState: UpstreamProxyState; isActive: boolean; } export function createRemoteSession( context: RemoteSessionContext, proxyState: UpstreamProxyState ): RemoteSession { return { id: context.sessionId || `remote-${Date.now()}`, baseUrl: context.baseUrl, token: proxyState.token, proxyState, isActive: context.enabled, }; } // ─── Heartbeat config ──────────────────────────────────────────────────────── export interface HeartbeatConfig { intervalMs: number; timeoutMs: number; maxRetries: number; } export function defaultHeartbeatConfig(): HeartbeatConfig { return { intervalMs: 30_000, timeoutMs: 10_000, maxRetries: 3, }; }