mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(config): model policy refs padded around the provider/model separator fail config validation (#114116)
* fix(config): accept model policy refs padded around the provider/model separator Model policy ref validation split refs on / and rejected any component containing whitespace, so " openai / gpt-5.5 " failed config validation even though parseProviderModelRef in @openclaw/model-catalog-core trims the provider and the model remainder and the runtime resolves the ref fine. Allow-list validation is newer than that resolution path, so configs that loaded before started failing at load. Validate the values parseModelCatalogRef returns — the resolved provider and the whole model remainder — keeping empty-path-segment rejection, so padding around the separator is accepted while whitespace inside the resolved model id stays rejected. Wildcard prefixes trim per segment, since their canonical key is matched on segment boundaries; a padded nested prefix now keeps its namespace instead of failing validation. * refactor(config): simplify model policy normalization * fix(config): normalize padded policy wildcards --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -213,6 +213,27 @@ describe("explicit model visibility policy", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps nested prefix wildcards scoped when segments carry boundary whitespace", () => {
|
||||
const policy = createPolicy({
|
||||
agents: {
|
||||
defaults: {
|
||||
modelPolicy: { allow: [" clawrouter / anthropic / * ", " openai / gpt-5.5 "] },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// The padded nested wildcard must keep its namespace rather than widening to
|
||||
// every clawrouter model.
|
||||
expect(policy.allowsKey("clawrouter/anthropic/claude-haiku-4-5")).toBe(true);
|
||||
expect(policy.allowsKey("clawrouter/google/gemini-3.5-flash")).toBe(false);
|
||||
expect(policy.allowsKey("openai/gpt-5.5")).toBe(true);
|
||||
expect(policy.allowsKey("openai/gpt-5.6-sol")).toBe(false);
|
||||
expect(policy.allowedCatalog.map((entry) => `${entry.provider}/${entry.id}`)).toEqual([
|
||||
"clawrouter/anthropic/claude-haiku-4-5",
|
||||
"openai/gpt-5.5",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps top-level provider wildcard behavior for nested model ids", () => {
|
||||
const policy = createPolicy({
|
||||
agents: {
|
||||
|
||||
@@ -174,4 +174,64 @@ describe("config model reference validation", () => {
|
||||
expect(res.config.models?.providers?.myproxy?.models?.[0]?.id).toBe("vendor/modern-model");
|
||||
}
|
||||
});
|
||||
it.each([
|
||||
[
|
||||
"default policy",
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
modelPolicy: {
|
||||
allow: [
|
||||
" openai / gpt-5.5 ",
|
||||
"clawrouter/ anthropic/claude-haiku-4-5",
|
||||
" openai / * ",
|
||||
" clawrouter / anthropic / * ",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"per-agent policy",
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "worker",
|
||||
modelPolicy: {
|
||||
allow: [" openai / gpt-5.5 ", " openai / * ", " openai / ns / * "],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
])("accepts separator padding in the %s", (_label, config) => {
|
||||
const res = validateConfigObjectWithPlugins(config, { pluginValidation: "skip" });
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["clawrouter/anthropic /claude-haiku-4-5", "openai/gpt 5.5", "openai//gpt-5.5"])(
|
||||
"still rejects malformed model policy ref %j",
|
||||
(ref) => {
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
modelPolicy: { allow: [ref] },
|
||||
},
|
||||
},
|
||||
},
|
||||
{ pluginValidation: "skip" },
|
||||
);
|
||||
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues[0]?.path).toBe("agents.defaults.modelPolicy.allow.0");
|
||||
expect(res.issues[0]?.message).toContain("invalid model policy ref");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,10 +39,9 @@ type ModelPolicyWildcardRef = {
|
||||
/** Parse and canonicalize a segment-boundary model-policy prefix wildcard. */
|
||||
export function parseModelPolicyWildcardRef(raw: string): ModelPolicyWildcardRef | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed.endsWith("/*")) {
|
||||
return null;
|
||||
}
|
||||
const segments = trimmed.split("/");
|
||||
// Wildcard keys match on segment boundaries, so normalize boundary padding
|
||||
// before building the canonical key used by policy matching.
|
||||
const segments = trimmed.split("/").map((segment) => segment.trim());
|
||||
if (
|
||||
segments.at(-1) !== "*" ||
|
||||
!hasValidSegments(segments.slice(0, -1), {
|
||||
@@ -63,9 +62,13 @@ export function parseModelPolicyWildcardRef(raw: string): ModelPolicyWildcardRef
|
||||
|
||||
/** True for a syntactically valid exact provider/model policy reference. */
|
||||
export function isValidExactModelPolicyRef(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
const parsed = parseModelCatalogRef(trimmed);
|
||||
return Boolean(parsed && hasValidSegments(trimmed.split("/"), { min: 2 }));
|
||||
const parsed = parseModelCatalogRef(raw);
|
||||
return Boolean(
|
||||
parsed &&
|
||||
hasValidSegments([parsed.provider, ...parsed.modelId.split("/")], {
|
||||
min: 2,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** True for a supported bare selector whose target is resolved from config. */
|
||||
|
||||
Reference in New Issue
Block a user