fix(security): resolve model aliases before audit classification (#74532)

* fix(security): resolve model aliases before audit classification

Before classification, model strings are now resolved through the alias
index so that configured aliases (e.g. 'gpt-prev') are translated to
their canonical provider/key form (e.g. 'openai/gpt-5.4') before hygene
and tier checks run.

Fixes #74455.

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix(security): share audit model alias resolution

---------

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Blasius Patrick
2026-04-30 01:26:02 +07:00
committed by GitHub
parent b1c515270e
commit f5aebe42e1
6 changed files with 143 additions and 120 deletions

View File

@@ -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 <provider> 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.

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
});
});

View File

@@ -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<typeof buildModelAliasIndex>,
): 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<typeof buildModelAliasIndex>;
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;
}

View File

@@ -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");
});
});