UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,138 lines 57.6 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { n as asNullableRecord } from "./record-coerce-DHZ4bFlT.js";
import { y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js";
import "./parse-finite-number-Z7n6tXLk.js";
import { s as resolveConfigPath, y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { a as inspectPathPermissions, i as formatPermissionRemediation, r as formatPermissionDetail } from "./permissions-ya3cPkFH.js";
import { s as hasConfiguredSecretInput } from "./types.secrets-_0JOMGE5.js";
import { i as normalizeProviderId } from "./provider-id-Dq06Bcx6.js";
import "./agent-scope-MrLta7Pq.js";
import { t as DEFAULT_AGENT_ID } from "./session-key-B_NoIfpX.js";
import { c as resolveDefaultAgentId, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js";
import "./audit-fs-CBe_wA_B.js";
import { n as resolveGatewayAuth } from "./auth-resolve-BSKl_nHz.js";
import { i as normalizeTrustedSafeBinDirs, o as listRiskyConfiguredSafeBins } from "./exec-safe-bin-trust-Do7n3gvF.js";
import { i as resolveSandboxConfigForAgent } from "./config-zzXWNRgc.js";
import { l as isInterpreterLikeAllowlistPattern } from "./risks-DRxJ1pW5.js";
import { P as resolveExecApprovalsFromFile, f as loadExecApprovals, h as minSecurity, p as maxAsk } from "./exec-approvals-C6M6SGY3.js";
import { i as resolveMergedSafeBinProfileFixtures, n as listInterpreterLikeSafeBins } from "./exec-safe-bin-runtime-policy-DKOVy8lE.js";
import { r as collectCoreInsecureOrDangerousFlags } from "./dangerous-config-flags-current-BDtKyYpY.js";
import { t as collectEnabledInsecureOrDangerousFlags } from "./dangerous-config-flags-BfqGh65Q.js";
import { n as resolveExecDefaults } from "./exec-defaults-BenGbA5f.js";
import { r as materializeGatewayAuthSecretRefs, t as canMaterializeGatewayAuthSecretRefsWithoutExec } from "./auth-config-utils-Cgy-Dcqz.js";
import { t as DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools-1CBnzkwG.js";
import { t as resolveGatewayAuthTokenSourceConflict } from "./auth-token-source-conflict-DjnAqb2k.js";
import { t as collectExecFilesystemPolicyDriftHits } from "./exec-filesystem-policy-D7p63mZA.js";
import path from "node:path";
import { isIP } from "node:net";
//#region src/security/audit-deep-code-safety.ts
let auditDeepModulePromise;
/** Lazily load deep audit code paths so normal audits avoid plugin/skill scans. */
async function loadAuditDeepModule() {
	auditDeepModulePromise ??= import("./audit.deep.runtime.js");
	return await auditDeepModulePromise;
}
/** Collect plugin and installed-skill code safety findings when deep audit is enabled. */
async function collectDeepCodeSafetyFindings(params) {
	if (!params.deep) return [];
	const auditDeep = await loadAuditDeepModule();
	return [...await auditDeep.collectPluginsCodeSafetyFindings({
		stateDir: params.stateDir,
		summaryCache: params.summaryCache
	}), ...await auditDeep.collectInstalledSkillsCodeSafetyFindings({
		cfg: params.cfg,
		stateDir: params.stateDir,
		summaryCache: params.summaryCache
	})];
}
//#endregion
//#region src/security/audit-deep-probe-findings.ts
/**
* Convert optional deep gateway probe results into security audit findings.
* This keeps CLI/audit callers aligned on check ids, titles, and remediation text.
*/
function collectDeepProbeFindings(params) {
	const findings = [];
	if (params.deep?.gateway?.attempted && !params.deep.gateway.ok) findings.push({
		checkId: "gateway.probe_failed",
		severity: "warn",
		title: "Gateway probe failed (deep)",
		detail: params.deep.gateway.error ?? "gateway unreachable",
		remediation: `Run "${formatCliCommand("openclaw status --all")}" to debug connectivity/auth, then re-run "${formatCliCommand("openclaw security audit --deep")}".`
	});
	if (params.authWarning) findings.push({
		checkId: "gateway.probe_auth_secretref_unavailable",
		severity: "warn",
		title: "Gateway probe auth SecretRef is unavailable",
		detail: params.authWarning,
		remediation: `Set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD in this shell or resolve the external secret provider, then re-run "${formatCliCommand("openclaw security audit --deep")}".`
	});
	return findings;
}
//#endregion
//#region src/security/audit-gateway-config.ts
function hasNonEmptyString(value) {
	return typeof value === "string" && value.trim().length > 0;
}
function collectGatewayConfigFindings$1(cfg, sourceConfig, env, options = {}) {
	const findings = [];
	const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
	const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
	const auth = resolveGatewayAuth({
		authConfig: cfg.gateway?.auth,
		authOverride: options.gatewayAuthOverride,
		tailscaleMode,
		env
	});
	const controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false;
	const controlUiAllowedOrigins = normalizeStringEntries(cfg.gateway?.controlUi?.allowedOrigins ?? []);
	const dangerouslyAllowHostHeaderOriginFallback = cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true;
	const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies) ? cfg.gateway.trustedProxies : [];
	const hasToken = typeof auth.token === "string" && auth.token.trim().length > 0;
	const hasPassword = typeof auth.password === "string" && auth.password.trim().length > 0;
	const envTokenConfigured = hasNonEmptyString(env.OPENCLAW_GATEWAY_TOKEN);
	const envPasswordConfigured = hasNonEmptyString(env.OPENCLAW_GATEWAY_PASSWORD);
	const tokenConfiguredFromConfig = hasConfiguredSecretInput(sourceConfig.gateway?.auth?.token, sourceConfig.secrets?.defaults);
	const passwordConfiguredFromConfig = hasConfiguredSecretInput(sourceConfig.gateway?.auth?.password, sourceConfig.secrets?.defaults);
	const remoteTokenConfigured = hasConfiguredSecretInput(sourceConfig.gateway?.remote?.token, sourceConfig.secrets?.defaults);
	const explicitAuthMode = options.gatewayAuthOverride?.mode ?? sourceConfig.gateway?.auth?.mode;
	const tokenCanWin = hasToken || envTokenConfigured || tokenConfiguredFromConfig || remoteTokenConfigured;
	const passwordCanWin = explicitAuthMode === "password" || explicitAuthMode !== "token" && explicitAuthMode !== "none" && explicitAuthMode !== "trusted-proxy" && !tokenCanWin;
	const tokenConfigured = tokenCanWin;
	const passwordConfigured = hasPassword || passwordCanWin && (envPasswordConfigured || passwordConfiguredFromConfig);
	const hasSharedSecret = explicitAuthMode === "token" ? tokenConfigured : explicitAuthMode === "password" ? passwordConfigured : explicitAuthMode === "none" || explicitAuthMode === "trusted-proxy" ? false : tokenConfigured || passwordConfigured;
	const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve";
	const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth;
	const allowRealIpFallback = cfg.gateway?.allowRealIpFallback === true;
	const mdnsMode = cfg.discovery?.mdns?.mode ?? "minimal";
	const gatewayToolsAllowRaw = Array.isArray(cfg.gateway?.tools?.allow) ? cfg.gateway?.tools?.allow : [];
	const gatewayToolsAllow = new Set(gatewayToolsAllowRaw.map((v) => normalizeOptionalLowercaseString(v) ?? "").filter(Boolean));
	const reenabledOverHttp = DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter((name) => gatewayToolsAllow.has(name));
	if (reenabledOverHttp.length > 0) {
		const extraRisk = bind !== "loopback" || tailscaleMode === "funnel";
		findings.push({
			checkId: "gateway.tools_invoke_http.dangerous_allow",
			severity: extraRisk ? "critical" : "warn",
			title: "Gateway HTTP /tools/invoke re-enables dangerous tools",
			detail: `gateway.tools.allow includes ${reenabledOverHttp.join(", ")} which removes them from the default HTTP deny list. This can allow remote session spawning / control-plane actions via HTTP and increases RCE blast radius if the gateway is reachable.`,
			remediation: "Remove these entries from gateway.tools.allow (recommended). If you keep them enabled, keep gateway.bind loopback-only (or tailnet-only), restrict network exposure, and treat the gateway token/password as full-admin."
		});
	}
	if (bind !== "loopback" && !hasSharedSecret && auth.mode !== "trusted-proxy") findings.push({
		checkId: "gateway.bind_no_auth",
		severity: "critical",
		title: "Gateway binds beyond loopback without auth",
		detail: `gateway.bind="${bind}" but no gateway.auth token/password is configured.`,
		remediation: `Set gateway.auth (token recommended) or bind to loopback.`
	});
	const tokenConflict = resolveGatewayAuthTokenSourceConflict({
		cfg: sourceConfig,
		env
	});
	if (tokenConflict) findings.push({
		checkId: tokenConflict.checkId,
		severity: "warn",
		title: tokenConflict.title,
		detail: tokenConflict.detail,
		remediation: tokenConflict.remediation
	});
	if (bind === "loopback" && controlUiEnabled && trustedProxies.length === 0) findings.push({
		checkId: "gateway.trusted_proxies_missing",
		severity: "warn",
		title: "Reverse proxy headers are not trusted",
		detail: "gateway.bind is loopback and gateway.trustedProxies is empty. If you expose the Control UI through a reverse proxy, configure trusted proxies so local-client checks cannot be spoofed.",
		remediation: "Set gateway.trustedProxies to your proxy IPs or keep the Control UI local-only."
	});
	if (bind === "loopback" && controlUiEnabled && !hasGatewayAuth) findings.push({
		checkId: "gateway.loopback_no_auth",
		severity: "critical",
		title: "Gateway auth missing on loopback",
		detail: "gateway.bind is loopback but no gateway auth secret is configured. If the Control UI is exposed through a reverse proxy, unauthenticated access is possible.",
		remediation: "Set gateway.auth (token recommended) or keep the Control UI local-only."
	});
	if (bind !== "loopback" && controlUiEnabled && controlUiAllowedOrigins.length === 0 && !dangerouslyAllowHostHeaderOriginFallback) findings.push({
		checkId: "gateway.control_ui.allowed_origins_required",
		severity: "critical",
		title: "Non-loopback Control UI missing explicit allowed origins",
		detail: "Control UI is enabled on a non-loopback bind but gateway.controlUi.allowedOrigins is empty. Strict origin policy requires explicit allowed origins for non-loopback deployments.",
		remediation: "Set gateway.controlUi.allowedOrigins to full trusted origins (for example https://control.example.com). If your deployment intentionally relies on Host-header origin fallback, set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true."
	});
	if (controlUiAllowedOrigins.includes("*")) {
		const exposed = bind !== "loopback";
		findings.push({
			checkId: "gateway.control_ui.allowed_origins_wildcard",
			severity: exposed ? "critical" : "warn",
			title: "Control UI allowed origins contains wildcard",
			detail: "gateway.controlUi.allowedOrigins includes \"*\" which means allow any browser origin for Control UI/WebChat requests. This disables origin allowlisting and should be treated as an intentional allow-all policy.",
			remediation: "Replace wildcard origins with explicit trusted origins (for example https://control.example.com). Do not use \"*\" outside tightly controlled local testing."
		});
	}
	if (dangerouslyAllowHostHeaderOriginFallback) {
		const exposed = bind !== "loopback";
		findings.push({
			checkId: "gateway.control_ui.host_header_origin_fallback",
			severity: exposed ? "critical" : "warn",
			title: "DANGEROUS: Host-header origin fallback enabled",
			detail: "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true enables Host-header origin fallback for Control UI/WebChat websocket checks and weakens DNS rebinding protections.",
			remediation: "Disable gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback and configure explicit gateway.controlUi.allowedOrigins."
		});
	}
	if (allowRealIpFallback) {
		const hasNonLoopbackTrustedProxy = trustedProxies.some((proxy) => !isStrictLoopbackTrustedProxyEntry(proxy));
		const exposed = bind !== "loopback" || auth.mode === "trusted-proxy" && hasNonLoopbackTrustedProxy;
		findings.push({
			checkId: "gateway.real_ip_fallback_enabled",
			severity: exposed ? "critical" : "warn",
			title: "X-Real-IP fallback is enabled",
			detail: "gateway.allowRealIpFallback=true trusts X-Real-IP when trusted proxies omit X-Forwarded-For. Misconfigured proxies that forward client-supplied X-Real-IP can spoof source IP and local-client checks.",
			remediation: "Keep gateway.allowRealIpFallback=false (default). Only enable this when your trusted proxy always overwrites X-Real-IP and cannot provide X-Forwarded-For."
		});
	}
	if (mdnsMode === "full") {
		const exposed = bind !== "loopback";
		findings.push({
			checkId: "discovery.mdns_full_mode",
			severity: exposed ? "critical" : "warn",
			title: "mDNS full mode can leak host metadata",
			detail: "discovery.mdns.mode=\"full\" publishes cliPath/sshPort in local-network TXT records. This can reveal usernames, filesystem layout, and management ports.",
			remediation: "Prefer discovery.mdns.mode=\"minimal\" (recommended) or \"off\", especially when gateway.bind is not loopback."
		});
	}
	if (tailscaleMode === "funnel") findings.push({
		checkId: "gateway.tailscale_funnel",
		severity: "critical",
		title: "Tailscale Funnel exposure enabled",
		detail: `gateway.tailscale.mode="funnel" exposes the Gateway publicly; keep auth strict and treat it as internet-facing.`,
		remediation: `Prefer tailscale.mode="serve" (tailnet-only) or set tailscale.mode="off".`
	});
	else if (tailscaleMode === "serve") findings.push({
		checkId: "gateway.tailscale_serve",
		severity: "info",
		title: "Tailscale Serve exposure enabled",
		detail: `gateway.tailscale.mode="serve" exposes the Gateway to your tailnet (loopback behind Tailscale).`
	});
	if (cfg.gateway?.controlUi?.allowInsecureAuth === true) findings.push({
		checkId: "gateway.control_ui.insecure_auth",
		severity: "warn",
		title: "Control UI insecure auth toggle enabled",
		detail: "gateway.controlUi.allowInsecureAuth=true does not bypass secure context or device identity checks; only dangerouslyDisableDeviceAuth disables Control UI device identity checks.",
		remediation: "Disable it or switch to HTTPS (Tailscale Serve) or localhost."
	});
	if (cfg.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true) findings.push({
		checkId: "gateway.control_ui.device_auth_disabled",
		severity: "critical",
		title: "DANGEROUS: Control UI device auth disabled",
		detail: "gateway.controlUi.dangerouslyDisableDeviceAuth=true disables device identity checks for the Control UI.",
		remediation: "Disable it unless you are in a short-lived break-glass scenario."
	});
	const enabledDangerousFlags = (options.collectDangerousConfigFlags ?? collectCoreInsecureOrDangerousFlags)(cfg);
	for (const enabledFlag of enabledDangerousFlags) findings.push({
		checkId: "config.insecure_or_dangerous_flags",
		severity: "warn",
		title: "Insecure or dangerous config flag enabled",
		detail: `Detected enabled flag: ${enabledFlag}.`,
		remediation: "Disable this flag when not actively debugging, or keep deployment scoped to trusted/local-only networks."
	});
	const token = typeof auth.token === "string" && auth.token.trim().length > 0 ? auth.token.trim() : null;
	if (auth.mode === "token" && token && token.length < 24) findings.push({
		checkId: "gateway.token_too_short",
		severity: "warn",
		title: "Gateway token looks short",
		detail: `gateway auth token is ${token.length} chars; prefer a long random token.`
	});
	if (auth.mode === "trusted-proxy") {
		const trustedProxiesLocal = cfg.gateway?.trustedProxies ?? [];
		const trustedProxyConfig = cfg.gateway?.auth?.trustedProxy;
		findings.push({
			checkId: "gateway.trusted_proxy_auth",
			severity: "critical",
			title: "Trusted-proxy auth mode enabled",
			detail: "gateway.auth.mode=\"trusted-proxy\" delegates authentication to a reverse proxy. Ensure your proxy (Pomerium, Caddy, nginx) handles auth correctly and that gateway.trustedProxies only contains IPs of your actual proxy servers.",
			remediation: "Verify: (1) Your proxy terminates TLS and authenticates users. (2) gateway.trustedProxies is restricted to proxy IPs only. (3) Direct access to the Gateway port is blocked by firewall. See /gateway/trusted-proxy-auth for setup guidance."
		});
		if (trustedProxiesLocal.length === 0) findings.push({
			checkId: "gateway.trusted_proxy_no_proxies",
			severity: "critical",
			title: "Trusted-proxy auth enabled but no trusted proxies configured",
			detail: "gateway.auth.mode=\"trusted-proxy\" but gateway.trustedProxies is empty. All requests will be rejected.",
			remediation: "Set gateway.trustedProxies to the IP(s) of your reverse proxy."
		});
		if (!trustedProxyConfig?.userHeader) findings.push({
			checkId: "gateway.trusted_proxy_no_user_header",
			severity: "critical",
			title: "Trusted-proxy auth missing userHeader config",
			detail: "gateway.auth.mode=\"trusted-proxy\" but gateway.auth.trustedProxy.userHeader is not configured.",
			remediation: "Set gateway.auth.trustedProxy.userHeader to the header name your proxy uses (e.g., \"x-forwarded-user\", \"x-pomerium-claim-email\")."
		});
		if (trustedProxyConfig?.allowLoopback === true) findings.push({
			checkId: "gateway.trusted_proxy_allow_loopback",
			severity: "warn",
			title: "Trusted-proxy auth allows loopback proxy sources",
			detail: "gateway.auth.trustedProxy.allowLoopback=true allows loopback-source requests from configured gateway.trustedProxies entries to satisfy trusted-proxy auth.",
			remediation: "Enable this only when a same-host reverse proxy is the intended trust boundary. Keep direct Gateway access private to the host and require the proxy to strip or overwrite identity headers."
		});
		if ((trustedProxyConfig?.allowUsers ?? []).length === 0) findings.push({
			checkId: "gateway.trusted_proxy_no_allowlist",
			severity: "warn",
			title: "Trusted-proxy auth allows all authenticated users",
			detail: "gateway.auth.trustedProxy.allowUsers is empty, so any user authenticated by your proxy can access the Gateway.",
			remediation: "Consider setting gateway.auth.trustedProxy.allowUsers to restrict access to specific users (e.g., [\"nick@example.com\"])."
		});
	}
	if (bind !== "loopback" && auth.mode !== "trusted-proxy" && !cfg.gateway?.auth?.rateLimit) findings.push({
		checkId: "gateway.auth_no_rate_limit",
		severity: "warn",
		title: "No auth rate limiting configured",
		detail: "gateway.bind is not loopback but no gateway.auth.rateLimit is configured. Without rate limiting, brute-force auth attacks are not mitigated.",
		remediation: "Set gateway.auth.rateLimit (e.g. { maxAttempts: 10, windowMs: 60000, lockoutMs: 300000 })."
	});
	return findings;
}
function isStrictLoopbackTrustedProxyEntry(entry) {
	const candidate = entry.trim();
	if (!candidate) return false;
	if (!candidate.includes("/")) return candidate === "127.0.0.1" || candidate.toLowerCase() === "::1";
	const [rawIp, rawPrefix] = candidate.split("/", 2);
	if (!rawIp || !rawPrefix) return false;
	const ipVersion = isIP(rawIp.trim());
	const prefix = parseStrictNonNegativeInteger(rawPrefix);
	if (prefix === void 0) return false;
	if (ipVersion === 4) return rawIp.trim() === "127.0.0.1" && prefix === 32;
	if (ipVersion === 6) return prefix === 128 && normalizeLowercaseStringOrEmpty(rawIp) === "::1";
	return false;
}
//#endregion
//#region src/security/audit.ts
let readOnlyChannelPluginsModulePromise;
let auditNonDeepModulePromise;
let auditChannelModulePromise;
let pluginMetadataRegistryLoaderModulePromise;
let pluginAutoEnableModulePromise;
let channelPluginIdsModulePromise;
let pluginRuntimeModulePromise;
let gatewayProbeDepsPromise;
async function loadReadOnlyChannelPlugins() {
	readOnlyChannelPluginsModulePromise ??= import("./read-only-Bq9Svq_O.js");
	return await readOnlyChannelPluginsModulePromise;
}
async function loadAuditNonDeepModule() {
	auditNonDeepModulePromise ??= import("./audit.nondeep.runtime.js");
	return await auditNonDeepModulePromise;
}
async function loadAuditChannelModule() {
	auditChannelModulePromise ??= import("./audit-channel.collect.runtime.js");
	return await auditChannelModulePromise;
}
async function loadPluginMetadataRegistryLoaderModule() {
	pluginMetadataRegistryLoaderModulePromise ??= import("./metadata-registry-loader-Da9KYtnm.js");
	return await pluginMetadataRegistryLoaderModulePromise;
}
async function loadPluginAutoEnableModule() {
	pluginAutoEnableModulePromise ??= import("./plugin-auto-enable-Bm6cIZBX.js");
	return await pluginAutoEnableModulePromise;
}
async function loadChannelPluginIdsModule() {
	channelPluginIdsModulePromise ??= import("./channel-plugin-ids-JT38R6k8.js");
	return await channelPluginIdsModulePromise;
}
async function loadPluginRuntimeModule() {
	pluginRuntimeModulePromise ??= import("./runtime-Ccg99mbJ.js");
	return await pluginRuntimeModulePromise;
}
async function loadGatewayProbeDeps() {
	gatewayProbeDepsPromise ??= Promise.all([
		import("./call-BEl78mhZ.js"),
		import("./probe-auth-CFLBxNcR.js"),
		import("./probe-BFjltCXj.js")
	]).then(([callModule, probeAuthModule, probeModule]) => ({
		buildGatewayConnectionDetails: callModule.buildGatewayConnectionDetails,
		resolveGatewayProbeAuthSafe: probeAuthModule.resolveGatewayProbeAuthSafe,
		resolveGatewayProbeTarget: probeAuthModule.resolveGatewayProbeTarget,
		probeGateway: probeModule.probeGateway
	}));
	return await gatewayProbeDepsPromise;
}
function countBySeverity(findings) {
	let critical = 0;
	let warn = 0;
	let info = 0;
	for (const f of findings) if (f.severity === "critical") critical += 1;
	else if (f.severity === "warn") warn += 1;
	else info += 1;
	return {
		critical,
		warn,
		info
	};
}
function normalizeSuppressionText(value) {
	return (value ?? "").trim().toLowerCase();
}
async function materializeAuditGatewayAuthRefs(params) {
	const materializeParams = {
		cfg: params.cfg,
		env: params.env,
		mode: params.cfg.gateway?.auth?.mode,
		hasTokenCandidate: Boolean(normalizeOptionalString(params.env.OPENCLAW_GATEWAY_TOKEN)),
		hasPasswordCandidate: Boolean(normalizeOptionalString(params.env.OPENCLAW_GATEWAY_PASSWORD))
	};
	if (!canMaterializeGatewayAuthSecretRefsWithoutExec(materializeParams)) return params.cfg;
	try {
		return await materializeGatewayAuthSecretRefs(materializeParams);
	} catch {
		return params.cfg;
	}
}
function shouldMaterializeHooksGatewayAuthRefs(cfg) {
	return cfg.hooks?.enabled === true && Boolean(normalizeOptionalString(cfg.hooks.token));
}
function findingMatchesSuppression(finding, suppression) {
	const checkId = suppression.checkId.trim();
	if (!checkId || finding.checkId !== checkId) return false;
	const titleNeedle = normalizeSuppressionText(suppression.titleIncludes);
	if (titleNeedle && !finding.title.toLowerCase().includes(titleNeedle)) return false;
	const detailNeedle = normalizeSuppressionText(suppression.detailIncludes);
	if (detailNeedle && !finding.detail.toLowerCase().includes(detailNeedle)) return false;
	return true;
}
function buildSecurityAuditSuppressionsActiveFinding(params) {
	return {
		checkId: "security.audit.suppressions.active",
		severity: "info",
		title: "Security audit suppressions configured",
		detail: `security.audit.suppressions has ${params.configuredCount} configured suppression(s); ${params.suppressedCount} finding(s) moved to suppressedFindings.`,
		remediation: "Review suppressedFindings and remove suppressions when the accepted risk no longer applies."
	};
}
function applySecurityAuditSuppressions(findings, suppressions) {
	if (!Array.isArray(suppressions) || suppressions.length === 0) return {
		findings,
		suppressedFindings: []
	};
	const active = [];
	const suppressedFindings = [];
	for (const finding of findings) {
		const suppression = suppressions.find((candidate) => findingMatchesSuppression(finding, candidate));
		if (!suppression) {
			active.push(finding);
			continue;
		}
		const reason = suppression.reason?.trim();
		suppressedFindings.push({
			...finding,
			suppression: reason ? { reason } : {}
		});
	}
	return {
		findings: active,
		suppressedFindings
	};
}
function normalizeAllowFromList(list) {
	if (!Array.isArray(list)) return [];
	return normalizeStringEntries(list);
}
async function collectFilesystemFindings(params) {
	const findings = [];
	const stateDirPerms = await inspectPathPermissions(params.stateDir, {
		env: params.env,
		platform: params.platform,
		exec: params.execIcacls
	});
	if (stateDirPerms.ok) {
		if (stateDirPerms.isSymlink) findings.push({
			checkId: "fs.state_dir.symlink",
			severity: "warn",
			title: "State dir is a symlink",
			detail: `${params.stateDir} is a symlink; treat this as an extra trust boundary.`
		});
		if (stateDirPerms.worldWritable) findings.push({
			checkId: "fs.state_dir.perms_world_writable",
			severity: "critical",
			title: "State dir is world-writable",
			detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your OpenClaw state.`,
			remediation: formatPermissionRemediation({
				targetPath: params.stateDir,
				perms: stateDirPerms,
				isDir: true,
				posixMode: 448,
				env: params.env
			})
		});
		else if (stateDirPerms.groupWritable) findings.push({
			checkId: "fs.state_dir.perms_group_writable",
			severity: "warn",
			title: "State dir is group-writable",
			detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your OpenClaw state.`,
			remediation: formatPermissionRemediation({
				targetPath: params.stateDir,
				perms: stateDirPerms,
				isDir: true,
				posixMode: 448,
				env: params.env
			})
		});
		else if (stateDirPerms.groupReadable || stateDirPerms.worldReadable) findings.push({
			checkId: "fs.state_dir.perms_readable",
			severity: "warn",
			title: "State dir is readable by others",
			detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; consider restricting to 700.`,
			remediation: formatPermissionRemediation({
				targetPath: params.stateDir,
				perms: stateDirPerms,
				isDir: true,
				posixMode: 448,
				env: params.env
			})
		});
	}
	const configPerms = await inspectPathPermissions(params.configPath, {
		env: params.env,
		platform: params.platform,
		exec: params.execIcacls
	});
	if (configPerms.ok) {
		const skipReadablePermWarnings = configPerms.isSymlink;
		if (configPerms.isSymlink) findings.push({
			checkId: "fs.config.symlink",
			severity: "warn",
			title: "Config file is a symlink",
			detail: `${params.configPath} is a symlink; make sure you trust its target.`
		});
		if (configPerms.worldWritable || configPerms.groupWritable) findings.push({
			checkId: "fs.config.perms_writable",
			severity: "critical",
			title: "Config file is writable by others",
			detail: `${formatPermissionDetail(params.configPath, configPerms)}; another user could change gateway/auth/tool policies.`,
			remediation: formatPermissionRemediation({
				targetPath: params.configPath,
				perms: configPerms,
				isDir: false,
				posixMode: 384,
				env: params.env
			})
		});
		else if (!skipReadablePermWarnings && configPerms.worldReadable) findings.push({
			checkId: "fs.config.perms_world_readable",
			severity: "critical",
			title: "Config file is world-readable",
			detail: `${formatPermissionDetail(params.configPath, configPerms)}; config can contain tokens and private settings.`,
			remediation: formatPermissionRemediation({
				targetPath: params.configPath,
				perms: configPerms,
				isDir: false,
				posixMode: 384,
				env: params.env
			})
		});
		else if (!skipReadablePermWarnings && configPerms.groupReadable) findings.push({
			checkId: "fs.config.perms_group_readable",
			severity: "warn",
			title: "Config file is group-readable",
			detail: `${formatPermissionDetail(params.configPath, configPerms)}; config can contain tokens and private settings.`,
			remediation: formatPermissionRemediation({
				targetPath: params.configPath,
				perms: configPerms,
				isDir: false,
				posixMode: 384,
				env: params.env
			})
		});
	}
	return findings;
}
function collectGatewayConfigFindings(cfg, sourceConfig, env, options = {}) {
	return collectGatewayConfigFindings$1(cfg, sourceConfig, env, {
		collectDangerousConfigFlags: collectEnabledInsecureOrDangerousFlags,
		gatewayAuthOverride: options.gatewayAuthOverride
	});
}
async function collectPluginSecurityAuditFindings(context) {
	if (!context.loadPluginSecurityCollectors) return [];
	const { getActivePluginRegistry } = await loadPluginRuntimeModule();
	let collectors = getActivePluginRegistry()?.securityAuditCollectors ?? [];
	if (collectors.length === 0) {
		const { applyPluginAutoEnable } = await loadPluginAutoEnableModule();
		const autoEnabled = applyPluginAutoEnable({
			config: context.sourceConfig,
			env: context.env
		});
		const requestedPluginIds = /* @__PURE__ */ new Set();
		for (const pluginId of Object.keys(autoEnabled.autoEnabledReasons)) {
			const normalized = pluginId.trim();
			if (normalized) requestedPluginIds.add(normalized);
		}
		for (const pluginId of autoEnabled.config.plugins?.allow ?? []) {
			if (typeof pluginId !== "string") continue;
			const normalized = pluginId.trim();
			if (normalized) requestedPluginIds.add(normalized);
		}
		for (const [pluginId, entry] of Object.entries(autoEnabled.config.plugins?.entries ?? {})) {
			if (entry?.enabled === false) continue;
			const normalized = pluginId.trim();
			if (normalized) requestedPluginIds.add(normalized);
		}
		if (context.includeChannelSecurity && context.plugins !== void 0) {
			const { resolveConfiguredChannelPluginIds } = await loadChannelPluginIdsModule();
			const auditedChannelPluginIds = new Set(context.plugins.map((plugin) => plugin.id));
			for (const pluginId of resolveConfiguredChannelPluginIds({
				config: autoEnabled.config,
				activationSourceConfig: context.sourceConfig,
				workspaceDir: context.workspaceDir,
				env: context.env
			})) if (auditedChannelPluginIds.has(pluginId)) requestedPluginIds.delete(pluginId);
		}
		if (requestedPluginIds.size === 0) return [];
		collectors = (await loadPluginMetadataRegistryLoaderModule()).loadPluginMetadataRegistrySnapshot({
			config: autoEnabled.config,
			activationSourceConfig: context.sourceConfig,
			env: context.env,
			workspaceDir: context.workspaceDir,
			onlyPluginIds: [...requestedPluginIds]
		}).securityAuditCollectors ?? [];
	}
	return (await Promise.all(collectors.map(async (entry) => {
		try {
			return await entry.collector({
				config: context.cfg,
				sourceConfig: context.sourceConfig,
				env: context.env,
				stateDir: context.stateDir,
				configPath: context.configPath
			});
		} catch (err) {
			return [{
				checkId: `plugins.${entry.pluginId}.security_audit_failed`,
				severity: "warn",
				title: "Plugin security audit collector failed",
				detail: `${entry.pluginId}: ${String(err)}`
			}];
		}
	}))).flat();
}
function collectLoggingFindings(cfg) {
	if (cfg.logging?.redactSensitive !== "off") return [];
	return [{
		checkId: "logging.redact_off",
		severity: "warn",
		title: "Tool summary redaction is disabled",
		detail: `logging.redactSensitive="off" can leak secrets into logs and status output.`,
		remediation: `Set logging.redactSensitive="tools".`
	}];
}
function collectElevatedFindings(cfg) {
	const findings = [];
	const enabled = cfg.tools?.elevated?.enabled;
	const allowFrom = cfg.tools?.elevated?.allowFrom ?? {};
	const anyAllowFromKeys = Object.keys(allowFrom).length > 0;
	if (enabled === false) return findings;
	if (!anyAllowFromKeys) return findings;
	for (const [provider, list] of Object.entries(allowFrom)) {
		const normalized = normalizeAllowFromList(list);
		if (normalized.includes("*")) findings.push({
			checkId: `tools.elevated.allowFrom.${provider}.wildcard`,
			severity: "critical",
			title: "Elevated exec allowlist contains wildcard",
			detail: `tools.elevated.allowFrom.${provider} includes "*" which effectively approves everyone on that channel for elevated mode.`
		});
		else if (normalized.length > 25) findings.push({
			checkId: `tools.elevated.allowFrom.${provider}.large`,
			severity: "warn",
			title: "Elevated exec allowlist is large",
			detail: `tools.elevated.allowFrom.${provider} has ${normalized.length} entries; consider tightening elevated access.`
		});
	}
	return findings;
}
const CLAUDE_PERMISSION_MODE_FLAG = "--permission-mode";
const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions";
function extractClaudePermissionMode(args) {
	if (!Array.isArray(args)) return;
	for (let i = args.length - 1; i >= 0; i -= 1) {
		const arg = args[i] ?? "";
		if (arg === CLAUDE_PERMISSION_MODE_FLAG) {
			const value = args[i + 1];
			if (typeof value === "string" && value.trim().length > 0 && !value.startsWith("-")) return value.trim();
			continue;
		}
		if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_FLAG}=`)) {
			const value = arg.slice(`${CLAUDE_PERMISSION_MODE_FLAG}=`.length).trim();
			if (value.length > 0 && !value.startsWith("-")) return value;
		}
	}
}
function collectRestrictiveClaudePermissionModeHits(backend) {
	if (!isManagedClaudeLiveBackendConfig(backend)) return [];
	const hits = [];
	const argsMode = extractClaudePermissionMode(backend.args);
	if (argsMode && argsMode !== CLAUDE_BYPASS_PERMISSION_MODE) hits.push({
		argSet: "args",
		mode: argsMode
	});
	const resumeArgsMode = extractClaudePermissionMode(backend.resumeArgs);
	if (resumeArgsMode && resumeArgsMode !== CLAUDE_BYPASS_PERMISSION_MODE) hits.push({
		argSet: "resumeArgs",
		mode: resumeArgsMode
	});
	return hits;
}
function isManagedClaudeLiveBackendConfig(backend) {
	if (!backend) return false;
	const output = backend.output ?? "jsonl";
	const input = backend.input ?? "stdin";
	return (backend.liveSession ?? (output === "jsonl" && input === "stdin" ? "claude-stdio" : void 0)) === "claude-stdio" && output === "jsonl" && input === "stdin";
}
function findClaudeCliBackendConfig(backends) {
	if (!backends) return;
	const directKey = Object.keys(backends).find((key) => normalizeOptionalLowercaseString(key) === "claude-cli");
	if (directKey) return backends[directKey];
	for (const [key, backend] of Object.entries(backends)) {
		const normalizedKey = normalizeProviderId(key);
		const command = normalizeOptionalLowercaseString(backend.command);
		if (normalizedKey === "claude-cli" || normalizedKey === "anthropic-cli" || command === "claude") return backend;
	}
}
function collectYoloExecScopeIds(cfg, approvals) {
	const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
	return [{ id: DEFAULT_AGENT_ID }, ...agents.filter((entry) => Boolean(entry) && typeof entry === "object" && typeof entry.id === "string").map((entry) => ({ id: entry.id }))].filter((entry) => {
		const execDefaults = resolveExecDefaults({
			cfg,
			agentId: entry.id === "main" ? void 0 : entry.id
		});
		const resolvedApprovals = resolveExecApprovalsFromFile({
			file: approvals,
			agentId: entry.id === "main" ? void 0 : entry.id,
			overrides: {
				security: execDefaults.security,
				ask: execDefaults.ask
			}
		});
		return minSecurity(execDefaults.security, resolvedApprovals.agent.security) === "full" && maxAsk(execDefaults.ask, resolvedApprovals.agent.ask) === "off";
	}).map((entry) => entry.id);
}
function collectExecRuntimeFindings(cfg) {
	const findings = [];
	const globalExecHost = cfg.tools?.exec?.host;
	const globalStrictInlineEval = cfg.tools?.exec?.strictInlineEval === true;
	const defaultSandboxMode = resolveSandboxConfigForAgent(cfg).mode;
	const defaultHostIsExplicitSandbox = globalExecHost === "sandbox";
	const approvals = loadExecApprovals();
	const claudePermissionModeHits = collectRestrictiveClaudePermissionModeHits(findClaudeCliBackendConfig(cfg.agents?.defaults?.cliBackends));
	const yoloExecScopeIds = claudePermissionModeHits.length > 0 ? collectYoloExecScopeIds(cfg, approvals) : [];
	if (defaultHostIsExplicitSandbox && defaultSandboxMode === "off") findings.push({
		checkId: "tools.exec.host_sandbox_no_sandbox_defaults",
		severity: "warn",
		title: "Exec host is sandbox but sandbox mode is off",
		detail: "tools.exec.host is explicitly set to sandbox while agents.defaults.sandbox.mode=off. In this mode, exec fails closed because no sandbox runtime is available.",
		remediation: "Enable sandbox mode (`agents.defaults.sandbox.mode=\"non-main\"` or `\"all\"`) or set tools.exec.host to \"gateway\" with approvals."
	});
	const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
	const riskyAgents = agents.filter((entry) => entry && typeof entry === "object" && typeof entry.id === "string" && entry.tools?.exec?.host === "sandbox" && resolveSandboxConfigForAgent(cfg, entry.id).mode === "off").map((entry) => entry.id).slice(0, 5);
	if (riskyAgents.length > 0) findings.push({
		checkId: "tools.exec.host_sandbox_no_sandbox_agents",
		severity: "warn",
		title: "Agent exec host uses sandbox while sandbox mode is off",
		detail: `agents.list.*.tools.exec.host is set to sandbox for: ${riskyAgents.join(", ")}. With sandbox mode off, exec fails closed for those agents.`,
		remediation: "Enable sandbox mode for these agents (`agents.list[].sandbox.mode`) or set their tools.exec.host to \"gateway\"."
	});
	const effectiveExecScopes = Array.from(new Map([{
		id: DEFAULT_AGENT_ID,
		security: cfg.tools?.exec?.security ?? "deny",
		host: cfg.tools?.exec?.host ?? "auto"
	}, ...agents.filter((entry) => Boolean(entry) && typeof entry === "object" && typeof entry.id === "string").map((entry) => ({
		id: entry.id,
		security: entry.tools?.exec?.security ?? cfg.tools?.exec?.security ?? "deny",
		host: entry.tools?.exec?.host ?? cfg.tools?.exec?.host ?? "auto"
	}))].map((entry) => [entry.id, entry])).values());
	const fullExecScopes = effectiveExecScopes.filter((entry) => entry.security === "full");
	const execEnabledScopes = effectiveExecScopes.filter((entry) => entry.security !== "deny");
	const openExecSurfacePaths = collectOpenExecSurfacePaths(cfg);
	if (fullExecScopes.length > 0) findings.push({
		checkId: "tools.exec.security_full_configured",
		severity: openExecSurfacePaths.length > 0 ? "critical" : "warn",
		title: "Exec security=full is configured",
		detail: `Full exec trust is enabled for: ${fullExecScopes.map((entry) => entry.id).join(", ")}.` + (openExecSurfacePaths.length > 0 ? ` Open channel access was also detected at:\n${openExecSurfacePaths.map((entry) => `- ${entry}`).join("\n")}` : ""),
		remediation: "Prefer tools.exec.security=\"allowlist\" with ask prompts, and reserve \"full\" for tightly scoped break-glass agents only."
	});
	if (claudePermissionModeHits.length > 0 && yoloExecScopeIds.length > 0) findings.push({
		checkId: "agents.claude_cli.permission_mode_overridden_by_yolo",
		severity: "warn",
		title: "Claude permission mode is ignored under YOLO exec",
		detail: `claude-cli sets ${claudePermissionModeHits.map((hit) => `${hit.argSet}=${hit.mode}`).join(", ")}, but OpenClaw exec is YOLO for: ${yoloExecScopeIds.join(", ")}. Managed Claude live sessions use --permission-mode bypassPermissions.`,
		remediation: "Restrict OpenClaw tools.exec.security/tools.exec.ask, or remove the Claude --permission-mode override."
	});
	if (openExecSurfacePaths.length > 0 && execEnabledScopes.length > 0) findings.push({
		checkId: "security.exposure.open_channels_with_exec",
		severity: fullExecScopes.length > 0 ? "critical" : "warn",
		title: "Open channels can reach exec-enabled agents",
		detail: `Open DM/group access detected at:\n${openExecSurfacePaths.map((entry) => `- ${entry}`).join("\n")}\nExec-enabled scopes:\n${execEnabledScopes.map((entry) => `- ${entry.id}: security=${entry.security}, host=${entry.host}`).join("\n")}`,
		remediation: "Tighten dmPolicy/groupPolicy to pairing or allowlist, or disable exec for agents reachable from shared/public channels."
	});
	const execFilesystemPolicyHits = collectExecFilesystemPolicyDriftHits(cfg);
	if (execFilesystemPolicyHits.length > 0) findings.push({
		checkId: "tools.exec.fs_tools_disabled_but_exec_enabled",
		severity: "warn",
		title: "Filesystem tool policy does not make exec read-only",
		detail: `Found scopes where write/edit/apply_patch are unavailable but exec remains available:\n${execFilesystemPolicyHits.map((hit) => `- ${hit.scopeLabel}: runtime=[${hit.runtimeTools.join(", ")}], disabledFs=[${hit.disabledFilesystemTools.join(", ")}], exec.host=${hit.execHost}, sandbox=${hit.sandboxMode}, workspaceAccess=${hit.sandboxWorkspaceAccess}`).join("\n")}\nThe exec tool is a shell and can still write files wherever the selected host or sandbox filesystem permits it.`,
		remediation: "For read-only agents, deny exec and process too. If shell access is intentional, constrain the filesystem boundary with sandbox mode \"all\" and workspaceAccess \"ro\" or \"none\"."
	});
	const autoAllowSkillsHits = collectAutoAllowSkillsHits(approvals);
	if (autoAllowSkillsHits.length > 0) findings.push({
		checkId: "tools.exec.auto_allow_skills_enabled",
		severity: "warn",
		title: "autoAllowSkills is enabled for exec approvals",
		detail: `Implicit skill-bin allowlisting is enabled at:\n${autoAllowSkillsHits.map((entry) => `- ${entry}`).join("\n")}\nThis widens host exec trust beyond explicit manual allowlist entries.`,
		remediation: "Disable autoAllowSkills in exec approvals and keep manual allowlists tight when you need explicit host-exec trust."
	});
	const interpreterAllowlistHits = collectInterpreterAllowlistHits({
		approvals,
		strictInlineEvalForAgentId: (agentId) => {
			if (!agentId || agentId === "*" || agentId === "main") return globalStrictInlineEval;
			return agents.find((entry) => entry?.id === agentId)?.tools?.exec?.strictInlineEval === true || globalStrictInlineEval;
		}
	});
	if (interpreterAllowlistHits.length > 0) findings.push({
		checkId: "tools.exec.allowlist_interpreter_without_strict_inline_eval",
		severity: "warn",
		title: "Interpreter allowlist entries are missing strictInlineEval hardening",
		detail: `Interpreter/runtime allowlist entries were found without strictInlineEval enabled:\n${interpreterAllowlistHits.map((entry) => `- ${entry}`).join("\n")}`,
		remediation: "Set tools.exec.strictInlineEval=true (or per-agent tools.exec.strictInlineEval=true) when allowlisting interpreters like python, node, ruby, perl, php, lua, or osascript."
	});
	const normalizeConfiguredSafeBins = (entries) => {
		if (!Array.isArray(entries)) return [];
		return Array.from(new Set(entries.map((entry) => normalizeOptionalLowercaseString(entry) ?? "").filter((entry) => entry.length > 0))).toSorted();
	};
	const normalizeConfiguredTrustedDirs = (entries) => {
		if (!Array.isArray(entries)) return [];
		return normalizeTrustedSafeBinDirs(entries.filter((entry) => typeof entry === "string"));
	};
	const classifyRiskySafeBinTrustedDir = (entry) => {
		const raw = entry.trim();
		if (!raw) return null;
		if (!path.isAbsolute(raw)) return "relative path (trust boundary depends on process cwd)";
		const normalized = path.resolve(raw).replace(/\\/g, "/").toLowerCase();
		if (normalized === "/tmp" || normalized.startsWith("/tmp/") || normalized === "/var/tmp" || normalized.startsWith("/var/tmp/") || normalized === "/private/tmp" || normalized.startsWith("/private/tmp/")) return "temporary directory is mutable and easy to poison";
		if (normalized === "/usr/local/bin" || normalized === "/opt/homebrew/bin" || normalized === "/opt/local/bin" || normalized === "/home/linuxbrew/.linuxbrew/bin") return "package-manager bin directory (often user-writable)";
		if (normalized.startsWith("/users/") || normalized.startsWith("/home/") || normalized.includes("/.local/bin")) return "home-scoped bin directory (typically user-writable)";
		if (/^[a-z]:\/users\//.test(normalized)) return "home-scoped bin directory (typically user-writable)";
		return null;
	};
	const globalExec = cfg.tools?.exec;
	const riskyTrustedDirHits = [];
	const collectRiskyTrustedDirHits = (scopePath, entries) => {
		for (const entry of normalizeConfiguredTrustedDirs(entries)) {
			const reason = classifyRiskySafeBinTrustedDir(entry);
			if (!reason) continue;
			riskyTrustedDirHits.push(`- ${scopePath}.safeBinTrustedDirs: ${entry} (${reason})`);
		}
	};
	collectRiskyTrustedDirHits("tools.exec", globalExec?.safeBinTrustedDirs);
	for (const entry of agents) {
		if (!entry || typeof entry !== "object" || typeof entry.id !== "string") continue;
		collectRiskyTrustedDirHits(`agents.list.${entry.id}.tools.exec`, entry.tools?.exec?.safeBinTrustedDirs);
	}
	const interpreterHits = [];
	const riskySemanticSafeBinHits = [];
	const globalSafeBins = normalizeConfiguredSafeBins(globalExec?.safeBins);
	if (globalSafeBins.length > 0) {
		const merged = resolveMergedSafeBinProfileFixtures({ global: globalExec }) ?? {};
		const interpreters = listInterpreterLikeSafeBins(globalSafeBins).filter((bin) => !merged[bin]);
		if (interpreters.length > 0) interpreterHits.push(`- tools.exec.safeBins: ${interpreters.join(", ")}`);
		for (const hit of listRiskyConfiguredSafeBins(globalSafeBins)) riskySemanticSafeBinHits.push(`- tools.exec.safeBins: ${hit.bin} (${hit.warning})`);
	}
	for (const entry of agents) {
		if (!entry || typeof entry !== "object" || typeof entry.id !== "string") continue;
		const agentExec = entry.tools?.exec;
		const agentSafeBins = normalizeConfiguredSafeBins(agentExec?.safeBins);
		if (agentSafeBins.length === 0) continue;
		const merged = resolveMergedSafeBinProfileFixtures({
			global: globalExec,
			local: agentExec
		}) ?? {};
		const interpreters = listInterpreterLikeSafeBins(agentSafeBins).filter((bin) => !merged[bin]);
		if (interpreters.length === 0) {
			for (const hit of listRiskyConfiguredSafeBins(agentSafeBins)) riskySemanticSafeBinHits.push(`- agents.list.${entry.id}.tools.exec.safeBins: ${hit.bin} (${hit.warning})`);
			continue;
		}
		interpreterHits.push(`- agents.list.${entry.id}.tools.exec.safeBins: ${interpreters.join(", ")}`);
		for (const hit of listRiskyConfiguredSafeBins(agentSafeBins)) riskySemanticSafeBinHits.push(`- agents.list.${entry.id}.tools.exec.safeBins: ${hit.bin} (${hit.warning})`);
	}
	if (interpreterHits.length > 0) findings.push({
		checkId: "tools.exec.safe_bins_interpreter_unprofiled",
		severity: "warn",
		title: "safeBins includes interpreter/runtime binaries without explicit profiles",
		detail: `Detected interpreter-like safeBins entries missing explicit profiles:\n${interpreterHits.join("\n")}\nThese entries can turn safeBins into a broad execution surface when used with permissive argv profiles.`,
		remediation: "Remove interpreter/runtime bins from safeBins (prefer allowlist entries) or define hardened tools.exec.safeBinProfiles.<bin> rules."
	});
	if (riskySemanticSafeBinHits.length > 0) findings.push({
		checkId: "tools.exec.safe_bins_broad_behavior",
		severity: "warn",
		title: "safeBins includes binaries with broader semantics than low-risk stream filters",
		detail: `Detected risky safeBins entries:\n${riskySemanticSafeBinHits.join("\n")}\nThese tools expose semantics that do not fit the low-risk stdin-filter fast path.`,
		remediation: "Remove these binaries from safeBins and prefer explicit allowlist entries or approval-gated execution."
	});
	if (riskyTrustedDirHits.length > 0) findings.push({
		checkId: "tools.exec.safe_bin_trusted_dirs_risky",
		severity: "warn",
		title: "safeBinTrustedDirs includes risky mutable directories",
		detail: `Detected risky safeBinTrustedDirs entries:\n${riskyTrustedDirHits.slice(0, 10).join("\n")}` + (riskyTrustedDirHits.length > 10 ? `\n- +${riskyTrustedDirHits.length - 10} more entries.` : ""),
		remediation: "Prefer root-owned immutable bins, keep default trust dirs (/bin, /usr/bin), and avoid trusting temporary/home/package-manager paths unless tightly controlled."
	});
	return findings;
}
function collectOpenExecSurfacePaths(cfg) {
	const channels = asNullableRecord(cfg.channels);
	if (!channels) return [];
	const hits = /* @__PURE__ */ new Set();
	const seen = /* @__PURE__ */ new WeakSet();
	const visit = (value, scope) => {
		const record = asNullableRecord(value);
		if (!record || seen.has(record)) return;
		seen.add(record);
		if (record.groupPolicy === "open") hits.add(`${scope}.groupPolicy`);
		if (record.dmPolicy === "open") hits.add(`${scope}.dmPolicy`);
		for (const [key, nested] of Object.entries(record)) {
			if (key === "groups" || key === "accounts" || key === "dms") {
				visit(nested, `${scope}.${key}`);
				continue;
			}
			if (asNullableRecord(nested)) visit(nested, `${scope}.${key}`);
		}
	};
	for (const [channelId, channelValue] of Object.entries(channels)) visit(channelValue, `channels.${channelId}`);
	return Array.from(hits).toSorted();
}
function collectAutoAllowSkillsHits(approvals) {
	const hits = [];
	if (approvals.defaults?.autoAllowSkills === true) hits.push("defaults.autoAllowSkills");
	for (const [agentId, agent] of Object.entries(approvals.agents ?? {})) if (agent?.autoAllowSkills === true) hits.push(`agents.${agentId}.autoAllowSkills`);
	return hits;
}
function collectInterpreterAllowlistHits(params) {
	const hits = [];
	for (const [agentId, agent] of Object.entries(params.approvals.agents ?? {})) {
		if (!agent || params.strictInlineEvalForAgentId(agentId)) continue;
		for (const entry of agent.allowlist ?? []) {
			if (!isInterpreterLikeAllowlistPattern(entry.pattern)) continue;
			hits.push(`agents.${agentId}.allowlist: ${entry.pattern}`);
		}
	}
	return hits;
}
async function maybeProbeGateway(params) {
	const { buildGatewayConnectionDetails, resolveGatewayProbeAuthSafe, resolveGatewayProbeTarget } = await loadGatewayProbeDeps();
	const url = buildGatewayConnectionDetails({ config: params.cfg }).url;
	const probeTarget = resolveGatewayProbeTarget(params.cfg);
	const authResolution = resolveGatewayProbeAuthSafe({
		cfg: params.cfg,
		env: params.env,
		mode: probeTarget.mode,
		explicitAuth: params.explicitAuth
	});
	const res = await params.probe({
		url,
		auth: authResolution.auth,
		timeoutMs: params.timeoutMs
	}).catch((err) => ({
		ok: false,
		url,
		connectLatencyMs: null,
		error: String(err),
		close: null,
		health: null,
		status: null,
		presence: null,
		configSnapshot: null
	}));
	if (authResolution.warning && !res.ok) res.error = res.error ? `${res.error}; ${authResolution.warning}` : authResolution.warning;
	return {
		deep: { gateway: {
			attempted: true,
			url,
			ok: res.ok,
			error: res.ok ? null : res.error,
			close: res.close ? {
				code: res.close.code,
				reason: res.close.reason
			} : null
		} },
		authWarning: authResolution.warning
	};
}
async function createAuditExecutionContext(opts) {
	const cfg = opts.config;
	const sourceConfig = opts.sourceConfig ?? opts.config;
	const env = opts.env ?? process.env;
	const platform = opts.platform ?? process.platform;
	const includeFilesystem = opts.includeFilesystem !== false;
	const includeChannelSecurity = opts.includeChannelSecurity !== false;
	const deep = opts.deep === true;
	const deepTimeoutMs = Math.max(250, opts.deepTimeoutMs ?? 5e3);
	const stateDir = opts.stateDir ?? resolveStateDir(env);
	const configPath = opts.configPath ?? resolveConfigPath(env, stateDir);
	const workspaceDir = opts.workspaceDir ?? resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
	const { readConfigSnapshotForAudit } = await loadAuditNonDeepModule();
	const configSnapshot = includeFilesystem ? opts.configSnapshot !== void 0 ? opts.configSnapshot : await readConfigSnapshotForAudit({
		env,
		configPath
	}).catch(() => null) : null;
	return {
		cfg,
		sourceConfig,
		env,
		platform,
		includeFilesystem,
		includeChannelSecurity,
		deep,
		deepTimeoutMs,
		stateDir,
		configPath,
		execIcacls: opts.execIcacls,
		execDockerRawFn: opts.execDockerRawFn,
		probeGatewayFn: opts.probeGatewayFn,
		plugins: opts.plugins,
		loadPluginSecurityCollectors: opts.loadPluginSecurityCollectors ?? deep,
		workspaceDir,
		configSnapshot,
		codeSafetySummaryCache: opts.codeSafetySummaryCache ?? /* @__PURE__ */ new Map(),
		deepProbeAuth: opts.deepProbeAuth,
		auditGatewayAuthOverride: opts.auditGatewayAuthOverride
	};
}
async function runSecurityAudit(opts) {
	const findings = [];
	const context = await createAuditExecutionContext(opts);
	const { cfg, env, platform, stateDir, configPath } = context;
	const auditNonDeep = await loadAuditNonDeepModule();
	findings.push(...auditNonDeep.collectAttackSurfaceSummaryFindings(cfg));
	findings.push(...auditNonDeep.collectSyncedFolderFindings({
		stateDir,
		configPath
	}));
	findings.push(...collectGatewayConfigFindings(cfg, context.sourceConfig, env, { gatewayAuthOverride: context.auditGatewayAuthOverride }));
	findings.push(...await collectPluginSecurityAuditFindings(context));
	findings.push(...collectLoggingFindings(cfg));
	findings.push(...collectElevatedFindings(cfg));
	findings.push(...collectExecRuntimeFindings(cfg));
	const hooksGatewayAuthCfg = shouldMaterializeHooksGatewayAuthRefs(cfg) ? await materializeAuditGatewayAuthRefs({
		cfg,
		env
	}) : cfg;
	findings.push(...auditNonDeep.collectHooksHardeningFindings(hooksGatewayAuthCfg, env, { gatewayAuthOverride: context.auditGatewayAuthOverride }));
	findings.push(...auditNonDeep.collectGatewayHttpNoAuthFindings(cfg, env, { gatewayAuthOverride: context.auditGatewayAuthOverride }));
	findings.push(...auditNonDeep.collectGatewayHttpSessionKeyOverrideFindings(cfg));
	findings.push(...auditNonDeep.collectSandboxDockerNoopFindings(cfg));
	findings.push(...auditNonDeep.collectSandboxDangerousConfigFindings(cfg));
	findings.push(...auditNonDeep.collectNodeDenyCommandPatternFindings(cfg));
	findings.push(...auditNonDeep.collectNodeDangerousAllowCommandFindings(cfg));
	findings.push(...auditNonDeep.collectMinimalProfileOverrideFindings(cfg));
	findings.push(...auditNonDeep.collectSecretsInConfigFindings(cfg));
	findings.push(...auditNonDeep.collectModelHygieneFindings(cfg));
	findings.push(...auditNonDeep.collectSmallModelRiskFindings({
		cfg,
		env
	}));
	findings.push(...auditNonDeep.collectExposureMatrixFindings(cfg));
	findings.push(...auditNonDeep.collectLikelyMultiUserSetupFindings(cfg));
	if (context.includeFilesystem) {
		findings.push(...await collectFilesystemFindings({
			stateDir,
			configPath,
			env,
			platform,
			execIcacls: context.execIcacls
		}));
		if (context.configSnapshot) findings.push(...await auditNonDeep.collectIncludeFilePermFindings({
			configSnapshot: context.configSnapshot,
			env,
			platform,
			execIcacls: context.execIcacls
		}));
		findings.push(...await auditNonDeep.collectStateDeepFilesystemFindings({
			cfg,
			env,
			stateDir,
			platform,
			execIcacls: context.execIcacls
		}));
		findings.push(...await auditNonDeep.collectWorkspaceSkillSymlinkEscapeFindings({ cfg }));
		findings.push(...await auditNonDeep.collectSandboxBrowserHashLabelFindings({
			execDockerRawFn: context.execDockerRawFn,
			timeoutMs: context.deepTimeoutMs
		}));
		findings.push(...await auditNonDeep.collectPluginsTrustFindings({
			cfg,
			stateDir
		}));
		findings.push(...await collectDeepCodeSafetyFindings({
			cfg,
			stateDir,
			deep: context.deep,
			summaryCache: context.codeSafetySummaryCache
		}));
	}
	let shouldAuditChannelSecurity = false;
	if (context.includeChannelSecurity) if (context.plugins !== void 0) shouldAuditChannelSecurity = true;
	else {
		const { hasConfiguredChannelsForReadOnlyScope, resolveConfiguredChannelPluginIds } = await loadChannelPluginIdsModule();
		shouldAuditChannelSecurity = hasConfiguredChannelsForReadOnlyScope({
			config: cfg,
			activationSourceConfig: context.sourceConfig,
			workspaceDir: context.workspaceDir,
			env
		}) || resolveConfiguredChannelPluginIds({
			config: cfg,
			activationSourceConfig: context.sourceConfig,
			workspaceDir: context.workspaceDir,
			env
		}).length > 0;
	}
	if (shouldAuditChannelSecurity) {
		const channelPlugins = context.plugins ?? (await loadReadOnlyChannelPlugins()).listReadOnlyChannelPluginsForConfig(cfg, {
			activationSourceConfig: context.sourceConfig,
			workspaceDir: context.workspaceDir,
			env,
			stateDir,
			includePersistedAuthState: true,
			includeSetupFallbackPlugins: true
		});
		const { collectChannelSecurityFindings } = await loadAuditChannelModule();
		findings.push(...await collectChannelSecurityFindings({
			cfg,
			sourceConfig: context.sourceConfig,
			plugins: channelPlugins
		}));
	}
	const deepProbeResult = context.deep ? await maybeProbeGateway({
		cfg,
		env,
		timeoutMs: context.deepTimeoutMs,
		probe: context.probeGatewayFn ?? (await loadGatewayProbeDeps()).probeGateway,
		explicitAuth: context.deepProbeAuth
	}) : void 0;
	const deep = deepProbeResult?.deep;
	findings.push(...collectDeepProbeFindings({
		deep,
		authWarning: deepProbeResult?.authWarning
	}));
	const configuredSuppressions = cfg.security?.audit?.suppressions;
	const filtered = applySecurityAuditSuppressions(findings, configuredSuppressions);
	const configuredSuppressionCount = configuredSuppressions?.length ?? 0;
	const activeFindings = configuredSuppressionCount > 0 ? [...filtered.findings, buildSecurityAuditSuppressionsActiveFinding({
		configuredCount: configuredSuppressionCount,
		suppressedCount: filtered.suppressedFindings.length
	})] : filtered.findings;
	const summary = countBySeverity(activeFindings);
	return {
		ts: Date.now(),
		summary,
		findings: activeFindings,
		...filtered.suppressedFindings.length > 0 ? { suppressedFindings: filtered.suppressedFindings } : {},
		deep
	};
}
//#endregion
export { runSecurityAudit as t };