Skip to content

Commit f5daa84

Browse files
committed
fix(agents): document runtime policy ambiguity
1 parent fd48e52 commit f5daa84

4 files changed

Lines changed: 24 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
3333
- Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`.
3434
- Live tests: fail Gateway live model sweeps when selected coverage is lost to timeouts or stale high-signal filters instead of reporting false missing-profile coverage, and pin Docker OpenAI gateway coverage to the current `gpt-5.5` lane.
3535
- Tests: fail Docker resource-ceiling checks when stats samples or configured limits are invalid instead of silently reporting zero peaks.
36+
- Agents: fail closed when provider-less session models match multiple provider-prefixed runtime policies so CLI runtime routing no longer depends on config order. (#85970) Thanks @potterdigital.
3637

3738
## 2026.5.24
3839

src/agents/model-runtime-aliases.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,6 @@ export function resolveCliRuntimeExecutionProvider(params: {
288288
if (!runtime || runtime === "auto") {
289289
return resolveCliRuntimeFromAuthProfile({ ...params, provider });
290290
}
291-
// When the caller provider is empty (legacy session entries that stored a
292-
// bare model id without a provider prefix), validate the CLI runtime against
293-
// the provider carried forward from the matched policy entry. This rejects
294-
// misconfigured pairings like `agents.defaults.models["openrouter/X"]:
295-
// claude-cli` (claude-cli is only a legal runtime for anthropic).
296291
const effectiveProvider = provider || normalizeProviderId(matchedProvider ?? "");
297292
if (!effectiveProvider) {
298293
return undefined;

src/agents/model-runtime-policy.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,13 +338,13 @@ describe("resolveModelRuntimePolicy", () => {
338338
});
339339
});
340340

341-
it("does not fall back to a provider wildcard when exact bare-model matches are ambiguous", () => {
341+
it("fails closed for duplicate provider-prefixed bare-model policies", () => {
342342
const config = {
343343
agents: {
344344
defaults: {
345345
models: {
346346
"openai/foo-1": { agentRuntime: { id: "codex" } },
347-
"azure/foo-1": { agentRuntime: { id: "codex" } },
347+
"anthropic/foo-1": { agentRuntime: { id: "claude-cli" } },
348348
"anthropic/*": { agentRuntime: { id: "claude-cli" } },
349349
},
350350
},

src/agents/model-runtime-policy.ts

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ export type ModelRuntimePolicySource = "model" | "provider";
1111
export type ResolvedModelRuntimePolicy = {
1212
policy?: AgentRuntimePolicyConfig;
1313
source?: ModelRuntimePolicySource;
14-
/**
15-
* Provider id from the matched entry key (e.g. "anthropic" for an
16-
* `anthropic/foo` agent model entry). Lets downstream resolvers validate
17-
* provider/runtime pairings when the caller passed an empty provider.
18-
*/
1914
matchedProvider?: string;
2015
};
2116

@@ -89,6 +84,24 @@ function providerMatchesCaller(provider: string, callerProvider: string): boolea
8984
return !callerProvider || provider === callerProvider;
9085
}
9186

87+
function resolvePolicyMatch(
88+
matches: AgentModelRuntimePolicyMatch[],
89+
callerProvider: string,
90+
): AgentModelRuntimePolicyResolution {
91+
const [first] = matches;
92+
if (!first) {
93+
return {};
94+
}
95+
if (!callerProvider && matches.some((match) => match.provider !== first.provider)) {
96+
return { ambiguous: true };
97+
}
98+
return {
99+
policy: first.policy,
100+
source: "model",
101+
matchedProvider: first.provider || callerProvider,
102+
};
103+
}
104+
92105
function modelEntryMatches(params: {
93106
entry: Pick<ModelDefinitionConfig, "id">;
94107
provider: string | undefined;
@@ -184,25 +197,10 @@ function resolveAgentModelEntryRuntimePolicy(params: {
184197
}
185198
scopeMatches.push({ provider: parseProviderModelKey(key)?.provider ?? "", policy });
186199
}
187-
if (scopeMatches.length === 0) {
188-
continue;
189-
}
190-
if (callerProvider) {
191-
return {
192-
policy: scopeMatches[0].policy,
193-
source: "model",
194-
matchedProvider: scopeMatches[0].provider || callerProvider,
195-
};
196-
}
197-
const distinctProviders = new Set(scopeMatches.map((m) => m.provider));
198-
if (distinctProviders.size > 1) {
199-
return { ambiguous: true };
200+
const resolved = resolvePolicyMatch(scopeMatches, callerProvider);
201+
if (resolved.policy || resolved.ambiguous) {
202+
return resolved;
200203
}
201-
return {
202-
policy: scopeMatches[0].policy,
203-
source: "model",
204-
matchedProvider: scopeMatches[0].provider,
205-
};
206204
}
207205
return {};
208206
}

0 commit comments

Comments
 (0)