openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
298 lines (297 loc) • 9.66 kB
JavaScript
import { n as consumeRootOptionToken, s as isValueToken } from "./cli-root-options-CgExE0zj.js";
import "./src-vebZIeLe.js";
import { t as expectDefined } from "./expect-CyE8FADM.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { t as resolveCliArgvInvocation } from "./argv-invocation-C1WhAkJL.js";
import { t as parseInlineOptionToken } from "./inline-option-token-Dqt7rKG4.js";
import { t as resolveSubprocessExitCode } from "./subprocess-exit-code-AepaGf2z.js";
import { spawnSync } from "node:child_process";
import { isIP } from "node:net";
//#region src/cli/root-option-forward.ts
/** Copy one consumed root option and its value tokens into `out`, returning token count. */
function forwardConsumedCliRootOption(args, index, out) {
const consumedRootOption = consumeRootOptionToken(args, index);
if (consumedRootOption <= 0) return 0;
for (let offset = 0; offset < consumedRootOption; offset += 1) {
const token = args[index + offset];
if (token !== void 0) out.push(token);
}
return consumedRootOption;
}
//#endregion
//#region src/cli/root-option-scan.ts
/** Walk argv once, letting callers consume custom flags before forwarding root options. */
function scanCliRootOptions(argv, visit) {
if (argv.length < 2) return {
ok: true,
argv
};
const out = argv.slice(0, 2);
const args = argv.slice(2);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === void 0) continue;
if (arg === "--") {
out.push(arg, ...args.slice(i + 1));
break;
}
const visited = visit({
arg,
args,
index: i,
out
});
if (visited.kind === "error") return {
ok: false,
error: visited.error
};
if (visited.kind === "handled") {
if (visited.consumedNext) i += 1;
continue;
}
const consumedRootOption = forwardConsumedCliRootOption(args, i, out);
if (consumedRootOption > 0) {
i += consumedRootOption - 1;
continue;
}
out.push(arg);
}
return {
ok: true,
argv: out
};
}
//#endregion
//#region src/cli/root-option-value.ts
/** Return the normalized option value and whether the next argv token was consumed. */
function takeCliRootOptionValue(raw, next) {
const parsed = parseInlineOptionToken(raw);
if (parsed.hasInlineValue) return {
value: (parsed.inlineValue ?? "").trim() || null,
consumedNext: false
};
const consumedNext = isValueToken(next);
return {
value: (consumedNext ? next.trim() : "") || null,
consumedNext
};
}
//#endregion
//#region src/cli/container-target.ts
const CONTAINER_ALLOW_LOOPBACK_PROXY_URL_ENV = "OPENCLAW_CONTAINER_ALLOW_LOOPBACK_PROXY_URL";
const CONTAINER_RUNTIME_PROBE_TIMEOUT_MS = 1e4;
function parseCliContainerArgs(argv) {
let container = null;
const scanned = scanCliRootOptions(argv, ({ arg, args, index }) => {
if (arg === "--container" || arg.startsWith("--container=")) {
const next = args[index + 1];
const { value, consumedNext } = takeCliRootOptionValue(arg, next);
if (!value) return {
kind: "error",
error: "--container requires a value"
};
container = value;
return {
kind: "handled",
consumedNext
};
}
return { kind: "pass" };
});
if (!scanned.ok) return scanned;
return {
ok: true,
container,
argv: scanned.argv
};
}
function resolveCliContainerTarget(argv, env = process.env) {
const parsed = parseCliContainerArgs(argv);
if (!parsed.ok) throw new Error(parsed.error);
return parsed.container ?? normalizeOptionalString(env.OPENCLAW_CONTAINER) ?? null;
}
function isContainerRunning(params) {
const result = params.deps.spawnSync(params.exec.command, [
...params.exec.argsPrefix,
"inspect",
"--format",
"{{.State.Running}}",
params.containerName
], params.exec.command === "sudo" ? {
encoding: "utf8",
killSignal: "SIGKILL",
stdio: [
"inherit",
"pipe",
"inherit"
],
timeout: CONTAINER_RUNTIME_PROBE_TIMEOUT_MS
} : {
encoding: "utf8",
killSignal: "SIGKILL",
timeout: CONTAINER_RUNTIME_PROBE_TIMEOUT_MS
});
return result.status === 0 && result.stdout.trim() === "true";
}
function candidateContainerRuntimes() {
return [{
runtime: "podman",
command: "podman",
argsPrefix: []
}, {
runtime: "docker",
command: "docker",
argsPrefix: []
}];
}
function resolveRunningContainer(params) {
const matches = [];
const candidates = candidateContainerRuntimes();
for (const exec of candidates) if (isContainerRunning({
exec,
containerName: params.containerName,
deps: params.deps
})) {
matches.push({
...exec,
containerName: params.containerName
});
if (exec.runtime === "docker") break;
}
if (matches.length === 0) return null;
if (matches.length > 1) {
const runtimes = matches.map((match) => match.runtime).join(", ");
throw new Error(`Container "${params.containerName}" is running under multiple runtimes (${runtimes}); use a unique container name.`);
}
return expectDefined(matches[0], "matches capture group 0");
}
function buildContainerExecArgs(params) {
const envFlag = params.exec.runtime === "docker" ? "-e" : "--env";
const proxyUrl = normalizeOptionalString(params.env.OPENCLAW_PROXY_URL);
if (proxyUrl) assertContainerProxyUrlIsReachable(proxyUrl, params.env);
const proxyEnvArgs = proxyUrl ? [envFlag, `OPENCLAW_PROXY_URL=${proxyUrl}`] : [];
const interactiveFlags = ["-i", ...params.stdinIsTTY && params.stdoutIsTTY ? ["-t"] : []];
return [
...params.exec.argsPrefix,
"exec",
...interactiveFlags,
envFlag,
`OPENCLAW_CONTAINER_HINT=${params.containerName}`,
envFlag,
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
...proxyEnvArgs,
params.containerName,
"openclaw",
...params.argv
];
}
function assertContainerProxyUrlIsReachable(proxyUrl, env) {
if (env[CONTAINER_ALLOW_LOOPBACK_PROXY_URL_ENV] === "1") return;
let parsed;
try {
parsed = new URL(proxyUrl);
} catch {
return;
}
if (!isLoopbackProxyHostname(parsed.hostname)) return;
throw new Error(`OPENCLAW_PROXY_URL=${redactProxyUrlForMessage(proxyUrl)} is loopback; 127.0.0.1 inside a container points at the container, not the host. Use a container-reachable proxy address, or set ${CONTAINER_ALLOW_LOOPBACK_PROXY_URL_ENV}=1 if this is intentional.`);
}
function isLoopbackProxyHostname(hostname) {
const normalizedHostname = hostname.toLowerCase().replace(/\.+$/, "");
if (normalizedHostname === "localhost") return true;
if (isIP(normalizedHostname) === 4) return normalizedHostname.startsWith("127.");
const ipv6Hostname = normalizedHostname.replace(/^\[|\]$/g, "");
if (isIP(ipv6Hostname) !== 6) return false;
if (ipv6Hostname === "::1" || ipv6Hostname === "0:0:0:0:0:0:0:1") return true;
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(ipv6Hostname);
if (!mapped) return false;
const high = Number.parseInt(expectDefined(mapped[1], "mapped capture group 1"), 16);
return Number.isInteger(high) && high >= 32512 && high <= 32767;
}
function redactProxyUrlForMessage(raw) {
try {
const url = new URL(raw);
if (url.username || url.password) {
url.username = "redacted";
url.password = url.password ? "redacted" : "";
}
url.search = "";
url.hash = "";
return url.toString();
} catch {
return "<invalid URL>";
}
}
function buildContainerExecEnv(env) {
const next = { ...env };
delete next.OPENCLAW_PROFILE;
delete next.OPENCLAW_GATEWAY_PORT;
delete next.OPENCLAW_GATEWAY_URL;
delete next.OPENCLAW_GATEWAY_TOKEN;
delete next.OPENCLAW_GATEWAY_PASSWORD;
next.OPENCLAW_CONTAINER = "";
return next;
}
function isBlockedContainerCommand(argv) {
if (resolveCliArgvInvocation([
"node",
"openclaw",
...argv
]).primary === "update") return true;
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg || arg === "--") return false;
if (arg === "--update") return true;
const consumedRootOption = consumeRootOptionToken(argv, i);
if (consumedRootOption > 0) {
i += consumedRootOption - 1;
continue;
}
if (!arg.startsWith("-")) return false;
}
return false;
}
function maybeRunCliInContainer(argv, deps) {
const resolvedDeps = {
env: deps?.env ?? process.env,
spawnSync: deps?.spawnSync ?? spawnSync,
stdinIsTTY: deps?.stdinIsTTY ?? process.stdin.isTTY,
stdoutIsTTY: deps?.stdoutIsTTY ?? process.stdout.isTTY
};
if (resolvedDeps.env.OPENCLAW_CLI_CONTAINER_BYPASS === "1") return {
handled: false,
argv
};
const parsed = parseCliContainerArgs(argv);
if (!parsed.ok) throw new Error(parsed.error);
const containerName = resolveCliContainerTarget(argv, resolvedDeps.env);
if (!containerName) return {
handled: false,
argv: parsed.argv
};
if (isBlockedContainerCommand(parsed.argv.slice(2))) throw new Error("openclaw update is not supported with --container; rebuild or restart the container image instead.");
const runningContainer = resolveRunningContainer({
containerName,
env: resolvedDeps.env,
deps: resolvedDeps
});
if (!runningContainer) throw new Error(`No running container matched "${containerName}" under podman or docker.`);
const result = resolvedDeps.spawnSync(runningContainer.command, buildContainerExecArgs({
exec: runningContainer,
containerName: runningContainer.containerName,
argv: parsed.argv.slice(2),
env: resolvedDeps.env,
stdinIsTTY: resolvedDeps.stdinIsTTY,
stdoutIsTTY: resolvedDeps.stdoutIsTTY
}), {
stdio: "inherit",
env: buildContainerExecEnv(resolvedDeps.env)
});
if (result.error) throw result.error;
return {
handled: true,
exitCode: resolveSubprocessExitCode(result.status, result.signal)
};
}
//#endregion
export { scanCliRootOptions as a, takeCliRootOptionValue as i, parseCliContainerArgs as n, resolveCliContainerTarget as r, maybeRunCliInContainer as t };