mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
93
src/security/audit-model-refs.ts
Normal file
93
src/security/audit-model-refs.ts
Normal 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;
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user