openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,938 lines • 101 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, o as normalizeNullableString } from "./string-coerce-mnp54Vah.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { t as parseInlineOptionToken } from "./inline-option-token-Dqt7rKG4.js";
import { t as formatDocsLink } from "./links-CsLBrRff.js";
import { r as theme, t as colorize } from "./theme-vjDs9tao.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { l as sanitizeSystemRunEnvOverrides, s as sanitizeHostExecEnv, t as inspectHostExecEnvOverrides } from "./host-env-security-RnaFYKhk.js";
import { t as sameFileIdentity } from "./file-identity-BKNyWMFA.js";
import { _ as resolveNodeSystemdServiceName, g as resolveNodeLaunchAgentLabel, l as formatNodeServiceDescription, v as resolveNodeWindowsTaskName } from "./constants-e049XCW0.js";
import { n as resolveDaemonInstallRuntimeInputs, r as resolveDaemonNodeBinDir, s as resolveNodeProgramArguments, t as emitDaemonInstallRuntimeWarning } from "./daemon-install-plan.shared-Dr7I1qWf.js";
import { n as VERSION } from "./version-Crcn9X9T.js";
import { u as buildNodeServiceEnvironment } from "./runtime-paths-DS_J3TGI.js";
import { n as defaultRuntime } from "./runtime-B4lgFmsS.js";
import { u as normalizeAgentId } from "./session-key-B_NoIfpX.js";
import "./fs-safe-advanced-CBe_wA_B.js";
import { r as isGatewayDaemonRuntime, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-C76za6vm.js";
import { a as decodeWindowsOutputBuffer, o as resolveWindowsConsoleEncoding } from "./windows-command-CggaAJAY.js";
import { a as logWarn } from "./logger-lqqYRtFw.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { _ as resolveExecutableFromPathEnv, a as resolveApprovalAuditTrustPath, s as resolveCommandResolutionFromArgv } from "./exec-command-resolution-CqhcxBz-.js";
import { O as normalizeExecutableToken, S as unwrapKnownDispatchWrapperInvocation, d as unwrapKnownShellMultiplexerInvocation, f as POSIX_INLINE_COMMAND_FLAGS, g as resolveInlineCommandMatch, i as extractShellWrapperCommand, k as splitShellArgs, l as isShellWrapperInvocation, p as advancePosixInlineOptionScan, s as isBlockedShellWrapperCommand, t as POSIX_SHELL_WRAPPERS, u as resolveShellWrapperTransportArgv, v as extractEnvAssignmentKeysFromDispatchWrappers } from "./shell-wrapper-resolution-CFL_Vekh.js";
import "./config-C9RxTsn1.js";
import { n as formatInvalidConfigPort, r as formatInvalidPortOption } from "./error-format-COoNML-C.js";
import { n as buildPlatformServiceStartHints, r as formatRuntimeStatus, t as buildPlatformRuntimeLogHints } from "./runtime-hints-BXTNKBW9.js";
import { d as resolveRuntimeStatusColor, g as installDaemonServiceAndEmit, n as createDaemonInstallActionContext, p as buildDaemonServiceSnapshot, r as failIfNixDaemonInstallMode, t as createCliStatusTextStyles } from "./shared-BEkveSUA.js";
import { t as parsePort } from "./parse-port-CbiRuE9n.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-CcqJJIan.js";
import { n as GatewayClient } from "./client-C2g2lFC5.js";
import { r as loadOrCreateDeviceIdentity } from "./device-identity-CEPJolq9.js";
import { t as startGatewayClientWhenEventLoopReady } from "./client-start-readiness-BIoVQZze.js";
import { t as ConnectErrorDetailCodes } from "./connect-error-details-BXqba0zp.js";
import { i as runServiceUninstall, n as runServiceStart, r as runServiceStop, t as runServiceRestart } from "./lifecycle-core-BDdRclCf.js";
import { o as getActivePluginRegistry } from "./runtime-B2ROiq5l.js";
import { i as NODE_SYSTEM_RUN_COMMANDS, n as NODE_EXEC_APPROVALS_COMMANDS } from "./node-commands-SnI8Vs7F.js";
import { t as getMachineDisplayName } from "./machine-name-CBTz5R12.js";
import "./exec-wrapper-resolution-VZBsT_RG.js";
import { c as describeInterpreterInlineEval } from "./risks-DRxJ1pW5.js";
import { i as buildSafeBinsShellCommand, n as analyzeShellCommand, s as resolvePlannedSegmentArgv, t as analyzeArgvCommand } from "./exec-approvals-analysis-B4N2JXzl.js";
import { n as evaluateShellAllowlist, t as evaluateExecAllowlist } from "./exec-approvals-allowlist-VYEHBfj3.js";
import { B as resolveExecPolicyForMode, C as readExecApprovalsSnapshot, H as saveExecApprovals, M as resolveExecApprovals, O as requiresExecApproval, S as persistAllowAlwaysPatterns, U as requestJsonlSocket, c as ensureExecApprovals, g as normalizeExecApprovals, h as minSecurity, k as resolveAllowAlwaysPatternCoverage, l as hasDurableExecApproval, m as mergeExecApprovalsSocketDefaults, o as addDurableCommandApproval, p as maxAsk, s as commandRequiresSecurityAuditSuppressionApproval, w as recordAllowlistMatchesUse, z as resolveExecModePolicy } from "./exec-approvals-C6M6SGY3.js";
import { r as resolveExecSafeBinRuntimePolicy, t as isInterpreterLikeSafeBin } from "./exec-safe-bin-runtime-policy-DKOVy8lE.js";
import { t as ensureOpenClawCliOnPath } from "./path-env-CDAYsLgD.js";
import { t as resolveGatewayConnectionAuth } from "./connection-auth-zbD0QFhI.js";
import { t as formatHelpExamples } from "./help-format-CAcwboTs.js";
import { n as detectPolicyInlineEval } from "./policy-KTeYxilK.js";
import { c as normalizeSystemRunApprovalPlan, n as formatExecCommand, r as resolveSystemRunCommandRequest } from "./system-run-command-BO3cgBjR.js";
import { n as loadNodeHostConfig, r as saveNodeHostConfig, t as ensureNodeHostConfig } from "./config-QGn_oQSd.js";
import { t as resolveNodeService } from "./node-service-njT72UFj.js";
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import crypto from "node:crypto";
//#region src/infra/exec-host.ts
/** Send an authenticated exec request over the host JSONL socket. */
async function requestExecHostViaSocket(params) {
const { socketPath, token, request } = params;
if (!socketPath || !token) return null;
const timeoutMs = params.timeoutMs ?? 2e4;
const requestJson = JSON.stringify(request);
const nonce = crypto.randomBytes(16).toString("hex");
const ts = Date.now();
const hmac = crypto.createHmac("sha256", token).update(`${nonce}:${ts}:${requestJson}`).digest("hex");
return await requestJsonlSocket({
socketPath,
requestLine: JSON.stringify({
type: "exec",
id: crypto.randomUUID(),
nonce,
ts,
hmac,
requestJson
}),
timeoutMs,
accept: (value) => {
const msg = value;
if (msg?.type !== "exec-res") return;
if (msg.ok === true && msg.payload) return {
ok: true,
payload: msg.payload
};
if (msg.ok === false && msg.error) return {
ok: false,
error: msg.error
};
return null;
}
});
}
//#endregion
//#region src/node-host/exec-policy.ts
/** Evaluates node-host exec policy from security, approval, and allowlist context. */
/** Normalizes raw approval decisions from node-host payloads. */
function resolveExecApprovalDecision(value) {
if (value === "allow-once" || value === "allow-always") return value;
return null;
}
function formatSystemRunAllowlistMissMessage(params) {
if (params?.windowsShellWrapperBlocked) return "SYSTEM_RUN_DENIED: allowlist miss (Windows shell wrappers like cmd.exe /c require approval; approve once/always or run with --ask on-miss|always)";
if (params?.shellWrapperBlocked) return "SYSTEM_RUN_DENIED: allowlist miss (shell wrappers like sh/bash/zsh -c require approval; approve once/always or run with --ask on-miss|always)";
return "SYSTEM_RUN_DENIED: allowlist miss";
}
/** Combines exec security, allowlist analysis, and approval state into an allow/deny decision. */
function evaluateSystemRunPolicy(params) {
const windowsShellWrapperBlocked = params.security === "allowlist" && params.shellWrapperInvocation && params.isWindows && params.cmdInvocation;
const shellWrapperBlocked = windowsShellWrapperBlocked;
const analysisOk = shellWrapperBlocked ? false : params.analysisOk;
const allowlistSatisfied = shellWrapperBlocked ? false : params.allowlistSatisfied;
const approvedByAsk = params.approvalDecision !== null || params.approved === true;
if (params.security === "deny") return {
allowed: false,
eventReason: "security=deny",
errorMessage: "SYSTEM_RUN_DISABLED: security=deny",
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk: false,
approvalDecision: params.approvalDecision,
approvedByAsk
};
const requiresAsk = requiresExecApproval({
ask: params.ask,
security: params.security,
analysisOk,
allowlistSatisfied,
durableApprovalSatisfied: params.durableApprovalSatisfied
});
if (requiresAsk && !approvedByAsk) return {
allowed: false,
eventReason: "approval-required",
errorMessage: "SYSTEM_RUN_DENIED: approval required",
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk
};
if (params.security === "allowlist" && (!analysisOk || !allowlistSatisfied) && !approvedByAsk) {
if (params.durableApprovalSatisfied) return {
allowed: true,
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk
};
return {
allowed: false,
eventReason: "allowlist-miss",
errorMessage: formatSystemRunAllowlistMissMessage({
shellWrapperBlocked,
windowsShellWrapperBlocked
}),
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk
};
}
return {
allowed: true,
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk
};
}
//#endregion
//#region src/node-host/invoke-system-run-allowlist.ts
/** Resolves system.run allowlist matches, argv plans, and truncated command output. */
/**
* Allowlist analysis and argv rewriting for node-host system.run.
*
* This module keeps command approval analysis separate from process execution,
* and only rewrites shell transports when the rebuilt command still satisfies policy.
*/
const POSIX_SHELL_WRAPPER_NAMES = POSIX_SHELL_WRAPPERS;
/** Evaluate system.run argv or shell command against the exec allowlist policy. */
/** Evaluates analyzed command segments against allowlist and trusted safe-bin policy. */
function evaluateSystemRunAllowlist(params) {
if (params.shellCommand) {
const allowlistEval = evaluateShellAllowlist({
command: params.shellCommand,
allowlist: params.approvals.allowlist,
safeBins: params.safeBins,
safeBinProfiles: params.safeBinProfiles,
cwd: params.cwd,
env: params.env,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
platform: process.platform
});
return {
analysisOk: allowlistEval.analysisOk,
allowlistMatches: allowlistEval.allowlistMatches,
allowlistSatisfied: params.security === "allowlist" && allowlistEval.analysisOk ? allowlistEval.allowlistSatisfied : false,
segments: allowlistEval.segments,
segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,
segmentSatisfiedBy: allowlistEval.segmentSatisfiedBy
};
}
const analysis = analyzeArgvCommand({
argv: params.argv,
cwd: params.cwd,
env: params.env
});
const allowlistEval = evaluateExecAllowlist({
analysis,
allowlist: params.approvals.allowlist,
safeBins: params.safeBins,
safeBinProfiles: params.safeBinProfiles,
cwd: params.cwd,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills
});
return {
analysisOk: analysis.ok,
allowlistMatches: allowlistEval.allowlistMatches,
allowlistSatisfied: params.security === "allowlist" && analysis.ok ? allowlistEval.allowlistSatisfied : false,
segments: analysis.segments,
segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,
segmentSatisfiedBy: allowlistEval.segmentSatisfiedBy
};
}
/** Resolve the single planned argv that can replace the caller argv after allowlist approval. */
function resolvePlannedAllowlistArgv(params) {
if (params.security !== "allowlist" || params.policy.approvedByAsk || params.shellCommand || !params.policy.analysisOk || !params.policy.allowlistSatisfied || params.segments.length !== 1) return;
const plannedAllowlistArgv = resolvePlannedSegmentArgv(params.segments[0]);
return plannedAllowlistArgv && plannedAllowlistArgv.length > 0 ? plannedAllowlistArgv : null;
}
/** Resolve final argv after safe-bin shell rewriting and allowlist revalidation. */
function resolveSystemRunExecArgv(params) {
let execArgv = params.plannedAllowlistArgv ?? params.argv;
if (params.security === "allowlist" && params.isWindows && !params.policy.approvedByAsk && params.shellCommand && params.policy.analysisOk && params.policy.allowlistSatisfied && params.segments.length === 1 && params.segments[0]?.argv.length > 0) execArgv = params.segments[0].argv;
if (params.security === "allowlist" && !params.isWindows && !params.policy.approvedByAsk && params.shellCommand && params.policy.analysisOk && params.policy.allowlistSatisfied && params.segmentSatisfiedBy.some((entry) => entry === "safeBins" || entry === "inlineChain") && isPosixShellInlineCommandTransport(params.argv)) {
const rebuilt = buildSafeBinsShellCommand({
command: params.shellCommand,
segments: params.segments,
segmentSatisfiedBy: params.segmentSatisfiedBy,
cwd: params.cwd,
env: params.env,
platform: process.platform
});
if (!rebuilt.ok || !rebuilt.command) return null;
const rewrittenArgv = replacePosixShellInlineCommand({
argv: params.argv,
oldCommand: params.shellCommand,
nextCommand: rebuilt.command
});
if (!rewrittenArgv) return null;
const rebuiltAllowlist = evaluateSystemRunAllowlist({
shellCommand: rebuilt.command,
argv: rewrittenArgv,
approvals: params.approvals,
security: params.security,
safeBins: params.safeBins,
safeBinProfiles: params.safeBinProfiles,
trustedSafeBinDirs: params.trustedSafeBinDirs,
cwd: params.cwd,
env: params.env,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills
});
if (!rebuiltAllowlist.analysisOk || !rebuiltAllowlist.allowlistSatisfied) return null;
execArgv = rewrittenArgv;
}
return execArgv;
}
function isPosixShellInlineCommandTransport(argv) {
const transportArgv = resolveShellWrapperTransportArgv(argv);
return Boolean(transportArgv && POSIX_SHELL_WRAPPER_NAMES.has(normalizeExecutableToken(transportArgv[0] ?? "")));
}
function findSubsequence(haystack, needle) {
if (needle.length === 0 || needle.length > haystack.length) return -1;
for (let start = 0; start <= haystack.length - needle.length; start += 1) {
let matches = true;
for (let offset = 0; offset < needle.length; offset += 1) if (haystack[start + offset] !== needle[offset]) {
matches = false;
break;
}
if (matches) return start;
}
return -1;
}
function replacePosixShellInlineCommand(params) {
const transportArgv = resolveShellWrapperTransportArgv(params.argv);
if (!transportArgv || !POSIX_SHELL_WRAPPER_NAMES.has(normalizeExecutableToken(transportArgv[0] ?? ""))) return null;
const transportStart = findSubsequence(params.argv, transportArgv);
if (transportStart < 0) return null;
const match = resolveInlineCommandMatch(transportArgv, POSIX_INLINE_COMMAND_FLAGS, { allowCombinedC: true });
if (match.valueTokenIndex === null) return null;
const absoluteValueIndex = transportStart + match.valueTokenIndex;
const token = params.argv[absoluteValueIndex];
if (token === void 0) return null;
const rewritten = [...params.argv];
if (token === params.oldCommand) {
rewritten[absoluteValueIndex] = params.nextCommand;
return rewritten;
}
if (token.endsWith(params.oldCommand)) {
rewritten[absoluteValueIndex] = token.slice(0, token.length - params.oldCommand.length) + params.nextCommand;
return rewritten;
}
return null;
}
/** Mark truncated output in stderr when possible, otherwise stdout. */
/** Truncates captured stdout/stderr in place to the node-host output cap. */
function applyOutputTruncation(result) {
if (!result.truncated) return;
const suffix = "... (truncated)";
if (result.stderr.trim().length > 0) result.stderr = `${result.stderr}\n${suffix}`;
else result.stdout = `${result.stdout}\n${suffix}`;
}
//#endregion
//#region src/node-host/invoke-system-run-plan.ts
/** Builds and revalidates system.run approval plans for cwd and mutable executable operands. */
const MUTABLE_ARGV1_INTERPRETER_PATTERNS = [
/^(?:node|nodejs)$/,
/^perl$/,
/^php$/,
/^python(?:\d+(?:\.\d+)*)?$/,
/^ruby$/
];
const GENERIC_MUTABLE_SCRIPT_RUNNERS = new Set([
"esno",
"jiti",
"ts-node",
"ts-node-esm",
"tsx",
"vite-node"
]);
const OPAQUE_MUTABLE_SCRIPT_RUNNERS = new Set(["busybox", "toybox"]);
const BUN_SUBCOMMANDS = new Set([
"add",
"audit",
"completions",
"create",
"exec",
"help",
"init",
"install",
"link",
"outdated",
"patch",
"pm",
"publish",
"remove",
"repl",
"run",
"test",
"unlink",
"update",
"upgrade",
"x"
]);
const BUN_OPTIONS_WITH_VALUE = new Set([
"--backend",
"--bunfig",
"--conditions",
"--config",
"--console-depth",
"--cwd",
"--define",
"--elide-lines",
"--env-file",
"--extension-order",
"--filter",
"--hot",
"--inspect",
"--inspect-brk",
"--inspect-wait",
"--install",
"--jsx-factory",
"--jsx-fragment",
"--jsx-import-source",
"--loader",
"--origin",
"--port",
"--preload",
"--smol",
"--tsconfig-override",
"-c",
"-e",
"-p",
"-r"
]);
const DENO_RUN_OPTIONS_WITH_VALUE = new Set([
"--cached-only",
"--cert",
"--config",
"--env-file",
"--ext",
"--harmony-import-attributes",
"--import-map",
"--inspect",
"--inspect-brk",
"--inspect-wait",
"--location",
"--log-level",
"--lock",
"--node-modules-dir",
"--no-check",
"--preload",
"--reload",
"--seed",
"--strace-ops",
"--unstable-bare-node-builtins",
"--v8-flags",
"--watch",
"--watch-exclude",
"-L"
]);
const NODE_OPTIONS_WITH_FILE_VALUE = new Set([
"-r",
"--experimental-loader",
"--import",
"--loader",
"--require"
]);
const RUBY_UNSAFE_APPROVAL_FLAGS = new Set([
"-I",
"-r",
"--require"
]);
const PERL_UNSAFE_APPROVAL_FLAGS = new Set([
"-I",
"-M",
"-m"
]);
function normalizeOptionFlag(token) {
return normalizeLowercaseStringOrEmpty(parseInlineOptionToken(token).name);
}
function readTrimmedArgToken(argv, index) {
return normalizeNullableString(argv[index]) ?? "";
}
const POSIX_SHELL_OPTIONS_WITH_VALUE = new Set([
"--init-file",
"--rcfile",
"--startup-script",
"-O",
"-o",
"+O",
"+o"
]);
const POSIX_SHELLS_WITH_PLUS_OPTIONS = new Set([
"ash",
"bash",
"dash",
"ksh",
"sh",
"zsh"
]);
function isPosixShellOptionToken(token, supportsPlusOptions) {
return token.startsWith("-") || supportsPlusOptions && token.startsWith("+");
}
const NPM_EXEC_OPTIONS_WITH_VALUE = new Set([
"--cache",
"--package",
"--prefix",
"--script-shell",
"--userconfig",
"--workspace",
"-p",
"-w"
]);
const NPM_EXEC_FLAG_OPTIONS = new Set([
"--no",
"--quiet",
"--ws",
"--workspaces",
"--yes",
"-q",
"-y"
]);
const PNPM_OPTIONS_WITH_VALUE = new Set([
"--config",
"--dir",
"--filter",
"--reporter",
"--stream",
"--test-pattern",
"--workspace-concurrency",
"-C"
]);
const PNPM_FLAG_OPTIONS = new Set([
"--aggregate-output",
"--color",
"--parallel",
"--recursive",
"--silent",
"--workspace-root",
"-r",
"-s",
"-w"
]);
const PNPM_DLX_OPTIONS_WITH_VALUE = new Set([
"--allow-build",
"--package",
"-p"
]);
function pathComponentsFromRootSync(targetPath) {
const absolute = path.resolve(targetPath);
const parts = [];
let cursor = absolute;
while (true) {
parts.unshift(cursor);
const parent = path.dirname(cursor);
if (parent === cursor) return parts;
cursor = parent;
}
}
function isOwnedByCurrentProcessSync(candidate) {
if (process.platform === "win32" || typeof process.getuid !== "function") return false;
try {
return fs.statSync(candidate).uid === process.getuid();
} catch {
return false;
}
}
function isMutableByCurrentProcessSync(candidate) {
try {
fs.accessSync(candidate, fs.constants.W_OK);
return true;
} catch {
return isOwnedByCurrentProcessSync(candidate);
}
}
function hasMutableSymlinkPathComponentSync(targetPath) {
for (const component of pathComponentsFromRootSync(targetPath)) try {
if (!fs.lstatSync(component).isSymbolicLink()) continue;
if (isMutableByCurrentProcessSync(path.dirname(component))) return true;
} catch {
return true;
}
return false;
}
function pathLooksMutableForShellPayloadSync(targetPath) {
if (isMutableByCurrentProcessSync(targetPath) || isMutableByCurrentProcessSync(path.dirname(targetPath)) || hasMutableSymlinkPathComponentSync(targetPath)) return true;
let realPath;
try {
realPath = fs.realpathSync(targetPath);
} catch {
return true;
}
return isMutableByCurrentProcessSync(realPath) || isMutableByCurrentProcessSync(path.dirname(realPath)) || hasMutableSymlinkPathComponentSync(realPath);
}
function shouldPinExecutableForApproval(params) {
if (params.shellCommand !== null) return false;
return (params.wrapperChain?.length ?? 0) === 0;
}
function hashFileContentsSync(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function looksLikePathToken(token) {
return token.startsWith(".") || token.startsWith("/") || token.startsWith("\\") || token.includes("/") || token.includes("\\") || path.extname(token).length > 0;
}
function resolvesToExistingFileSync(rawOperand, cwd) {
if (!rawOperand) return false;
try {
return fs.statSync(path.resolve(cwd ?? process.cwd(), rawOperand)).isFile();
} catch {
return false;
}
}
function isKnownBinaryExecutableHeader(buffer) {
if (buffer.length >= 4 && buffer.subarray(0, 4).equals(Buffer.from([
127,
69,
76,
70
]))) return true;
if (buffer.length >= 4 && (buffer.subarray(0, 4).equals(Buffer.from([
254,
237,
250,
206
])) || buffer.subarray(0, 4).equals(Buffer.from([
206,
250,
237,
254
])) || buffer.subarray(0, 4).equals(Buffer.from([
254,
237,
250,
207
])) || buffer.subarray(0, 4).equals(Buffer.from([
207,
250,
237,
254
])) || buffer.subarray(0, 4).equals(Buffer.from([
202,
254,
186,
190
])) || buffer.subarray(0, 4).equals(Buffer.from([
190,
186,
254,
202
])) || buffer.subarray(0, 4).equals(Buffer.from([
202,
254,
186,
191
])) || buffer.subarray(0, 4).equals(Buffer.from([
191,
186,
254,
202
])))) return true;
if (buffer.length < 64 || !buffer.subarray(0, 2).equals(Buffer.from([77, 90]))) return false;
const peOffset = buffer.readUInt32LE(60);
return peOffset >= 0 && peOffset <= buffer.length - 4 && buffer.subarray(peOffset, peOffset + 4).equals(Buffer.from([
80,
69,
0,
0
]));
}
function isLikelyScriptLikePathSync(targetPath) {
let stat;
try {
stat = fs.statSync(targetPath);
} catch {
return true;
}
if (!stat.isFile()) return true;
let header;
try {
const fd = fs.openSync(targetPath, "r");
try {
header = Buffer.alloc(1024);
const bytesRead = fs.readSync(fd, header, 0, header.length, 0);
header = header.subarray(0, bytesRead);
} finally {
fs.closeSync(fd);
}
} catch {
return true;
}
if (header.length === 0) return true;
if (header.subarray(0, 2).equals(Buffer.from("#!"))) return true;
if (isKnownBinaryExecutableHeader(header)) return false;
return true;
}
function unwrapArgvForMutableOperand(argv) {
let current = argv;
let baseIndex = 0;
let opaqueMultiplexerSeen = false;
while (true) {
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(current);
if (dispatchUnwrap.kind === "unwrapped") {
baseIndex += current.length - dispatchUnwrap.argv.length;
current = dispatchUnwrap.argv;
continue;
}
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(current);
if (shellMultiplexerUnwrap.kind === "unwrapped") {
if (OPAQUE_MUTABLE_SCRIPT_RUNNERS.has(shellMultiplexerUnwrap.wrapper)) opaqueMultiplexerSeen = true;
baseIndex += current.length - shellMultiplexerUnwrap.argv.length;
current = shellMultiplexerUnwrap.argv;
continue;
}
const packageManagerUnwrap = unwrapKnownPackageManagerExecInvocation(current);
if (packageManagerUnwrap) {
baseIndex += current.length - packageManagerUnwrap.length;
current = packageManagerUnwrap;
continue;
}
return {
argv: current,
baseIndex,
opaqueMultiplexerSeen
};
}
}
function unwrapKnownPackageManagerExecInvocation(argv) {
switch (normalizePackageManagerExecToken(argv[0] ?? "")) {
case "npm": return unwrapNpmExecInvocation(argv);
case "npx":
case "bunx": return unwrapDirectPackageExecInvocation(argv);
case "pnpm": return unwrapPnpmExecInvocation(argv);
default: return null;
}
}
function normalizePackageManagerExecToken(token) {
const normalized = normalizeExecutableToken(token);
if (!normalized) return normalized;
return normalized.replace(/\.(?:c|m)?js$/i, "");
}
function unwrapPnpmExecInvocation(argv) {
let idx = 1;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (token === "--") {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
if (token === "exec") {
if (idx + 1 >= argv.length) return null;
const tail = argv.slice(idx + 1);
return tail[0] === "--" ? tail.length > 1 ? tail.slice(1) : null : tail;
}
if (token === "dlx") return unwrapPnpmDlxInvocation(argv.slice(idx + 1));
if (token === "node") {
const tail = argv.slice(idx + 1);
return ["node", ...tail[0] === "--" ? tail.slice(1) : tail];
}
return null;
}
const flag = normalizeOptionFlag(token);
if (PNPM_OPTIONS_WITH_VALUE.has(flag) || PNPM_DLX_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapPnpmDlxInvocation(argv) {
let idx = 0;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (token === "--") {
const tail = argv.slice(idx + 1);
return tail.length > 0 ? tail : null;
}
if (!token.startsWith("-")) return argv.slice(idx);
const flag = normalizeOptionFlag(token);
if (flag === "-c" || flag === "--shell-mode") return null;
if (PNPM_OPTIONS_WITH_VALUE.has(flag) || PNPM_DLX_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapDirectPackageExecInvocation(argv) {
let idx = 1;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (!token.startsWith("-")) return argv.slice(idx);
const flag = normalizeOptionFlag(token);
if (flag === "-c" || flag === "--call") return null;
if (NPM_EXEC_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (NPM_EXEC_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapNpmExecInvocation(argv) {
let idx = 1;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
if (token !== "exec") return null;
idx += 1;
break;
}
if ((token === "-C" || token === "--prefix" || token === "--userconfig") && !token.includes("=")) {
idx += 2;
continue;
}
idx += 1;
}
if (idx >= argv.length) return null;
const tail = argv.slice(idx);
if (tail[0] === "--") return tail.length > 1 ? tail.slice(1) : null;
return unwrapDirectPackageExecInvocation(["npx", ...tail]);
}
function resolvePosixShellScriptOperandIndex(argv, executable) {
const supportsPlusOptions = POSIX_SHELLS_WITH_PLUS_OPTIONS.has(executable);
if (resolveInlineCommandMatch(argv, POSIX_INLINE_COMMAND_FLAGS, {
allowCombinedC: true,
isOptionToken: (token) => isPosixShellOptionToken(token, supportsPlusOptions),
stopAtFirstNonOption: true
}).valueTokenIndex !== null) return null;
let afterDoubleDash = false;
for (let i = 1; i < argv.length; i += 1) {
const token = readTrimmedArgToken(argv, i);
if (!token) continue;
if (token === "-") return null;
if (!afterDoubleDash && token === "--") {
afterDoubleDash = true;
continue;
}
if (!afterDoubleDash && token === "-s") return null;
if (!afterDoubleDash && isPosixShellOptionToken(token, supportsPlusOptions)) {
const flag = normalizeOptionFlag(token);
if (POSIX_SHELL_OPTIONS_WITH_VALUE.has(flag)) {
if (!token.includes("=")) i += 1;
continue;
}
i += advancePosixInlineOptionScan(token) - 1;
continue;
}
return i;
}
return null;
}
function resolveOptionFilteredFileOperandIndex(params) {
let afterDoubleDash = false;
for (let i = params.startIndex; i < params.argv.length; i += 1) {
const token = readTrimmedArgToken(params.argv, i);
if (!token) continue;
if (afterDoubleDash) return resolvesToExistingFileSync(token, params.cwd) ? i : null;
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-") return null;
if (token.startsWith("-")) {
if (!token.includes("=") && params.optionsWithValue?.has(token)) i += 1;
continue;
}
return resolvesToExistingFileSync(token, params.cwd) ? i : null;
}
return null;
}
function resolveOptionFilteredPositionalIndex(params) {
let afterDoubleDash = false;
for (let i = params.startIndex; i < params.argv.length; i += 1) {
const token = readTrimmedArgToken(params.argv, i);
if (!token) continue;
if (afterDoubleDash) return i;
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-") return null;
if (token.startsWith("-")) {
if (!token.includes("=") && params.optionsWithValue?.has(token)) i += 1;
continue;
}
return i;
}
return null;
}
function collectExistingFileOperandIndexes(params) {
let afterDoubleDash = false;
const hits = [];
for (let i = params.startIndex; i < params.argv.length; i += 1) {
const token = readTrimmedArgToken(params.argv, i);
if (!token) continue;
if (afterDoubleDash) {
if (resolvesToExistingFileSync(token, params.cwd)) hits.push(i);
continue;
}
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-") return {
hits: [],
sawOptionValueFile: false
};
if (token.startsWith("-")) {
const option = parseInlineOptionToken(token);
const flag = option.name;
const inlineValue = option.hasInlineValue ? option.inlineValue : void 0;
if (params.optionsWithFileValue?.has(normalizeLowercaseStringOrEmpty(flag))) {
if (inlineValue && resolvesToExistingFileSync(inlineValue, params.cwd)) {
hits.push(i);
return {
hits,
sawOptionValueFile: true
};
}
const nextToken = readTrimmedArgToken(params.argv, i + 1);
if (!inlineValue && nextToken && resolvesToExistingFileSync(nextToken, params.cwd)) {
hits.push(i + 1);
return {
hits,
sawOptionValueFile: true
};
}
}
continue;
}
if (resolvesToExistingFileSync(token, params.cwd)) hits.push(i);
}
return {
hits,
sawOptionValueFile: false
};
}
function resolveGenericInterpreterScriptOperandIndex(params) {
const collection = collectExistingFileOperandIndexes({
argv: params.argv,
startIndex: 1,
cwd: params.cwd,
optionsWithFileValue: params.optionsWithFileValue
});
if (collection.sawOptionValueFile) return null;
return collection.hits.length === 1 ? collection.hits[0] : null;
}
function resolveBunScriptOperandIndex(params) {
const directIndex = resolveOptionFilteredPositionalIndex({
argv: params.argv,
startIndex: 1,
optionsWithValue: BUN_OPTIONS_WITH_VALUE
});
if (directIndex === null) return null;
const directToken = readTrimmedArgToken(params.argv, directIndex);
if (directToken === "run") return resolveOptionFilteredFileOperandIndex({
argv: params.argv,
startIndex: directIndex + 1,
cwd: params.cwd,
optionsWithValue: BUN_OPTIONS_WITH_VALUE
});
if (BUN_SUBCOMMANDS.has(directToken)) return null;
if (!looksLikePathToken(directToken)) return null;
return directIndex;
}
function resolveDenoRunScriptOperandIndex(params) {
if (readTrimmedArgToken(params.argv, 1) !== "run") return null;
return resolveOptionFilteredFileOperandIndex({
argv: params.argv,
startIndex: 2,
cwd: params.cwd,
optionsWithValue: DENO_RUN_OPTIONS_WITH_VALUE
});
}
function hasRubyUnsafeApprovalFlag(argv) {
let afterDoubleDash = false;
for (let i = 1; i < argv.length; i += 1) {
const token = readTrimmedArgToken(argv, i);
if (!token) continue;
if (afterDoubleDash) return false;
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-I" || token === "-r") return true;
if (token.startsWith("-I") || token.startsWith("-r")) return true;
if (RUBY_UNSAFE_APPROVAL_FLAGS.has(normalizeLowercaseStringOrEmpty(token))) return true;
}
return false;
}
function hasPerlUnsafeApprovalFlag(argv) {
let afterDoubleDash = false;
for (let i = 1; i < argv.length; i += 1) {
const token = readTrimmedArgToken(argv, i);
if (!token) continue;
if (afterDoubleDash) return false;
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-I" || token === "-M" || token === "-m") return true;
if (token.startsWith("-I") || token.startsWith("-M") || token.startsWith("-m")) return true;
if (PERL_UNSAFE_APPROVAL_FLAGS.has(token)) return true;
}
return false;
}
function isMutableScriptRunner(executable) {
return GENERIC_MUTABLE_SCRIPT_RUNNERS.has(executable) || OPAQUE_MUTABLE_SCRIPT_RUNNERS.has(executable) || isInterpreterLikeSafeBin(executable);
}
function resolveMutableFileOperandIndex(argv, cwd) {
const unwrapped = unwrapArgvForMutableOperand(argv);
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
if (!executable) return null;
if (unwrapped.opaqueMultiplexerSeen || OPAQUE_MUTABLE_SCRIPT_RUNNERS.has(executable)) return null;
if (POSIX_SHELL_WRAPPERS.has(executable)) {
const shellIndex = resolvePosixShellScriptOperandIndex(unwrapped.argv, executable);
return shellIndex === null ? null : unwrapped.baseIndex + shellIndex;
}
if (MUTABLE_ARGV1_INTERPRETER_PATTERNS.some((pattern) => pattern.test(executable))) {
const operand = readTrimmedArgToken(unwrapped.argv, 1);
if (operand && operand !== "-" && !operand.startsWith("-")) return unwrapped.baseIndex + 1;
}
if (executable === "bun") {
const bunIndex = resolveBunScriptOperandIndex({
argv: unwrapped.argv,
cwd
});
if (bunIndex !== null) return unwrapped.baseIndex + bunIndex;
}
if (executable === "deno") {
const denoIndex = resolveDenoRunScriptOperandIndex({
argv: unwrapped.argv,
cwd
});
if (denoIndex !== null) return unwrapped.baseIndex + denoIndex;
}
if (executable === "ruby" && hasRubyUnsafeApprovalFlag(unwrapped.argv)) return null;
if (executable === "perl" && hasPerlUnsafeApprovalFlag(unwrapped.argv)) return null;
if (!isMutableScriptRunner(executable)) return null;
const genericIndex = resolveGenericInterpreterScriptOperandIndex({
argv: unwrapped.argv,
cwd,
optionsWithFileValue: executable === "node" || executable === "nodejs" ? NODE_OPTIONS_WITH_FILE_VALUE : void 0
});
return genericIndex === null ? null : unwrapped.baseIndex + genericIndex;
}
function shellPayloadNeedsStableBinding(shellCommand, cwd) {
const argv = splitShellArgs(shellCommand);
if (!argv || argv.length === 0) return false;
const snapshot = resolveMutableFileOperandSnapshotSync({
argv,
cwd,
shellCommand: null
});
if (!snapshot.ok) return true;
if (snapshot.snapshot) return true;
const firstToken = readTrimmedArgToken(argv, 0);
if (!resolvesToExistingFileSync(firstToken, cwd)) return false;
if (!path.isAbsolute(firstToken)) return true;
const resolvedPath = path.resolve(cwd ?? process.cwd(), firstToken);
if (pathLooksMutableForShellPayloadSync(resolvedPath)) return true;
return isLikelyScriptLikePathSync(resolvedPath);
}
function requiresStableInterpreterApprovalBindingWithShellCommand(params) {
const unwrapped = unwrapArgvForMutableOperand(params.argv);
if (unwrapped.opaqueMultiplexerSeen) return true;
if (params.shellCommand !== null) return shellPayloadNeedsStableBinding(params.shellCommand, params.cwd);
if (pnpmDlxInvocationNeedsFailClosedBinding(params.argv, params.cwd)) return true;
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
if (!executable) return false;
if (POSIX_SHELL_WRAPPERS.has(executable)) return false;
return isMutableScriptRunner(executable);
}
function pnpmDlxInvocationNeedsFailClosedBinding(argv, cwd) {
if (normalizePackageManagerExecToken(argv[0] ?? "") !== "pnpm") return false;
let idx = 1;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (token === "--") {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
if (token !== "dlx") return false;
return pnpmDlxTailNeedsFailClosedBinding(argv.slice(idx + 1), cwd);
}
const flag = normalizeOptionFlag(token);
if (PNPM_OPTIONS_WITH_VALUE.has(flag) || PNPM_DLX_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return true;
}
return false;
}
function pnpmDlxTailNeedsFailClosedBinding(argv, cwd) {
let idx = 0;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (token === "--") return pnpmDlxTailMayNeedStableBinding(argv.slice(idx + 1), cwd);
if (!token.startsWith("-")) return pnpmDlxTailMayNeedStableBinding(argv.slice(idx), cwd);
const flag = normalizeOptionFlag(token);
if (flag === "-c" || flag === "--shell-mode") return false;
if (PNPM_OPTIONS_WITH_VALUE.has(flag) || PNPM_DLX_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return true;
}
return true;
}
function pnpmDlxTailMayNeedStableBinding(argv, cwd) {
const snapshot = resolveMutableFileOperandSnapshotSync({
argv,
cwd,
shellCommand: null
});
return snapshot.ok && snapshot.snapshot !== null;
}
/** Captures file identity for a mutable script operand that approval is bound to. */
function resolveMutableFileOperandSnapshotSync(params) {
const argvIndex = resolveMutableFileOperandIndex(params.argv, params.cwd);
if (argvIndex === null) {
if (requiresStableInterpreterApprovalBindingWithShellCommand({
argv: params.argv,
shellCommand: params.shellCommand,
cwd: params.cwd
})) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command"
};
return {
ok: true,
snapshot: null
};
}
const rawOperand = readTrimmedArgToken(params.argv, argvIndex);
if (!rawOperand) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires a stable script operand"
};
const resolvedPath = path.resolve(params.cwd ?? process.cwd(), rawOperand);
let realPath;
let stat;
try {
realPath = fs.realpathSync(resolvedPath);
stat = fs.statSync(realPath);
} catch {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires an existing script operand"
};
}
if (!stat.isFile()) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires a file script operand"
};
return {
ok: true,
snapshot: {
argvIndex,
path: realPath,
sha256: hashFileContentsSync(realPath)
}
};
}
function resolveCanonicalApprovalCwdSync(cwd) {
const requestedCwd = path.resolve(cwd);
let cwdLstat;
let cwdStat;
let cwdReal;
let cwdRealStat;
try {
cwdLstat = fs.lstatSync(requestedCwd);
cwdStat = fs.statSync(requestedCwd);
cwdReal = fs.realpathSync(requestedCwd);
cwdRealStat = fs.statSync(cwdReal);
} catch {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires an existing canonical cwd"
};
}
if (!cwdStat.isDirectory()) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires cwd to be a directory"
};
if (hasMutableSymlinkPathComponentSync(requestedCwd)) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires canonical cwd (no symlink path components)"
};
if (cwdLstat.isSymbolicLink()) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires canonical cwd (no symlink cwd)"
};
if (!sameFileIdentity(cwdStat, cwdLstat) || !sameFileIdentity(cwdStat, cwdRealStat) || !sameFileIdentity(cwdLstat, cwdRealStat)) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval cwd identity mismatch"
};
return {
ok: true,
snapshot: {
cwd: cwdReal,
stat: cwdStat
}
};
}
/** Rechecks that the approved cwd still points at the same directory identity. */
function revalidateApprovedCwdSnapshot(params) {
const current = resolveCanonicalApprovalCwdSync(params.snapshot.cwd);
if (!current.ok) return false;
return sameFileIdentity(params.snapshot.stat, current.snapshot.stat);
}
function revalidateApprovedMutableFileOperand(params) {
const operand = params.argv[params.snapshot.argvIndex]?.trim();
if (!operand) return false;
const resolvedPath = path.resolve(params.cwd ?? process.cwd(), operand);
let realPath;
try {
realPath = fs.realpathSync(resolvedPath);
} catch {
return false;
}
if (realPath !== params.snapshot.path) return false;
try {
return hashFileContentsSync(realPath) === params.snapshot.sha256;
} catch {
return false;
}
}
function hardenApprovedExecutionPaths(params) {
if (!params.approvedByAsk) return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: params.cwd,
approvedCwdSnapshot: void 0
};
let hardenedCwd = params.cwd;
let approvedCwdSnapshot;
if (hardenedCwd) {
const canonicalCwd = resolveCanonicalApprovalCwdSync(hardenedCwd);
if (!canonicalCwd.ok) return canonicalCwd;
hardenedCwd = canonicalCwd.snapshot.cwd;
approvedCwdSnapshot = canonicalCwd.snapshot;
}
if (params.argv.length === 0) return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: hardenedCwd,
approvedCwdSnapshot
};
const resolution = resolveCommandResolutionFromArgv(params.argv, hardenedCwd);
if (!shouldPinExecutableForApproval({
shellCommand: params.shellCommand,
wrapperChain: resolution?.wrapperChain
})) return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: hardenedCwd,
approvedCwdSnapshot
};
const pinnedExecutable = resolution?.execution.resolvedRealPath ?? resolution?.execution.resolvedPath;
if (!pinnedExecutable) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires a stable executable path"
};
if (pinnedExecutable === params.argv[0]) return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: hardenedCwd,
approvedCwdSnapshot
};
const argv = [...params.argv];
argv[0] = pinnedExecutable;
return {
ok: true,
argv,
argvChanged: true,
cwd: hardenedCwd,
approvedCwdSnapshot
};
}
function buildSystemRunApprovalPlan(params) {
const command = resolveSystemRunCommandRequest({
command: params.command,
rawCommand: params.rawCommand
});
if (!command.ok) return {
ok: false,
message: command.message
};
if (command.argv.length === 0) return {
ok: false,
message: "command required"
};
if (command.shellPayload === null && isBlockedShellWrapperCommand(command.argv)) return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command"
};
const hardening = hardenApprovedExecutionPaths({
approvedByAsk: true,
argv: command.argv,
shellCommand: command.shellPayload,
cwd: normalizeNullableString(params.cwd) ?? void 0
});
if (!hardening.ok) return {
ok: false,
message: hardening.message
};
const commandText = formatExecCommand(hardening.argv);
const commandPreview = command.previewText?.trim() && command.previewText.trim() !== commandText ? command.previewText.trim() : null;
const mutableFileOperand = resolveMutableFileOperandSnapshotSync({
argv: hardening.argv,
cwd: hardening.cwd,
shellCommand: command.shellPayload
});
if (!mutableFileOperand.ok) return {
ok: false,
message: mutableFileOperand.message
};
return {
ok: true,
plan: {
argv: hardening.argv,
cwd: hardening.cwd ?? null,
commandText,
commandPreview,
agentId: normalizeNullableString(params.agentId),
sessionKey: normalizeNullableString(params.sessionKey),
mutableFileOperand: mutableFileOperand.snapshot ?? void 0
}
};
}
//#endregion
//#region src/node-host/invoke-system-run.ts
/** Policy and execution pipeline for approved node-host system.run requests. */
const safeBinTrustedDirWarningCache = /* @__PURE__ */ new Set();
const APPROVAL_CWD_DRIFT_DENIED_MESSAGE = "SYSTEM_RUN_DENIED: approval cwd changed before execution";
const APPROVAL_SCRIPT_OPERAND_BINDING_DENIED_MESSAGE = "SYSTEM_RUN_DENIED: approval missing script operand binding";
const APPROVAL_SCRIPT_OPERAND_DRIFT_DENIED_MESSAGE = "SYSTEM_RUN_DENIED: approval script operand changed before execution";
function hasLegacyExecPolicyOverride(exec) {
return exec?.security !== void 0 || exec?.ask !== void 0;
}
function applyExecPolicyLayer(base, layer) {
if (!layer) return base;
if (layer.mode) return {
mode: layer.mode,
...resolveExecPolicyForMode(layer.mode)
};
if (hasLegacyExecPolicyOverride(layer)) return {
security: layer.security ?? base.security,
ask: layer.ask ?? base.ask
};
return base;
}
function warnWritableTrustedDirOnce(message) {
if (safeBinTrustedDirWarningCache.has(message)) return;
safeBinTrustedDirWarningCache.add(message);
logWarn(message);
}
function normalizeDeniedReason(reason) {
switch (reason) {
case "security=deny":
case "approval-required":
case "allowlist-miss":
case "execution-plan-miss":
case "companion-unavailable":
case "permission:screenRecording": return reason;
default: return "approval-required";
}
}
function resolveAgentExecConfig(cfg, agentId) {
if (!agentId) return;
const normalizedAgentId = normalizeAgentId(agentId);
return (cfg.agents?.list?.find((candidate) => candidate !== null && typeof candidate === "object" && normalizeAgentId(candidate.id) === normalizedAgentId))?.tools?.exec;
}
/** Resolves the effective exec security/ask policy for one system.run request. */
function resolveEffectiveSystemRunExecPolicy(params) {
const agentExec = resolveAgentExecConfig(params.cfg, params.agentId);
const globalExec = params.cfg.tools?.exec;
const layeredPolicy = applyExecPolicyLayer(applyExecPolicyLayer({
security: params.defaultSecurity,
ask: params.defaultAsk
}, globalExec), agentExec);
const modePolicy = resolveExecModePolicy({
mode: layeredPolicy.mode,
security: layeredPolicy.security,
ask: layeredPolicy.ask
});
const approvals = resolveExecApprovals(params.agentId, {
security: modePolicy.security,
ask: modePolicy.ask,
requireSocket: params.requireSocket
});
return {
agentExec,
globalExec,
approvals,
security: minSecurity(modePolicy.security, approvals.agent.security),
ask: maxAsk(modePolicy.ask, approvals.agent.ask),
autoReview: modePolicy.autoReview
};
}
async function resolveSystemRunAutoReviewer(params) {
if (params.opts.autoReviewer) return params.opts.autoReviewer;
const { createModelExecAutoReviewer } = await import("./exec-auto-reviewer-Bs5cW_kH.js");
return createModelExecAutoReviewer({
cfg: params.cfg,
agentId: params.agentId,
reviewer: params.agentExec?.reviewer ?? params.globalExec?.reviewer
});
}
async function loadSystemRunConfig(opts) {
if (opts.getRuntimeConfig) return opts.getRuntimeConfig();
const { getRuntimeConfig } = await import("./config/config.js");
return getRuntimeConfig();
}
async function sendSystemRunDenied(opts, execution, params) {
await opts.sendNodeEvent(opts.client, "exec.denied", opts.buildExecEventPayload({
sessionKey: execution.sessionKey,
runId: execution.runId,
host: "node",
command: execution.commandText,
reason: params.reason,
suppressNotifyOnExit: execution.suppressNotifyOnExit
}));
await opts.sendInvokeResult({
ok: false,
error: {
code: "UNAVAILABLE",
message: params.message
}
});
}
async function sendSystemRunCompleted(opts, execution, result, payloadJSON) {
await opts.sendExecFinishedEvent({
sessionKey: execution.sessionKey,
runId: execution.runId,
commandText: execution.commandText,
result,
suppressNotifyOnExit: execution.suppressNotifyOnExit
});
await opts.sendInvokeResult({
ok: true,
payloadJSON
});
}
function argvArraysMatch(left, right) {
return left !== void 0 && left.length === right.length && left.every((entry, index) => entry === right[index]);
}
async function parseSystemRunPhase(opts) {
const command = resolveSystemRunCommandRequest({
command: opts.params.command,
rawCommand: opts.params.rawCommand
});
if (!command.ok) {
await opts.sendInvokeResult({
ok: false,
error: {
code: "INVALID_REQUEST",
message: command.message
}
});
return null;
}
if (command.argv.length === 0) {
await opts.sendInvokeResult({
ok: false,
error: {
code: "INVALID_REQUEST",
message: "command required"
}
});
return null;
}
const shellPayload = command.shellPayload;
const shellWrapperInvocation = isShellWrapperInvocation(command.argv);
const commandText = command.commandText;
const approvalPlan = opts.params.systemRunPlan === void 0 ? null : normalizeSystemRunApprovalPlan(opts.params.systemRunPlan);
if (opts.params.systemRunPlan !== void 0 && !approvalPlan) {
await opts.sendInvokeResult({
ok: false,
error: {
code: "INVALID_REQUEST",
message: "systemRunPlan invalid"
}
});
return null;
}
const agentId = normalizeOptionalString(opts.params.agentId);
const sessionKey = normalizeOptionalString(opts.params.sessionKey) ?? "node";
const runId = normalizeOptionalString(opts.params.runId) ?? crypto.randomUUID();
const suppressNotifyOnExit = opts.params.suppressNotifyOnExit === true;
const envAssignmentKeys = extractEnvAssignmentKeysFromDispatchWrappers(command.argv);
const envAssignmentDiagnostics = inspectHostExecEnvOverrides({
overrides: envAssignmentKeys.length > 0 ? Object.fromEntries(envAssignmentKeys.map((key) => [key, "1"])) : void 0,
blockPathOverrides: true
});
if (envAssignmentDiagnostics.rejectedOverrideBlockedKeys.length > 0) {
await opts.sendInvokeResult({
ok: false,
error: {
code: "INVALID_REQUEST",
message: `SYSTEM_RUN_DENIED: command env assignment rejected (blocked env assignment keys: ${envAssignmentDiagnostics.rejectedOverrideBlockedKeys.join(", ")})`
}
});
return null;
}
const envOverrideDiagnostics = inspectHostExecEnvOverrides({
overrides: opts.params.env ?? void 0,
blockPathOverrides: true
});
if (envOverrideDiagnostics.rejectedOverrideBlockedKeys.length > 0 || envOverrideDiagnostics.rejectedOverrideInvalidKeys.length > 0) {
const details = [];
if (envOverrideDiagnostics.rejectedOverrideBlockedKeys.length > 0) details.push(`blocked override keys: ${envOverrideDiagnostics.rejectedOverrideBlockedKeys.join(", ")}`);
if (envOverrideDiagnostics.rejectedOverrideInvalidKeys.length > 0) details.push(`invalid non-portable override keys: ${envOverrideDiagnostics.rejectedOverrideInvalidKeys.join(", ")}`);
await opts.sendInvokeResult({
ok: false,
error: {
code: "INVALID_REQUEST",
message: `SYSTEM_RUN_DENIED: environment override rejected (${details.join("; ")})`
}
});
return null;
}
const envOverrides = sanitizeSystemRunEnvOverrides({
overrides: opts.params.env ?? void 0,
shellWrapper: shellWrapperInvocation
});
return {
argv: command.argv,
shellPayload,
shellWrapperInvocation,
commandText,
commandPreview: command.previewText,
approvalPlan,
agentId,
sessionKey,
runId,
execution: {
sessionKey,
runId,
commandText,
suppressNotifyOnExit
},
approvalDecision: resolveExecApprovalDecision(opts.params.approvalDecision),
envOverrides,
env: opts.sanitizeEnv(envOverrides),
cwd: normalizeOptionalString(opts.params.cwd),
timeoutMs: opts.params.timeoutMs ?? void 0,
needsScreenRecording: opts.params.needsScreenRecording === true,
approved: opts.params.approved === true,
suppressNotifyOnExit
};
}
async function evaluateSystemRunPolicyPhase(opts, parsed) {
const cfg = await loadSystemRunConfig(opts);
const effectivePolicy = resolveEffectiveSystemRunExecPolicy({
cfg,
agentId: parsed.agentId,
defaultSecurity: opts.resolveExecSecurity(void 0),
defaultAsk: opts.resolveExecAsk(void 0),
requireSocket: opts.preferMacAppExecHost
});
const { agentExec, globalExec, approvals, security, ask } = effectivePolicy;
const autoAllowSkills = approvals.agent.autoAllowSkills;
const { safeBins, safeBinProfiles, trustedSafeBinDirs } = resolveExecSafeBinRuntimePolicy({
global: cfg.tools?.exec,
local: agentExec,
onWarning: warnWritableTrustedDirOnce
});
const bins = autoAllowSkills ? await opts.skillBins.current() : [];
const allowlistEvaluation = evaluateSystemRunAllowlist({
shellCommand: parsed.shellPayload,
argv: parsed.argv,
approvals,
security,
safeBins,
safeBinProfiles,
trustedSafeBinDirs,
cwd: parsed.cwd,
env: parsed.env,
skillBins: bins,
autoAllowSkills
});
const { allowlistMatches, segments, segmentAllowlistEntries, segmentSatisfiedBy } = allowlistEvaluation;
let { analysisOk, allowlistSatisfied } = allowlistEvaluation;
const strictInlineEval = agentExec?.strictInlineEval === true || cfg.tools?.exec?.strictInlineEval === true;
const inlineEvalHit = strictInlineEval ? detectPolicyInlineEval(segments) : null;
const isWindows = process.platform === "win32";
const cmdDetectionArgv = resolveShellWrapperTransportArgv(parsed.argv) ?? parsed.argv;
const cmdInvocation = opts.isCmdExeInvocation(cmdDetectionArgv);
const durableApprovalSatisfied = hasDurableExecApproval({
analysisOk,
segmentAllowlistEntries,
allowlist: approvals.allowlist,
commandText: parsed.commandText
});
const inlineEvalExecutableTrusted = inlineEvalHit !== null && segmentAllowlistEntries.some((entry) => entry?.source === "allow-always");
let approvalDecision = parsed.approvalDecision;
let policy = evaluateSystemRunPolicy({
security,
ask,
analysisOk,
allowlistSatisfied,
durableApprovalSatisfied: durableApprovalSatisfied || inlineEvalExecutableTrusted,
approvalDecision,
approved: parsed.approved,
isWindows,
cmdInvocation,
shellWrapperInvocation: parsed.shellPayload !== null
});
const requiresSecurityAuditSuppressionApproval = commandRequiresSecurityAuditSuppressionApproval({
command: parsed.commandText,
cwd: parsed.cwd,
env: parsed.env,
segments
}) && !(security === "full" && ask === "off");
if (requiresSecurityAuditSuppressionApproval && !policy.approvedByAsk) policy = {
allowed: false,
eventReason: "approval-required",
errorMessage: "SYSTEM_RUN_DENIED: approval required",
analysisOk: policy.analysisOk,
allowlistSatisfied: policy.allowlistSatisfied,
shellWrapperBlocked: policy.shellWrapperBlocked,
windowsShellWrapperBlocked: policy.windowsShellWrapperBlocked,
requiresAsk: true,
approvalDecision: policy.approvalDecision,
approvedByAsk: policy.approvedByAsk
};
let autoReviewDeferredMessage;
analysisOk = policy.analysisOk;
allowlistSatisfied = policy.allowlistSatisfied;
if (inlineEvalHit !== null && !policy.approvedByAsk && (policy.allowed ? true : policy.eventReason !== "security=deny")) {
await sendSystemRunDenied(opts, parsed.execution, {
reason: "approval-required",
message: `SYSTEM_RUN_DENIED: approval required (${describeInterpreterInlineEval(inlineEvalHit)} requires explicit approval in strictInlineEval mode)`
});
return null;
}
if (!policy.allowed) {
const [autoReviewSegment] = segments;
const directAutoReviewArgvMatchesRequest = parsed.shellPayload !== null || argvArraysMatch(autoReviewSegment?.argv, parsed.argv);
const autoReviewArgv = segments.length === 1 && directAutoReviewArgvMatchesRequest && (parsed.shellPayload === null || autoReviewSegment?.raw !== void 0 && autoReviewSegment.raw.trim() === parsed.shellPayload.trim()) ? autoReviewSegment?.argv : void 0;
if (effectivePolicy.autoReview && ask !== "always" && analysisOk && autoReviewArgv !== void 0 && parsed.approvalPlan !== null && inlineEvalHit === null && !requiresSecurityAuditSuppressionApproval && policy.eventReason !== "security=deny") {
const decision = await (await resolveSystemRunAutoReviewer({
opts,
cfg,
agentId: parsed.agentId,
agentExec,
globalExec
}))({
command: parsed.commandText,
argv: autoReviewArgv,
cwd: parsed.cwd,
envKeys: Object.keys(parsed.envOverrides ?? {}).toSorted(),
host: "node",
reason: policy.eventReason === "allowlist-miss" ? "allowlist-miss" : "approval-required",
analysis: {
parsed: analysisOk,
allowlistMatched: allowlistSatisfied,
durableApprovalMatched: durableApprovalSatisfied,
inlineEval: false,
shellWrapper: parsed.shellWrapperInvocation
},
agent: {
id: parsed.agentId,
sessionKey: parsed.sessionKey
}
});
if (decision.decision === "allow-once") {
approvalDecision = "allow-once";
policy = evaluateSystemRunPolicy({
security,
ask,
analysisOk,
allowlistSatisfied,
durableApprovalSatisfied: durableApprovalSatisfied || inlineEvalExecutableTrusted,
approvalDecision,
approved: true,
isWindows,
cmdInvocation,
shellWrapperInvocation: parsed.shellPayload !== null
});
} else autoReviewDeferredMessage = `${policy.errorMessage} (exec auto-review deferred to human approval: ${decision.rationale})`;
}
}
if (!policy.allowed) {
await sendSystemRunDenied(opts, parsed.execution, {
reason: policy.eventReason,
message: autoReviewDeferredMessage ?? policy.errorMessage
});
return null;
}
if (policy.shellWrapperBlocked && !policy.approvedByAsk && !durableApprovalSatisfied) {
await sendSystemRunDenied(opts, parsed.execution, {
reason: "approval-required",
message: "SYSTEM_RUN_DENIED: approval required"
});
return null;
}
const hardenedPaths = hardenApprovedExecutionPaths({
approvedByAsk: policy.approvedByAsk,
argv: parsed.argv,
shellCommand: parsed.shellPayload,
cwd: parsed.cwd
});
if (!hardenedPaths.ok) {
await sendSystemRunDenied(opts, parsed.execution, {
reason: "approval-required",
message: hardenedPaths.message
});
return null;
}
const approvedCwdSnapshot = policy.approvedByAsk ? hardenedPaths.approvedCwdSnapshot : void 0;
if (policy.approvedByAsk && hardenedPaths.cwd && !approvedCwdSnapshot) {
await sendSystemRunDenied(opts, parsed.execution, {
reason: "approval-required",
message: APPROVAL_CWD_DRIFT_DENIED_MESSAGE
});
return null;
}
const plannedAllowlistArgv = resolvePlannedAllowlistArgv({
security,
shellCommand: parsed.shellPayload,
policy,
segments
});
if (plannedAllowlistArgv === null) {
await sendSystemRunDenied(opts, parsed.execution, {
reason: "execution-plan-miss",
message: "SYSTEM_RUN_DENIED: execution plan mismatch"
});
return null;
}
return {
...parsed,
approvalDecision,
argv: hardenedPaths.argv,
cwd: hardenedPaths.cwd,
approvals,
security,
ask,
policy,
durableApprovalSatisfied,
strictInlineEval,
inlineEvalHit,
allowlistMatches,
analysisOk,
allowlistSatisfied,
safeBins,
safeBinProfiles,
trustedSafeBinDirs,
skillBins: bins,
autoAllowSkills,
segments,
segmentSatisfiedBy,
plannedAllowlistArgv: plannedAllowlistArgv ?? void 0,
isWindows,
approvedCwdSnapshot
};
}
async function executeSystemRunPhase(opts, phase) {
if (phase.approvedCwdSnapshot && !revalidateApprovedCwdSnapshot({ snapshot: phase.approvedCwdSnapshot })) {
logWarn(`security: system.run approval cwd drift blocked (runId=${phase.runId})`);
await sendSystemRunDenied(opts, phase.execution, {
reason: "approval-required",
message: APPROVAL_CWD_DRIFT_DENIED_MESSAGE
});
return;
}
const expectedMutableFileOperand = phase.approvalPlan ? resolveMutableFileOperandSnapshotSync({
argv: phase.argv,
cwd: phase.cwd,
shellCommand: phase.shellPayload
}) : null;
if (expectedMutableFileOperand && !expectedMutableFileOperand.ok) {
logWarn(`security: system.run approval script binding blocked (runId=${phase.runId})`);
await sendSystemRunDenied(opts, phase.execution, {
reason: "approval-required",
message: expectedMutableFileOperand.message
});
return;
}
if (expectedMutableFileOperand?.snapshot && !phase.approvalPlan?.mutableFileOperand) {
logWarn(`security: system.run approval script binding missing (runId=${phase.runId})`);
await sendSystemRunDenied(opts, phase.execution, {
reason: "approval-required",
message: APPROVAL_SCRIPT_OPERAND_BINDING_DENIED_MESSAGE
});
return;
}
if (phase.approvalPlan?.mutableFileOperand && !revalidateApprovedMutableFileOperand({
snapshot: phase.approvalPlan.mutableFileOperand,
argv: phase.argv,
cwd: phase.cwd
})) {
logWarn(`security: system.run approval script drift blocked (runId=${phase.runId})`);
await sendSystemRunDenied(opts, phase.execution, {
reason: "approval-required",
message: APPROVAL_SCRIPT_OPERAND_DRIFT_DENIED_MESSAGE
});
return;
}
const execArgv = resolveSystemRunExecArgv({
plannedAllowlistArgv: phase.plannedAllowlistArgv,
argv: phase.argv,
security: phase.security,
approvals: phase.approvals,
safeBins: phase.safeBins,
safeBinProfiles: phase.safeBinProfiles,
trustedSafeBinDirs: phase.trustedSafeBinDirs,
skillBins: phase.skillBins,
autoAllowSkills: phase.autoAllowSkills,
isWindows: phase.isWindows,
policy: phase.policy,
shellCommand: phase.shellPayload,
segments: phase.segments,
segmentSatisfiedBy: phase.segmentSatisfiedBy,
cwd: phase.cwd,
env: phase.env
});
if (!execArgv) {
await sendSystemRunDenied(opts, phase.execution, {
reason: "execution-plan-miss",
message: "SYSTEM_RUN_DENIED: execution plan mismatch"
});
return;
}
if (opts.preferMacAppExecHost) {
const execRequest = {
command: execArgv,
rawCommand: execArgv === phase.argv ? phase.commandText || null : formatExecCommand(execArgv),
cwd: phase.cwd ?? null,
env: phase.envOverrides ?? null,
timeoutMs: phase.timeoutMs ?? null,
needsScreenRecording: phase.needsScreenRecording,
agentId: phase.agentId ?? null,
sessionKey: phase.sessionKey ?? null,
approvalDecision: phase.approvalDecision
};
const response = await opts.runViaMacAppExecHost({
approvals: phase.approvals,
request: execRequest
});
if (!response) {
if (opts.execHostEnforced || !opts.execHostFallbackAllowed) {
await sendSystemRunDenied(opts, phase.execution, {
reason: "companion-unavailable",
message: "COMPANION_APP_UNAVAILABLE: macOS app exec host unreachable"
});
return;
}
} else if (!response.ok) {
await sendSystemRunDenied(opts, phase.execution, {
reason: normalizeDeniedReason(response.error.reason),
message: response.error.message
});
return;
} else {
const result = response.payload;
await sendSystemRunCompleted(opts, phase.execution, result, JSON.stringify(result));
return;
}
}
if (phase.policy.approvalDecision === "allow-always" && phase.inlineEvalHit === null) {
if ((phase.policy.analysisOk ? persistAllowAlwaysPatterns({
approvals: phase.approvals.file,
agentId: phase.agentId,
segments: phase.segments,
cwd: phase.cwd,
env: phase.env,
platform: process.platform,
commandText: phase.commandText,
strictInlineEval: phase.strictInlineEval
}) : []).length === 0) addDurableCommandApproval(phase.approvals.file, phase.agentId, phase.commandText);
}
recordAllowlistMatchesUse({
approvals: phase.approvals.file,
agentId: phase.agentId,
matches: phase.allowlistMatches,
command: phase.commandText,
resolvedPath: resolveApprovalAuditTrustPath(phase.segments[0]?.resolution ?? null, phase.cwd)
});
if (phase.needsScreenRecording) {
await sendSystemRunDenied(opts, phase.execution, {
reason: "permission:screenRecording",
message: "PERMISSION_MISSING: screenRecording"
});
return;
}
const result = await opts.runCommand(execArgv, phase.cwd, phase.env, phase.timeoutMs);
applyOutputTruncation(result);
await sendSystemRunCompleted(opts, phase.execution, result, JSON.stringify({
exitCode: result.exitCode,
timedOut: result.timedOut,
success: result.success,
stdout: result.stdout,
stderr: result.stderr,
error: result.error ?? null
}));
}
/** Executes a validated system.run request, emitting lifecycle events and approvals. */
async function handleSystemRunInvoke(opts) {
const parsed = await parseSystemRunPhase(opts);
if (!parsed) return;
const policyPhase = await evaluateSystemRunPolicyPhase(opts, parsed);
if (!policyPhase) return;
await executeSystemRunPhase(opts, policyPhase);
}
//#endregion
//#region src/node-host/plugin-node-host.ts
/**
* Plugin node-host command registry bridge.
*
* Node hosts load the active plugin registry, expose registered capabilities
* and commands, and dispatch incoming node-host commands by exact command id.
*/
let pluginRegistryLoaderModulePromise;
async function loadPluginRegistryLoaderModule() {
pluginRegistryLoaderModulePromise ??= import("./runtime-registry-loader-PUy5EEon.js");
return await pluginRegistryLoaderModulePromise;
}
/** Ensure plugin registry data is loaded before node-host command dispatch. */
async function ensureNodeHostPluginRegistry(params) {
(await loadPluginRegistryLoaderModule()).ensurePluginRegistryLoaded({
scope: "all",
config: params.config,
activationSourceConfig: params.config,
env: params.env
});
}
/** List registered node-host capabilities and command ids in deterministic order. */
function listRegisteredNodeHostCapsAndCommands() {
const registry = getActivePluginRegistry();
const caps = /* @__PURE__ */ new Set();
const commands = /* @__PURE__ */ new Set();
for (const entry of registry?.nodeHostCommands ?? []) {
if (entry.command.cap) caps.add(entry.command.cap);
commands.add(entry.command.command);
}
return {
caps: [...caps].toSorted((left, right) => left.localeCompare(right)),
commands: [...commands].toSorted((left, right) => left.localeCompare(right))
};
}
/** Invoke a registered node-host plugin command, or return null for unknown commands. */
async function invokeRegisteredNodeHostCommand(command, paramsJSON) {
const match = (getActivePluginRegistry()?.nodeHostCommands ?? []).find((entry) => entry.command.command === command);
if (!match) return null;
return await match.command.handle(paramsJSON);
}
//#endregion
//#region src/node-host/invoke.ts
/** Node-host command dispatcher for system commands, approvals, env policy, and plugin commands. */
const OUTPUT_CAP = 2e5;
const OUTPUT_EVENT_TAIL = 2e4;
const DEFAULT_NODE_PATH$1 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
const execHostEnforced = normalizeLowercaseStringOrEmpty(process.env.OPENCLAW_NODE_EXEC_HOST ?? "") === "app";
const execHostFallbackAllowed = normalizeLowercaseStringOrEmpty(process.env.OPENCLAW_NODE_EXEC_FALLBACK ?? "") !== "0";
const preferMacAppExecHost = process.platform === "darwin" && execHostEnforced;
function buildEnvOverrideRejectionMessage(params) {
const details = [];
if (params.rejectedOverrideBlockedKeys.length > 0) details.push(`blocked override keys: ${params.rejectedOverrideBlockedKeys.join(", ")}`);
if (params.rejectedOverrideInvalidKeys.length > 0) details.push(`invalid non-portable override keys: ${params.rejectedOverrideInvalidKeys.join(", ")}`);
return `SYSTEM_RUN_DENIED: environment override rejected (${details.join("; ")})`;
}
function buildSystemRunPrepareCoverageEnv(params) {
const diagnostics = inspectHostExecEnvOverrides({
overrides: params.env ?? void 0,
blockPathOverrides: true
});
if (diagnostics.rejectedOverrideBlockedKeys.length > 0 || diagnostics.rejectedOverrideInvalidKeys.length > 0) return {
ok: false,
message: buildEnvOverrideRejectionMessage(diagnostics)
};
return {
ok: true,
env: sanitizeEnv(sanitizeSystemRunEnvOverrides({
overrides: params.env ?? void 0,
shellWrapper: isShellWrapperInvocation(params.argv)
}))
};
}
function buildSystemRunAllowAlwaysCoverage(params) {
const cwd = params.cwd ?? void 0;
const shellWrapper = extractShellWrapperCommand(params.argv, params.rawCommand);
if (shellWrapper.isWrapper) {
if (!shellWrapper.command) return {
complete: false,
patterns: []
};
const analysis = analyzeShellCommand({
command: shellWrapper.command,
cwd,
env: params.env,
platform: process.platform
});
if (!analysis.ok) return {
complete: false,
patterns: []
};
return resolveAllowAlwaysPatternCoverage({
segments: analysis.segments,
cwd,
env: params.env,
platform: process.platform,
strictInlineEval: params.strictInlineEval
});
}
const analysis = analyzeArgvCommand({
argv: params.argv,
cwd,
env: params.env
});
if (!analysis.ok) return {
complete: false,
patterns: []
};
return resolveAllowAlwaysPatternCoverage({
segments: analysis.segments,
cwd,
env: params.env,
platform: process.platform,
strictInlineEval: params.strictInlineEval
});
}
function resolveExecSecurity(value) {
return value === "deny" || value === "allowlist" || value === "full" ? value : "allowlist";
}
function isCmdExeInvocation(argv) {
const token = argv[0]?.trim();
if (!token) return false;
const base = normalizeLowercaseStringOrEmpty(path.win32.basename(token));
return base === "cmd.exe" || base === "cmd";
}
function resolveExecAsk(value) {
return value === "off" || value === "on-miss" || value === "always" ? value : "on-miss";
}
/** Builds a sanitized execution environment with controlled PATH and approved overrides. */
function sanitizeEnv(overrides) {
return sanitizeHostExecEnv({
overrides,
blockPathOverrides: true
});
}
function truncateOutput(raw, maxChars) {
if (raw.length <= maxChars) return {
text: raw,
truncated: false
};
return {
text: `... (truncated) ${raw.slice(raw.length - maxChars)}`,
truncated: true
};
}
function decodeCapturedOutputBuffer(params) {
return decodeWindowsOutputBuffer(params);
}
function redactExecApprovals(file) {
const socketPath = file.socket?.path?.trim();
return {
...file,
socket: socketPath ? { path: socketPath } : void 0
};
}
function requireExecApprovalsBaseHash(params, snapshot) {
if (!snapshot.exists) return;
if (!snapshot.hash) throw new Error("INVALID_REQUEST: exec approvals base hash unavailable; reload and retry");
const baseHash = typeof params.baseHash === "string" ? params.baseHash.trim() : "";
if (!baseHash) throw new Error("INVALID_REQUEST: exec approvals base hash required; reload and retry");
if (baseHash !== snapshot.hash) throw new Error("INVALID_REQUEST: exec approvals changed; reload and retry");
}
async function runCommand(argv, cwd, env, timeoutMs) {
return await new Promise((resolve) => {
const stdoutChunks = [];
const stderrChunks = [];
let outputLen = 0;
let truncated = false;
let timedOut = false;
let settled = false;
const windowsEncoding = resolveWindowsConsoleEncoding();
const child = spawn(argv[0], argv.slice(1), {
cwd,
env,
stdio: [
"ignore",
"pipe",
"pipe"
],
windowsHide: true
});
const onChunk = (chunk, target) => {
if (outputLen >= OUTPUT_CAP) {
truncated = true;
return;
}
const remaining = OUTPUT_CAP - outputLen;
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
outputLen += slice.length;
if (target === "stdout") stdoutChunks.push(slice);
else stderrChunks.push(slice);
if (chunk.length > remaining) truncated = true;
};
child.stdout?.on("data", (chunk) => onChunk(chunk, "stdout"));
child.stderr?.on("data", (chunk) => onChunk(chunk, "stderr"));
let timer;
if (timeoutMs && timeoutMs > 0) timer = setTimeout(() => {
timedOut = true;
try {
child.kill("SIGKILL");
} catch {}
}, timeoutMs);
const finalize = (exitCode, error) => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
const stdout = decodeCapturedOutputBuffer({
buffer: Buffer.concat(stdoutChunks),
windowsEncoding
});
const stderr = decodeCapturedOutputBuffer({
buffer: Buffer.concat(stderrChunks),
windowsEncoding
});
resolve({
exitCode,
timedOut,
success: exitCode === 0 && !timedOut && !error,
stdout,
stderr,
error: error ?? null,
truncated
});
};
child.on("error", (err) => {
finalize(void 0, err.message);
});
child.on("exit", (code) => {
finalize(code === null ? void 0 : code, null);
});
});
}
function resolveEnvPath(env) {
return (env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path ?? DEFAULT_NODE_PATH$1).split(path.delimiter).filter(Boolean);
}
function resolveExecutable(bin, env) {
if (bin.includes("/") || bin.includes("\\")) return null;
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? process.env.PathExt ?? ".EXE;.CMD;.BAT;.COM").split(";").map((ext) => normalizeLowercaseStringOrEmpty(ext)) : [""];
for (const dir of resolveEnvPath(env)) for (const ext of extensions) {
const candidate = path.join(dir, bin + ext);
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
async function handleSystemWhich(params, env) {
const bins = normalizeStringEntries(params.bins);
const found = {};
for (const bin of bins) {
const pathLocal = resolveExecutable(bin, env);
if (pathLocal) found[bin] = pathLocal;
}
return { bins: found };
}
function buildExecEventPayload(payload) {
if (!payload.output) return payload;
const trimmed = payload.output.trim();
if (!trimmed) return payload;
const { text } = truncateOutput(trimmed, OUTPUT_EVENT_TAIL);
return {
...payload,
output: text
};
}
async function sendExecFinishedEvent(params) {
const combined = [
params.result.stdout,
params.result.stderr,
params.result.error
].filter(Boolean).join("\n");
await sendNodeEvent(params.client, "exec.finished", buildExecEventPayload({
sessionKey: params.sessionKey,
runId: params.runId,
host: "node",
command: params.commandText,
exitCode: params.result.exitCode ?? void 0,
timedOut: params.result.timedOut,
success: params.result.success,
output: combined,
suppressNotifyOnExit: params.suppressNotifyOnExit
}));
}
async function runViaMacAppExecHost(params) {
const { approvals, request } = params;
return await requestExecHostViaSocket({
socketPath: approvals.socketPath,
token: approvals.token,
request
});
}
async function sendJsonPayloadResult(client, frame, payload) {
await sendInvokeResult(client, frame, {
ok: true,
payloadJSON: JSON.stringify(payload)
});
}
async function sendRawPayloadResult(client, frame, payloadJSON) {
await sendInvokeResult(client, frame, {
ok: true,
payloadJSON
});
}
async function sendErrorResult(client, frame, code, message) {
await sendInvokeResult(client, frame, {
ok: false,
error: {
code,
message
}
});
}
async function sendInvalidRequestResult(client, frame, err) {
await sendErrorResult(client, frame, "INVALID_REQUEST", String(err));
}
/** Handles one node-host command invocation payload and returns serialized results. */
async function handleInvoke(frame, client, skillBins) {
const command = frame.command ?? "";
if (command === "system.execApprovals.get") {
try {
ensureExecApprovals();
const snapshot = readExecApprovalsSnapshot();
await sendJsonPayloadResult(client, frame, {
path: snapshot.path,
exists: snapshot.exists,
hash: snapshot.hash,
file: redactExecApprovals(snapshot.file)
});
} catch (err) {
const message = String(err);
await sendErrorResult(client, frame, normalizeLowercaseStringOrEmpty(message).includes("timed out") ? "TIMEOUT" : "INVALID_REQUEST", message);
}
return;
}
if (command === "system.execApprovals.set") {
try {
const params = decodeParams(frame.paramsJSON);
if (!params.file || typeof params.file !== "object") throw new Error("INVALID_REQUEST: exec approvals file required");
ensureExecApprovals();
const snapshot = readExecApprovalsSnapshot();
requireExecApprovalsBaseHash(params, snapshot);
saveExecApprovals(mergeExecApprovalsSocketDefaults({
normalized: normalizeExecApprovals(params.file),
current: snapshot.file
}));
const nextSnapshot = readExecApprovalsSnapshot();
await sendJsonPayloadResult(client, frame, {
path: nextSnapshot.path,
exists: nextSnapshot.exists,
hash: nextSnapshot.hash,
file: redactExecApprovals(nextSnapshot.file)
});
} catch (err) {
await sendInvalidRequestResult(client, frame, err);
}
return;
}
if (command === "system.which") {
try {
const params = decodeParams(frame.paramsJSON);
if (!Array.isArray(params.bins)) throw new Error("INVALID_REQUEST: bins required");
await sendJsonPayloadResult(client, frame, await handleSystemWhich(params, sanitizeEnv(void 0)));
} catch (err) {
await sendInvalidRequestResult(client, frame, err);
}
return;
}
try {
const pluginNodeHostResult = await invokeRegisteredNodeHostCommand(command, frame.paramsJSON);
if (pluginNodeHostResult !== null) {
await sendRawPayloadResult(client, frame, pluginNodeHostResult);
return;
}
} catch (err) {
await sendInvalidRequestResult(client, frame, err);
return;
}
if (command === "system.run.prepare") {
try {
const params = decodeParams(frame.paramsJSON);
const prepared = buildSystemRunApprovalPlan(params);
if (!prepared.ok) {
await sendErrorResult(client, frame, "INVALID_REQUEST", prepared.message);
return;
}
const prepareEnv = buildSystemRunPrepareCoverageEnv({
argv: prepared.plan.argv,
env: params.env ?? void 0
});
if (!prepareEnv.ok) {
await sendErrorResult(client, frame, "INVALID_REQUEST", prepareEnv.message);
return;
}
const { getRuntimeConfig } = await import("./config/config.js");
const execPolicy = resolveEffectiveSystemRunExecPolicy({
cfg: getRuntimeConfig(),
agentId: prepared.plan.agentId ?? void 0,
defaultSecurity: resolveExecSecurity(void 0),
defaultAsk: resolveExecAsk(void 0),
requireSocket: preferMacAppExecHost
});
await sendJsonPayloadResult(client, frame, {
plan: prepared.plan,
execPolicy: {
security: execPolicy.security,
ask: execPolicy.ask
},
allowAlwaysCoverage: buildSystemRunAllowAlwaysCoverage({
argv: prepared.plan.argv,
rawCommand: typeof params.rawCommand === "string" ? params.rawCommand : null,
cwd: prepared.plan.cwd,
env: prepareEnv.env,
strictInlineEval: params.strictInlineEval === true
})
});
} catch (err) {
await sendInvalidRequestResult(client, frame, err);
}
return;
}
if (command !== "system.run") {
await sendErrorResult(client, frame, "UNAVAILABLE", "command not supported");
return;
}
let params;
try {
params = decodeParams(frame.paramsJSON);
} catch (err) {
await sendInvalidRequestResult(client, frame, err);
return;
}
if (!Array.isArray(params.command) || params.command.length === 0) {
await sendErrorResult(client, frame, "INVALID_REQUEST", "command required");
return;
}
await handleSystemRunInvoke({
client,
params,
skillBins,
execHostEnforced,
execHostFallbackAllowed,
resolveExecSecurity,
resolveExecAsk,
isCmdExeInvocation,
sanitizeEnv,
runCommand,
runViaMacAppExecHost,
sendNodeEvent,
buildExecEventPayload,
sendInvokeResult: async (result) => {
await sendInvokeResult(client, frame, result);
},
sendExecFinishedEvent: async ({ sessionKey, runId, commandText, result }) => {
await sendExecFinishedEvent({
client,
sessionKey,
runId,
commandText,
result
});
},
preferMacAppExecHost
});
}
function decodeParams(raw) {
if (!raw) throw new Error("INVALID_REQUEST: paramsJSON required");
try {
return JSON.parse(raw);
} catch {
throw new Error("INVALID_REQUEST: paramsJSON malformed JSON");
}
}
function coerceNodeInvokePayload(payload) {
if (!payload || typeof payload !== "object") return null;
const obj = payload;
const id = typeof obj.id === "string" ? obj.id.trim() : "";
const nodeId = typeof obj.nodeId === "string" ? obj.nodeId.trim() : "";
const command = typeof obj.command === "string" ? obj.command.trim() : "";
if (!id || !nodeId || !command) return null;
return {
id,
nodeId,
command,
paramsJSON: typeof obj.paramsJSON === "string" ? obj.paramsJSON : obj.params !== void 0 ? JSON.stringify(obj.params) : null,
timeoutMs: typeof obj.timeoutMs === "number" ? obj.timeoutMs : null,
idempotencyKey: typeof obj.idempotencyKey === "string" ? obj.idempotencyKey : null
};
}
async function sendInvokeResult(client, frame, result) {
try {
await client.request("node.invoke.result", buildNodeInvokeResultParams(frame, result));
} catch {}
}
function buildNodeInvokeResultParams(frame, result) {
const params = {
id: frame.id,
nodeId: frame.nodeId,
ok: result.ok
};
if (result.payload !== void 0) params.payload = result.payload;
if (typeof result.payloadJSON === "string") params.payloadJSON = result.payloadJSON;
if (result.error) params.error = result.error;
return params;
}
function buildNodeEventParams(event, payload) {
const payloadJSON = payload === void 0 ? void 0 : JSON.stringify(payload);
return {
event,
payloadJSON: typeof payloadJSON === "string" ? payloadJSON : null
};
}
async function sendNodeEvent(client, event, payload) {
try {
await client.request("node.event", buildNodeEventParams(event, payload));
} catch {}
}
//#endregion
//#region src/node-host/runner.ts
/** CLI runner for node-host stdin/stdout command dispatch. */
const DEFAULT_NODE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
function resolveNodeHostGatewayPlatform(platform) {
switch (platform) {
case "darwin": return "macos";
case "win32": return "windows";
case "linux": return "linux";
default: return "unknown";
}
}
function resolveNodeHostGatewayDeviceFamily(platform) {
switch (platform) {
case "darwin": return "Mac";
case "win32": return "Windows";
case "linux": return "Linux";
default: return;
}
}
function writeStderrLine(message) {
process.stderr.write(`${message}\n`);
}
const NODE_HOST_EXIT_ON_RECONNECT_PAUSE_CODES = new Set([
ConnectErrorDetailCodes.AUTH_TOKEN_MISSING,
ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH,
ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID,
ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING,
ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH,
ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH
]);
function shouldExitNodeHostOnReconnectPaused(detailCode) {
return detailCode !== null && NODE_HOST_EXIT_ON_RECONNECT_PAUSE_CODES.has(detailCode);
}
function formatNodeHostReconnectPausedMessage(info, params) {
const detail = info.detailCode ? ` detail=${info.detailCode}` : "";
const reason = info.reason.trim() || "no close reason";
const action = params?.exiting ? "exiting for supervisor restart" : "waiting for operator action";
return `node host gateway reconnect paused after close (${info.code}): ${reason}${detail}; ${action}`;
}
function handleNodeHostReconnectPaused(info, deps = {}) {
const shouldExit = shouldExitNodeHostOnReconnectPaused(info.detailCode);
(deps.writeLine ?? writeStderrLine)(formatNodeHostReconnectPausedMessage(info, { exiting: shouldExit }));
if (!shouldExit) return;
(deps.exit ?? ((code) => process.exit(code)))(1);
}
function resolveExecutablePathFromEnv(bin, pathEnv) {
if (bin.includes("/") || bin.includes("\\")) return null;
return resolveExecutableFromPathEnv(bin, pathEnv) ?? null;
}
function resolveExecutableTrustPathFromEnv(bin, pathEnv) {
const resolvedPath = resolveExecutablePathFromEnv(bin, pathEnv);
if (!resolvedPath) return null;
try {
return fs.realpathSync(resolvedPath);
} catch {
return resolvedPath;
}
}
function resolveSkillBinTrustEntries(bins, pathEnv) {
const trustEntries = [];
const seen = /* @__PURE__ */ new Set();
for (const bin of bins) {
const name = bin.trim();
if (!name) continue;
const resolvedPath = resolveExecutableTrustPathFromEnv(name, pathEnv);
if (!resolvedPath) continue;
const key = `${name}\u0000${resolvedPath}`;
if (seen.has(key)) continue;
seen.add(key);
trustEntries.push({
name,
resolvedPath
});
}
return trustEntries.toSorted((left, right) => left.name.localeCompare(right.name) || left.resolvedPath.localeCompare(right.resolvedPath));
}
var SkillBinsCache = class {
constructor(fetch, pathEnv) {
this.bins = [];
this.lastRefresh = 0;
this.ttlMs = 9e4;
this.fetch = fetch;
this.pathEnv = pathEnv;
}
async current(force = false) {
if (force || Date.now() - this.lastRefresh > this.ttlMs) await this.refresh();
return this.bins;
}
async refresh() {
try {
const bins = await this.fetch();
this.bins = resolveSkillBinTrustEntries(bins, this.pathEnv);
this.lastRefresh = Date.now();
} catch {
if (!this.lastRefresh) this.bins = [];
}
}
};
function ensureNodePathEnv() {
ensureOpenClawCliOnPath({ pathEnv: process.env.PATH ?? "" });
const current = process.env.PATH ?? "";
if (current.trim()) return current;
process.env.PATH = DEFAULT_NODE_PATH;
return DEFAULT_NODE_PATH;
}
async function resolveNodeHostGatewayCredentials(params) {
return await resolveGatewayConnectionAuth({
config: (params.config.gateway?.mode === "remote" ? "remote" : "local") === "local" ? buildNodeHostLocalAuthConfig(params.config) : params.config,
env: params.env,
localTokenPrecedence: "env-first",
localPasswordPrecedence: "env-first",
remoteTokenPrecedence: "env-first",
remotePasswordPrecedence: "env-first"
});
}
function buildNodeHostLocalAuthConfig(config) {
if (!config.gateway?.remote?.token && !config.gateway?.remote?.password) return config;
const nextConfig = structuredClone(config);
if (nextConfig.gateway?.remote) {
nextConfig.gateway.remote.token = void 0;
nextConfig.gateway.remote.password = void 0;
}
return nextConfig;
}
async function runNodeHost(opts) {
const config = await ensureNodeHostConfig();
const nodeId = opts.nodeId?.trim() || config.nodeId;
if (nodeId !== config.nodeId) config.nodeId = nodeId;
const displayName = opts.displayName?.trim() || config.displayName || await getMachineDisplayName();
config.displayName = displayName;
const gateway = {
host: opts.gatewayHost,
port: opts.gatewayPort,
tls: opts.gatewayTls ?? getRuntimeConfig().gateway?.tls?.enabled ?? false,
tlsFingerprint: opts.gatewayTlsFingerprint
};
config.gateway = gateway;
await saveNodeHostConfig(config);
const cfg = getRuntimeConfig();
await ensureNodeHostPluginRegistry({
config: cfg,
env: process.env
});
const pluginNodeHost = listRegisteredNodeHostCapsAndCommands();
const { token, password } = await resolveNodeHostGatewayCredentials({
config: cfg,
env: process.env
});
const host = gateway.host ?? "127.0.0.1";
const port = gateway.port ?? 18789;
const url = `${gateway.tls ? "wss" : "ws"}://${host}:${port}`;
const pathEnv = ensureNodePathEnv();
const client = new GatewayClient({
url,
token: token || void 0,
password: password || void 0,
preauthHandshakeTimeoutMs: cfg.gateway?.handshakeTimeoutMs,
instanceId: nodeId,
clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
clientDisplayName: displayName,
clientVersion: VERSION,
platform: resolveNodeHostGatewayPlatform(process.platform),
deviceFamily: resolveNodeHostGatewayDeviceFamily(process.platform),
mode: GATEWAY_CLIENT_MODES.NODE,
role: "node",
scopes: [],
caps: ["system", ...pluginNodeHost.caps],
commands: [
...NODE_SYSTEM_RUN_COMMANDS,
...NODE_EXEC_APPROVALS_COMMANDS,
...pluginNodeHost.commands
],
pathEnv,
permissions: void 0,
deviceIdentity: loadOrCreateDeviceIdentity(),
tlsFingerprint: gateway.tlsFingerprint,
onEvent: (evt) => {
if (evt.event !== "node.invoke.request") return;
const payload = coerceNodeInvokePayload(evt.payload);
if (!payload) return;
handleInvoke(payload, client, skillBins);
},
onConnectError: (err) => {
writeStderrLine(`node host gateway connect failed: ${err.message}`);
},
onReconnectPaused: (info) => {
handleNodeHostReconnectPaused(info);
},
onClose: (code, reason) => {
writeStderrLine(`node host gateway closed (${code}): ${reason}`);
}
});
const skillBins = new SkillBinsCache(async () => {
const res = await client.request("skills.bins", {});
return Array.isArray(res?.bins) ? res.bins.map((bin) => String(bin)) : [];
}, pathEnv);
if (!(await startGatewayClientWhenEventLoopReady(client, { clientOptions: { preauthHandshakeTimeoutMs: cfg.gateway?.handshakeTimeoutMs } })).ready) throw new Error("node host gateway event loop readiness timeout");
await new Promise(() => {});
}
//#endregion
//#region src/commands/node-daemon-install-helpers.ts
/** Node-based daemon install plan builder for managed gateway services. */
function buildNodeInstallEnvironmentValueSources() {
return { OPENCLAW_GATEWAY_TOKEN: "file" };
}
/** Builds launch arguments, environment, and metadata for a Node daemon service install. */
async function buildNodeInstallPlan(params) {
const { devMode, nodePath } = await resolveDaemonInstallRuntimeInputs({
env: params.env,
runtime: params.runtime,
devMode: params.devMode,
nodePath: params.nodePath
});
const { programArguments, workingDirectory } = await resolveNodeProgramArguments({
host: params.host,
port: params.port,
tls: params.tls,
tlsFingerprint: params.tlsFingerprint,
nodeId: params.nodeId,
displayName: params.displayName,
dev: devMode,
runtime: params.runtime,
nodePath
});
await emitDaemonInstallRuntimeWarning({
env: params.env,
runtime: params.runtime,
programArguments,
warn: params.warn,
title: "Node daemon runtime"
});
const environment = buildNodeServiceEnvironment({
env: params.env,
extraPathDirs: resolveDaemonNodeBinDir(nodePath)
});
const description = formatNodeServiceDescription({ version: environment.OPENCLAW_SERVICE_VERSION });
return {
programArguments,
workingDirectory,
environment,
environmentValueSources: buildNodeInstallEnvironmentValueSources(),
description
};
}
//#endregion
//#region src/commands/node-daemon-runtime.ts
/** Compatibility exports for the Node daemon runtime selector. */
/** Default Node daemon runtime, currently shared with the gateway daemon runtime. */
const DEFAULT_NODE_DAEMON_RUNTIME = DEFAULT_GATEWAY_DAEMON_RUNTIME;
/** Returns true when a string is a supported Node daemon runtime id. */
function isNodeDaemonRuntime(value) {
return isGatewayDaemonRuntime(value);
}
//#endregion
//#region src/cli/node-cli/daemon.ts
function renderNodeServiceStartHints() {
return buildPlatformServiceStartHints({
installCommand: formatCliCommand("openclaw node install"),
startCommand: formatCliCommand("openclaw node start"),
launchAgentPlistPath: `~/Library/LaunchAgents/${resolveNodeLaunchAgentLabel()}.plist`,
systemdServiceName: resolveNodeSystemdServiceName(),
windowsTaskName: resolveNodeWindowsTaskName()
});
}
function buildNodeRuntimeHints(env = process.env) {
return buildPlatformRuntimeLogHints({
env,
systemdServiceName: resolveNodeSystemdServiceName(),
windowsTaskName: resolveNodeWindowsTaskName()
});
}
function resolveNodeDefaults(opts, config) {
const host = normalizeOptionalString(opts.host) || config?.gateway?.host || "127.0.0.1";
const portOverride = parsePort(opts.port);
if (opts.port !== void 0 && portOverride === null) return {
host,
port: null
};
return {
host,
port: portOverride ?? config?.gateway?.port ?? 18789
};
}
async function runNodeDaemonInstall(opts) {
const { json, stdout, warnings, emit, fail } = createDaemonInstallActionContext(opts.json);
if (failIfNixDaemonInstallMode(fail)) return;
const config = await loadNodeHostConfig();
const { host, port } = resolveNodeDefaults(opts, config);
if (!Number.isFinite(port ?? NaN) || (port ?? 0) <= 0 || (port ?? 0) > 65535) {
fail(opts.port !== void 0 ? formatInvalidPortOption("--port") : formatInvalidConfigPort("node.gateway.port"));
return;
}
const runtimeRaw = opts.runtime ? opts.runtime : DEFAULT_NODE_DAEMON_RUNTIME;
if (!isNodeDaemonRuntime(runtimeRaw)) {
fail("Invalid --runtime (use \"node\" or \"bun\")");
return;
}
const service = resolveNodeService();
let loaded;
try {
loaded = await service.isLoaded({ env: process.env });
} catch (err) {
fail(`Node service check failed: ${String(err)}`);
return;
}
if (loaded && !opts.force) {
emit({
ok: true,
result: "already-installed",
message: `Node service already ${service.loadedText}.`,
service: buildDaemonServiceSnapshot(service, loaded),
warnings: warnings.length ? warnings : void 0
});
if (!json) {
defaultRuntime.log(`Node service already ${service.loadedText}.`);
defaultRuntime.log(`Reinstall with: ${formatCliCommand("openclaw node install --force")}`);
}
return;
}
const tlsFingerprint = normalizeOptionalString(opts.tlsFingerprint) || config?.gateway?.tlsFingerprint;
const tls = Boolean(opts.tls) || Boolean(tlsFingerprint) || Boolean(config?.gateway?.tls);
const { programArguments, workingDirectory, environment, environmentValueSources, description } = await buildNodeInstallPlan({
env: process.env,
host,
port: port ?? 18789,
tls,
tlsFingerprint: tlsFingerprint || void 0,
nodeId: opts.nodeId,
displayName: opts.displayName,
runtime: runtimeRaw,
warn: (message) => {
if (json) warnings.push(message);
else defaultRuntime.log(message);
}
});
await installDaemonServiceAndEmit({
serviceNoun: "Node",
service,
warnings,
emit,
fail,
install: async () => {
await service.install({
env: process.env,
stdout,
programArguments,
workingDirectory,
environment,
environmentValueSources,
description
});
}
});
}
async function runNodeDaemonUninstall(opts = {}) {
return await runServiceUninstall({
serviceNoun: "Node",
service: resolveNodeService(),
opts,
stopBeforeUninstall: false,
assertNotLoadedAfterUninstall: false
});
}
async function runNodeDaemonStart(opts = {}) {
return await runServiceStart({
serviceNoun: "Node",
service: resolveNodeService(),
renderStartHints: renderNodeServiceStartHints,
opts
});
}
async function runNodeDaemonRestart(opts = {}) {
await runServiceRestart({
serviceNoun: "Node",
service: resolveNodeService(),
renderStartHints: renderNodeServiceStartHints,
opts
});
}
async function runNodeDaemonStop(opts = {}) {
return await runServiceStop({
serviceNoun: "Node",
service: resolveNodeService(),
opts
});
}
async function runNodeDaemonStatus(opts = {}) {
const json = Boolean(opts.json);
const service = resolveNodeService();
const [loaded, command, runtime] = await Promise.all([
service.isLoaded({ env: process.env }).catch(() => false),
service.readCommand(process.env).catch(() => null),
service.readRuntime(process.env).catch((err) => ({
status: "unknown",
detail: String(err)
}))
]);
const payload = { service: {
...buildDaemonServiceSnapshot(service, loaded),
command,
runtime
} };
if (json) {
defaultRuntime.writeJson(payload);
return;
}
const { rich, label, accent, infoText, okText, warnText, errorText } = createCliStatusTextStyles();
const serviceStatus = loaded ? okText(service.loadedText) : warnText(service.notLoadedText);
defaultRuntime.log(`${label("Service:")} ${accent(service.label)} (${serviceStatus})`);
if (command?.programArguments?.length) defaultRuntime.log(`${label("Command:")} ${infoText(command.programArguments.join(" "))}`);
if (command?.sourcePath) defaultRuntime.log(`${label("Service file:")} ${infoText(command.sourcePath)}`);
if (command?.workingDirectory) defaultRuntime.log(`${label("Working dir:")} ${infoText(command.workingDirectory)}`);
const runtimeLine = formatRuntimeStatus(runtime);
if (runtimeLine) {
const runtimeColor = resolveRuntimeStatusColor(runtime?.status);
defaultRuntime.log(`${label("Runtime:")} ${colorize(rich, runtimeColor, runtimeLine)}`);
}
if (!loaded) {
defaultRuntime.log("");
for (const hint of renderNodeServiceStartHints()) defaultRuntime.log(`${warnText("Start with:")} ${infoText(hint)}`);
return;
}
const baseEnv = {
...process.env,
...command?.environment ?? void 0
};
const hintEnv = {
...baseEnv,
OPENCLAW_LOG_PREFIX: baseEnv.OPENCLAW_LOG_PREFIX ?? "node"
};
if (runtime?.missingUnit) {
defaultRuntime.error(errorText("Service unit not found."));
for (const hint of buildNodeRuntimeHints(hintEnv)) defaultRuntime.log(errorText(hint));
return;
}
if (runtime?.status === "stopped") {
defaultRuntime.error(errorText("Service is loaded but not running."));
for (const hint of buildNodeRuntimeHints(hintEnv)) defaultRuntime.log(errorText(hint));
}
}
//#endregion
//#region src/cli/node-cli/register.ts
function parsePortOption(value, fallback) {
if (value === void 0) return fallback;
return parsePort(value);
}
function registerNodeCli(program) {
const node = program.command("node").description("Run and manage the headless node host service").addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([
["openclaw node run --host 127.0.0.1 --port 18789", "Run the node host in the foreground."],
["openclaw node status", "Check node host service status."],
["openclaw node install", "Install the node host service."],
["openclaw node start", "Start the installed node host service."],
["openclaw node restart", "Restart the installed node host service."]
])}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/node", "docs.openclaw.ai/cli/node")}\n`);
node.command("run").description("Run the headless node host (foreground)").option("--host <host>", "Gateway host").option("--port <port>", "Gateway port").option("--tls", "Use TLS for the gateway connection").option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)").option("--node-id <id>", "Override node id (clears pairing token)").option("--display-name <name>", "Override node display name").action(async (opts) => {
const existing = await loadNodeHostConfig();
const host = normalizeOptionalString(opts.host) || existing?.gateway?.host || "127.0.0.1";
const port = parsePortOption(opts.port, existing?.gateway?.port ?? 18789);
if (port === null) {
defaultRuntime.error(formatInvalidPortOption("--port"));
defaultRuntime.exit(1);
return;
}
const retargetedGateway = opts.host !== void 0 || opts.port !== void 0;
const tlsFingerprint = opts.tlsFingerprint ?? (retargetedGateway ? void 0 : existing?.gateway?.tlsFingerprint);
const inheritedTls = retargetedGateway ? void 0 : existing?.gateway?.tls;
await runNodeHost({
gatewayHost: host,
gatewayPort: port,
gatewayTls: typeof opts.tls === "boolean" ? opts.tls : Boolean(tlsFingerprint) || inheritedTls,
gatewayTlsFingerprint: tlsFingerprint,
nodeId: opts.nodeId,
displayName: opts.displayName
});
});
node.command("status").description("Show node host status").option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonStatus(opts);
});
node.command("install").description("Install the node host service (launchd/systemd/schtasks)").option("--host <host>", "Gateway host").option("--port <port>", "Gateway port").option("--tls", "Use TLS for the gateway connection", false).option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)").option("--node-id <id>", "Override node id (clears pairing token)").option("--display-name <name>", "Override node display name").option("--runtime <runtime>", "Service runtime (node|bun). Default: node").option("--force", "Reinstall/overwrite if already installed", false).option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonInstall(opts);
});
node.command("uninstall").description("Uninstall the node host service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonUninstall(opts);
});
node.command("stop").description("Stop the node host service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonStop(opts);
});
node.command("start").description("Start the node host service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonStart(opts);
});
node.command("restart").description("Restart the node host service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (opts) => {
await runNodeDaemonRestart(opts);
});
}
//#endregion
export { registerNodeCli };