openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
664 lines (663 loc) • 23.3 kB
JavaScript
import "./src-SskUVPJG.mjs";
import { t as expectDefined } from "./expect-lbe3Hgrh.mjs";
import { t as resolveSubprocessExitCode } from "./subprocess-exit-code-AepaGf2z.mjs";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-BC29JSZp.mjs";
import { n as isTruthyEnvValue } from "./env-Bw1K_ti_.mjs";
import { n as isRich, r as theme, t as colorize } from "./theme-DzaUZY4q.mjs";
import { r as captureOpenClawStateWorkerContext } from "./openclaw-state-worker-context-epCM4Gdh.mjs";
import { p as loadPinnedRuntimeConfigAsync } from "./runtime-snapshot-CifcQ780.mjs";
import { r as resolveDebugProxySettings, t as applyDebugProxyEnv } from "./env-DJfhsVbp.mjs";
import { f as reportCapturePersistenceFailure, o as initializeDebugProxyCaptureAsync, u as finalizeDebugProxyCaptureAsync, v as acquireDebugProxyCaptureStoreAsync, y as redactedCaptureHeaders } from "./runtime-BUbuVAU6.mjs";
import { t as ensureDebugProxyCa } from "./ca-Ccx4iT7M.mjs";
import { n as runProxyValidation } from "./proxy-validation-D6chtyol.mjs";
import { t as buildDebugProxyCoverageReport } from "./coverage-CsML8ggA.mjs";
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 { randomUUID } from "node:crypto";
import { StringDecoder } from "node:string_decoder";
import { createServer as createServer$1, request } from "node:http";
import { request as request$1 } from "node:https";
//#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 assertDebugProxyDirectUpstreamAllowed(env = process.env) {
if (!isTruthyEnvValue(env["OPENCLAW_PROXY_ACTIVE"]) || isTruthyEnvValue(env[DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE])) 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) => {
const operation = params.store.recordEvent({
sessionId: params.settings.sessionId,
ts: Date.now(),
sourceScope: "openclaw",
sourceProcess: params.settings.sourceProcess,
...event
});
params.pending.add(operation);
operation.then(() => params.pending.delete(operation), (error) => {
params.pending.delete(operation);
reportCapturePersistenceFailure(params, error);
});
return operation;
};
}
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;
}
endProxyErrorResponse(res, 502, BAD_GATEWAY_BODY);
}
function endProxyErrorResponse(res, status, body) {
res.writeHead(status, {
Connection: "close",
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": Buffer.byteLength(body)
});
res.end(body);
}
async function startDebugProxyServer(params) {
const settings = { ...params.settings };
const env = { ...params.env ?? process.env };
await ensureDebugProxyCa(settings.certDir);
const lease = await acquireDebugProxyCaptureStoreAsync({ env });
const pending = /* @__PURE__ */ new Set();
const errors = [];
const recordProxyEvent = createProxyCaptureRecorder({
store: lease.store,
settings,
pending,
errors
});
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) {
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)
});
endProxyErrorResponse(res, 400, "Invalid proxy target URL\n");
return;
}
const targetProtocol = target.protocol === "https:" ? "https" : "http";
const targetPath = `${target.pathname}${target.search}`;
const recordTargetEvent = (event) => void 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
});
endProxyErrorResponse(res, 403, `${message}\n`);
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";
const recordConnectEvent = (event) => void recordProxyEvent({
protocol: "connect",
direction: "local",
flowId,
host: hostname,
path: req.url ?? "",
...event
});
let port;
try {
const parsed = parseConnectTarget(req.url);
hostname = parsed.hostname;
port = parsed.port;
} catch (error) {
recordConnectEvent({
kind: "error",
errorText: error instanceof Error ? error.message : String(error)
});
clientSocket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
return;
}
recordConnectEvent({
kind: "connect",
headersJson: JSON.stringify(redactedCaptureHeaders(req.headers))
});
try {
assertDebugProxyDirectUpstreamAllowed();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
recordConnectEvent({
kind: "error",
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() {
recordConnectEvent({
kind: "error",
errorText: `CONNECT upstream opening timed out after ${DEBUG_PROXY_CONNECT_TIMEOUT_MS}ms of inactivity`
});
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) => {
recordConnectEvent({
kind: "error",
errorText: error.message
});
upstreamSocket.destroy();
});
upstreamSocket.on("error", (error) => {
recordConnectEvent({
kind: "error",
errorText: error.message
});
clientSocket.destroy();
});
});
try {
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");
let stopping;
return {
proxyUrl: `http://${host}:${address.port}`,
stop: () => {
stopping ??= (async () => {
try {
await new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
} catch (error) {
errors.push(error);
}
await Promise.allSettled(pending);
try {
await lease.release();
} catch (error) {
errors.push(error);
}
if (errors.length) throw new AggregateError(errors, "Debug proxy capture shutdown failed.");
})();
return stopping;
}
};
} catch (error) {
try {
await lease.release();
} catch (closeError) {
throw new AggregateError([error, closeError], "Debug proxy capture startup failed.", { cause: closeError });
}
throw error;
}
}
//#endregion
//#region src/cli/proxy-cli.runtime.ts
async function finalizeProxyCommand(errors, finalizers) {
for (const finalize of finalizers) try {
await finalize();
} catch (error) {
errors.push(error);
}
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Debug proxy command and capture cleanup failed.");
}
async function runDebugProxyStartCommand(opts) {
const settings = resolveDebugProxySettings();
const { environment: env } = captureOpenClawStateWorkerContext();
const errors = [];
const finalizers = [];
try {
if (settings.enabled) {
finalizers.push(() => finalizeDebugProxyCaptureAsync(settings));
await initializeDebugProxyCaptureAsync("proxy-start", settings);
}
const lease = await acquireDebugProxyCaptureStoreAsync({ env });
const { store } = lease;
finalizers.push(lease.release);
if (!settings.enabled) {
finalizers.unshift(() => store.endSession(settings.sessionId));
await store.upsertSession({
id: settings.sessionId,
startedAt: Date.now(),
mode: "proxy-start",
sourceScope: "openclaw",
sourceProcess: "openclaw",
proxyUrl: settings.proxyUrl
});
}
const ca = await ensureDebugProxyCa(settings.certDir);
const server = await startDebugProxyServer({
host: opts.host,
port: opts.port,
settings,
env
});
finalizers.unshift(() => server.stop());
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");
await new Promise((resolve) => {
const onSignal = () => {
process$1.off("SIGINT", onSignal);
process$1.off("SIGTERM", onSignal);
resolve();
};
process$1.on("SIGINT", onSignal);
process$1.on("SIGTERM", onSignal);
});
} catch (error) {
errors.push(error);
}
await finalizeProxyCommand(errors, finalizers);
process$1.exit(0);
}
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
};
const { environment: env } = captureOpenClawStateWorkerContext();
const lease = await acquireDebugProxyCaptureStoreAsync({ env });
const { store } = lease;
const errors = [];
const finalizers = [() => store.endSession(sessionId), lease.release];
try {
await store.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,
env
});
finalizers.unshift(() => server.stop());
const [command, ...args] = opts.commandArgs;
const childEnv = applyDebugProxyEnv(process$1.env, {
proxyUrl: server.proxyUrl,
sessionId,
certDir: settings.certDir
});
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();
});
});
} catch (error) {
errors.push(error);
}
await finalizeProxyCommand(errors, finalizers);
}
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 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 config = await loadPinnedRuntimeConfigAsync(async (assertCurrent) => {
const { getRuntimeConfig } = await import("./config/config.js");
assertCurrent();
return { config: getRuntimeConfig() };
});
const result = await runProxyValidation({
config: config?.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 = {
...result,
config: {
...result.config,
proxyUrl: redactProxyUrl(result.config.proxyUrl)
}
};
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 lease = await acquireDebugProxyCaptureStoreAsync();
try {
const sessions = await lease.store.listSessions(opts.limit ?? 20);
writeRuntimeJson(defaultRuntime, opts.json ? { sessions } : sessions);
} finally {
await lease.release();
}
}
async function runDebugProxyQueryCommand(opts) {
const lease = await acquireDebugProxyCaptureStoreAsync();
try {
const rows = await lease.store.queryPreset(opts.preset, opts.sessionId);
writeRuntimeJson(defaultRuntime, opts.json ? { rows } : rows);
} finally {
await lease.release();
}
}
async function runDebugProxyCoverageCommand() {
const report = buildDebugProxyCoverageReport();
writeRuntimeJson(defaultRuntime, report);
}
async function runDebugProxyPurgeCommand() {
const lease = await acquireDebugProxyCaptureStoreAsync();
try {
const result = await lease.store.purgeAll();
process$1.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} finally {
await lease.release();
}
}
async function readDebugProxyBlobCommand(opts) {
const lease = await acquireDebugProxyCaptureStoreAsync();
try {
const content = await lease.store.readBlob(opts.blobId);
if (content == null) throw new Error(`Unknown blob: ${opts.blobId}`);
process$1.stdout.write(content);
} finally {
await lease.release();
}
}
//#endregion
export { readDebugProxyBlobCommand, runDebugProxyCoverageCommand, runDebugProxyPurgeCommand, runDebugProxyQueryCommand, runDebugProxyRunCommand, runDebugProxySessionsCommand, runDebugProxyStartCommand, runProxyValidateCommand };