import { i as formatErrorMessage } from "./errors-DT-4GRK9.js";
import { i as isPathInside } from "./path-BlG8lhgR.js";
import { a as inspectPathPermissions, d as safeStat } from "./permissions-ya3cPkFH.js";
import { p as resolveUserPath } from "./utils-CCC-BEJH.js";
import { s as resolveRuntimeServiceVersion } from "./version-Crcn9X9T.js";
import { i as normalizePositiveTimerMs, r as normalizePositiveInt } from "./shared-CA363coJ.js";
import "./audit-fs-CBe_wA_B.js";
import "./scan-paths-Bve2UhXh.js";
import path from "node:path";
import fs from "node:fs/promises";
import { spawn } from "node:child_process";
//#region src/security/install-policy.ts
const DEFAULT_TIMEOUT_MS = 1e4;
const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
const DEFAULT_MAX_REQUEST_BYTES = 256 * 1024;
const MAX_REASON_CHARS = 1e3;
const MAX_FINDINGS = 100;
const MAX_FINDING_TEXT_CHARS = 1e3;
const WINDOWS_ABS_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
const WINDOWS_UNC_PATH_PATTERN = /^\\\\[^\\]+\\[^\\]+/;
const POLICY_INTERPRETER_NAMES = 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}. Set allowInsecurePath=true for this policy to bypass this check when the path is trusted.`);
	}
}
async function assertSecureCommandPath(params) {
	if (!isAbsolutePathname(params.targetPath)) throw new Error(`${params.label} must be an absolute path.`);
	let effectivePath = params.targetPath;
	let stat = await readFileStatOrThrow(effectivePath, params.label);
	if (stat.isSymlink) {
		if (!params.allowSymlinkPath) throw new Error(`${params.label} must not be a symlink: ${effectivePath}`);
		try {
			effectivePath = await fs.realpath(effectivePath);
		} catch {
			throw new Error(`${params.label} symlink target is not readable: ${params.targetPath}`);
		}
		if (!isAbsolutePathname(effectivePath)) throw new Error(`${params.label} resolved symlink target must be an absolute path.`);
		stat = await readFileStatOrThrow(effectivePath, params.label);
		if (stat.isSymlink) throw new Error(`${params.label} symlink target 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}`);
	}
	if (params.allowInsecurePath) return 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}. Set allowInsecurePath=true for this policy to bypass this check when the path is trusted.`);
	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,
		allowInsecurePath: params.allowInsecurePath,
		allowSymlinkPath: false
	});
}
function truncateText(value, maxChars) {
	return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
}
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 blockedByFailure(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 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: blockedByFailure("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,
			allowInsecurePath: policy.exec.allowInsecurePath,
			allowSymlinkPath: policy.exec.allowSymlinkCommand
		});
	} catch (err) {
		issues.push({
			severity: "error",
			message: formatErrorMessage(err)
		});
	}
	try {
		await assertSecurePolicyScriptArg({
			command: policy.exec.command,
			args: policy.exec.args ?? [],
			trustedDirs: policy.exec.trustedDirs,
			allowInsecurePath: policy.exec.allowInsecurePath,
			allowSymlinkPath: policy.exec.allowSymlinkCommand
		});
	} catch (err) {
		issues.push({
			severity: "error",
			message: formatErrorMessage(err)
		});
	}
	return {
		enabled: true,
		targets,
		issues
	};
}
function isIgnorableStdinWriteError(error) {
	if (typeof error !== "object" || error === null || !("code" in error)) return false;
	const code = String(error.code);
	return code === "EPIPE" || code === "ERR_STREAM_DESTROYED";
}
async function runPolicyCommand(params) {
	return await new Promise((resolve, reject) => {
		const child = spawn(params.command, params.args, {
			cwd: params.cwd,
			env: params.env,
			stdio: [
				"pipe",
				"pipe",
				"pipe"
			],
			shell: false,
			windowsHide: true
		});
		let settled = false;
		let stdout = "";
		let stderr = "";
		let timedOut = false;
		let noOutputTimedOut = false;
		let outputBytes = 0;
		let noOutputTimer = null;
		const timeoutTimer = setTimeout(() => {
			timedOut = true;
			child.kill("SIGKILL");
		}, params.timeoutMs);
		const clearTimers = () => {
			clearTimeout(timeoutTimer);
			if (noOutputTimer) {
				clearTimeout(noOutputTimer);
				noOutputTimer = null;
			}
		};
		const armNoOutputTimer = () => {
			if (noOutputTimer) clearTimeout(noOutputTimer);
			noOutputTimer = setTimeout(() => {
				noOutputTimedOut = true;
				child.kill("SIGKILL");
			}, params.noOutputTimeoutMs);
		};
		const append = (chunk, target) => {
			const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
			outputBytes += Buffer.byteLength(text, "utf8");
			if (outputBytes > params.maxOutputBytes) {
				child.kill("SIGKILL");
				if (!settled) {
					settled = true;
					clearTimers();
					reject(/* @__PURE__ */ new Error(`output exceeded maxOutputBytes (${params.maxOutputBytes})`));
				}
				return;
			}
			if (target === "stdout") stdout += text;
			else stderr += text;
			armNoOutputTimer();
		};
		armNoOutputTimer();
		child.on("error", (error) => {
			if (settled) return;
			settled = true;
			clearTimers();
			reject(error);
		});
		child.stdout?.on("data", (chunk) => append(chunk, "stdout"));
		child.stderr?.on("data", (chunk) => append(chunk, "stderr"));
		child.on("close", (code, signal) => {
			if (settled) return;
			settled = true;
			clearTimers();
			resolve({
				stdout,
				stderr,
				code,
				signal,
				termination: noOutputTimedOut ? "no-output-timeout" : timedOut ? "timeout" : "exit"
			});
		});
		const handleStdinError = (error) => {
			if (isIgnorableStdinWriteError(error) || settled) return;
			settled = true;
			clearTimers();
			reject(error instanceof Error ? error : new Error(String(error)));
		};
		child.stdin?.on("error", handleStdinError);
		try {
			child.stdin?.end(params.input);
		} catch (error) {
			handleStdinError(error);
		}
	});
}
function normalizeFinding(value) {
	if (typeof value !== "object" || value === null) return null;
	const record = value;
	const ruleId = typeof record.ruleId === "string" ? record.ruleId.trim() : "";
	const severity = record.severity;
	const file = typeof record.file === "string" ? record.file.trim() : "";
	const lineNumber = typeof record.line === "number" && Number.isFinite(record.line) ? Math.max(1, Math.floor(record.line)) : void 0;
	const message = typeof record.message === "string" ? record.message.trim() : "";
	if (!ruleId || !message || severity !== "info" && severity !== "warn" && severity !== "critical") return null;
	const evidence = typeof record.evidence === "string" ? record.evidence.trim() : "";
	return {
		ruleId: truncateText(ruleId, MAX_FINDING_TEXT_CHARS),
		severity,
		message: truncateText(message, MAX_FINDING_TEXT_CHARS),
		...file ? { file: truncateText(file, MAX_FINDING_TEXT_CHARS) } : {},
		...lineNumber ? { line: lineNumber } : {},
		...evidence ? { evidence: truncateText(evidence, MAX_FINDING_TEXT_CHARS) } : {}
	};
}
function parsePolicyResponse(stdout) {
	const trimmed = stdout.trim();
	if (!trimmed) return blockedByFailure("policy command returned empty stdout");
	let parsed;
	try {
		parsed = JSON.parse(trimmed);
	} catch (err) {
		return blockedByFailure(`policy command returned invalid JSON (${formatErrorMessage(err)})`);
	}
	if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return blockedByFailure("policy response must be a JSON object");
	const record = parsed;
	if (record.protocolVersion !== 1) return blockedByFailure("policy response protocolVersion must be 1");
	const decision = record.decision;
	if (decision !== "allow" && decision !== "block") return blockedByFailure("policy response decision must be \"allow\" or \"block\"");
	const normalizedFindings = Array.isArray(record.findings) ? record.findings.slice(0, MAX_FINDINGS).map(normalizeFinding).filter(Boolean) : [];
	if (decision === "allow") return normalizedFindings.length > 0 ? { findings: normalizedFindings } : {};
	const reason = typeof record.reason === "string" ? record.reason.trim() : "";
	if (!reason) return blockedByFailure("policy response decision \"block\" requires a non-empty reason");
	return blockedByPolicy(reason, normalizedFindings);
}
async function runInstallPolicy(params) {
	const decisionContext = formatDecisionContext(params.request);
	const logBlocked = (result) => {
		if (result.blocked) params.logger?.warn?.(`Install policy ${decisionContext}: ${result.blocked.reason}`);
		return result;
	};
	const failClosed = (message) => logBlocked(blockedByFailure(message));
	let config = params.config;
	if (!config) try {
		const { getRuntimeConfig } = await import("./io-Cy1ofLD3.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,
			allowInsecurePath: policy.exec.allowInsecurePath,
			allowSymlinkPath: policy.exec.allowSymlinkCommand
		});
	} catch (err) {
		return failClosed(formatErrorMessage(err));
	}
	try {
		await assertSecurePolicyScriptArg({
			command: secureCommandPath,
			args: policy.exec.args ?? [],
			trustedDirs: policy.exec.trustedDirs,
			allowInsecurePath: policy.exec.allowInsecurePath,
			allowSymlinkPath: policy.exec.allowSymlinkCommand
		});
	} 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 runPolicyCommand({
			command: secureCommandPath,
			args: policy.exec.args ?? [],
			cwd,
			env: childEnv,
			input,
			timeoutMs,
			noOutputTimeoutMs,
			maxOutputBytes
		});
	} 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.code !== 0) return failClosed(`policy command exited with code ${String(result.code)}`);
	const parsed = parsePolicyResponse(result.stdout);
	if (parsed.blocked) return logBlocked(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 };
