@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,127 lines (1,122 loc) • 110 kB
JavaScript
import { g as resolveStateDir, m as resolveOAuthDir, o as resolveConfigPath } from "./paths-B4BZAPZh.js";
import { l as normalizeAgentId } from "./session-key-CZ6OwgSB.js";
import { n as runExec } from "./exec-DYqRzFbo.js";
import { l as resolveDefaultAgentId } from "./agent-scope-BnZW9Gh2.js";
import { t as formatCliCommand } from "./command-format-DEKzLnLg.js";
import { h as GATEWAY_CLIENT_NAMES, m as GATEWAY_CLIENT_MODES } from "./message-channel-Bena1Tzd.js";
import { I as INCLUDE_KEY, L as MAX_INCLUDE_DEPTH, r as createConfigIO } from "./config-PQiujvsf.js";
import { i as isPathInside, n as MANIFEST_KEY, r as extensionUsesSkippedScannerPath } from "./legacy-names-DyDKpCnk.js";
import { u as normalizePluginsConfig } from "./manifest-registry-4k4vkhPS.js";
import { d as resolveSandboxConfigForAgent, l as getBlockedBindReason, p as resolveSandboxToolPolicyForAgent, x as resolveToolProfilePolicy } from "./sandbox-UAzvS0V6.js";
import { i as resolveGatewayAuth } from "./auth-DZSWPd8D.js";
import { a as resolveProfile, i as resolveBrowserConfig, m as resolveBrowserControlAuth } from "./server-context-DAWsUNUs.js";
import { i as loadWorkspaceSkillEntries } from "./skills-CLmzWt48.js";
import { n as formatErrorMessage } from "./errors-kfGqPQ4b.js";
import { l as normalizeStringEntries } from "./dock-7F-MiPtF.js";
import { n as listChannelPlugins } from "./plugins-CKbXkuXd.js";
import { t as GatewayClient } from "./client-BdSkEtCd.js";
import { c as READ_SCOPE, t as buildGatewayConnectionDetails } from "./call-BCz_8mqq.js";
import { i as readChannelAllowFromStore } from "./pairing-store-T5DpOfoL.js";
import { t as listAgentWorkspaceDirs } from "./workspace-dirs-CE3CxabZ.js";
import { l as resolveNativeCommandsEnabled, n as isToolAllowedByPolicies, o as pickSandboxToolPolicy, u as resolveNativeSkillsEnabled } from "./pi-tools.policy-DTy1DnPK.js";
import { n as DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools-DyngmY3-.js";
import { t as resolveChannelDefaultAccountId } from "./helpers-tGq4L3A8.js";
import { t as scanDirectoryWithSummary } from "./skill-scanner-D56xvwQF.js";
import { n as normalizeTelegramAllowFromEntry, t as isNumericTelegramUserId } from "./allow-from-BIJVrwCW.js";
import { t as resolveDmAllowState } from "./dm-policy-shared-XAIBK3tz.js";
import { t as inferParamBFromIdOrName } from "./model-param-b-Cp9d4e-0.js";
import os from "node:os";
import path from "node:path";
import JSON5 from "json5";
import * as fs$1 from "node:fs/promises";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
//#region src/gateway/probe.ts
async function probeGateway(opts) {
const startedAt = Date.now();
const instanceId = randomUUID();
let connectLatencyMs = null;
let connectError = null;
let close = null;
return await new Promise((resolve) => {
let settled = false;
const settle = (result) => {
if (settled) return;
settled = true;
clearTimeout(timer);
client.stop();
resolve({
url: opts.url,
...result
});
};
const client = new GatewayClient({
url: opts.url,
token: opts.auth?.token,
password: opts.auth?.password,
scopes: [READ_SCOPE],
clientName: GATEWAY_CLIENT_NAMES.CLI,
clientVersion: "dev",
mode: GATEWAY_CLIENT_MODES.PROBE,
instanceId,
onConnectError: (err) => {
connectError = formatErrorMessage(err);
},
onClose: (code, reason) => {
close = {
code,
reason
};
},
onHelloOk: async () => {
connectLatencyMs = Date.now() - startedAt;
try {
const [health, status, presence, configSnapshot] = await Promise.all([
client.request("health"),
client.request("status"),
client.request("system-presence"),
client.request("config.get", {})
]);
settle({
ok: true,
connectLatencyMs,
error: null,
close,
health,
status,
presence: Array.isArray(presence) ? presence : null,
configSnapshot
});
} catch (err) {
settle({
ok: false,
connectLatencyMs,
error: formatErrorMessage(err),
close,
health: null,
status: null,
presence: null,
configSnapshot: null
});
}
}
});
const timer = setTimeout(() => {
settle({
ok: false,
connectLatencyMs,
error: connectError ? `connect failed: ${connectError}` : "timeout",
close,
health: null,
status: null,
presence: null,
configSnapshot: null
});
}, Math.max(250, opts.timeoutMs));
client.start();
});
}
//#endregion
//#region src/gateway/node-command-policy.ts
const CANVAS_COMMANDS = [
"canvas.present",
"canvas.hide",
"canvas.navigate",
"canvas.eval",
"canvas.snapshot",
"canvas.a2ui.push",
"canvas.a2ui.pushJSONL",
"canvas.a2ui.reset"
];
const CAMERA_COMMANDS = ["camera.list"];
const CAMERA_DANGEROUS_COMMANDS = ["camera.snap", "camera.clip"];
const SCREEN_DANGEROUS_COMMANDS = ["screen.record"];
const LOCATION_COMMANDS = ["location.get"];
const DEVICE_COMMANDS = ["device.info", "device.status"];
const CONTACTS_COMMANDS = ["contacts.search"];
const CONTACTS_DANGEROUS_COMMANDS = ["contacts.add"];
const CALENDAR_COMMANDS = ["calendar.events"];
const CALENDAR_DANGEROUS_COMMANDS = ["calendar.add"];
const REMINDERS_COMMANDS = ["reminders.list"];
const REMINDERS_DANGEROUS_COMMANDS = ["reminders.add"];
const PHOTOS_COMMANDS = ["photos.latest"];
const MOTION_COMMANDS = ["motion.activity", "motion.pedometer"];
const SMS_DANGEROUS_COMMANDS = ["sms.send"];
const IOS_SYSTEM_COMMANDS = ["system.notify"];
const SYSTEM_COMMANDS = [
"system.run",
"system.which",
"system.notify",
"browser.proxy"
];
const DEFAULT_DANGEROUS_NODE_COMMANDS = [
...CAMERA_DANGEROUS_COMMANDS,
...SCREEN_DANGEROUS_COMMANDS,
...CONTACTS_DANGEROUS_COMMANDS,
...CALENDAR_DANGEROUS_COMMANDS,
...REMINDERS_DANGEROUS_COMMANDS,
...SMS_DANGEROUS_COMMANDS
];
const PLATFORM_DEFAULTS = {
ios: [
...CANVAS_COMMANDS,
...CAMERA_COMMANDS,
...LOCATION_COMMANDS,
...DEVICE_COMMANDS,
...CONTACTS_COMMANDS,
...CALENDAR_COMMANDS,
...REMINDERS_COMMANDS,
...PHOTOS_COMMANDS,
...MOTION_COMMANDS,
...IOS_SYSTEM_COMMANDS
],
android: [
...CANVAS_COMMANDS,
...CAMERA_COMMANDS,
...LOCATION_COMMANDS,
...DEVICE_COMMANDS,
...CONTACTS_COMMANDS,
...CALENDAR_COMMANDS,
...REMINDERS_COMMANDS,
...PHOTOS_COMMANDS,
...MOTION_COMMANDS
],
macos: [
...CANVAS_COMMANDS,
...CAMERA_COMMANDS,
...LOCATION_COMMANDS,
...DEVICE_COMMANDS,
...CONTACTS_COMMANDS,
...CALENDAR_COMMANDS,
...REMINDERS_COMMANDS,
...PHOTOS_COMMANDS,
...MOTION_COMMANDS,
...SYSTEM_COMMANDS
],
linux: [...SYSTEM_COMMANDS],
windows: [...SYSTEM_COMMANDS],
unknown: [
...CANVAS_COMMANDS,
...CAMERA_COMMANDS,
...LOCATION_COMMANDS,
...SYSTEM_COMMANDS
]
};
function normalizePlatformId(platform, deviceFamily) {
const raw = (platform ?? "").trim().toLowerCase();
if (raw.startsWith("ios")) return "ios";
if (raw.startsWith("android")) return "android";
if (raw.startsWith("mac")) return "macos";
if (raw.startsWith("darwin")) return "macos";
if (raw.startsWith("win")) return "windows";
if (raw.startsWith("linux")) return "linux";
const family = (deviceFamily ?? "").trim().toLowerCase();
if (family.includes("iphone") || family.includes("ipad") || family.includes("ios")) return "ios";
if (family.includes("android")) return "android";
if (family.includes("mac")) return "macos";
if (family.includes("windows")) return "windows";
if (family.includes("linux")) return "linux";
return "unknown";
}
function resolveNodeCommandAllowlist(cfg, node) {
const base = PLATFORM_DEFAULTS[normalizePlatformId(node?.platform, node?.deviceFamily)] ?? PLATFORM_DEFAULTS.unknown;
const extra = cfg.gateway?.nodes?.allowCommands ?? [];
const deny = new Set(cfg.gateway?.nodes?.denyCommands ?? []);
const allow = new Set([...base, ...extra].map((cmd) => cmd.trim()).filter(Boolean));
for (const blocked of deny) {
const trimmed = blocked.trim();
if (trimmed) allow.delete(trimmed);
}
return allow;
}
function isNodeCommandAllowed(params) {
const command = params.command.trim();
if (!command) return {
ok: false,
reason: "command required"
};
if (!params.allowlist.has(command)) return {
ok: false,
reason: "command not allowlisted"
};
if (Array.isArray(params.declaredCommands) && params.declaredCommands.length > 0) {
if (!params.declaredCommands.includes(command)) return {
ok: false,
reason: "command not declared by node"
};
} else return {
ok: false,
reason: "node did not declare commands"
};
return { ok: true };
}
//#endregion
//#region src/gateway/probe-auth.ts
function resolveGatewayProbeAuth(params) {
const env = params.env ?? process.env;
const authToken = params.cfg.gateway?.auth?.token;
const authPassword = params.cfg.gateway?.auth?.password;
const remote = params.cfg.gateway?.remote;
return {
token: params.mode === "remote" ? typeof remote?.token === "string" && remote.token.trim() ? remote.token.trim() : void 0 : env.OPENCLAW_GATEWAY_TOKEN?.trim() || (typeof authToken === "string" && authToken.trim() ? authToken.trim() : void 0),
password: env.OPENCLAW_GATEWAY_PASSWORD?.trim() || (params.mode === "remote" ? typeof remote?.password === "string" && remote.password.trim() ? remote.password.trim() : void 0 : typeof authPassword === "string" && authPassword.trim() ? authPassword.trim() : void 0)
};
}
//#endregion
//#region src/security/audit-channel.ts
function normalizeAllowFromList$1(list) {
return normalizeStringEntries(Array.isArray(list) ? list : void 0);
}
function classifyChannelWarningSeverity(message) {
const s = message.toLowerCase();
if (s.includes("dms: open") || s.includes("grouppolicy=\"open\"") || s.includes("dmpolicy=\"open\"")) return "critical";
if (s.includes("allows any") || s.includes("anyone can dm") || s.includes("public")) return "critical";
if (s.includes("locked") || s.includes("disabled")) return "info";
return "warn";
}
async function collectChannelSecurityFindings(params) {
const findings = [];
const coerceNativeSetting = (value) => {
if (value === true) return true;
if (value === false) return false;
if (value === "auto") return "auto";
};
const warnDmPolicy = async (input) => {
const policyPath = input.policyPath ?? `${input.allowFromPath}policy`;
const { hasWildcard, isMultiUserDm } = await resolveDmAllowState({
provider: input.provider,
allowFrom: input.allowFrom,
normalizeEntry: input.normalizeEntry
});
const dmScope = params.cfg.session?.dmScope ?? "main";
if (input.dmPolicy === "open") {
const allowFromKey = `${input.allowFromPath}allowFrom`;
findings.push({
checkId: `channels.${input.provider}.dm.open`,
severity: "critical",
title: `${input.label} DMs are open`,
detail: `${policyPath}="open" allows anyone to DM the bot.`,
remediation: `Use pairing/allowlist; if you really need open DMs, ensure ${allowFromKey} includes "*".`
});
if (!hasWildcard) findings.push({
checkId: `channels.${input.provider}.dm.open_invalid`,
severity: "warn",
title: `${input.label} DM config looks inconsistent`,
detail: `"open" requires ${allowFromKey} to include "*".`
});
}
if (input.dmPolicy === "disabled") {
findings.push({
checkId: `channels.${input.provider}.dm.disabled`,
severity: "info",
title: `${input.label} DMs are disabled`,
detail: `${policyPath}="disabled" ignores inbound DMs.`
});
return;
}
if (dmScope === "main" && isMultiUserDm) findings.push({
checkId: `channels.${input.provider}.dm.scope_main_multiuser`,
severity: "warn",
title: `${input.label} DMs share the main session`,
detail: "Multiple DM senders currently share the main session, which can leak context across users.",
remediation: "Run: " + formatCliCommand("openclaw config set session.dmScope \"per-channel-peer\"") + " (or \"per-account-channel-peer\" for multi-account channels) to isolate DM sessions per sender."
});
};
for (const plugin of params.plugins) {
if (!plugin.security) continue;
const accountIds = plugin.config.listAccountIds(params.cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg: params.cfg,
accountIds
});
const account = plugin.config.resolveAccount(params.cfg, defaultAccountId);
if (!(plugin.config.isEnabled ? plugin.config.isEnabled(account, params.cfg) : true)) continue;
if (!(plugin.config.isConfigured ? await plugin.config.isConfigured(account, params.cfg) : true)) continue;
if (plugin.id === "discord") {
const discordCfg = account?.config ?? {};
const nativeEnabled = resolveNativeCommandsEnabled({
providerId: "discord",
providerSetting: coerceNativeSetting(discordCfg.commands?.native),
globalSetting: params.cfg.commands?.native
});
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
providerId: "discord",
providerSetting: coerceNativeSetting(discordCfg.commands?.nativeSkills),
globalSetting: params.cfg.commands?.nativeSkills
});
if (nativeEnabled || nativeSkillsEnabled) {
const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
const groupPolicy = discordCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const guildEntries = discordCfg.guilds ?? {};
const guildsConfigured = Object.keys(guildEntries).length > 0;
const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => {
if (!guild || typeof guild !== "object") return false;
const g = guild;
if (Array.isArray(g.users) && g.users.length > 0) return true;
const channels = g.channels;
if (!channels || typeof channels !== "object") return false;
return Object.values(channels).some((channel) => {
if (!channel || typeof channel !== "object") return false;
const c = channel;
return Array.isArray(c.users) && c.users.length > 0;
});
});
const dmAllowFromRaw = discordCfg.dm?.allowFrom;
const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
const ownerAllowFromConfigured = normalizeAllowFromList$1([...dmAllowFrom, ...storeAllowFrom]).length > 0;
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
if (!useAccessGroups && groupPolicy !== "disabled" && guildsConfigured && !hasAnyUserAllowlist) findings.push({
checkId: "channels.discord.commands.native.unrestricted",
severity: "critical",
title: "Discord slash commands are unrestricted",
detail: "commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.",
remediation: "Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users)."
});
else if (useAccessGroups && groupPolicy !== "disabled" && guildsConfigured && !ownerAllowFromConfigured && !hasAnyUserAllowlist) findings.push({
checkId: "channels.discord.commands.native.no_allowlists",
severity: "warn",
title: "Discord slash commands have no allowlists",
detail: "Discord slash commands are enabled, but neither an owner allowFrom list nor any per-guild/channel users allowlist is configured; /… commands will be rejected for everyone.",
remediation: "Add your user id to channels.discord.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users."
});
}
}
if (plugin.id === "slack") {
const slackCfg = account?.config ?? {};
const nativeEnabled = resolveNativeCommandsEnabled({
providerId: "slack",
providerSetting: coerceNativeSetting(slackCfg.commands?.native),
globalSetting: params.cfg.commands?.native
});
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
providerId: "slack",
providerSetting: coerceNativeSetting(slackCfg.commands?.nativeSkills),
globalSetting: params.cfg.commands?.nativeSkills
});
if (nativeEnabled || nativeSkillsEnabled || slackCfg.slashCommand?.enabled === true) if (!(params.cfg.commands?.useAccessGroups !== false)) findings.push({
checkId: "channels.slack.commands.slash.useAccessGroups_off",
severity: "critical",
title: "Slack slash commands bypass access groups",
detail: "Slack slash/native commands are enabled while commands.useAccessGroups=false; this can allow unrestricted /… command execution from channels/users you didn't explicitly authorize.",
remediation: "Set commands.useAccessGroups=true (recommended)."
});
else {
const allowFromRaw = account?.config?.allowFrom;
const legacyAllowFromRaw = account?.dm?.allowFrom;
const allowFrom = Array.isArray(allowFromRaw) ? allowFromRaw : Array.isArray(legacyAllowFromRaw) ? legacyAllowFromRaw : [];
const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []);
const ownerAllowFromConfigured = normalizeAllowFromList$1([...allowFrom, ...storeAllowFrom]).length > 0;
const channels = slackCfg.channels ?? {};
const hasAnyChannelUsersAllowlist = Object.values(channels).some((value) => {
if (!value || typeof value !== "object") return false;
const channel = value;
return Array.isArray(channel.users) && channel.users.length > 0;
});
if (!ownerAllowFromConfigured && !hasAnyChannelUsersAllowlist) findings.push({
checkId: "channels.slack.commands.slash.no_allowlists",
severity: "warn",
title: "Slack slash commands have no allowlists",
detail: "Slack slash/native commands are enabled, but neither an owner allowFrom list nor any channels.<id>.users allowlist is configured; /… commands will be rejected for everyone.",
remediation: "Approve yourself via pairing (recommended), or set channels.slack.allowFrom and/or channels.slack.channels.<id>.users."
});
}
}
const dmPolicy = plugin.security.resolveDmPolicy?.({
cfg: params.cfg,
accountId: defaultAccountId,
account
});
if (dmPolicy) await warnDmPolicy({
label: plugin.meta.label ?? plugin.id,
provider: plugin.id,
dmPolicy: dmPolicy.policy,
allowFrom: dmPolicy.allowFrom,
policyPath: dmPolicy.policyPath,
allowFromPath: dmPolicy.allowFromPath,
normalizeEntry: dmPolicy.normalizeEntry
});
if (plugin.security.collectWarnings) {
const warnings = await plugin.security.collectWarnings({
cfg: params.cfg,
accountId: defaultAccountId,
account
});
for (const message of warnings ?? []) {
const trimmed = String(message).trim();
if (!trimmed) continue;
findings.push({
checkId: `channels.${plugin.id}.warning.${findings.length + 1}`,
severity: classifyChannelWarningSeverity(trimmed),
title: `${plugin.meta.label ?? plugin.id} security warning`,
detail: trimmed.replace(/^-\s*/, "")
});
}
}
if (plugin.id === "telegram") {
if (!(params.cfg.commands?.text !== false)) continue;
const telegramCfg = account?.config ?? {};
const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const groups = telegramCfg.groups;
const groupsConfigured = Boolean(groups) && Object.keys(groups ?? {}).length > 0;
if (!(groupPolicy === "open" || groupPolicy === "allowlist" && groupsConfigured)) continue;
const storeAllowFrom = await readChannelAllowFromStore("telegram").catch(() => []);
const storeHasWildcard = storeAllowFrom.some((v) => String(v).trim() === "*");
const invalidTelegramAllowFromEntries = /* @__PURE__ */ new Set();
for (const entry of storeAllowFrom) {
const normalized = normalizeTelegramAllowFromEntry(entry);
if (!normalized || normalized === "*") continue;
if (!isNumericTelegramUserId(normalized)) invalidTelegramAllowFromEntries.add(normalized);
}
const groupAllowFrom = Array.isArray(telegramCfg.groupAllowFrom) ? telegramCfg.groupAllowFrom : [];
const groupAllowFromHasWildcard = groupAllowFrom.some((v) => String(v).trim() === "*");
for (const entry of groupAllowFrom) {
const normalized = normalizeTelegramAllowFromEntry(entry);
if (!normalized || normalized === "*") continue;
if (!isNumericTelegramUserId(normalized)) invalidTelegramAllowFromEntries.add(normalized);
}
const dmAllowFrom = Array.isArray(telegramCfg.allowFrom) ? telegramCfg.allowFrom : [];
for (const entry of dmAllowFrom) {
const normalized = normalizeTelegramAllowFromEntry(entry);
if (!normalized || normalized === "*") continue;
if (!isNumericTelegramUserId(normalized)) invalidTelegramAllowFromEntries.add(normalized);
}
const anyGroupOverride = Boolean(groups && Object.values(groups).some((value) => {
if (!value || typeof value !== "object") return false;
const group = value;
const allowFrom = Array.isArray(group.allowFrom) ? group.allowFrom : [];
if (allowFrom.length > 0) {
for (const entry of allowFrom) {
const normalized = normalizeTelegramAllowFromEntry(entry);
if (!normalized || normalized === "*") continue;
if (!isNumericTelegramUserId(normalized)) invalidTelegramAllowFromEntries.add(normalized);
}
return true;
}
const topics = group.topics;
if (!topics || typeof topics !== "object") return false;
return Object.values(topics).some((topicValue) => {
if (!topicValue || typeof topicValue !== "object") return false;
const topic = topicValue;
const topicAllow = Array.isArray(topic.allowFrom) ? topic.allowFrom : [];
for (const entry of topicAllow) {
const normalized = normalizeTelegramAllowFromEntry(entry);
if (!normalized || normalized === "*") continue;
if (!isNumericTelegramUserId(normalized)) invalidTelegramAllowFromEntries.add(normalized);
}
return topicAllow.length > 0;
});
}));
const hasAnySenderAllowlist = storeAllowFrom.length > 0 || groupAllowFrom.length > 0 || anyGroupOverride;
if (invalidTelegramAllowFromEntries.size > 0) {
const examples = Array.from(invalidTelegramAllowFromEntries).slice(0, 5);
const more = invalidTelegramAllowFromEntries.size > examples.length ? ` (+${invalidTelegramAllowFromEntries.size - examples.length} more)` : "";
findings.push({
checkId: "channels.telegram.allowFrom.invalid_entries",
severity: "warn",
title: "Telegram allowlist contains non-numeric entries",
detail: `Telegram sender authorization requires numeric Telegram user IDs. Found non-numeric allowFrom entries: ${examples.join(", ")}${more}.`,
remediation: "Replace @username entries with numeric Telegram user IDs (use onboarding to resolve), then re-run the audit."
});
}
if (storeHasWildcard || groupAllowFromHasWildcard) {
findings.push({
checkId: "channels.telegram.groups.allowFrom.wildcard",
severity: "critical",
title: "Telegram group allowlist contains wildcard",
detail: "Telegram group sender allowlist contains \"*\", which allows any group member to run /… commands and control directives.",
remediation: "Remove \"*\" from channels.telegram.groupAllowFrom and pairing store; prefer explicit numeric Telegram user IDs."
});
continue;
}
if (!hasAnySenderAllowlist) {
const providerSetting = telegramCfg.commands?.nativeSkills;
const skillsEnabled = resolveNativeSkillsEnabled({
providerId: "telegram",
providerSetting,
globalSetting: params.cfg.commands?.nativeSkills
});
findings.push({
checkId: "channels.telegram.groups.allowFrom.missing",
severity: "critical",
title: "Telegram group commands have no sender allowlist",
detail: `Telegram group access is enabled but no sender allowlist is configured; this allows any group member to invoke /… commands` + (skillsEnabled ? " (including skill commands)." : "."),
remediation: "Approve yourself via pairing (recommended), or set channels.telegram.groupAllowFrom (or per-group groups.<id>.allowFrom)."
});
}
}
}
return findings;
}
//#endregion
//#region src/security/audit-extra.sync.ts
const SMALL_MODEL_PARAM_B_MAX = 300;
function summarizeGroupPolicy(cfg) {
const channels = cfg.channels;
if (!channels || typeof channels !== "object") return {
open: 0,
allowlist: 0,
other: 0
};
let open = 0;
let allowlist = 0;
let other = 0;
for (const value of Object.values(channels)) {
if (!value || typeof value !== "object") continue;
const policy = value.groupPolicy;
if (policy === "open") open += 1;
else if (policy === "allowlist") allowlist += 1;
else other += 1;
}
return {
open,
allowlist,
other
};
}
function isProbablySyncedPath(p) {
const s = p.toLowerCase();
return s.includes("icloud") || s.includes("dropbox") || s.includes("google drive") || s.includes("googledrive") || s.includes("onedrive");
}
function looksLikeEnvRef(value) {
const v = value.trim();
return v.startsWith("${") && v.endsWith("}");
}
function isGatewayRemotelyExposed(cfg) {
if ((typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback") !== "loopback") return true;
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
return tailscaleMode === "serve" || tailscaleMode === "funnel";
}
function addModel(models, raw, source) {
if (typeof raw !== "string") return;
const id = raw.trim();
if (!id) return;
models.push({
id,
source
});
}
function collectModels(cfg) {
const out = [];
addModel(out, cfg.agents?.defaults?.model?.primary, "agents.defaults.model.primary");
for (const f of cfg.agents?.defaults?.model?.fallbacks ?? []) addModel(out, f, "agents.defaults.model.fallbacks");
addModel(out, cfg.agents?.defaults?.imageModel?.primary, "agents.defaults.imageModel.primary");
for (const f of cfg.agents?.defaults?.imageModel?.fallbacks ?? []) addModel(out, f, "agents.defaults.imageModel.fallbacks");
const list = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
for (const agent of list ?? []) {
if (!agent || typeof agent !== "object") continue;
const id = typeof agent.id === "string" ? agent.id : "";
const model = agent.model;
if (typeof model === "string") addModel(out, model, `agents.list.${id}.model`);
else if (model && typeof model === "object") {
addModel(out, model.primary, `agents.list.${id}.model.primary`);
const fallbacks = model.fallbacks;
if (Array.isArray(fallbacks)) for (const f of fallbacks) addModel(out, f, `agents.list.${id}.model.fallbacks`);
}
}
return out;
}
const LEGACY_MODEL_PATTERNS = [
{
id: "openai.gpt35",
re: /\bgpt-3\.5\b/i,
label: "GPT-3.5 family"
},
{
id: "anthropic.claude2",
re: /\bclaude-(instant|2)\b/i,
label: "Claude 2/Instant family"
},
{
id: "openai.gpt4_legacy",
re: /\bgpt-4-(0314|0613)\b/i,
label: "Legacy GPT-4 snapshots"
}
];
const WEAK_TIER_MODEL_PATTERNS = [{
id: "anthropic.haiku",
re: /\bhaiku\b/i,
label: "Haiku tier (smaller model)"
}];
function isGptModel(id) {
return /\bgpt-/i.test(id);
}
function isGpt5OrHigher(id) {
return /\bgpt-5(?:\b|[.-])/i.test(id);
}
function isClaudeModel(id) {
return /\bclaude-/i.test(id);
}
function isClaude45OrHigher(id) {
return /\bclaude-[^\s/]*?(?:-4-?(?:[5-9]|[1-9]\d)\b|4\.(?:[5-9]|[1-9]\d)\b|-[5-9](?:\b|[.-]))/i.test(id);
}
function extractAgentIdFromSource(source) {
return source.match(/^agents\.list\.([^.]*)\./)?.[1] ?? null;
}
function hasConfiguredDockerConfig(docker) {
if (!docker || typeof docker !== "object") return false;
return Object.values(docker).some((value) => value !== void 0);
}
function normalizeNodeCommand(value) {
return typeof value === "string" ? value.trim() : "";
}
function listKnownNodeCommands(cfg) {
const baseCfg = {
...cfg,
gateway: {
...cfg.gateway,
nodes: {
...cfg.gateway?.nodes,
denyCommands: []
}
}
};
const out = /* @__PURE__ */ new Set();
for (const platform of [
"ios",
"android",
"macos",
"linux",
"windows",
"unknown"
]) {
const allow = resolveNodeCommandAllowlist(baseCfg, { platform });
for (const cmd of allow) {
const normalized = normalizeNodeCommand(cmd);
if (normalized) out.add(normalized);
}
}
return out;
}
function looksLikeNodeCommandPattern(value) {
if (!value) return false;
if (/[?*[\]{}(),|]/.test(value)) return true;
if (value.startsWith("/") || value.endsWith("/") || value.startsWith("^") || value.endsWith("$")) return true;
return /\s/.test(value) || value.includes("group:");
}
function resolveToolPolicies$1(params) {
const policies = [];
const profilePolicy = resolveToolProfilePolicy(params.agentTools?.profile ?? params.cfg.tools?.profile);
if (profilePolicy) policies.push(profilePolicy);
const globalPolicy = pickSandboxToolPolicy(params.cfg.tools ?? void 0);
if (globalPolicy) policies.push(globalPolicy);
const agentPolicy = pickSandboxToolPolicy(params.agentTools);
if (agentPolicy) policies.push(agentPolicy);
if (params.sandboxMode === "all") {
const sandboxPolicy = resolveSandboxToolPolicyForAgent(params.cfg, params.agentId ?? void 0);
policies.push(sandboxPolicy);
}
return policies;
}
function hasWebSearchKey(cfg, env) {
const search = cfg.tools?.web?.search;
return Boolean(search?.apiKey || search?.perplexity?.apiKey || env.BRAVE_API_KEY || env.PERPLEXITY_API_KEY || env.OPENROUTER_API_KEY);
}
function isWebSearchEnabled(cfg, env) {
const enabled = cfg.tools?.web?.search?.enabled;
if (enabled === false) return false;
if (enabled === true) return true;
return hasWebSearchKey(cfg, env);
}
function isWebFetchEnabled(cfg) {
if (cfg.tools?.web?.fetch?.enabled === false) return false;
return true;
}
function isBrowserEnabled(cfg) {
try {
return resolveBrowserConfig(cfg.browser, cfg).enabled;
} catch {
return true;
}
}
function listGroupPolicyOpen(cfg) {
const out = [];
const channels = cfg.channels;
if (!channels || typeof channels !== "object") return out;
for (const [channelId, value] of Object.entries(channels)) {
if (!value || typeof value !== "object") continue;
const section = value;
if (section.groupPolicy === "open") out.push(`channels.${channelId}.groupPolicy`);
const accounts = section.accounts;
if (accounts && typeof accounts === "object") for (const [accountId, accountVal] of Object.entries(accounts)) {
if (!accountVal || typeof accountVal !== "object") continue;
if (accountVal.groupPolicy === "open") out.push(`channels.${channelId}.accounts.${accountId}.groupPolicy`);
}
}
return out;
}
function collectAttackSurfaceSummaryFindings(cfg) {
const group = summarizeGroupPolicy(cfg);
const elevated = cfg.tools?.elevated?.enabled !== false;
const webhooksEnabled = cfg.hooks?.enabled === true;
const internalHooksEnabled = cfg.hooks?.internal?.enabled === true;
const browserEnabled = cfg.browser?.enabled ?? true;
return [{
checkId: "summary.attack_surface",
severity: "info",
title: "Attack surface summary",
detail: `groups: open=${group.open}, allowlist=${group.allowlist}\ntools.elevated: ${elevated ? "enabled" : "disabled"}\nhooks.webhooks: ${webhooksEnabled ? "enabled" : "disabled"}\nhooks.internal: ${internalHooksEnabled ? "enabled" : "disabled"}\nbrowser control: ${browserEnabled ? "enabled" : "disabled"}`
}];
}
function collectSyncedFolderFindings(params) {
const findings = [];
if (isProbablySyncedPath(params.stateDir) || isProbablySyncedPath(params.configPath)) findings.push({
checkId: "fs.synced_dir",
severity: "warn",
title: "State/config path looks like a synced folder",
detail: `stateDir=${params.stateDir}, configPath=${params.configPath}. Synced folders (iCloud/Dropbox/OneDrive/Google Drive) can leak tokens and transcripts onto other devices.`,
remediation: `Keep OPENCLAW_STATE_DIR on a local-only volume and re-run "${formatCliCommand("openclaw security audit --fix")}".`
});
return findings;
}
function collectSecretsInConfigFindings(cfg) {
const findings = [];
const password = typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway.auth.password.trim() : "";
if (password && !looksLikeEnvRef(password)) findings.push({
checkId: "config.secrets.gateway_password_in_config",
severity: "warn",
title: "Gateway password is stored in config",
detail: "gateway.auth.password is set in the config file; prefer environment variables for secrets when possible.",
remediation: "Prefer OPENCLAW_GATEWAY_PASSWORD (env) and remove gateway.auth.password from disk."
});
const hooksToken = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : "";
if (cfg.hooks?.enabled === true && hooksToken && !looksLikeEnvRef(hooksToken)) findings.push({
checkId: "config.secrets.hooks_token_in_config",
severity: "info",
title: "Hooks token is stored in config",
detail: "hooks.token is set in the config file; keep config perms tight and treat it like an API secret."
});
return findings;
}
function collectHooksHardeningFindings(cfg, env = process.env) {
const findings = [];
if (cfg.hooks?.enabled !== true) return findings;
const token = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : "";
if (token && token.length < 24) findings.push({
checkId: "hooks.token_too_short",
severity: "warn",
title: "Hooks token looks short",
detail: `hooks.token is ${token.length} chars; prefer a long random token.`
});
const gatewayAuth = resolveGatewayAuth({
authConfig: cfg.gateway?.auth,
tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
env
});
const openclawGatewayToken = typeof env.OPENCLAW_GATEWAY_TOKEN === "string" && env.OPENCLAW_GATEWAY_TOKEN.trim() ? env.OPENCLAW_GATEWAY_TOKEN.trim() : null;
const gatewayToken = gatewayAuth.mode === "token" && typeof gatewayAuth.token === "string" && gatewayAuth.token.trim() ? gatewayAuth.token.trim() : openclawGatewayToken ? openclawGatewayToken : null;
if (token && gatewayToken && token === gatewayToken) findings.push({
checkId: "hooks.token_reuse_gateway_token",
severity: "critical",
title: "Hooks token reuses the Gateway token",
detail: "hooks.token matches gateway.auth token; compromise of hooks expands blast radius to the Gateway API.",
remediation: "Use a separate hooks.token dedicated to hook ingress."
});
if ((typeof cfg.hooks?.path === "string" ? cfg.hooks.path.trim() : "") === "/") findings.push({
checkId: "hooks.path_root",
severity: "critical",
title: "Hooks base path is '/'",
detail: "hooks.path='/' would shadow other HTTP endpoints and is unsafe.",
remediation: "Use a dedicated path like '/hooks'."
});
const allowRequestSessionKey = cfg.hooks?.allowRequestSessionKey === true;
const defaultSessionKey = typeof cfg.hooks?.defaultSessionKey === "string" ? cfg.hooks.defaultSessionKey.trim() : "";
const allowedPrefixes = Array.isArray(cfg.hooks?.allowedSessionKeyPrefixes) ? cfg.hooks.allowedSessionKeyPrefixes.map((prefix) => prefix.trim()).filter((prefix) => prefix.length > 0) : [];
const remoteExposure = isGatewayRemotelyExposed(cfg);
if (!defaultSessionKey) findings.push({
checkId: "hooks.default_session_key_unset",
severity: "warn",
title: "hooks.defaultSessionKey is not configured",
detail: "Hook agent runs without explicit sessionKey use generated per-request keys. Set hooks.defaultSessionKey to keep hook ingress scoped to a known session.",
remediation: "Set hooks.defaultSessionKey (for example, \"hook:ingress\")."
});
if (allowRequestSessionKey) findings.push({
checkId: "hooks.request_session_key_enabled",
severity: remoteExposure ? "critical" : "warn",
title: "External hook payloads may override sessionKey",
detail: "hooks.allowRequestSessionKey=true allows `/hooks/agent` callers to choose the session key. Treat hook token holders as full-trust unless you also restrict prefixes.",
remediation: "Set hooks.allowRequestSessionKey=false (recommended) or constrain hooks.allowedSessionKeyPrefixes."
});
if (allowRequestSessionKey && allowedPrefixes.length === 0) findings.push({
checkId: "hooks.request_session_key_prefixes_missing",
severity: remoteExposure ? "critical" : "warn",
title: "Request sessionKey override is enabled without prefix restrictions",
detail: "hooks.allowRequestSessionKey=true and hooks.allowedSessionKeyPrefixes is unset/empty, so request payloads can target arbitrary session key shapes.",
remediation: "Set hooks.allowedSessionKeyPrefixes (for example, [\"hook:\"]) or disable request overrides."
});
return findings;
}
function collectGatewayHttpSessionKeyOverrideFindings(cfg) {
const findings = [];
const chatCompletionsEnabled = cfg.gateway?.http?.endpoints?.chatCompletions?.enabled === true;
const responsesEnabled = cfg.gateway?.http?.endpoints?.responses?.enabled === true;
if (!chatCompletionsEnabled && !responsesEnabled) return findings;
const enabledEndpoints = [chatCompletionsEnabled ? "/v1/chat/completions" : null, responsesEnabled ? "/v1/responses" : null].filter((entry) => Boolean(entry));
findings.push({
checkId: "gateway.http.session_key_override_enabled",
severity: "info",
title: "HTTP API session-key override is enabled",
detail: `${enabledEndpoints.join(", ")} accept x-openclaw-session-key for per-request session routing. Treat API credential holders as trusted principals.`
});
return findings;
}
function collectGatewayHttpNoAuthFindings(cfg, env) {
const findings = [];
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
if (resolveGatewayAuth({
authConfig: cfg.gateway?.auth,
tailscaleMode,
env
}).mode !== "none") return findings;
const chatCompletionsEnabled = cfg.gateway?.http?.endpoints?.chatCompletions?.enabled === true;
const responsesEnabled = cfg.gateway?.http?.endpoints?.responses?.enabled === true;
const enabledEndpoints = [
"/tools/invoke",
chatCompletionsEnabled ? "/v1/chat/completions" : null,
responsesEnabled ? "/v1/responses" : null
].filter((entry) => Boolean(entry));
const remoteExposure = isGatewayRemotelyExposed(cfg);
findings.push({
checkId: "gateway.http.no_auth",
severity: remoteExposure ? "critical" : "warn",
title: "Gateway HTTP APIs are reachable without auth",
detail: `gateway.auth.mode="none" leaves ${enabledEndpoints.join(", ")} callable without a shared secret. Treat this as trusted-local only and avoid exposing the gateway beyond loopback.`,
remediation: "Set gateway.auth.mode to token/password (recommended). If you intentionally keep mode=none, keep gateway.bind=loopback and disable optional HTTP endpoints."
});
return findings;
}
function collectSandboxDockerNoopFindings(cfg) {
const findings = [];
const configuredPaths = [];
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
const defaultsSandbox = cfg.agents?.defaults?.sandbox;
const hasDefaultDocker = hasConfiguredDockerConfig(defaultsSandbox?.docker);
const defaultMode = defaultsSandbox?.mode ?? "off";
const hasAnySandboxEnabledAgent = agents.some((entry) => {
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") return false;
return resolveSandboxConfigForAgent(cfg, entry.id).mode !== "off";
});
if (hasDefaultDocker && defaultMode === "off" && !hasAnySandboxEnabledAgent) configuredPaths.push("agents.defaults.sandbox.docker");
for (const entry of agents) {
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") continue;
if (!hasConfiguredDockerConfig(entry.sandbox?.docker)) continue;
if (resolveSandboxConfigForAgent(cfg, entry.id).mode === "off") configuredPaths.push(`agents.list.${entry.id}.sandbox.docker`);
}
if (configuredPaths.length === 0) return findings;
findings.push({
checkId: "sandbox.docker_config_mode_off",
severity: "warn",
title: "Sandbox docker settings configured while sandbox mode is off",
detail: "These docker settings will not take effect until sandbox mode is enabled:\n" + configuredPaths.map((entry) => `- ${entry}`).join("\n"),
remediation: "Enable sandbox mode (`agents.defaults.sandbox.mode=\"non-main\"` or `\"all\"`) where needed, or remove unused docker settings."
});
return findings;
}
function collectSandboxDangerousConfigFindings(cfg) {
const findings = [];
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
const configs = [];
const defaultDocker = cfg.agents?.defaults?.sandbox?.docker;
if (defaultDocker && typeof defaultDocker === "object") configs.push({
source: "agents.defaults.sandbox.docker",
docker: defaultDocker
});
for (const entry of agents) {
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") continue;
const agentDocker = entry.sandbox?.docker;
if (agentDocker && typeof agentDocker === "object") configs.push({
source: `agents.list.${entry.id}.sandbox.docker`,
docker: agentDocker
});
}
for (const { source, docker } of configs) {
const binds = Array.isArray(docker.binds) ? docker.binds : [];
for (const bind of binds) {
if (typeof bind !== "string") continue;
const blocked = getBlockedBindReason(bind);
if (!blocked) continue;
if (blocked.kind === "non_absolute") {
findings.push({
checkId: "sandbox.bind_mount_non_absolute",
severity: "warn",
title: "Sandbox bind mount uses a non-absolute source path",
detail: `${source}.binds contains "${bind}" which uses source path "${blocked.sourcePath}". Non-absolute bind sources are hard to validate safely and may resolve unexpectedly.`,
remediation: `Rewrite "${bind}" to use an absolute host path (for example: /home/user/project:/project:ro).`
});
continue;
}
const verb = blocked.kind === "covers" ? "covers" : "targets";
findings.push({
checkId: "sandbox.dangerous_bind_mount",
severity: "critical",
title: "Dangerous bind mount in sandbox config",
detail: `${source}.binds contains "${bind}" which ${verb} blocked path "${blocked.blockedPath}". This can expose host system directories or the Docker socket to sandbox containers.`,
remediation: `Remove "${bind}" from ${source}.binds. Use project-specific paths instead.`
});
}
const network = typeof docker.network === "string" ? docker.network : void 0;
if (network && network.trim().toLowerCase() === "host") findings.push({
checkId: "sandbox.dangerous_network_mode",
severity: "critical",
title: "Network host mode in sandbox config",
detail: `${source}.network is "host" which bypasses container network isolation entirely.`,
remediation: `Set ${source}.network to "bridge" or "none".`
});
const seccompProfile = typeof docker.seccompProfile === "string" ? docker.seccompProfile : void 0;
if (seccompProfile && seccompProfile.trim().toLowerCase() === "unconfined") findings.push({
checkId: "sandbox.dangerous_seccomp_profile",
severity: "critical",
title: "Seccomp unconfined in sandbox config",
detail: `${source}.seccompProfile is "unconfined" which disables syscall filtering.`,
remediation: `Remove ${source}.seccompProfile or use a custom seccomp profile file.`
});
const apparmorProfile = typeof docker.apparmorProfile === "string" ? docker.apparmorProfile : void 0;
if (apparmorProfile && apparmorProfile.trim().toLowerCase() === "unconfined") findings.push({
checkId: "sandbox.dangerous_apparmor_profile",
severity: "critical",
title: "AppArmor unconfined in sandbox config",
detail: `${source}.apparmorProfile is "unconfined" which disables AppArmor enforcement.`,
remediation: `Remove ${source}.apparmorProfile or use a named AppArmor profile.`
});
}
return findings;
}
function collectNodeDenyCommandPatternFindings(cfg) {
const findings = [];
const denyListRaw = cfg.gateway?.nodes?.denyCommands;
if (!Array.isArray(denyListRaw) || denyListRaw.length === 0) return findings;
const denyList = denyListRaw.map(normalizeNodeCommand).filter(Boolean);
if (denyList.length === 0) return findings;
const knownCommands = listKnownNodeCommands(cfg);
const patternLike = denyList.filter((entry) => looksLikeNodeCommandPattern(entry));
const unknownExact = denyList.filter((entry) => !looksLikeNodeCommandPattern(entry) && !knownCommands.has(entry));
if (patternLike.length === 0 && unknownExact.length === 0) return findings;
const detailParts = [];
if (patternLike.length > 0) detailParts.push(`Pattern-like entries (not supported by exact matching): ${patternLike.join(", ")}`);
if (unknownExact.length > 0) detailParts.push(`Unknown command names (not in defaults/allowCommands): ${unknownExact.join(", ")}`);
const examples = Array.from(knownCommands).slice(0, 8);
findings.push({
checkId: "gateway.nodes.deny_commands_ineffective",
severity: "warn",
title: "Some gateway.nodes.denyCommands entries are ineffective",
detail: "gateway.nodes.denyCommands uses exact command-name matching only.\n" + detailParts.map((entry) => `- ${entry}`).join("\n"),
remediation: `Use exact command names (for example: ${examples.join(", ")}). If you need broader restrictions, remove risky commands from allowCommands/default workflows.`
});
return findings;
}
function collectMinimalProfileOverrideFindings(cfg) {
const findings = [];
if (cfg.tools?.profile !== "minimal") return findings;
const overrides = (cfg.agents?.list ?? []).filter((entry) => {
return Boolean(entry && typeof entry === "object" && typeof entry.id === "string" && entry.tools?.profile && entry.tools.profile !== "minimal");
}).map((entry) => `${entry.id}=${entry.tools?.profile}`);
if (overrides.length === 0) return findings;
findings.push({
checkId: "tools.profile_minimal_overridden",
severity: "warn",
title: "Global tools.profile=minimal is overridden by agent profiles",
detail: "Global minimal profile is set, but these agent profiles take precedence:\n" + overrides.map((entry) => `- agents.list.${entry}`).join("\n"),
remediation: "Set those agents to `tools.profile=\"minimal\"` (or remove the agent override) if you want minimal tools enforced globally."
});
return findings;
}
function collectModelHygieneFindings(cfg) {
const findings = [];
const models = collectModels(cfg);
if (models.length === 0) return findings;
const weakMatches = /* @__PURE__ */ new Map();
const addWeakMatch = (model, source, reason) => {
const key = `${model}@@${source}`;
const existing = weakMatches.get(key);
if (!existing) {
weakMatches.set(key, {
model,
source,
reasons: [reason]
});
return;
}
if (!existing.reasons.includes(reason)) existing.reasons.push(reason);
};
for (const entry of models) {
for (const pat of WEAK_TIER_MODEL_PATTERNS) if (pat.re.test(entry.id)) {
addWeakMatch(entry.id, entry.source, pat.label);
break;
}
if (isGptModel(entry.id) && !isGpt5OrHigher(entry.id)) addWeakMatch(entry.id, entry.source, "Below GPT-5 family");
if (isClaudeModel(entry.id) && !isClaude45OrHigher(entry.id)) addWeakMatch(entry.id, entry.source, "Below Claude 4.5");
}
const matches = [];
for (const entry of models) for (const pat of LEGACY_MODEL_PATTERNS) if (pat.re.test(entry.id)) {
matches.push({
model: entry.id,
source: entry.source,
reason: pat.label
});
break;
}
if (matches.length > 0) {
const lines = matches.slice(0, 12).map((m) => `- ${m.model} (${m.reason}) @ ${m.source}`).join("\n");
const more = matches.length > 12 ? `\n…${matches.length - 12} more` : "";
findings.push({
checkId: "models.legacy",
severity: "warn",
title: "Some configured models look legacy",
detail: "Older/legacy models can be less robust against prompt injection and tool misuse.\n" + lines + more,
remediation: "Prefer modern, instruction-hardened models for any bot that can run tools."
});
}
if (weakMatches.size > 0) {
const lines = Array.from(weakMatches.values()).slice(0, 12).map((m) => `- ${m.model} (${m.reasons.join("; ")}) @ ${m.source}`).join("\n");
const more = weakMatches.size > 12 ? `\n…${weakMatches.size - 12} more` : "";
findings.push({
checkId: "models.weak_tier",
severity: "warn",
title: "Some configured models are below recommended tiers",
detail: "Smaller/older models are generally more susceptible to prompt injection and tool misuse.\n" + lines + more,
remediation: "Use the latest, top-tier model for any bot with tools or untrusted inboxes. Avoid Haiku tiers; prefer GPT-5+ and Claude 4.5+."
});
}
return findings;
}
function collectSmallModelRiskFindings(params) {
const findings = [];
const models = collectModels(params.cfg).filter((entry) => !entry.source.includes("imageModel"));
if (models.length === 0) return findings;
const smallModels = models.map((entry) => {
const paramB = inferParamBFromIdOrName(entry.id);
if (!paramB || paramB > SMALL_MODEL_PARAM_B_MAX) return null;
return {
...entry,
paramB
};
}).filter((entry) => Boolean(entry));
if (smallModels.length === 0) return findings;
let hasUnsafe = false;
const modelLines = [];
const exposureSet = /* @__PURE__ */ new Set();
for (const entry of smallModels) {
const agentId = extractAgentIdFromSource(entry.source);
const sandboxMode = resolveSandboxConfigForAgent(params.cfg, agentId ?? void 0).mode;
const agentTools = agentId && params.cfg.agents?.list ? params.cfg.agents.list.find((agent) => agent?.id === agentId)?.tools : void 0;
const policies = resolveToolPolicies$1({
cfg: params.cfg,
agentTools,
sandboxMode,
agentId
});
const exposed = [];
if (isWebSearchEnabled(params.cfg, params.env)) {
if (isToolAllowedByPolicies("web_search", policies)) exposed.push("web_search");
}
if (isWebFetchEnabled(params.cfg)) {
if (isToolAllowedByPolicies("web_fetch", policies)) exposed.push("web_fetch");
}
if (isBrowserEnabled(params.cfg)) {
if (isToolAllowedByPolicies("browser", policies)) exposed.push("browser");
}
for (const tool of exposed) exposureSet.add(tool);
const sandboxLabel = sandboxMode === "all" ? "sandbox=all" : `sandbox=${sandboxMode}`;
const exposureLabel = exposed.length > 0 ? ` web=[${expose