openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
485 lines (484 loc) • 18.6 kB
JavaScript
import { i as truncateWithMarker } from "./utf16-slice-D_ngcYKd.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { Et as array, Rn as string, Tn as object, Zn as unknown, dn as literal, wn as number, yt as _enum } from "./schemas-zxit8y5H.js";
import { n as isPathInside } from "./path-safety-Bi0ppMWC.js";
import "./redact-BtvPPfTi.js";
import "./utils-P__uGsPB.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { l as resolveRuntimeServiceVersion } from "./version-v1kuAkGj.js";
import { n as normalizePositiveInt, r as normalizePositiveTimerMs } from "./shared-CeAc-MzA.js";
import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js";
import { a as inspectPathPermissions, o as safeStat } from "./permissions-BhjKuixU.js";
import "./audit-fs-BZ701GLw.js";
import path from "node:path";
import fs from "node:fs/promises";
import { createHash } from "node:crypto";
//#region src/security/install-policy-response.ts
const MAX_REASON_CHARS = 1e3;
const MAX_FINDINGS = 100;
const MAX_FINDING_TEXT_CHARS = 1e3;
const TRUNCATION_MARKER = "...";
const installPolicyResponseEnvelopeSchema = object({
protocolVersion: literal(1),
decision: _enum([
"allow",
"warn",
"block"
]),
reason: unknown().optional(),
findings: array(unknown()).optional().catch(void 0)
});
const installPolicyReasonSchema = string().trim().min(1);
const findingTextSchema = string().trim().min(1);
const optionalFindingTextSchema = findingTextSchema.optional().catch(void 0);
const installPolicyFindingSchema = object({
ruleId: findingTextSchema,
severity: _enum([
"info",
"warn",
"critical"
]),
message: findingTextSchema,
file: optionalFindingTextSchema,
line: number().finite().transform((value) => Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, Math.floor(value)))).optional().catch(void 0),
evidence: optionalFindingTextSchema
}).transform(({ evidence, file, line, ...finding }) => ({
...finding,
...file ? { file } : {},
...line !== void 0 ? { line } : {},
...evidence ? { evidence } : {}
}));
function truncateText(value, maxChars) {
return truncateWithMarker(value, maxChars, {
marker: TRUNCATION_MARKER,
reserve: 3,
trimEnd: false
});
}
function createInstallPolicyFailure(message) {
return { blocked: {
code: "security_scan_failed",
reason: `install policy failed closed: ${truncateText(message, MAX_REASON_CHARS)}`
} };
}
function blockedByPolicy(reason, findings) {
return {
blocked: {
code: "security_scan_blocked",
reason: `blocked by install policy: ${truncateText(reason, MAX_REASON_CHARS)}`
},
...findings && findings.length > 0 ? { findings } : {}
};
}
function normalizeFinding(value) {
const parsed = installPolicyFindingSchema.safeParse(value);
return parsed.success ? parsed.data : null;
}
function truncateFinding(finding) {
return {
ruleId: truncateText(finding.ruleId, MAX_FINDING_TEXT_CHARS),
severity: finding.severity,
message: truncateText(finding.message, MAX_FINDING_TEXT_CHARS),
...finding.file ? { file: truncateText(finding.file, MAX_FINDING_TEXT_CHARS) } : {},
...finding.line !== void 0 ? { line: finding.line } : {},
...finding.evidence ? { evidence: truncateText(finding.evidence, MAX_FINDING_TEXT_CHARS) } : {}
};
}
function fingerprintWarning(reason, findings) {
return createHash("sha256").update(JSON.stringify({
reason,
findings
})).digest("hex");
}
function formatPolicyResponseEnvelopeError(error) {
const invalidPath = error.issues[0]?.path[0];
return invalidPath === void 0 ? "policy response must be a JSON object" : invalidPath === "protocolVersion" ? "policy response protocolVersion must be 1" : "policy response decision must be \"allow\", \"warn\", or \"block\"";
}
function parseInstallPolicyResponse(stdout) {
const trimmed = stdout.trim();
if (!trimmed) return createInstallPolicyFailure("policy command returned empty stdout");
let parsed;
try {
parsed = JSON.parse(trimmed);
} catch (err) {
return createInstallPolicyFailure(`policy command returned invalid JSON (${formatErrorMessage(err)})`);
}
const response = installPolicyResponseEnvelopeSchema.safeParse(parsed);
if (!response.success) return createInstallPolicyFailure(formatPolicyResponseEnvelopeError(response.error));
const fullFindings = (response.data.findings ?? []).map(normalizeFinding).filter((finding) => finding !== null);
const normalizedFindings = fullFindings.slice(0, MAX_FINDINGS).map(truncateFinding);
if (response.data.decision === "allow") return normalizedFindings.length > 0 ? { findings: normalizedFindings } : {};
const reason = installPolicyReasonSchema.safeParse(response.data.reason);
if (!reason.success) return createInstallPolicyFailure(`policy response decision "${response.data.decision}" requires a non-empty reason`);
if (response.data.decision === "warn") {
if (fullFindings.length > MAX_FINDINGS) return createInstallPolicyFailure(`policy warning returned more than ${String(MAX_FINDINGS)} valid findings; reduce the findings before retrying`);
return {
warning: {
reason: truncateText(reason.data, MAX_REASON_CHARS),
fingerprint: fingerprintWarning(reason.data, fullFindings)
},
...normalizedFindings.length > 0 ? { findings: normalizedFindings } : {}
};
}
return blockedByPolicy(reason.data, normalizedFindings);
}
//#endregion
//#region src/security/install-policy.ts
const DEFAULT_TIMEOUT_MS = 1e4;
const DEFAULT_MAX_OUTPUT_BYTES = 1048576;
const DEFAULT_MAX_REQUEST_BYTES = 262144;
const WINDOWS_ABS_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
const WINDOWS_UNC_PATH_PATTERN = /^\\\\[^\\]+\\[^\\]+/;
const POLICY_INTERPRETER_NAMES = /* @__PURE__ */ new Set([
"bash",
"bun",
"deno",
"env",
"fish",
"node",
"perl",
"powershell",
"pwsh",
"python",
"python3",
"ruby",
"sh",
"zsh"
]);
const POLICY_SCRIPT_ARG_PATTERN = /\.(?:bash|cjs|cts|js|mjs|mts|pl|ps1|py|rb|sh|ts|zsh)$/i;
function isAbsolutePathname(value) {
if (path.isAbsolute(value)) return true;
return process.platform === "win32" && (WINDOWS_ABS_PATH_PATTERN.test(value) || WINDOWS_UNC_PATH_PATTERN.test(value));
}
function executableName(commandPath) {
return path.basename(commandPath).replace(/\.exe$/i, "").toLowerCase();
}
function isPolicyScriptArg(value) {
return isAbsolutePathname(value) || value.startsWith(".") || value.includes("/") || value.includes("\\") || POLICY_SCRIPT_ARG_PATTERN.test(value);
}
function resolvePolicyScriptArg(params) {
const interpreterName = executableName(params.command);
const startIndex = 0;
if (interpreterName === "env") return {
kind: "unsupported",
message: "security.installPolicy.exec.command must not use env; configure the policy executable directly."
};
if (!POLICY_INTERPRETER_NAMES.has(interpreterName) || interpreterName === "env") return;
const scripts = [];
for (let index = startIndex; index < params.args.length; index += 1) {
const arg = params.args[index];
if (!arg) continue;
if (arg.startsWith("-")) {
const equalsIndex = arg.indexOf("=");
if (equalsIndex > 0) {
const optionValue = arg.slice(equalsIndex + 1);
if (isPolicyScriptArg(optionValue)) scripts.push({
index,
path: optionValue
});
}
continue;
}
if (isPolicyScriptArg(arg)) scripts.push({
index,
path: arg
});
}
return scripts.length > 0 ? {
kind: "scripts",
scripts
} : void 0;
}
async function readFileStatOrThrow(pathname, label) {
const stat = await safeStat(pathname);
if (!stat.ok) throw new Error(`${label} is not readable: ${pathname}`);
if (stat.isDir) throw new Error(`${label} must be a file: ${pathname}`);
return stat;
}
function collectPathAncestorDirs(targetPath) {
const dirs = [];
let current = path.resolve(path.dirname(targetPath));
while (true) {
dirs.push(current);
const parent = path.dirname(current);
if (parent === current) return dirs;
current = parent;
}
}
async function assertSecureCommandAncestorDirs(params) {
const currentUid = typeof process.getuid === "function" ? process.getuid() : void 0;
for (const dir of collectPathAncestorDirs(params.targetPath)) {
const perms = await inspectPathPermissions(dir);
if (!perms.ok) throw new Error(`${params.label} parent directory permissions could not be verified: ${dir}`);
let sticky = false;
if (process.platform !== "win32" && (perms.worldWritable || perms.groupWritable)) try {
sticky = ((await fs.stat(dir)).mode & 512) !== 0;
} catch {
sticky = false;
}
if ((perms.worldWritable || perms.groupWritable) && !sticky) throw new Error(`${params.label} parent directory permissions are too open: ${dir}`);
if (process.platform !== "win32" && currentUid !== void 0) {
let stat;
try {
stat = await fs.stat(dir);
} catch {
throw new Error(`${params.label} parent directory ownership could not be verified: ${dir}`);
}
if (stat.uid !== 0 && stat.uid !== currentUid) throw new Error(`${params.label} parent directory owner is not trusted: ${dir}`);
}
if (process.platform === "win32" && perms.source === "unknown") throw new Error(`${params.label} parent directory ACL verification unavailable on Windows for ${dir}. Move ${params.label} to a direct path whose ACLs can be verified.`);
}
}
async function assertSecureCommandPath(params) {
if (!isAbsolutePathname(params.targetPath)) throw new Error(`${params.label} must be an absolute path.`);
const effectivePath = params.targetPath;
const stat = await readFileStatOrThrow(effectivePath, params.label);
if (stat.isSymlink) throw new Error(`${params.label} must not be a symlink: ${effectivePath}`);
if (params.trustedDirs && params.trustedDirs.length > 0) {
if (!params.trustedDirs.map((entry) => resolveUserPath(entry)).some((dir) => isPathInside(dir, effectivePath))) throw new Error(`${params.label} is outside trustedDirs: ${effectivePath}`);
}
const perms = await inspectPathPermissions(effectivePath);
if (!perms.ok) throw new Error(`${params.label} permissions could not be verified: ${effectivePath}`);
if (perms.worldWritable || perms.groupWritable) throw new Error(`${params.label} permissions are too open: ${effectivePath}`);
await assertSecureCommandAncestorDirs({
targetPath: effectivePath,
label: params.label
});
if (process.platform === "win32" && perms.source === "unknown") throw new Error(`${params.label} ACL verification unavailable on Windows for ${effectivePath}. Move ${params.label} to a direct path whose ACLs can be verified.`);
if (process.platform !== "win32" && typeof process.getuid === "function" && stat.uid != null) {
const uid = process.getuid();
if (stat.uid !== uid && stat.uid !== 0) throw new Error(`${params.label} must be owned by the current user (uid=${uid}) or root: ${effectivePath}`);
}
return effectivePath;
}
async function assertSecurePolicyScriptArg(params) {
const scriptArg = resolvePolicyScriptArg({
command: params.command,
args: params.args
});
if (!scriptArg) return;
if (scriptArg.kind === "unsupported") throw new Error(scriptArg.message);
for (const script of scriptArg.scripts) await assertSecureCommandPath({
targetPath: script.path,
label: `security.installPolicy.exec.args[${script.index}]`,
trustedDirs: params.trustedDirs
});
}
function createPolicyChildEnv(_sourceEnv) {
return {};
}
function readPassEnvValue(env, key) {
const exact = env[key];
if (exact !== void 0 || process.platform !== "win32") return exact;
const lowerKey = key.toLowerCase();
const matchedKey = Object.keys(env).find((candidate) => candidate.toLowerCase() === lowerKey);
return matchedKey ? env[matchedKey] : void 0;
}
function isTargetEnabled(params) {
const targets = params.policy.targets;
if (!targets || targets.length === 0) return true;
return targets.includes(params.targetType);
}
function resolvePolicy(config, targetType) {
const policy = config?.security?.installPolicy;
if (!policy || policy.enabled !== true) return { kind: "disabled" };
if (!isTargetEnabled({
policy,
targetType
})) return { kind: "disabled" };
if (!policy.exec) return {
kind: "failure",
result: createInstallPolicyFailure("security.installPolicy is enabled but security.installPolicy.exec is not configured")
};
return {
kind: "configured",
exec: policy.exec
};
}
function resolveConfiguredTargets(policy) {
const targets = policy.targets;
return targets && targets.length > 0 ? [...new Set(targets)] : ["skill", "plugin"];
}
async function validateInstallPolicyStatic(config) {
const policy = config?.security?.installPolicy;
if (!policy || policy.enabled !== true) return {
enabled: false,
targets: [],
issues: []
};
const targets = resolveConfiguredTargets(policy);
const issues = [];
if (!policy.exec) {
issues.push({
severity: "error",
message: "security.installPolicy is enabled but security.installPolicy.exec is not configured."
});
return {
enabled: true,
targets,
issues
};
}
if (!isAbsolutePathname(policy.exec.command)) {
issues.push({
severity: "error",
message: "security.installPolicy.exec.command must be an absolute path."
});
return {
enabled: true,
targets,
issues
};
}
try {
await assertSecureCommandPath({
targetPath: policy.exec.command,
label: "security.installPolicy.exec.command",
trustedDirs: policy.exec.trustedDirs
});
} catch (err) {
issues.push({
severity: "error",
message: formatErrorMessage(err)
});
}
try {
await assertSecurePolicyScriptArg({
command: policy.exec.command,
args: policy.exec.args ?? [],
trustedDirs: policy.exec.trustedDirs
});
} catch (err) {
issues.push({
severity: "error",
message: formatErrorMessage(err)
});
}
return {
enabled: true,
targets,
issues
};
}
async function runInstallPolicy(params) {
const decisionContext = formatDecisionContext(params.request);
const logBlocked = (result) => {
if (result.blocked) params.logger?.debug?.(`Install policy ${decisionContext}: ${result.blocked.reason}`);
return result;
};
const failClosed = (message) => logBlocked(createInstallPolicyFailure(message));
let config = params.config;
if (!config) try {
const { getRuntimeConfig } = await import("./io-ssMwu02B.js");
config = getRuntimeConfig({ skipPluginValidation: true });
} catch (err) {
return failClosed(`could not load OpenClaw config (${formatErrorMessage(err)})`);
}
const policy = resolvePolicy(config, params.request.targetType);
if (policy.kind === "disabled") return;
if (policy.kind === "failure") return logBlocked(policy.result);
const input = JSON.stringify({
protocolVersion: 1,
openclawVersion: resolveRuntimeServiceVersion(params.env ?? process.env),
...params.request
});
if (Buffer.byteLength(input, "utf8") > DEFAULT_MAX_REQUEST_BYTES) return failClosed(`policy request exceeded maxInputBytes (${DEFAULT_MAX_REQUEST_BYTES})`);
const commandPath = policy.exec.command;
if (!isAbsolutePathname(commandPath)) return failClosed("security.installPolicy.exec.command must be an absolute path.");
let secureCommandPath;
try {
secureCommandPath = await assertSecureCommandPath({
targetPath: commandPath,
label: "security.installPolicy.exec.command",
trustedDirs: policy.exec.trustedDirs
});
} catch (err) {
return failClosed(formatErrorMessage(err));
}
try {
await assertSecurePolicyScriptArg({
command: secureCommandPath,
args: policy.exec.args ?? [],
trustedDirs: policy.exec.trustedDirs
});
} catch (err) {
return failClosed(formatErrorMessage(err));
}
const env = params.env ?? process.env;
const childEnv = createPolicyChildEnv(env);
for (const key of policy.exec.passEnv ?? []) {
const value = readPassEnvValue(env, key);
if (value !== void 0) childEnv[key] = value;
}
for (const [key, value] of Object.entries(policy.exec.env ?? {})) childEnv[key] = value;
const timeoutMs = normalizePositiveTimerMs(policy.exec.timeoutMs, DEFAULT_TIMEOUT_MS);
const noOutputTimeoutMs = normalizePositiveTimerMs(policy.exec.noOutputTimeoutMs, timeoutMs);
const maxOutputBytes = normalizePositiveInt(policy.exec.maxOutputBytes, DEFAULT_MAX_OUTPUT_BYTES);
const cwd = path.dirname(secureCommandPath);
let result;
try {
result = await runCommandWithTimeout([secureCommandPath, ...policy.exec.args ?? []], {
baseEnv: {},
cwd,
env: childEnv,
input,
killProcessTree: true,
maxCombinedOutputBytes: maxOutputBytes,
maxOutputBytes,
noOutputTimeoutMs,
outputCapture: "head",
terminateOnOutputLimit: true,
timeoutMs
});
} catch (err) {
return failClosed(formatErrorMessage(err));
}
if (result.termination === "timeout") return failClosed(`policy command timed out after ${timeoutMs}ms`);
if (result.termination === "no-output-timeout") return failClosed(`policy command produced no output for ${noOutputTimeoutMs}ms`);
if (result.outputLimitExceeded) return failClosed(`output exceeded maxOutputBytes (${maxOutputBytes})`);
if (result.code !== 0) return failClosed(`policy command exited with code ${String(result.code)}`);
const parsed = parseInstallPolicyResponse(result.stdout);
if (parsed.blocked) return logBlocked(parsed);
if (parsed.warning) {
params.logger?.debug?.(`Install policy ${decisionContext}: warned`);
return parsed;
}
params.logger?.debug?.(`Install policy ${decisionContext}: allowed`);
return parsed;
}
function formatDecisionContext(request) {
const source = request.source ? ` source=${request.source.kind}/${request.source.authority}` : "";
const origin = typeof request.origin.type === "string" ? request.origin.type : "unknown";
return [
`target=${request.targetType}:${request.targetName}`,
`request=${request.request.kind}/${request.request.mode}`,
`origin=${origin}`,
`pathKind=${request.sourcePathKind}`,
source.trim()
].filter(Boolean).join(" ");
}
async function probeInstallPolicy(params) {
const validation = await validateInstallPolicyStatic(params.config);
if (!validation.enabled || validation.issues.some((issue) => issue.severity === "error")) return;
const targetType = validation.targets.includes("skill") ? "skill" : validation.targets[0];
if (!targetType) return;
return await runInstallPolicy({
config: params.config,
env: params.env,
logger: params.logger,
request: {
targetType,
targetName: "doctor-install-policy-probe",
sourcePath: params.sourcePath,
sourcePathKind: "directory",
origin: { type: "doctor" },
request: {
kind: targetType === "skill" ? "skill-install" : "plugin-dir",
mode: "install",
requestedSpecifier: "doctor:install-policy-probe"
}
}
});
}
//#endregion
export { runInstallPolicy as n, validateInstallPolicyStatic as r, probeInstallPolicy as t };