diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d4a793818..f9a33b6b012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Security/audit: resolve configured model aliases before model-tier and small-parameter checks, so alias-based GPT-5/Codex configs no longer report false weak-model warnings. Fixes #74455. Thanks @blaspat. - Models/UI: hide unauthenticated providers from the default Web chat, `/models`, and model setup pickers while keeping explicit full-catalog browse paths through `view: "all"`, `/models all`, and `models list --all`. Fixes #74423. Thanks @guarismo and @SymbolStar. - Slack/prompts: rely on Slack `interactiveReplies` guidance instead of generic `inlineButtons` config hints so enabled Slack button directives are not contradicted. Fixes #46647. Thanks @jeremykoerber. - Slack/reactions: treat duplicate `already_reacted` responses as idempotent success so repeated agent reaction adds no longer surface as tool failures. Fixes #69005. Thanks @shipitsteven and @martingarramon. diff --git a/src/security/audit-extra.summary.ts b/src/security/audit-extra.summary.ts index edada8549e1..0011bd1900a 100644 --- a/src/security/audit-extra.summary.ts +++ b/src/security/audit-extra.summary.ts @@ -3,15 +3,12 @@ import { resolveSandboxToolPolicyForAgent } from "../agents/sandbox/tool-policy. import type { SandboxToolPolicy } from "../agents/sandbox/types.js"; import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js"; import { resolveToolProfilePolicy } from "../agents/tool-policy.js"; -import { - resolveAgentModelFallbackValues, - resolveAgentModelPrimaryValue, -} from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { AgentToolsConfig } from "../config/types.tools.js"; import { hasConfiguredInternalHooks } from "../hooks/configured.js"; import { hasConfiguredWebSearchCredential } from "../plugins/web-search-credential-presence.js"; import { inferParamBFromIdOrName } from "../shared/model-param-b.js"; +import { collectAuditModelRefs } from "./audit-model-refs.js"; import { pickSandboxToolPolicy } from "./audit-tool-policy.js"; export type SecurityAuditFinding = { @@ -24,8 +21,6 @@ export type SecurityAuditFinding = { const SMALL_MODEL_PARAM_B_MAX = 300; -type ModelRef = { id: string; source: string }; - function summarizeGroupPolicy(cfg: OpenClawConfig): { open: number; allowlist: number; @@ -55,59 +50,6 @@ function summarizeGroupPolicy(cfg: OpenClawConfig): { return { open, allowlist, other }; } -function addModel(models: ModelRef[], raw: unknown, source: string) { - if (typeof raw !== "string") { - return; - } - const id = raw.trim(); - if (!id) { - return; - } - models.push({ id, source }); -} - -function collectModels(cfg: OpenClawConfig): ModelRef[] { - const out: ModelRef[] = []; - addModel( - out, - resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model), - "agents.defaults.model.primary", - ); - for (const fallback of resolveAgentModelFallbackValues(cfg.agents?.defaults?.model)) { - addModel(out, fallback, "agents.defaults.model.fallbacks"); - } - addModel( - out, - resolveAgentModelPrimaryValue(cfg.agents?.defaults?.imageModel), - "agents.defaults.imageModel.primary", - ); - for (const fallback of resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel)) { - addModel(out, fallback, "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 as { id?: unknown }).id === "string" ? (agent as { id: string }).id : ""; - const model = (agent as { model?: unknown }).model; - if (typeof model === "string") { - addModel(out, model, `agents.list.${id}.model`); - } else if (model && typeof model === "object") { - addModel(out, (model as { primary?: unknown }).primary, `agents.list.${id}.model.primary`); - const fallbacks = (model as { fallbacks?: unknown }).fallbacks; - if (Array.isArray(fallbacks)) { - for (const fallback of fallbacks) { - addModel(out, fallback, `agents.list.${id}.model.fallbacks`); - } - } - } - } - return out; -} - function extractAgentIdFromSource(source: string): string | null { const match = source.match(/^agents\.list\.([^.]*)\./); return match?.[1] ?? null; @@ -210,7 +152,9 @@ export function collectSmallModelRiskFindings(params: { env: NodeJS.ProcessEnv; }): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; - const models = collectModels(params.cfg).filter((entry) => !entry.source.includes("imageModel")); + const models = collectAuditModelRefs(params.cfg).filter( + (entry) => !entry.source.includes("imageModel"), + ); if (models.length === 0) { return findings; } diff --git a/src/security/audit-extra.sync.ts b/src/security/audit-extra.sync.ts index b094e16c665..cfa7c9e7773 100644 --- a/src/security/audit-extra.sync.ts +++ b/src/security/audit-extra.sync.ts @@ -6,10 +6,6 @@ import { getBlockedBindReason } from "../agents/sandbox/validate-sandbox-securit import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js"; import { resolveToolProfilePolicy } from "../agents/tool-policy.js"; import { formatCliCommand } from "../cli/command-format.js"; -import { - resolveAgentModelFallbackValues, - resolveAgentModelPrimaryValue, -} from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { AgentToolsConfig } from "../config/types.tools.js"; import { resolveGatewayAuth } from "../gateway/auth.js"; @@ -23,6 +19,7 @@ import { normalizeOptionalString, normalizeStringifiedOptionalString, } from "../shared/string-coerce.js"; +import { collectAuditModelRefs } from "./audit-model-refs.js"; import { pickSandboxToolPolicy } from "./audit-tool-policy.js"; /** @@ -59,61 +56,6 @@ function looksLikeEnvRef(value: string): boolean { return v.startsWith("${") && v.endsWith("}"); } -type ModelRef = { id: string; source: string }; - -function addModel(models: ModelRef[], raw: unknown, source: string) { - if (typeof raw !== "string") { - return; - } - const id = raw.trim(); - if (!id) { - return; - } - models.push({ id, source }); -} - -function collectModels(cfg: OpenClawConfig): ModelRef[] { - const out: ModelRef[] = []; - addModel( - out, - resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model), - "agents.defaults.model.primary", - ); - for (const fallback of resolveAgentModelFallbackValues(cfg.agents?.defaults?.model)) { - addModel(out, fallback, "agents.defaults.model.fallbacks"); - } - addModel( - out, - resolveAgentModelPrimaryValue(cfg.agents?.defaults?.imageModel), - "agents.defaults.imageModel.primary", - ); - for (const fallback of resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel)) { - addModel(out, fallback, "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 as { id?: unknown }).id === "string" ? (agent as { id: string }).id : ""; - const model = (agent as { model?: unknown }).model; - if (typeof model === "string") { - addModel(out, model, `agents.list.${id}.model`); - } else if (model && typeof model === "object") { - addModel(out, (model as { primary?: unknown }).primary, `agents.list.${id}.model.primary`); - const fallbacks = (model as { fallbacks?: unknown }).fallbacks; - if (Array.isArray(fallbacks)) { - for (const fallback of fallbacks) { - addModel(out, fallback, `agents.list.${id}.model.fallbacks`); - } - } - } - } - return out; -} - function isGatewayRemotelyExposed(cfg: OpenClawConfig): boolean { const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback"; if (bind !== "loopback") { @@ -986,7 +928,7 @@ export function collectMinimalProfileOverrideFindings(cfg: OpenClawConfig): Secu export function collectModelHygieneFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; - const models = collectModels(cfg); + const models = collectAuditModelRefs(cfg); if (models.length === 0) { return findings; } diff --git a/src/security/audit-model-hygiene.test.ts b/src/security/audit-model-hygiene.test.ts index 45f573c6601..6ceb7f31ddf 100644 --- a/src/security/audit-model-hygiene.test.ts +++ b/src/security/audit-model-hygiene.test.ts @@ -52,4 +52,24 @@ describe("security audit model hygiene findings", () => { } } }); + + it("resolves configured aliases before tier classification", () => { + const findings = collectModelHygieneFindings({ + agents: { + defaults: { + model: { + primary: "gpt", + fallbacks: ["gpt-prev", "gpt-mini"], + }, + models: { + "openai-codex/gpt-5.5": { alias: "gpt" }, + "openai-codex/gpt-5.4": { alias: "gpt-prev" }, + "openai/gpt-5-mini": { alias: "gpt-mini" }, + }, + }, + }, + } satisfies OpenClawConfig); + + expect(findings.some((finding) => finding.checkId === "models.weak_tier")).toBe(false); + }); }); diff --git a/src/security/audit-model-refs.ts b/src/security/audit-model-refs.ts new file mode 100644 index 00000000000..cd20ce55e40 --- /dev/null +++ b/src/security/audit-model-refs.ts @@ -0,0 +1,93 @@ +import { DEFAULT_PROVIDER } from "../agents/defaults.js"; +import { modelKey } from "../agents/model-selection-normalize.js"; +import { + buildModelAliasIndex, + resolveModelRefFromString, +} from "../agents/model-selection-shared.js"; +import { + resolveAgentModelFallbackValues, + resolveAgentModelPrimaryValue, +} from "../config/model-input.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; + +export type AuditModelRef = { id: string; source: string }; + +function resolveAuditModelId( + cfg: OpenClawConfig, + raw: string, + aliasIndex: ReturnType, +): string { + const resolved = resolveModelRefFromString({ + cfg, + raw, + defaultProvider: DEFAULT_PROVIDER, + aliasIndex, + allowPluginNormalization: false, + })?.ref; + return resolved ? modelKey(resolved.provider, resolved.model) : raw; +} + +function addModelRef(params: { + out: AuditModelRef[]; + cfg: OpenClawConfig; + aliasIndex: ReturnType; + raw: unknown; + source: string; +}): void { + if (typeof params.raw !== "string") { + return; + } + const raw = params.raw.trim(); + if (!raw) { + return; + } + params.out.push({ + id: resolveAuditModelId(params.cfg, raw, params.aliasIndex), + source: params.source, + }); +} + +export function collectAuditModelRefs(cfg: OpenClawConfig): AuditModelRef[] { + const aliasIndex = buildModelAliasIndex({ + cfg, + defaultProvider: DEFAULT_PROVIDER, + allowPluginNormalization: false, + }); + const out: AuditModelRef[] = []; + const add = (raw: unknown, source: string) => addModelRef({ out, cfg, aliasIndex, raw, source }); + + add(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model), "agents.defaults.model.primary"); + for (const fallback of resolveAgentModelFallbackValues(cfg.agents?.defaults?.model)) { + add(fallback, "agents.defaults.model.fallbacks"); + } + add( + resolveAgentModelPrimaryValue(cfg.agents?.defaults?.imageModel), + "agents.defaults.imageModel.primary", + ); + for (const fallback of resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel)) { + add(fallback, "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 as { id?: unknown }).id === "string" ? (agent as { id: string }).id : ""; + const model = (agent as { model?: unknown }).model; + if (typeof model === "string") { + add(model, `agents.list.${id}.model`); + } else if (model && typeof model === "object") { + add((model as { primary?: unknown }).primary, `agents.list.${id}.model.primary`); + const fallbacks = (model as { fallbacks?: unknown }).fallbacks; + if (Array.isArray(fallbacks)) { + for (const fallback of fallbacks) { + add(fallback, `agents.list.${id}.model.fallbacks`); + } + } + } + } + + return out; +} diff --git a/src/security/audit-small-model-risk.test.ts b/src/security/audit-small-model-risk.test.ts index 52c7a92541a..7d788b96b9d 100644 --- a/src/security/audit-small-model-risk.test.ts +++ b/src/security/audit-small-model-risk.test.ts @@ -45,4 +45,27 @@ describe("security audit small-model risk findings", () => { } } }); + + it("resolves configured aliases before parameter-size classification", () => { + const [finding] = collectSmallModelRiskFindings({ + cfg: { + agents: { + defaults: { + model: { primary: "tiny" }, + models: { + "ollama/mistral-8b": { alias: "tiny" }, + }, + }, + }, + tools: { web: { search: { enabled: true }, fetch: { enabled: true } } }, + browser: { enabled: true }, + } satisfies OpenClawConfig, + env: {}, + }); + + expect(finding?.checkId).toBe("models.small_params"); + expect(finding?.detail).toContain("ollama/mistral-8b"); + expect(finding?.detail).toContain("@ agents.defaults.model.primary"); + expect(finding?.detail).not.toContain("- tiny"); + }); });