claw-web-v2 / server /runtime /remote.ts
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,
};
}