Spaces:
Sleeping
Sleeping
Claw Web
Full parity with original Rust: session validation, sandbox detection, remote proxy, hooks payload, LSP shutdown
49cbb33 | // βββ 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<string, string>; | |
| } | |
| // βββ Remote session context (from env) βββββββββββββββββββββββββββββββββββββββ | |
| export function remoteSessionContextFromEnv(): RemoteSessionContext { | |
| return remoteSessionContextFromEnvMap(process.env as Record<string, string>); | |
| } | |
| export function remoteSessionContextFromEnvMap( | |
| env: Record<string, string> | |
| ): 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<string, string> | |
| ): Record<string, string> { | |
| // 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<string, string> = {}; | |
| 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<string, string> | |
| ); | |
| } | |
| export function upstreamProxyBootstrapFromEnvMap( | |
| env: Record<string, string> | |
| ): 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<string, string> { | |
| const noProxy = noProxyList(); | |
| const result: Record<string, string> = { | |
| 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, | |
| }; | |
| } | |