mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +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
|
### 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.
|
- 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/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.
|
- 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 type { SandboxToolPolicy } from "../agents/sandbox/types.js";
|
||||||
import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js";
|
import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js";
|
||||||
import { resolveToolProfilePolicy } from "../agents/tool-policy.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 { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import type { AgentToolsConfig } from "../config/types.tools.js";
|
import type { AgentToolsConfig } from "../config/types.tools.js";
|
||||||
import { hasConfiguredInternalHooks } from "../hooks/configured.js";
|
import { hasConfiguredInternalHooks } from "../hooks/configured.js";
|
||||||
import { hasConfiguredWebSearchCredential } from "../plugins/web-search-credential-presence.js";
|
import { hasConfiguredWebSearchCredential } from "../plugins/web-search-credential-presence.js";
|
||||||
import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
|
import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
|
||||||
|
import { collectAuditModelRefs } from "./audit-model-refs.js";
|
||||||
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
|
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
|
||||||
|
|
||||||
export type SecurityAuditFinding = {
|
export type SecurityAuditFinding = {
|
||||||
@@ -24,8 +21,6 @@ export type SecurityAuditFinding = {
|
|||||||
|
|
||||||
const SMALL_MODEL_PARAM_B_MAX = 300;
|
const SMALL_MODEL_PARAM_B_MAX = 300;
|
||||||
|
|
||||||
type ModelRef = { id: string; source: string };
|
|
||||||
|
|
||||||
function summarizeGroupPolicy(cfg: OpenClawConfig): {
|
function summarizeGroupPolicy(cfg: OpenClawConfig): {
|
||||||
open: number;
|
open: number;
|
||||||
allowlist: number;
|
allowlist: number;
|
||||||
@@ -55,59 +50,6 @@ function summarizeGroupPolicy(cfg: OpenClawConfig): {
|
|||||||
return { open, allowlist, other };
|
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 {
|
function extractAgentIdFromSource(source: string): string | null {
|
||||||
const match = source.match(/^agents\.list\.([^.]*)\./);
|
const match = source.match(/^agents\.list\.([^.]*)\./);
|
||||||
return match?.[1] ?? null;
|
return match?.[1] ?? null;
|
||||||
@@ -210,7 +152,9 @@ export function collectSmallModelRiskFindings(params: {
|
|||||||
env: NodeJS.ProcessEnv;
|
env: NodeJS.ProcessEnv;
|
||||||
}): SecurityAuditFinding[] {
|
}): SecurityAuditFinding[] {
|
||||||
const findings: 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) {
|
if (models.length === 0) {
|
||||||
return findings;
|
return findings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ import { getBlockedBindReason } from "../agents/sandbox/validate-sandbox-securit
|
|||||||
import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js";
|
import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js";
|
||||||
import { resolveToolProfilePolicy } from "../agents/tool-policy.js";
|
import { resolveToolProfilePolicy } from "../agents/tool-policy.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.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 { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import type { AgentToolsConfig } from "../config/types.tools.js";
|
import type { AgentToolsConfig } from "../config/types.tools.js";
|
||||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||||
@@ -23,6 +19,7 @@ import {
|
|||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
normalizeStringifiedOptionalString,
|
normalizeStringifiedOptionalString,
|
||||||
} from "../shared/string-coerce.js";
|
} from "../shared/string-coerce.js";
|
||||||
|
import { collectAuditModelRefs } from "./audit-model-refs.js";
|
||||||
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
|
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,61 +56,6 @@ function looksLikeEnvRef(value: string): boolean {
|
|||||||
return v.startsWith("${") && v.endsWith("}");
|
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 {
|
function isGatewayRemotelyExposed(cfg: OpenClawConfig): boolean {
|
||||||
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
|
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
|
||||||
if (bind !== "loopback") {
|
if (bind !== "loopback") {
|
||||||
@@ -986,7 +928,7 @@ export function collectMinimalProfileOverrideFindings(cfg: OpenClawConfig): Secu
|
|||||||
|
|
||||||
export function collectModelHygieneFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
|
export function collectModelHygieneFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
|
||||||
const findings: SecurityAuditFinding[] = [];
|
const findings: SecurityAuditFinding[] = [];
|
||||||
const models = collectModels(cfg);
|
const models = collectAuditModelRefs(cfg);
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
return findings;
|
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