UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,271 lines 63.5 kB
import { C as parseStrictNonNegativeInteger } from "./number-coercion-CLj0HTDM.js";
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { D as tryResolveLegacyCompatibilityAgentId, E as tryResolveDefaultAgentId, i as listAgentEntries, n as hasAgentRosterProperty } from "./agent-scope-config-DcbEhP0R.js";
import { f as resolveConfigPath, w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { o as redactSensitiveUrlLikeString } from "./redact-sensitive-url-BN1NZvXG.js";
import { c as emitTrustedSecurityEvent } from "./diagnostic-events-Cwe92uV3.js";
import { n as resolvePluginControlPlaneWorkspace } from "./control-plane-workspace-BWhgt2gz.js";
import { c as hasUnresolvedConfigPath, n as copyConfigResolutionFacts } from "./resolution-facts-Dks1tbik.js";
import "./agent-scope-DbtJyKUL.js";
import { o as listRiskyConfiguredSafeBins } from "./exec-safe-bin-trust-BL7TeFlZ.js";
import { t as createGatewayCredentialPlan } from "./credential-planner-DAMjVc9j.js";
import { a as inspectPathPermissions, i as formatPermissionRemediation, r as formatPermissionDetail } from "./permissions-BhjKuixU.js";
import { n as resolveGatewayAuth } from "./auth-resolve-O5AKX-sb.js";
import { x as resolveExecModePolicy } from "./exec-approvals-policy-CCFUzTzd.js";
import { i as resolveSandboxConfigForAgent } from "./config-RoLkL_H5.js";
import "./exec-approvals-BSZ-fPIY.js";
import { r as loadExecApprovals } from "./exec-approvals-store-DBR0neS0.js";
import { u as isInterpreterLikeAllowlistPattern } from "./risks-CTJz9z0K.js";
import { t as resolveExecDefaults } from "./exec-defaults-CET-_UfH.js";
import { t as DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools-Caeym4XK.js";
import { i as resolveMergedSafeBinProfileFixtures, n as listInterpreterLikeSafeBins } from "./exec-safe-bin-runtime-policy-30aRZNai.js";
import "./audit-fs-BZ701GLw.js";
import { r as materializeGatewayAuthSecretRefs, t as canMaterializeGatewayAuthSecretRefsWithoutExec } from "./auth-config-utils-mqnk38xp.js";
import { n as normalizeConfiguredTrustedSafeBinDirs, t as normalizeConfiguredSafeBins } from "./exec-safe-bin-config-C0jI-Rxy.js";
import { t as resolveGatewayAuthTokenSourceConflict } from "./auth-token-source-conflict-C5ureq4M.js";
import { r as collectCoreInsecureOrDangerousFlags } from "./dangerous-config-flags-current-DLfMMhx8.js";
import { t as collectEnabledInsecureOrDangerousFlags } from "./dangerous-config-flags-DpyNFEK5.js";
import { t as collectExecFilesystemPolicyDriftHits } from "./exec-filesystem-policy-B4K_yFM5.js";
import { constants } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { isIP } from "node:net";
//#region src/security/audit-deep-code-safety.ts
/** Lazily load deep audit code paths so normal audits avoid plugin/skill scans. */
const loadAuditDeepModule = createLazyRuntimeModule(() => import("./audit.deep.runtime.js"));
/** 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,
		workspaceDir: params.workspaceDir,
		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 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 && !hasUnresolvedConfigPath(sourceConfig, "gateway.auth.token");
	const hasPassword = typeof auth.password === "string" && auth.password.trim().length > 0 && !hasUnresolvedConfigPath(sourceConfig, "gateway.auth.password");
	const plan = createGatewayCredentialPlan({
		config: sourceConfig,
		env
	});
	const explicitAuthMode = options.gatewayAuthOverride?.mode ?? sourceConfig.gateway?.auth?.mode;
	const tokenConfigured = Boolean(hasToken || plan.envToken || plan.localToken.value || plan.localToken.hasSecretRef || plan.remoteToken.value || plan.remoteToken.hasSecretRef);
	const passwordConfigured = Boolean(hasPassword || (explicitAuthMode === "password" || explicitAuthMode !== "token" && explicitAuthMode !== "none" && explicitAuthMode !== "trusted-proxy" && !tokenConfigured) && (plan.envPassword || plan.localPassword.value || plan.localPassword.hasSecretRef));
	const hasGatewayAuth = (explicitAuthMode === "token" ? tokenConfigured : explicitAuthMode === "password" ? passwordConfigured : explicitAuthMode === "none" || explicitAuthMode === "trusted-proxy" ? false : tokenConfigured || passwordConfigured) || auth.mode === "trusted-proxy";
	const hasLoopbackAuth = hasGatewayAuth || auth.allowTailscale && tailscaleMode === "serve";
	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" && !hasGatewayAuth) 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 && !hasLoopbackAuth) 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.mcp?.apps?.enabled === true) findings.push({
		checkId: "mcp.apps.enabled",
		severity: "warn",
		title: "MCP Apps UI bridge enabled",
		detail: "mcp.apps.enabled=true allows configured MCP servers to provide interactive HTML. Views are CSP-restricted and origin-isolated, but they can call app-visible tools on their owning MCP server while the session runtime remains active.",
		remediation: "Keep this enabled only for MCP servers you trust. Disable with `openclaw config set mcp.apps.enabled false --strict-json` when it is not needed."
	});
	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 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. Same-host proxy requests are rejected unless gateway.auth.trustedProxy.allowLoopback=true and gateway.trustedProxies includes their loopback source; enable only for a deliberate same-host trust boundary. See /gateway/trusted-proxy-auth for setup guidance."
		});
		if (trustedProxies.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?.deviceAutoApprove?.enabled === true) {
			findings.push({
				checkId: "gateway.trusted_proxy_device_auto_approve",
				severity: "warn",
				title: "Trusted-proxy operator device auto-approval enabled",
				detail: "gateway.auth.trustedProxy.deviceAutoApprove.enabled=true delegates new browser and native UI operator device pairing entirely to the reverse-proxy identity.",
				remediation: "Enable this only when the proxy is the exclusive Gateway ingress, strongly authenticates users, overwrites identity headers, and restricts access with allowUsers."
			});
			if (trustedProxyConfig.deviceAutoApprove.scopes?.some((scope) => scope.trim() === "operator.admin")) findings.push({
				checkId: "gateway.trusted_proxy_device_auto_approve_admin",
				severity: "critical",
				title: "Trusted-proxy device auto-approval allows full admin",
				detail: "gateway.auth.trustedProxy.deviceAutoApprove.scopes includes operator.admin, so every proxy-authenticated user can auto-approve a new operator device with full admin; requests without scopes receive full admin automatically.",
				remediation: "Remove operator.admin and approve admin access manually, or grant admin per identity via gateway.auth.identityScopes."
			});
		}
		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-mcporter-registry.ts
const MAX_MCPORTER_REGISTRY_BYTES = 16777216;
const READ_CHUNK_SIZE = 65536;
function isEnoent(error) {
	return error?.code === "ENOENT";
}
async function readBoundedMcporterRegistry(stateDir) {
	const registryPath = path.join(stateDir, "skills", "config", "mcporter.json");
	let handle;
	try {
		handle = await fs$1.open(registryPath, constants.O_RDONLY | constants.O_NONBLOCK);
	} catch (error) {
		return isEnoent(error) ? { status: "missing" } : {
			status: "rejected",
			reason: "unreadable"
		};
	}
	try {
		const stat = await handle.stat();
		if (!stat.isFile()) return {
			status: "rejected",
			reason: "non-regular"
		};
		if (stat.size > MAX_MCPORTER_REGISTRY_BYTES) return {
			status: "rejected",
			reason: "oversized"
		};
		const chunks = [];
		const scratch = Buffer.allocUnsafe(Math.min(READ_CHUNK_SIZE, 16777217));
		let total = 0;
		while (true) {
			const { bytesRead } = await handle.read(scratch, 0, scratch.length, null);
			if (bytesRead === 0) break;
			total += bytesRead;
			if (total > MAX_MCPORTER_REGISTRY_BYTES) return {
				status: "rejected",
				reason: "oversized"
			};
			chunks.push(Buffer.from(scratch.subarray(0, bytesRead)));
		}
		let value;
		try {
			value = JSON.parse(Buffer.concat(chunks, total).toString("utf-8"));
		} catch {
			return {
				status: "rejected",
				reason: "malformed"
			};
		}
		return {
			status: "ok",
			value
		};
	} catch {
		return {
			status: "rejected",
			reason: "unreadable"
		};
	} finally {
		await handle.close();
	}
}
//#endregion
//#region src/security/audit.ts
const loadReadOnlyChannelPlugins = createLazyRuntimeModule(() => import("./read-only-B-AkN-GN.js"));
const loadAuditNonDeepModule = createLazyRuntimeModule(() => import("./audit.nondeep.runtime.js"));
const loadAuditChannelModule = createLazyRuntimeModule(() => import("./audit-channel.collect.runtime.js"));
const loadPluginMetadataRegistryLoaderModule = createLazyRuntimeModule(() => import("./metadata-registry-loader-Bh1Z9Hq0.js"));
const loadPluginAutoEnableModule = createLazyRuntimeModule(() => import("./plugin-auto-enable-BpmAQW6T.js"));
const loadChannelPluginIdsModule = createLazyRuntimeModule(() => import("./channel-plugin-ids-C82_9rfw.js"));
const loadPluginRuntimeModule = createLazyRuntimeModule(() => import("./runtime-BO2wGg2w.js"));
const loadGatewayProbeDeps = createLazyRuntimeModule(() => Promise.all([
	import("./call-Bo4Qx1ax.js"),
	import("./probe-auth-B1-ikyXl.js"),
	import("./probe-0m1wRR4M.js")
]).then(([callModule, probeAuthModule, probeModule]) => ({
	buildGatewayConnectionDetails: callModule.buildGatewayConnectionDetails,
	resolveGatewayProbeAuthSafe: probeAuthModule.resolveGatewayProbeAuthSafe,
	resolveGatewayProbeTarget: probeAuthModule.resolveGatewayProbeTarget,
	probeGateway: probeModule.probeGateway
})));
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 emitSecurityAuditReportEvent(params) {
	const hasCritical = params.summary.critical > 0;
	const hasWarnings = params.summary.warn > 0;
	emitTrustedSecurityEvent({
		category: "audit",
		action: "security.audit.completed",
		outcome: hasCritical || hasWarnings ? "failure" : "success",
		severity: hasCritical ? "critical" : hasWarnings ? "medium" : "info",
		actor: { kind: "operator" },
		target: {
			kind: "config",
			name: "security.audit"
		},
		policy: {
			id: "security.audit",
			decision: "not_applicable"
		},
		control: {
			id: "security.audit",
			family: "authorization"
		},
		attributes: {
			critical_count: params.summary.critical,
			warn_count: params.summary.warn,
			info_count: params.summary.info,
			suppressed_count: params.suppressedCount,
			deep: params.deep,
			include_filesystem: params.includeFilesystem,
			include_channel_security: params.includeChannelSecurity
		}
	});
}
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,
		hasTokenOverride: false,
		hasPasswordOverride: false,
		hasTokenFallback: Boolean(normalizeOptionalString(params.env.OPENCLAW_GATEWAY_TOKEN)),
		hasPasswordFallback: 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 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;
}
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();
	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 = listAgentEntries(cfg);
	const defaultAgentId = tryResolveDefaultAgentId(cfg);
	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.entries.*.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.entries.*.sandbox.mode`) or set their tools.exec.host to \"gateway\"."
	});
	const effectiveExecScopes = Array.from(new Map([{
		id: defaultAgentId ?? "global",
		security: resolveExecModePolicy({
			mode: cfg.tools?.exec?.mode,
			security: cfg.tools?.exec?.security ?? "deny",
			ask: cfg.tools?.exec?.ask ?? "off"
		}).security,
		host: cfg.tools?.exec?.host ?? "auto"
	}, ...agents.filter((entry) => Boolean(entry) && typeof entry === "object" && typeof entry.id === "string").map((entry) => {
		const inherited = resolveExecModePolicy({
			mode: cfg.tools?.exec?.mode,
			security: cfg.tools?.exec?.security ?? "deny",
			ask: cfg.tools?.exec?.ask ?? "off"
		});
		return {
			id: entry.id,
			security: resolveExecModePolicy({
				mode: entry.tools?.exec?.mode,
				security: entry.tools?.exec?.security ?? inherited.security,
				ask: entry.tools?.exec?.ask ?? inherited.ask
			}).security,
			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.mode=\"ask\" or \"allowlist\", and reserve \"full\" for tightly scoped break-glass agents only."
	});
	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 === "*") return globalStrictInlineEval;
			return agents.find((entry) => entry?.id === agentId)?.tools?.exec?.strictInlineEval ?? 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 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 normalizeConfiguredTrustedSafeBinDirs(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.entries.${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.entries.${entry.id}.tools.exec.safeBins: ${hit.bin} (${hit.warning})`);
			continue;
		}
		interpreterHits.push(`- agents.entries.${entry.id}.tools.exec.safeBins: ${interpreters.join(", ")}`);
		for (const hit of listRiskyConfiguredSafeBins(agentSafeBins)) riskySemanticSafeBinHits.push(`- agents.entries.${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 collectAgentRosterFindings(cfg) {
	const agents = listAgentEntries(cfg);
	if (agents.length === 0 && !hasAgentRosterProperty(cfg)) return [];
	const defaultCount = agents.filter((agent) => agent?.default === true).length;
	const explicitOwnership = cfg.agents?.ownership === "explicit";
	if (explicitOwnership ? defaultCount === 0 : tryResolveLegacyCompatibilityAgentId(cfg) !== void 0) return [];
	return [{
		checkId: "config.agent_roster.invalid_default_count",
		severity: "warn",
		title: "Agent roster has an invalid default selection",
		detail: explicitOwnership ? `Expected no agents.entries default=true entries with agents.ownership=explicit, found ${defaultCount}.` : `Expected a resolvable default agent (sole entry, one default=true marker, or agents.ownership=explicit); found ${defaultCount} default markers across ${agents.length} configured agents.`,
		remediation: "Run `openclaw doctor --fix` to repair the authored agent roster."
	}];
}
function formatNamesPreview(names) {
	const visible = names.slice(0, 6);
	const suffix = names.length > visible.length ? `, +${names.length - visible.length} more` : "";
	return `${visible.join(", ")}${suffix}`;
}
function listConfiguredMcpServerNames(cfg) {
	return Object.entries(cfg.mcp?.servers ?? {}).filter(([, server]) => server?.enabled !== false).map(([name]) => name).toSorted();
}
async function readGlobalMcporterRegistrySummary(stateDir) {
	const outcome = await readBoundedMcporterRegistry(stateDir);
	if (outcome.status === "missing") return { status: "absent" };
	if (outcome.status === "rejected") return outcome;
	const mcpServers = asNullableRecord(asNullableRecord(outcome.value)?.mcpServers);
	if (!mcpServers) return { status: "absent" };
	const names = Object.entries(mcpServers).filter(([, value]) => asNullableRecord(value)?.enabled !== false).map(([name]) => name).toSorted();
	return names.length > 0 ? {
		status: "source",
		summary: {
			label: "skills/config/mcporter.json",
			names
		}
	} : { status: "absent" };
}
function describeMcporterRegistryRejection(reason) {
	switch (reason) {
		case "oversized": return "larger than the 16 MiB audit cap";
		case "unreadable": return "unreadable";
		case "non-regular": return "not a regular file";
		case "malformed": return "not valid JSON";
		default: return reason;
	}
}
function hasOwnSkillsAllowlist(entry) {
	return Boolean(entry && Object.hasOwn(entry, "skills"));
}
function collectAgentSkillMcpBoundaryScopes(cfg) {
	const agents = listAgentEntries(cfg);
	const defaultsHaveSkillAllowlist = hasOwnSkillsAllowlist(cfg.agents?.defaults);
	return [...defaultsHaveSkillAllowlist ? [{
		kind: "defaults",
		id: "agents.defaults",
		skillSource: "agents.defaults.skills"
	}] : [], ...agents.filter((entry) => Boolean(entry) && typeof entry === "object" && typeof entry.id === "string").flatMap((entry) => {
		if (hasOwnSkillsAllowlist(entry)) return [{
			kind: "agent",
			id: entry.id,
			skillSource: "agents.entries.*.skills",
			agentId: entry.id
		}];
		if (defaultsHaveSkillAllowlist) return [{
			kind: "agent",
			id: entry.id,
			skillSource: "agents.defaults.skills (inherited)",
			agentId: entry.id
		}];
		return [];
	})].flatMap((candidate) => {
		const agentId = candidate.kind === "agent" ? candidate.agentId : void 0;
		const sandboxMode = resolveSandboxConfigForAgent(cfg, agentId).mode;
		const exec = resolveExecDefaults({
			cfg,
			...candidate.kind === "defaults" ? { scope: { kind: "defaults" } } : { agentId },
			sandboxAvailable: sandboxMode !== "off"
		});
		if (exec.security === "deny" || exec.effectiveHost === "sandbox") return [];
		return [{
			id: candidate.id,
			skillSource: candidate.skillSource,
			execHost: exec.effectiveHost,
			execSecurity: exec.security,
			execAsk: exec.ask
		}];
	});
}
async function collectAgentSkillMcpBoundaryFindings(params) {
	const scopes = collectAgentSkillMcpBoundaryScopes(params.cfg);
	if (scopes.length === 0) return [];
	const findings = [];
	const sources = [];
	const configServerNames = listConfiguredMcpServerNames(params.cfg);
	if (configServerNames.length > 0) sources.push({
		label: "mcp.servers",
		names: configServerNames
	});
	const globalMcporterRegistry = await readGlobalMcporterRegistrySummary(params.stateDir);
	if (globalMcporterRegistry.status === "rejected") findings.push({
		checkId: "tools.exec.mcporter_registry_inspection_incomplete",
		severity: "warn",
		title: "Global mcporter registry could not be inspected",
		detail: `skills/config/mcporter.json exists but could not be safely inspected (${describeMcporterRegistryRejection(globalMcporterRegistry.reason)}). The MCP boundary inspection is incomplete: the audit could not verify which MCP servers a host exec process can reach.`,
		remediation: "Repair or remove skills/config/mcporter.json so the audit can inspect it: keep it a regular file readable by the gateway user, valid JSON, and below the 16 MiB audit cap."
	});
	else if (globalMcporterRegistry.status === "source") sources.push(globalMcporterRegistry.summary);
	if (sources.length === 0) return findings;
	findings.push({
		checkId: "tools.exec.agent_skill_mcp_boundary_drift",
		severity: "warn",
		title: "Agent skill allowlists do not constrain host exec MCP clients",
		detail: `Detected agent skill allowlists on host-exec-capable scopes:\n${scopes.slice(0, 8).map((scope) => `- ${scope.id}: ${scope.skillSource}, exec.host=${scope.execHost}, security=${scope.execSecurity}, ask=${scope.execAsk}`).join("\n")}` + (scopes.length > 8 ? `\n- +${scopes.length - 8} more scopes.` : "") + `\nMCP server registries visible to the gateway configuration/state:\n${sources.map((source) => `- ${source.label}: ${formatNamesPreview(source.names)}`).join("\n")}\nagents.*.skills filters OpenClaw skill visibility and snapshots; it is not a shell-time authorization boundary. A host exec process can run external MCP clients or read a global mcporter registry unless sandbox, filesystem, network, or MCP credential boundaries block it.`,
		remediation: "For agents that need per-agent MCP isolation, set their exec policy to security=\"deny\" or a tight allowlist, run them in sandbox/container/OS-user isolation where the global MCP registry is not readable, split sensitive MCP servers into a separate gateway/trust boundary, or require per-agent MCP credentials at the server layer."
	});
	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: redactSensitiveUrlLikeString(url),
			ok: res.ok,
			error: res.ok || res.error === null ? null : redactSensitiveUrlLikeString(res.error),
			close: res.close ? {
				code: res.close.code,
				reason: redactSensitiveUrlLikeString(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 = resolvePluginControlPlaneWorkspace({
		config: cfg,
		workspaceDir: opts.workspaceDir,
		env
	}).workspaceDir;
	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 runSecurityAuditCore(opts) {
	const findings = [];
	const context = await createAuditExecutionContext(opts);
	const { cfg, env, platform, stateDir, configPath } = context;
	copyConfigResolutionFacts(context.sourceConfig, cfg);
	const auditNonDeep = await loadAuditNonDeepModule();
	findings.push(...auditNonDeep.collectAttackSurfaceSummaryFindings(cfg));
	findings.push(...collectAgentRosterFindings(context.sourceConfig));
	findings.push(...auditNonDeep.collectSyncedFolderFindings({
		stateDir,
		configPath
	}));
	findings.push(...collectGatewayConfigFindings(cfg, context.sourceConfig, env, { gatewayAuthOverride: context.auditGatewayAuthOverride }));
	findings.push(...await collectPluginSecurityAuditFindings(context));
	findings.push(...collectElevatedFindings(cfg));
	findings.push(...collectExecRuntimeFindings(cfg));
	findings.push(...await collectAgentSkillMcpBoundaryFindings({
		cfg,
		stateDir
	}));
	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(context.sourceConfig));
	findings.push(...auditNonDeep.collectModelHygieneFindings(cfg));
	findings.push(...auditNonDeep.collectSmallModelRiskFindings({
		cfg,
		env
	}));
	findings.push(...auditNonDeep.collectExposureMatrixFindings(cfg));
	findings.push(...auditNonDeep.collectLikelyMultiUserSetupFindings(cfg));
	findings.push(...auditNonDeep.collectCrossAgentSessionAccessFindings(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,
			workspaceDir: context.workspaceDir
		}));
		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,
			workspaceDir: context.workspaceDir,
			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);
	emitSecurityAuditReportEvent({
		summary,
		deep: context.deep,
		includeFilesystem: context.includeFilesystem,
		includeChannelSecurity: context.includeChannelSecurity,
		suppressedCount: filtered.suppressedFindings.length
	});
	return {
		ts: Date.now(),
		summary,
		findings: activeFindings,
		...filtered.suppressedFindings.length > 0 ? { suppressedFindings: filtered.suppressedFindings } : {},
		deep
	};
}
//#endregion
export { runSecurityAuditCore as t };