openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
997 lines (996 loc) • 32.6 kB
JavaScript
import "./src-vebZIeLe.js";
import { t as expectDefined } from "./expect-CyE8FADM.js";
import { t as resolveSubprocessExitCode } from "./subprocess-exit-code-AepaGf2z.js";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { n as isTruthyEnvValue } from "./env-M3R40TOb.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { n as isHttpUrl } from "./url-protocol-OU3K-ySz.js";
import "./config-Cs0XXL3x.js";
import { n as isRich, r as theme, t as colorize } from "./theme-vjDs9tao.js";
import { a as resolveManagedProxyCaFileForUrl, r as loadManagedProxyTlsOptions } from "./managed-proxy-undici-B8Nor2ch.js";
import { r as createHttp1ProxyAgent } from "./undici-runtime-BMM5FOrP.js";
import { n as fetchWithRuntimeDispatcher } from "./runtime-fetch-D7UijuA3.js";
import { r as resolveDebugProxySettings, t as applyDebugProxyEnv } from "./env-D-95VwzY.js";
import { c as closeDebugProxyCaptureStore, d as redactedCaptureHeaders, i as initializeDebugProxyCapture, l as getDebugProxyCaptureStore, r as finalizeDebugProxyCapture } from "./runtime-D3go1Kou.js";
import { o as probeApnsHttp2ReachabilityViaProxy } from "./push-apns-http2-DGTDI3Ss.js";
import { t as ensureDebugProxyCa } from "./ca-ZY7QWghJ.js";
import { t as buildDebugProxyCoverageReport } from "./coverage-DrWRuk1m.js";
import process$1 from "node:process";
import { URL as URL$1 } from "node:url";
import { spawn } from "node:child_process";
import net from "node:net";
import { StringDecoder } from "node:string_decoder";
import { randomUUID } from "node:crypto";
import { createServer as createServer$1, request } from "node:http";
import { request as request$1 } from "node:https";
//#region src/infra/net/proxy/proxy-validation.ts
const DEFAULT_PROXY_VALIDATION_ALLOWED_URLS = ["https://example.com/"];
const DEFAULT_PROXY_VALIDATION_APNS_AUTHORITY = "https://api.sandbox.push.apple.com";
const DEFAULT_PROXY_VALIDATION_TIMEOUT_MS = 5e3;
const DENIED_CANARY_HEADER = "x-openclaw-proxy-validation-canary";
const APNS_REACHABILITY_REASON = "InvalidProviderToken";
function normalizeProxyUrl(value) {
const trimmed = value?.trim();
return trimmed ? trimmed : void 0;
}
function validateProxyUrl(value) {
if (!value) return ["proxy validation requires proxy.proxyUrl, --proxy-url, or OPENCLAW_PROXY_URL"];
if (!isHttpUrl(value)) return ["proxyUrl must use http:// or https://"];
return [];
}
/** Resolves validation config precedence: explicit override, config, then env. */
function resolveProxyValidationConfig(options) {
const overrideUrl = normalizeProxyUrl(options.proxyUrlOverride);
if (overrideUrl) {
const proxyCaFile = resolveManagedProxyCaFileForUrl({
proxyUrl: overrideUrl,
caFileOverride: options.proxyCaFileOverride
});
return {
enabled: true,
proxyUrl: overrideUrl,
...proxyCaFile ? { proxyCaFile } : {},
source: "override",
errors: validateProxyUrl(overrideUrl)
};
}
const configUrl = normalizeProxyUrl(options.config?.proxyUrl);
if (configUrl) {
const proxyCaFile = resolveManagedProxyCaFileForUrl({
proxyUrl: configUrl,
config: options.config,
caFileOverride: options.proxyCaFileOverride
});
return {
enabled: options.config?.enabled !== false,
proxyUrl: configUrl,
...proxyCaFile ? { proxyCaFile } : {},
source: "config",
errors: options.config?.enabled === false ? ["proxy validation is disabled by proxy.enabled=false"] : validateProxyUrl(configUrl)
};
}
const envUrl = normalizeProxyUrl(options.env?.OPENCLAW_PROXY_URL);
if (envUrl) {
const proxyCaFile = resolveManagedProxyCaFileForUrl({
proxyUrl: envUrl,
config: options.config,
caFileOverride: options.proxyCaFileOverride
});
return {
enabled: options.config?.enabled !== false,
proxyUrl: envUrl,
...proxyCaFile ? { proxyCaFile } : {},
source: "env",
errors: options.config?.enabled === false ? ["proxy validation is disabled by proxy.enabled=false"] : validateProxyUrl(envUrl)
};
}
if (options.config?.enabled === true) return {
enabled: true,
source: "missing",
errors: validateProxyUrl(void 0)
};
return {
enabled: false,
source: "disabled",
errors: ["proxy validation requires proxy.proxyUrl, OPENCLAW_PROXY_URL, or --proxy-url"]
};
}
async function defaultProxyValidationFetchCheck({ proxyUrl, proxyTls, targetUrl, timeoutMs }) {
const dispatcher = createHttp1ProxyAgent({
uri: proxyUrl,
...proxyTls ? { proxyTls } : {}
}, timeoutMs);
try {
const response = await fetchWithRuntimeDispatcher(targetUrl, {
dispatcher,
redirect: "manual"
});
response.body?.cancel().catch(() => void 0);
return {
ok: response.ok,
status: response.status,
deniedCanaryToken: response.headers.get(DENIED_CANARY_HEADER) ?? void 0
};
} finally {
await dispatcher.close();
}
}
async function defaultProxyValidationApnsCheck({ proxyUrl, proxyTls, authority, timeoutMs }) {
const result = await probeApnsHttp2ReachabilityViaProxy({
proxyUrl,
...proxyTls ? { proxyTls } : {},
authority,
timeoutMs
});
return {
status: result.status,
apnsId: result.responseHeaders?.["apns-id"],
apnsReason: parseApnsErrorReason(result.body)
};
}
function parseApnsErrorReason(body) {
try {
const parsed = JSON.parse(body);
if (!parsed || typeof parsed !== "object") return;
const reason = parsed.reason;
return typeof reason === "string" && reason.trim() ? reason : void 0;
} catch {
return;
}
}
function hasApnsReachabilityProof(result) {
if (result.apnsId) return true;
return result.status === 403 && result.apnsReason === APNS_REACHABILITY_REASON;
}
function normalizeTimeoutMs(value) {
if (value === void 0 || !Number.isFinite(value) || value <= 0) return DEFAULT_PROXY_VALIDATION_TIMEOUT_MS;
return Math.floor(value);
}
function closeServer(server) {
return new Promise((resolve, reject) => {
server.close((err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
async function createLoopbackDeniedCanary() {
const token = randomUUID();
const server = createServer$1((_request, response) => {
response.writeHead(204, {
[DENIED_CANARY_HEADER]: token,
"cache-control": "no-store"
});
response.end();
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.off("error", reject);
resolve();
});
});
const address = server.address();
if (typeof address === "string" || address === null) {
await closeServer(server);
throw new Error("Unable to start loopback proxy validation canary");
}
return {
target: {
url: `http://127.0.0.1:${address.port}/`,
expectedCanaryToken: token,
transportErrorMeansBlocked: true
},
close: () => closeServer(server)
};
}
async function resolveDeniedTargets(deniedUrls) {
if (deniedUrls !== void 0) return {
targets: deniedUrls.map((url) => ({
url,
transportErrorMeansBlocked: false
})),
close: async () => void 0
};
const canary = await createLoopbackDeniedCanary();
return {
targets: [canary.target],
close: canary.close
};
}
async function runAllowedCheck(params) {
if (!isHttpUrl(params.url)) return {
kind: "allowed",
url: params.url,
ok: false,
error: "Invalid allowed destination URL"
};
try {
const result = await params.fetchCheck({
proxyUrl: params.proxyUrl,
...params.proxyTls ? { proxyTls: params.proxyTls } : {},
targetUrl: params.url,
timeoutMs: params.timeoutMs
});
if (!result.ok) return {
kind: "allowed",
url: params.url,
ok: false,
status: result.status,
error: `Allowed destination returned HTTP ${result.status}`
};
return {
kind: "allowed",
url: params.url,
ok: true,
status: result.status
};
} catch (err) {
return {
kind: "allowed",
url: params.url,
ok: false,
error: err instanceof Error ? err.message : String(err)
};
}
}
async function runDeniedCheck(params) {
if (!isHttpUrl(params.target.url)) return {
kind: "denied",
url: params.target.url,
ok: false,
error: "Invalid denied destination URL"
};
try {
const result = await params.fetchCheck({
proxyUrl: params.proxyUrl,
...params.proxyTls ? { proxyTls: params.proxyTls } : {},
targetUrl: params.target.url,
timeoutMs: params.timeoutMs
});
if (params.target.expectedCanaryToken !== void 0 && result.deniedCanaryToken !== params.target.expectedCanaryToken) {
if (result.ok) return {
kind: "denied",
url: params.target.url,
ok: false,
status: result.status,
error: `Denied loopback canary returned HTTP ${result.status} without the validation token`
};
return {
kind: "denied",
url: params.target.url,
ok: true,
status: result.status
};
}
return {
kind: "denied",
url: params.target.url,
ok: false,
status: result.status,
error: params.target.expectedCanaryToken === void 0 ? `Denied destination returned HTTP ${result.status}; expected the proxy to block the connection` : `Denied loopback canary was reachable through the proxy with HTTP ${result.status}`
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (params.target.transportErrorMeansBlocked) return {
kind: "denied",
url: params.target.url,
ok: true,
error: message
};
return {
kind: "denied",
url: params.target.url,
ok: false,
error: `Denied destination failed without a verifiable proxy-deny signal: ${message}`
};
}
}
async function runApnsReachabilityCheck(params) {
try {
const result = await params.apnsCheck({
proxyUrl: params.proxyUrl,
...params.proxyTls ? { proxyTls: params.proxyTls } : {},
authority: params.authority,
timeoutMs: params.timeoutMs
});
if (!hasApnsReachabilityProof(result)) return {
kind: "apns",
url: params.authority,
ok: false,
error: "APNs reachability check failed: response did not include an apns-id header or APNs InvalidProviderToken body. The proxy may be intercepting the connection instead of tunneling it."
};
return {
kind: "apns",
url: params.authority,
ok: true,
status: result.status
};
} catch (err) {
return {
kind: "apns",
url: params.authority,
ok: false,
error: err instanceof Error ? err.message : String(err)
};
}
}
/** Runs allowed, denied, and optional APNs proxy validation probes. */
async function runProxyValidation(options) {
const config = resolveProxyValidationConfig(options);
if (config.errors.length > 0) return {
ok: false,
config,
checks: []
};
if (!config.proxyUrl) {
if (!config.enabled && config.source === "disabled") return {
ok: false,
config: {
...config,
errors: ["Proxy validation is disabled. Configure proxy.proxyUrl, OPENCLAW_PROXY_URL, or pass --proxy-url to run validation."]
},
checks: []
};
return {
ok: false,
config,
checks: []
};
}
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
let proxyTls;
try {
proxyTls = await loadManagedProxyTlsOptions(config.proxyCaFile);
} catch (err) {
return {
ok: false,
config: {
...config,
errors: [...config.errors, err instanceof Error ? err.message : String(err)]
},
checks: []
};
}
const fetchCheck = options.fetchCheck ?? defaultProxyValidationFetchCheck;
const apnsCheck = options.apnsCheck ?? defaultProxyValidationApnsCheck;
const apnsAuthority = options.apnsAuthority ?? DEFAULT_PROXY_VALIDATION_APNS_AUTHORITY;
const allowedUrls = options.allowedUrls ?? DEFAULT_PROXY_VALIDATION_ALLOWED_URLS;
const deniedTargets = await resolveDeniedTargets(options.deniedUrls);
const checks = [];
try {
for (const url of allowedUrls) checks.push(await runAllowedCheck({
url,
proxyUrl: config.proxyUrl,
proxyTls,
timeoutMs,
fetchCheck
}));
for (const target of deniedTargets.targets) checks.push(await runDeniedCheck({
target,
proxyUrl: config.proxyUrl,
proxyTls,
timeoutMs,
fetchCheck
}));
if (options.apnsReachability === true) checks.push(await runApnsReachabilityCheck({
authority: apnsAuthority,
proxyUrl: config.proxyUrl,
proxyTls,
timeoutMs,
apnsCheck
}));
} finally {
await deniedTargets.close();
}
return {
ok: checks.every((check) => check.ok),
config,
checks
};
}
//#endregion
//#region src/proxy-capture/proxy-server.ts
const DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE = "OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY";
const CAPTURE_BODY_PREVIEW_BYTES = 8192;
const BAD_GATEWAY_BODY = "Bad Gateway\n";
const DEBUG_PROXY_CONNECT_TIMEOUT_MS = 3e4;
const GATEWAY_TIMEOUT_BODY = "Gateway Timeout\n";
function isManagedProxyActive(env = process.env) {
return isTruthyEnvValue(env["OPENCLAW_PROXY_ACTIVE"]);
}
function allowsDirectConnectWithManagedProxy(env = process.env) {
return isTruthyEnvValue(env[DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE]);
}
function assertDebugProxyDirectUpstreamAllowed(env = process.env) {
if (!isManagedProxyActive(env) || allowsDirectConnectWithManagedProxy(env)) return;
throw new Error(`Debug proxy direct upstream forwarding is disabled while managed proxy mode is active. Set ${DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE}=1 only for approved local diagnostics.`);
}
function createProxyCaptureRecorder(params) {
return (event) => {
params.store.recordEvent({
sessionId: params.settings.sessionId,
ts: Date.now(),
sourceScope: "openclaw",
sourceProcess: params.settings.sourceProcess,
...event
});
};
}
function parseConnectTarget(rawTarget) {
const trimmed = rawTarget?.trim() ?? "";
if (!trimmed) return {
hostname: "127.0.0.1",
port: 443
};
const bracketedMatch = trimmed.match(/^\[([^\]]+)\](?::(\d+))?$/);
if (bracketedMatch) {
const hostname = bracketedMatch[1]?.trim() || "127.0.0.1";
const port = Number(bracketedMatch[2] || 443);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid CONNECT target port");
return {
hostname,
port
};
}
const lastColon = trimmed.lastIndexOf(":");
if (lastColon <= 0 || lastColon === trimmed.length - 1) return {
hostname: trimmed,
port: 443
};
const hostname = trimmed.slice(0, lastColon).trim() || "127.0.0.1";
const portText = trimmed.slice(lastColon + 1).trim();
if (!/^\d+$/.test(portText)) throw new Error("Invalid CONNECT target port");
const port = Number(portText);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid CONNECT target port");
return {
hostname,
port
};
}
function normalizeTargetUrl(req) {
if (req.url?.startsWith("http://") || req.url?.startsWith("https://")) return new URL$1(req.url);
const host = req.headers.host ?? "127.0.0.1";
return new URL$1(`http://${host}${req.url ?? "/"}`);
}
function createBodyPreviewCapture() {
return {
chunks: [],
previewBytes: 0,
totalBytes: 0,
truncated: false
};
}
function appendBodyPreviewCapture(capture, chunk) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
capture.totalBytes += buffer.byteLength;
const remaining = CAPTURE_BODY_PREVIEW_BYTES - capture.previewBytes;
if (remaining <= 0) {
capture.truncated = capture.truncated || buffer.byteLength > 0;
return;
}
const slice = buffer.byteLength > remaining ? buffer.subarray(0, remaining) : buffer;
capture.chunks.push(slice);
capture.previewBytes += slice.byteLength;
if (slice.byteLength < buffer.byteLength) capture.truncated = true;
}
function finishBodyPreviewCapture(capture) {
return {
dataText: new StringDecoder("utf8").write(Buffer.concat(capture.chunks, capture.previewBytes)),
metaJson: capture.truncated ? JSON.stringify({
bodyBytes: capture.totalBytes,
capturePreviewBytes: CAPTURE_BODY_PREVIEW_BYTES,
captureTruncated: true
}) : void 0
};
}
function finishProxyResponseAfterUpstreamError(res) {
if (res.destroyed || res.writableEnded) return;
if (res.headersSent) {
res.destroy();
return;
}
res.writeHead(502, {
Connection: "close",
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": Buffer.byteLength(BAD_GATEWAY_BODY)
});
res.end(BAD_GATEWAY_BODY);
}
async function startDebugProxyServer(params) {
await ensureDebugProxyCa(params.settings.certDir);
const recordProxyEvent = createProxyCaptureRecorder({
store: getDebugProxyCaptureStore(),
settings: params.settings
});
const host = params.host?.trim() || "127.0.0.1";
const server = createServer$1((req, res) => {
(async () => {
const flowId = randomUUID();
let target;
try {
target = normalizeTargetUrl(req);
} catch (error) {
const message = "Invalid proxy target URL";
recordProxyEvent({
protocol: "http",
direction: "local",
kind: "error",
flowId,
method: req.method,
host: req.headers.host,
path: req.url ?? "",
errorText: error instanceof Error ? error.message : String(error)
});
const responseBody = `${message}\n`;
res.writeHead(400, {
Connection: "close",
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": Buffer.byteLength(responseBody)
});
res.end(responseBody);
return;
}
const targetProtocol = target.protocol === "https:" ? "https" : "http";
const targetPath = `${target.pathname}${target.search}`;
const recordTargetEvent = (event) => recordProxyEvent({
protocol: targetProtocol,
flowId,
method: req.method,
host: target.host,
path: targetPath,
...event
});
try {
assertDebugProxyDirectUpstreamAllowed();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
recordTargetEvent({
direction: "local",
kind: "error",
errorText: message
});
const responseBody = `${message}\n`;
res.writeHead(403, {
Connection: "close",
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": Buffer.byteLength(responseBody)
});
res.end(responseBody);
return;
}
const requestCapture = createBodyPreviewCapture();
const upstream = (target.protocol === "https:" ? request$1 : request)(target, {
method: req.method,
headers: req.headers
}, (upstreamRes) => {
const responseCapture = createBodyPreviewCapture();
let upstreamFinished = false;
let upstreamFailed = false;
let responseFinished = false;
let downstreamFailed = false;
let pausedForDownstream = false;
const resumeUpstreamResponse = () => {
pausedForDownstream = false;
if (!res.destroyed && !res.writableEnded && !upstreamRes.destroyed) upstreamRes.resume();
};
const handleDownstreamFailure = (error) => {
if (downstreamFailed || responseFinished || upstreamFailed) return;
downstreamFailed = true;
res.off("drain", resumeUpstreamResponse);
recordTargetEvent({
direction: "local",
kind: "error",
errorText: error?.message ?? "Downstream response closed before completion"
});
upstream.destroy();
upstreamRes.destroy();
};
res.on("finish", () => {
if (!upstreamFinished || downstreamFailed || upstreamFailed) return;
responseFinished = true;
res.off("drain", resumeUpstreamResponse);
recordTargetEvent({
direction: "inbound",
kind: "response",
status: upstreamRes.statusCode ?? void 0,
headersJson: JSON.stringify(redactedCaptureHeaders(upstreamRes.headers)),
...finishBodyPreviewCapture(responseCapture)
});
});
res.on("error", handleDownstreamFailure);
res.on("close", () => handleDownstreamFailure());
upstreamRes.on("data", (chunk) => {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
appendBodyPreviewCapture(responseCapture, buffer);
if (res.destroyed || res.writableEnded) {
handleDownstreamFailure();
return;
}
try {
if (!res.write(buffer) && !pausedForDownstream) {
pausedForDownstream = true;
upstreamRes.pause();
res.once("drain", resumeUpstreamResponse);
}
} catch (error) {
handleDownstreamFailure(error instanceof Error ? error : new Error(String(error)));
}
});
upstreamRes.on("end", () => {
upstreamFinished = true;
res.off("drain", resumeUpstreamResponse);
if (!res.destroyed && !res.writableEnded) res.end();
else if (!res.writableFinished) handleDownstreamFailure();
});
upstreamRes.on("error", (error) => {
if (downstreamFailed || responseFinished || upstreamFailed) return;
upstreamFailed = true;
res.off("drain", resumeUpstreamResponse);
recordTargetEvent({
direction: "inbound",
kind: "error",
errorText: error.message
});
finishProxyResponseAfterUpstreamError(res);
});
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
});
req.on("data", (chunk) => {
appendBodyPreviewCapture(requestCapture, chunk);
});
req.on("end", () => {
recordTargetEvent({
direction: "outbound",
kind: "request",
headersJson: JSON.stringify(redactedCaptureHeaders(req.headers)),
...finishBodyPreviewCapture(requestCapture)
});
});
req.on("error", (error) => {
recordTargetEvent({
direction: "local",
kind: "error",
errorText: error.message
});
upstream.destroy(error);
});
upstream.on("error", (error) => {
recordTargetEvent({
direction: "local",
kind: "error",
errorText: error.message
});
finishProxyResponseAfterUpstreamError(res);
});
req.pipe(upstream);
})();
});
server.on("connect", (req, clientSocket, head) => {
const flowId = randomUUID();
let hostname = "127.0.0.1";
let port;
try {
const parsed = parseConnectTarget(req.url);
hostname = parsed.hostname;
port = parsed.port;
} catch (error) {
recordProxyEvent({
protocol: "connect",
direction: "local",
kind: "error",
flowId,
host: hostname,
path: req.url ?? "",
errorText: error instanceof Error ? error.message : String(error)
});
clientSocket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
return;
}
recordProxyEvent({
protocol: "connect",
direction: "local",
kind: "connect",
flowId,
host: hostname,
path: req.url ?? "",
headersJson: JSON.stringify(redactedCaptureHeaders(req.headers))
});
try {
assertDebugProxyDirectUpstreamAllowed();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
recordProxyEvent({
protocol: "connect",
direction: "local",
kind: "error",
flowId,
host: hostname,
path: req.url ?? "",
errorText: message
});
const responseBody = `${message}\n`;
clientSocket.end(`HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: ${Buffer.byteLength(responseBody)}\r\n\r\n${responseBody}`);
return;
}
const upstreamSocket = net.connect(port, hostname, () => {
upstreamSocket.setTimeout(0);
upstreamSocket.off("timeout", onUpstreamConnectTimeout);
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
if (head.length > 0) upstreamSocket.write(head);
clientSocket.pipe(upstreamSocket);
upstreamSocket.pipe(clientSocket);
});
function onUpstreamConnectTimeout() {
const message = `CONNECT upstream opening timed out after ${DEBUG_PROXY_CONNECT_TIMEOUT_MS}ms of inactivity`;
recordProxyEvent({
protocol: "connect",
direction: "local",
kind: "error",
flowId,
host: hostname,
path: req.url ?? "",
errorText: message
});
upstreamSocket.destroy();
clientSocket.end(`HTTP/1.1 504 Gateway Timeout\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: ${Buffer.byteLength(GATEWAY_TIMEOUT_BODY)}\r\n\r\n${GATEWAY_TIMEOUT_BODY}`, () => clientSocket.destroy());
}
upstreamSocket.setTimeout(DEBUG_PROXY_CONNECT_TIMEOUT_MS, onUpstreamConnectTimeout);
clientSocket.on("error", (error) => {
recordProxyEvent({
protocol: "connect",
direction: "local",
kind: "error",
flowId,
host: hostname,
path: req.url ?? "",
errorText: error.message
});
upstreamSocket.destroy();
});
upstreamSocket.on("error", (error) => {
recordProxyEvent({
protocol: "connect",
direction: "local",
kind: "error",
flowId,
host: hostname,
path: req.url ?? "",
errorText: error.message
});
clientSocket.destroy();
});
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(params.port ?? 0, host, () => {
server.off("error", reject);
resolve();
});
});
const address = server.address();
if (!address || typeof address === "string") throw new Error("Failed to resolve debug proxy server address");
return {
proxyUrl: `http://${host}:${address.port}`,
stop: async () => await new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
})
};
}
//#endregion
//#region src/cli/proxy-cli.runtime.ts
async function runDebugProxyStartCommand(opts) {
const settings = resolveDebugProxySettings();
const store = getDebugProxyCaptureStore();
store.upsertSession({
id: settings.sessionId,
startedAt: Date.now(),
mode: "proxy-start",
sourceScope: "openclaw",
sourceProcess: "openclaw",
proxyUrl: settings.proxyUrl
});
initializeDebugProxyCapture("proxy-start", settings);
const ca = await ensureDebugProxyCa(settings.certDir);
const server = await startDebugProxyServer({
host: opts.host,
port: opts.port,
settings
});
process$1.stdout.write(`Debug proxy: ${server.proxyUrl}\n`);
process$1.stdout.write(`CA cert: ${ca.certPath}\n`);
process$1.stdout.write(`Capture DB: ${store.dbPath}\n`);
process$1.stdout.write("Press Ctrl+C to stop.\n");
const shutdown = async () => {
process$1.off("SIGINT", onSignal);
process$1.off("SIGTERM", onSignal);
await server.stop();
if (settings.enabled) finalizeDebugProxyCapture(settings);
else {
store.endSession(settings.sessionId);
closeDebugProxyCaptureStore();
}
process$1.exit(0);
};
const onSignal = () => {
shutdown();
};
process$1.on("SIGINT", onSignal);
process$1.on("SIGTERM", onSignal);
await new Promise(() => {});
}
async function runDebugProxyRunCommand(opts) {
if (opts.commandArgs.length === 0) throw new Error("proxy run requires a command after --");
const sessionId = randomUUID();
const settings = {
...resolveDebugProxySettings(),
sessionId
};
getDebugProxyCaptureStore().upsertSession({
id: sessionId,
startedAt: Date.now(),
mode: "proxy-run",
sourceScope: "openclaw",
sourceProcess: "openclaw",
proxyUrl: void 0
});
const server = await startDebugProxyServer({
host: opts.host,
port: opts.port,
settings
});
const [command, ...args] = opts.commandArgs;
const childEnv = applyDebugProxyEnv(process$1.env, {
proxyUrl: server.proxyUrl,
sessionId,
certDir: settings.certDir
});
try {
await new Promise((resolve, reject) => {
const child = spawn(expectDefined(command, "proxy cli.runtime command"), args, {
stdio: "inherit",
env: childEnv,
cwd: process$1.cwd()
});
child.once("error", reject);
child.once("exit", (code, signal) => {
process$1.exitCode = resolveSubprocessExitCode(code, signal);
resolve();
});
});
} finally {
await server.stop();
getDebugProxyCaptureStore().endSession(sessionId);
}
}
function redactProxyUrl(value) {
if (!value) return;
try {
const url = new URL(value);
if (url.username || url.password) {
url.username = "redacted";
url.password = "redacted";
}
url.search = "";
url.hash = "";
return url.toString();
} catch {
return "<invalid proxy URL>";
}
}
function redactProxyValidationResult(result) {
return {
...result,
config: {
...result.config,
proxyUrl: redactProxyUrl(result.config.proxyUrl)
}
};
}
function getProxyValidationTextColors() {
const rich = isRich();
const apply = (color) => (value) => colorize(rich, color, value);
return {
heading: apply(theme.heading),
success: apply(theme.success),
error: apply(theme.error),
muted: apply(theme.muted),
warn: apply(theme.warn)
};
}
function formatProxyCheckLine(check, colors) {
const icon = check.ok ? colors.success("✓") : colors.error("✗");
const paddedKind = colors.muted(check.kind.padEnd(7, " "));
const status = check.status === void 0 ? "" : ` ${check.ok ? colors.success(`HTTP ${check.status}`) : colors.error(`HTTP ${check.status}`)}`;
const detail = check.error ? ` — ${check.ok ? colors.muted(check.error) : colors.error(check.error)}` : "";
return ` ${icon} ${paddedKind} ${check.url}${status}${detail}`;
}
function formatProxyValidationNextSteps(result) {
if (result.ok) return [];
if (result.config.errors.some((error) => error.includes("proxy CA file could not be read"))) return ["Confirm proxy.tls.caFile or --proxy-ca-file points to a readable PEM CA file for the HTTPS proxy endpoint."];
if (result.config.errors.length > 0) return ["Fix proxy.proxyUrl, OPENCLAW_PROXY_URL, or --proxy-url so it uses a reachable http:// or https:// proxy."];
if (result.checks.some((check) => !check.ok && check.kind === "allowed")) return ["Confirm the proxy is reachable from this deployment context and permits the allowed destinations."];
if (result.checks.some((check) => !check.ok && check.kind === "denied")) return ["Update the proxy ACL so denied destinations are blocked, or pass the expected --denied-url values."];
return ["Review the failed checks above and update proxy configuration or validation destinations."];
}
function formatProxyValidationText(result) {
const colors = getProxyValidationTextColors();
const redactedProxyUrl = redactProxyUrl(result.config.proxyUrl);
const lines = [
result.ok ? colors.success("Proxy validation passed") : colors.error("Proxy validation failed"),
"",
colors.heading("Proxy"),
` Source: ${colors.muted(result.config.source)}`,
` URL: ${redactedProxyUrl ?? colors.muted("not configured")}`
];
if (result.config.errors.length > 0) {
lines.push("", colors.heading("Problems"));
for (const error of result.config.errors) lines.push(` - ${colors.error(error)}`);
}
if (result.checks.length > 0) {
lines.push("", colors.heading("Checks"));
for (const check of result.checks) lines.push(formatProxyCheckLine(check, colors));
}
const nextSteps = formatProxyValidationNextSteps(result);
if (nextSteps.length > 0) {
lines.push("", colors.heading("Next steps"));
for (const nextStep of nextSteps) lines.push(` ${colors.warn(nextStep)}`);
}
return `${lines.join("\n")}\n`;
}
async function runProxyValidateCommand(opts) {
const result = await runProxyValidation({
config: getRuntimeConfig()?.proxy,
env: process$1.env,
proxyUrlOverride: opts.proxyUrl,
proxyCaFileOverride: opts.proxyCaFile,
allowedUrls: opts.allowedUrls,
deniedUrls: opts.deniedUrls,
apnsReachability: opts.apnsReachability,
apnsAuthority: opts.apnsAuthority,
timeoutMs: opts.timeoutMs
});
const outputResult = redactProxyValidationResult(result);
process$1.stdout.write(opts.json === true ? `${JSON.stringify(outputResult, null, 2)}\n` : formatProxyValidationText(outputResult));
if (!result.ok) process$1.exitCode = 1;
}
async function runDebugProxySessionsCommand(opts) {
const sessions = getDebugProxyCaptureStore().listSessions(opts.limit ?? 20);
writeRuntimeJson(defaultRuntime, opts.json ? { sessions } : sessions);
closeDebugProxyCaptureStore();
}
async function runDebugProxyQueryCommand(opts) {
const rows = getDebugProxyCaptureStore().queryPreset(opts.preset, opts.sessionId);
writeRuntimeJson(defaultRuntime, opts.json ? { rows } : rows);
closeDebugProxyCaptureStore();
}
async function runDebugProxyCoverageCommand() {
const report = buildDebugProxyCoverageReport();
writeRuntimeJson(defaultRuntime, report);
closeDebugProxyCaptureStore();
}
async function runDebugProxyPurgeCommand() {
const result = getDebugProxyCaptureStore().purgeAll();
process$1.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
closeDebugProxyCaptureStore();
}
async function readDebugProxyBlobCommand(opts) {
const content = getDebugProxyCaptureStore().readBlob(opts.blobId);
if (content == null) {
closeDebugProxyCaptureStore();
throw new Error(`Unknown blob: ${opts.blobId}`);
}
process$1.stdout.write(content);
closeDebugProxyCaptureStore();
}
//#endregion
export { readDebugProxyBlobCommand, runDebugProxyCoverageCommand, runDebugProxyPurgeCommand, runDebugProxyQueryCommand, runDebugProxyRunCommand, runDebugProxySessionsCommand, runDebugProxyStartCommand, runProxyValidateCommand };