UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

997 lines (996 loc) 32.6 kB
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 };