fix: align claude cli permissions with exec policy

Derive Claude CLI bypass mode from OpenClaw exec YOLO policy, preserve raw Claude permission-mode overrides, update docs/changelog, and cover global/per-agent policy behavior.
This commit is contained in:
Peter Steinberger
2026-04-23 23:11:34 +01:00
committed by GitHub
parent 999caf530b
commit f523bbfcd1
19 changed files with 352 additions and 56 deletions

View File

@@ -26,7 +26,7 @@ Docs: https://docs.openclaw.ai
- Plugins/install: link the host OpenClaw package into external plugins that declare `openclaw` as a peer dependency, so peer-only plugin SDK imports resolve after install without bundling a duplicate host package. (#70462) Thanks @anishesg. - Plugins/install: link the host OpenClaw package into external plugins that declare `openclaw` as a peer dependency, so peer-only plugin SDK imports resolve after install without bundling a duplicate host package. (#70462) Thanks @anishesg.
- Teams/security: require shared Bot Framework audience tokens to name the configured Teams app via verified `appid` or `azp`, blocking cross-bot token replay on the global audience. (#70724) Thanks @vincentkoc. - Teams/security: require shared Bot Framework audience tokens to name the configured Teams app via verified `appid` or `azp`, blocking cross-bot token replay on the global audience. (#70724) Thanks @vincentkoc.
- Plugins/startup: resolve bundled plugin Jiti loads relative to the target plugin module instead of the central loader, so Bun global installs no longer hang while discovering bundled image providers. (#70073) Thanks @yidianyiko. - Plugins/startup: resolve bundled plugin Jiti loads relative to the target plugin module instead of the central loader, so Bun global installs no longer hang while discovering bundled image providers. (#70073) Thanks @yidianyiko.
- Anthropic/CLI security: stop Claude CLI backend defaults from forcing `bypassPermissions`, and strip malformed permission-mode overrides instead of silently falling back to a bypass. (#70723) Thanks @vincentkoc. - Anthropic/CLI security: derive Claude CLI `bypassPermissions` from OpenClaw's existing YOLO exec policy, preserve explicit raw Claude `--permission-mode` overrides, and strip malformed permission-mode args instead of silently falling back to a bypass. (#70723) Thanks @vincentkoc.
- Android/security: require loopback-only cleartext gateway connections on Android manual and scanned routes, so private-LAN and link-local `ws://` endpoints now fail closed unless TLS is enabled. (#70722) Thanks @vincentkoc. - Android/security: require loopback-only cleartext gateway connections on Android manual and scanned routes, so private-LAN and link-local `ws://` endpoints now fail closed unless TLS is enabled. (#70722) Thanks @vincentkoc.
- Pairing/security: require private-IP or loopback hosts for cleartext mobile pairing, and stop treating `.local` or dotless hostnames as safe cleartext endpoints. (#70721) Thanks @vincentkoc. - Pairing/security: require private-IP or loopback hosts for cleartext mobile pairing, and stop treating `.local` or dotless hostnames as safe cleartext endpoints. (#70721) Thanks @vincentkoc.
- Plugins/security: stop setup-api lookup from falling back to the launch directory, so workspace-local `extensions/<plugin>/setup-api.*` files cannot be executed during provider setup resolution. (#70718) Thanks @drobison00. - Plugins/security: stop setup-api lookup from falling back to the launch directory, so workspace-local `extensions/<plugin>/setup-api.*` files cannot be executed during provider setup resolution. (#70718) Thanks @drobison00.

View File

@@ -1,2 +1,2 @@
bc55649a80027756f37892424598653a81fec4bff7b074358fe34d08c7696ebc plugin-sdk-api-baseline.json f30c9e61b768ca10feca401aefca3cbc8d3a57c5020f85aa9106b4f1a61032c0 plugin-sdk-api-baseline.json
312a29d50b4959e4a8e242bb7559548d895a2e03d5ed1b5a395b1133de090578 plugin-sdk-api-baseline.jsonl 9e5e3e66ac23dddb80cceb8a785f167eec8a108c6c5abe77f3346b01895f6756 plugin-sdk-api-baseline.jsonl

View File

@@ -169,6 +169,15 @@ resolver sees the same filtered set that OpenClaw would otherwise advertise in
the prompt. Skill env/API key overrides are still applied by OpenClaw to the the prompt. Skill env/API key overrides are still applied by OpenClaw to the
child process environment for the run. child process environment for the run.
Claude CLI also has its own noninteractive permission mode. OpenClaw maps that
to the existing exec policy instead of adding Claude-specific config: when the
effective requested exec policy is YOLO (`tools.exec.security: "full"` and
`tools.exec.ask: "off"`), OpenClaw adds `--permission-mode bypassPermissions`.
Per-agent `agents.list[].tools.exec` settings override global `tools.exec` for
that agent. To force a different Claude mode, set explicit raw backend args
such as `--permission-mode default` or `--permission-mode acceptEdits` under
`agents.defaults.cliBackends.claude-cli.args` and matching `resumeArgs`.
Before OpenClaw can use the bundled `claude-cli` backend, Claude Code itself Before OpenClaw can use the bundled `claude-cli` backend, Claude Code itself
must already be logged in on the same host: must already be logged in on the same host:

View File

@@ -12,7 +12,7 @@ Cloudflare AI Gateway sits in front of provider APIs and lets you add analytics,
| ------------- | ---------------------------------------------------------------------------------------- | | ------------- | ---------------------------------------------------------------------------------------- |
| Provider | `cloudflare-ai-gateway` | | Provider | `cloudflare-ai-gateway` |
| Base URL | `https://gateway.ai.cloudflare.com/v1/<account_id>/<gateway_id>/anthropic` | | Base URL | `https://gateway.ai.cloudflare.com/v1/<account_id>/<gateway_id>/anthropic` |
| Default model | `cloudflare-ai-gateway/claude-sonnet-4-5` | | Default model | `cloudflare-ai-gateway/claude-sonnet-4-6` |
| API key | `CLOUDFLARE_AI_GATEWAY_API_KEY` (your provider API key for requests through the Gateway) | | API key | `CLOUDFLARE_AI_GATEWAY_API_KEY` (your provider API key for requests through the Gateway) |
<Note> <Note>
@@ -39,7 +39,7 @@ For Anthropic models routed through Cloudflare AI Gateway, use your **Anthropic
{ {
agents: { agents: {
defaults: { defaults: {
model: { primary: "cloudflare-ai-gateway/claude-sonnet-4-5" }, model: { primary: "cloudflare-ai-gateway/claude-sonnet-4-6" },
}, },
}, },
} }

View File

@@ -125,6 +125,11 @@ Important distinction:
- `tools.exec.host=auto` chooses where exec runs: sandbox when available, otherwise gateway. - `tools.exec.host=auto` chooses where exec runs: sandbox when available, otherwise gateway.
- YOLO chooses how host exec is approved: `security=full` plus `ask=off`. - YOLO chooses how host exec is approved: `security=full` plus `ask=off`.
- CLI-backed providers that expose their own noninteractive permission mode can follow this policy.
Claude CLI adds `--permission-mode bypassPermissions` when OpenClaw's requested exec policy is
YOLO. Override that backend behavior with explicit Claude args under
`agents.defaults.cliBackends.claude-cli.args` / `resumeArgs`, for example
`--permission-mode default`, `acceptEdits`, or `bypassPermissions`.
- In YOLO mode, OpenClaw does not add a separate heuristic command-obfuscation approval gate or script-preflight rejection layer on top of the configured host exec policy. - In YOLO mode, OpenClaw does not add a separate heuristic command-obfuscation approval gate or script-preflight rejection layer on top of the configured host exec policy.
- `auto` does not make gateway routing a free override from a sandboxed session. A per-call `host=node` request is allowed from `auto`, and `host=gateway` is only allowed from `auto` when no sandbox runtime is active. If you want a stable non-auto default, set `tools.exec.host` or use `/exec host=...` explicitly. - `auto` does not make gateway routing a free override from a sandboxed session. A per-call `host=node` request is allowed from `auto`, and `host=gateway` is only allowed from `auto` when no sandbox runtime is active. If you want a stable non-auto default, set `tools.exec.host` or use `/exec host=...` explicitly.

View File

@@ -36,6 +36,8 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
"--verbose", "--verbose",
"--setting-sources", "--setting-sources",
"user", "user",
"--allowedTools",
"mcp__openclaw__*",
], ],
resumeArgs: [ resumeArgs: [
"-p", "-p",
@@ -45,6 +47,8 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
"--verbose", "--verbose",
"--setting-sources", "--setting-sources",
"user", "user",
"--allowedTools",
"mcp__openclaw__*",
"--resume", "--resume",
"{sessionId}", "{sessionId}",
], ],
@@ -53,6 +57,8 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
input: "stdin", input: "stdin",
modelArg: "--model", modelArg: "--model",
modelAliases: CLAUDE_CLI_MODEL_ALIASES, modelAliases: CLAUDE_CLI_MODEL_ALIASES,
imageArg: "@",
imagePathScope: "workspace",
sessionArg: "--session-id", sessionArg: "--session-id",
sessionMode: "always", sessionMode: "always",
sessionIdFields: [...CLAUDE_CLI_SESSION_ID_FIELDS], sessionIdFields: [...CLAUDE_CLI_SESSION_ID_FIELDS],

View File

@@ -5,6 +5,7 @@ import {
normalizeClaudeBackendConfig, normalizeClaudeBackendConfig,
normalizeClaudePermissionArgs, normalizeClaudePermissionArgs,
normalizeClaudeSettingSourcesArgs, normalizeClaudeSettingSourcesArgs,
resolveClaudePermissionMode,
} from "./cli-shared.js"; } from "./cli-shared.js";
describe("normalizeClaudePermissionArgs", () => { describe("normalizeClaudePermissionArgs", () => {
@@ -89,6 +90,8 @@ describe("normalizeClaudeBackendConfig", () => {
"--verbose", "--verbose",
"--setting-sources", "--setting-sources",
"user", "user",
"--permission-mode",
"bypassPermissions",
]); ]);
expect(normalized.resumeArgs).toEqual([ expect(normalized.resumeArgs).toEqual([
"-p", "-p",
@@ -99,12 +102,67 @@ describe("normalizeClaudeBackendConfig", () => {
"{sessionId}", "{sessionId}",
"--setting-sources", "--setting-sources",
"user", "user",
"--permission-mode",
"bypassPermissions",
]); ]);
expect(normalized.output).toBe("jsonl"); expect(normalized.output).toBe("jsonl");
expect(normalized.liveSession).toBe("claude-stdio"); expect(normalized.liveSession).toBe("claude-stdio");
expect(normalized.input).toBe("stdin"); expect(normalized.input).toBe("stdin");
}); });
it("derives Claude bypass from OpenClaw YOLO policy and disables it for safer policy", () => {
expect(resolveClaudePermissionMode({ backendId: "claude-cli" })).toEqual({
mode: "bypassPermissions",
overrideExisting: false,
});
expect(
resolveClaudePermissionMode({
backendId: "claude-cli",
config: { tools: { exec: { security: "allowlist", ask: "on-miss" } } },
}),
).toEqual({ overrideExisting: false });
});
it("derives Claude bypass from per-agent OpenClaw exec policy", () => {
expect(
resolveClaudePermissionMode({
backendId: "claude-cli",
agentId: "safe-agent",
config: {
tools: { exec: { security: "full", ask: "off" } },
agents: {
list: [
{
id: "safe-agent",
tools: { exec: { security: "allowlist", ask: "on-miss" } },
},
],
},
},
}),
).toEqual({ overrideExisting: false });
expect(
resolveClaudePermissionMode({
backendId: "claude-cli",
agentId: "yolo-agent",
config: {
tools: { exec: { security: "allowlist", ask: "on-miss" } },
agents: {
list: [
{
id: "yolo-agent",
tools: { exec: { security: "full", ask: "off" } },
},
],
},
},
}),
).toEqual({
mode: "bypassPermissions",
overrideExisting: false,
});
});
it("does not infer live stdio when explicit transport overrides are incompatible", () => { it("does not infer live stdio when explicit transport overrides are incompatible", () => {
const normalized = normalizeClaudeBackendConfig({ const normalized = normalizeClaudeBackendConfig({
command: "claude", command: "claude",
@@ -131,8 +189,12 @@ describe("normalizeClaudeBackendConfig", () => {
expect(normalized?.args).toContain("--setting-sources"); expect(normalized?.args).toContain("--setting-sources");
expect(normalized?.args).toContain("user"); expect(normalized?.args).toContain("user");
expect(normalized?.args).toContain("--permission-mode");
expect(normalized?.args).toContain("bypassPermissions");
expect(normalized?.resumeArgs).toContain("--setting-sources"); expect(normalized?.resumeArgs).toContain("--setting-sources");
expect(normalized?.resumeArgs).toContain("user"); expect(normalized?.resumeArgs).toContain("user");
expect(normalized?.resumeArgs).toContain("--permission-mode");
expect(normalized?.resumeArgs).toContain("bypassPermissions");
expect(normalized?.liveSession).toBe("claude-stdio"); expect(normalized?.liveSession).toBe("claude-stdio");
}); });

View File

@@ -1,4 +1,7 @@
import type { CliBackendConfig } from "openclaw/plugin-sdk/cli-backend"; import type {
CliBackendConfig,
CliBackendNormalizeConfigContext,
} from "openclaw/plugin-sdk/cli-backend";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
import { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js"; import { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js";
export { export {
@@ -58,16 +61,40 @@ const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode"; const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
const CLAUDE_SETTING_SOURCES_ARG = "--setting-sources"; const CLAUDE_SETTING_SOURCES_ARG = "--setting-sources";
const CLAUDE_SAFE_SETTING_SOURCES = "user"; const CLAUDE_SAFE_SETTING_SOURCES = "user";
const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions";
export function isClaudeCliProvider(providerId: string): boolean { export function isClaudeCliProvider(providerId: string): boolean {
return normalizeOptionalLowercaseString(providerId) === CLAUDE_CLI_BACKEND_ID; return normalizeOptionalLowercaseString(providerId) === CLAUDE_CLI_BACKEND_ID;
} }
export function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined { function isOpenClawRequestedYolo(context?: CliBackendNormalizeConfigContext): boolean {
const agentExec = context?.agentId
? context.config?.agents?.list?.find((agent) => agent.id === context.agentId)?.tools?.exec
: undefined;
const exec = agentExec ?? context?.config?.tools?.exec;
const security = exec?.security ?? "full";
const ask = exec?.ask ?? "off";
return security === "full" && ask === "off";
}
export function resolveClaudePermissionMode(context?: CliBackendNormalizeConfigContext): {
mode?: string;
overrideExisting: boolean;
} {
return isOpenClawRequestedYolo(context)
? { mode: CLAUDE_BYPASS_PERMISSION_MODE, overrideExisting: false }
: { overrideExisting: false };
}
export function normalizeClaudePermissionArgs(
args?: string[],
options?: { mode?: string; overrideExisting?: boolean },
): string[] | undefined {
if (!args) { if (!args) {
return args; return options?.mode ? [CLAUDE_PERMISSION_MODE_ARG, options.mode] : args;
} }
const normalized: string[] = []; const normalized: string[] = [];
let hasPermissionMode = false;
for (let i = 0; i < args.length; i += 1) { for (let i = 0; i < args.length; i += 1) {
const arg = args[i]; const arg = args[i];
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) { if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
@@ -80,8 +107,11 @@ export function normalizeClaudePermissionArgs(args?: string[]): string[] | undef
maybeValue.trim().length > 0 && maybeValue.trim().length > 0 &&
!maybeValue.startsWith("-") !maybeValue.startsWith("-")
) { ) {
normalized.push(arg); hasPermissionMode = true;
normalized.push(maybeValue); if (!options?.overrideExisting) {
normalized.push(arg);
normalized.push(maybeValue);
}
i += 1; i += 1;
} }
continue; continue;
@@ -89,12 +119,18 @@ export function normalizeClaudePermissionArgs(args?: string[]): string[] | undef
if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) { if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) {
const maybeValue = arg.slice(`${CLAUDE_PERMISSION_MODE_ARG}=`.length).trim(); const maybeValue = arg.slice(`${CLAUDE_PERMISSION_MODE_ARG}=`.length).trim();
if (maybeValue.length > 0 && !maybeValue.startsWith("-")) { if (maybeValue.length > 0 && !maybeValue.startsWith("-")) {
normalized.push(`${CLAUDE_PERMISSION_MODE_ARG}=${maybeValue}`); hasPermissionMode = true;
if (!options?.overrideExisting) {
normalized.push(`${CLAUDE_PERMISSION_MODE_ARG}=${maybeValue}`);
}
} }
continue; continue;
} }
normalized.push(arg); normalized.push(arg);
} }
if (options?.mode && (!hasPermissionMode || options.overrideExisting)) {
normalized.push(CLAUDE_PERMISSION_MODE_ARG, options.mode);
}
return normalized; return normalized;
} }
@@ -132,13 +168,20 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u
return normalized; return normalized;
} }
export function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig { export function normalizeClaudeBackendConfig(
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,
): CliBackendConfig {
const output = config.output ?? "jsonl"; const output = config.output ?? "jsonl";
const input = config.input ?? "stdin"; const input = config.input ?? "stdin";
const permission = resolveClaudePermissionMode(context);
return { return {
...config, ...config,
args: normalizeClaudePermissionArgs(normalizeClaudeSettingSourcesArgs(config.args)), args: normalizeClaudePermissionArgs(normalizeClaudeSettingSourcesArgs(config.args), permission),
resumeArgs: normalizeClaudePermissionArgs(normalizeClaudeSettingSourcesArgs(config.resumeArgs)), resumeArgs: normalizeClaudePermissionArgs(
normalizeClaudeSettingSourcesArgs(config.resumeArgs),
permission,
),
output, output,
liveSession: liveSession:
config.liveSession ?? (output === "jsonl" && input === "stdin" ? "claude-stdio" : undefined), config.liveSession ?? (output === "jsonl" && input === "stdin" ? "claude-stdio" : undefined),

View File

@@ -1,7 +1,7 @@
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
export const CLOUDFLARE_AI_GATEWAY_PROVIDER_ID = "cloudflare-ai-gateway"; export const CLOUDFLARE_AI_GATEWAY_PROVIDER_ID = "cloudflare-ai-gateway";
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID = "claude-sonnet-4-5"; export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID = "claude-sonnet-4-6";
export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF = `${CLOUDFLARE_AI_GATEWAY_PROVIDER_ID}/${CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID}`; export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF = `${CLOUDFLARE_AI_GATEWAY_PROVIDER_ID}/${CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID}`;
const CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW = 200_000; const CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW = 200_000;
@@ -22,7 +22,7 @@ export function buildCloudflareAiGatewayModelDefinition(params?: {
const id = params?.id?.trim() || CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID; const id = params?.id?.trim() || CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID;
return { return {
id, id,
name: params?.name ?? "Claude Sonnet 4.5", name: params?.name ?? "Claude Sonnet 4.6",
reasoning: params?.reasoning ?? true, reasoning: params?.reasoning ?? true,
input: params?.input ?? ["text", "image"], input: params?.input ?? ["text", "image"],
cost: CLOUDFLARE_AI_GATEWAY_DEFAULT_COST, cost: CLOUDFLARE_AI_GATEWAY_DEFAULT_COST,

View File

@@ -6,8 +6,8 @@ describe("qa runner model catalog", () => {
expect( expect(
selectQaRunnerModelOptions([ selectQaRunnerModelOptions([
{ {
key: "anthropic/claude-sonnet-4-5", key: "anthropic/claude-sonnet-4-6",
name: "Claude Sonnet 4.5", name: "Claude Sonnet 4.6",
input: "text", input: "text",
available: true, available: true,
missing: false, missing: false,
@@ -27,6 +27,6 @@ describe("qa runner model catalog", () => {
missing: false, missing: false,
}, },
]).map((entry) => entry.key), ]).map((entry) => entry.key),
).toEqual(["openai/gpt-5.5", "anthropic/claude-sonnet-4-5"]); ).toEqual(["openai/gpt-5.5", "anthropic/claude-sonnet-4-6"]);
}); });
}); });

View File

@@ -1,7 +1,11 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import type { CliBackendConfig } from "../config/types.js"; import type { CliBackendConfig } from "../config/types.js";
import type { CliBackendAuthEpochMode, CliBundleMcpMode } from "../plugins/types.js"; import type {
CliBackendAuthEpochMode,
CliBackendNormalizeConfigContext,
CliBundleMcpMode,
} from "../plugins/types.js";
import { import {
__testing as cliBackendsTesting, __testing as cliBackendsTesting,
resolveCliBackendConfig, resolveCliBackendConfig,
@@ -27,7 +31,10 @@ function createBackendEntry(params: {
defaultAuthProfileId?: string; defaultAuthProfileId?: string;
authEpochMode?: CliBackendAuthEpochMode; authEpochMode?: CliBackendAuthEpochMode;
prepareExecution?: () => Promise<null>; prepareExecution?: () => Promise<null>;
normalizeConfig?: (config: CliBackendConfig) => CliBackendConfig; normalizeConfig?: (
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,
) => CliBackendConfig;
}) { }) {
return { return {
pluginId: params.pluginId, pluginId: params.pluginId,
@@ -111,12 +118,33 @@ const NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS = [
"user", "user",
]; ];
function normalizeTestClaudeArgs(args?: string[]): string[] | undefined { function isTestYoloConfig(context?: CliBackendNormalizeConfigContext): boolean {
const agentExec = context?.agentId
? context.config?.agents?.list?.find((agent) => agent.id === context.agentId)?.tools?.exec
: undefined;
const exec = agentExec ?? context?.config?.tools?.exec;
return (exec?.security ?? "full") === "full" && (exec?.ask ?? "off") === "off";
}
function normalizeTestPermissionMode(context?: CliBackendNormalizeConfigContext): {
mode?: string;
overrideExisting: boolean;
} {
return isTestYoloConfig(context)
? { mode: "bypassPermissions", overrideExisting: false }
: { overrideExisting: false };
}
function normalizeTestClaudeArgs(
args: string[] | undefined,
permission: { mode?: string; overrideExisting: boolean },
): string[] | undefined {
if (!args) { if (!args) {
return args; return permission.mode ? ["--permission-mode", permission.mode] : args;
} }
const normalized: string[] = []; const normalized: string[] = [];
let hasSettingSources = false; let hasSettingSources = false;
let hasPermissionMode = false;
for (let i = 0; i < args.length; i += 1) { for (let i = 0; i < args.length; i += 1) {
const arg = args[i]; const arg = args[i];
if (arg === "--dangerously-skip-permissions") { if (arg === "--dangerously-skip-permissions") {
@@ -139,7 +167,10 @@ function normalizeTestClaudeArgs(args?: string[]): string[] | undefined {
if (arg === "--permission-mode") { if (arg === "--permission-mode") {
const maybeValue = args[i + 1]; const maybeValue = args[i + 1];
if (maybeValue && !maybeValue.startsWith("-")) { if (maybeValue && !maybeValue.startsWith("-")) {
normalized.push(arg, maybeValue); hasPermissionMode = true;
if (!permission.overrideExisting) {
normalized.push(arg, maybeValue);
}
i += 1; i += 1;
} }
continue; continue;
@@ -147,7 +178,10 @@ function normalizeTestClaudeArgs(args?: string[]): string[] | undefined {
if (arg.startsWith("--permission-mode=")) { if (arg.startsWith("--permission-mode=")) {
const maybeValue = arg.slice("--permission-mode=".length).trim(); const maybeValue = arg.slice("--permission-mode=".length).trim();
if (maybeValue.length > 0 && !maybeValue.startsWith("-")) { if (maybeValue.length > 0 && !maybeValue.startsWith("-")) {
normalized.push(`--permission-mode=${maybeValue}`); hasPermissionMode = true;
if (!permission.overrideExisting) {
normalized.push(`--permission-mode=${maybeValue}`);
}
} }
continue; continue;
} }
@@ -156,14 +190,21 @@ function normalizeTestClaudeArgs(args?: string[]): string[] | undefined {
if (!hasSettingSources) { if (!hasSettingSources) {
normalized.push("--setting-sources", "user"); normalized.push("--setting-sources", "user");
} }
if (permission.mode && (!hasPermissionMode || permission.overrideExisting)) {
normalized.push("--permission-mode", permission.mode);
}
return normalized; return normalized;
} }
function normalizeTestClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig { function normalizeTestClaudeBackendConfig(
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,
): CliBackendConfig {
const permission = normalizeTestPermissionMode(context);
return { return {
...config, ...config,
args: normalizeTestClaudeArgs(config.args), args: normalizeTestClaudeArgs(config.args, permission),
resumeArgs: normalizeTestClaudeArgs(config.resumeArgs), resumeArgs: normalizeTestClaudeArgs(config.resumeArgs, permission),
}; };
} }
@@ -186,6 +227,8 @@ beforeEach(() => {
"--verbose", "--verbose",
"--setting-sources", "--setting-sources",
"user", "user",
"--allowedTools",
"mcp__openclaw__*",
], ],
resumeArgs: [ resumeArgs: [
"stream-json", "stream-json",
@@ -193,11 +236,15 @@ beforeEach(() => {
"--verbose", "--verbose",
"--setting-sources", "--setting-sources",
"user", "user",
"--allowedTools",
"mcp__openclaw__*",
"--resume", "--resume",
"{sessionId}", "{sessionId}",
], ],
output: "jsonl", output: "jsonl",
input: "stdin", input: "stdin",
imageArg: "@",
imagePathScope: "workspace",
clearEnv: [ clearEnv: [
"ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY_OLD", "ANTHROPIC_API_KEY_OLD",
@@ -410,7 +457,7 @@ describe("resolveCliBackendLiveTest", () => {
}); });
describe("resolveCliBackendConfig claude-cli defaults", () => { describe("resolveCliBackendConfig claude-cli defaults", () => {
it("keeps user-only setting sources without forcing a permission-mode default", () => { it("derives bypassPermissions from OpenClaw's default YOLO exec policy", () => {
const resolved = resolveCliBackendConfig("claude-cli"); const resolved = resolveCliBackendConfig("claude-cli");
expect(resolved).not.toBeNull(); expect(resolved).not.toBeNull();
@@ -422,18 +469,102 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
expect(resolved?.config.args).toContain("--verbose"); expect(resolved?.config.args).toContain("--verbose");
expect(resolved?.config.args).toContain("--setting-sources"); expect(resolved?.config.args).toContain("--setting-sources");
expect(resolved?.config.args).toContain("user"); expect(resolved?.config.args).toContain("user");
expect(resolved?.config.args).not.toContain("--permission-mode"); expect(resolved?.config.args).toContain("--allowedTools");
expect(resolved?.config.args).toContain("mcp__openclaw__*");
expect(resolved?.config.args).toContain("--permission-mode");
expect(resolved?.config.args).toContain("bypassPermissions");
expect(resolved?.config.args).not.toContain("--dangerously-skip-permissions"); expect(resolved?.config.args).not.toContain("--dangerously-skip-permissions");
expect(resolved?.config.input).toBe("stdin"); expect(resolved?.config.input).toBe("stdin");
expect(resolved?.config.imageArg).toBe("@");
expect(resolved?.config.imagePathScope).toBe("workspace");
expect(resolved?.config.resumeArgs).toContain("stream-json"); expect(resolved?.config.resumeArgs).toContain("stream-json");
expect(resolved?.config.resumeArgs).toContain("--include-partial-messages"); expect(resolved?.config.resumeArgs).toContain("--include-partial-messages");
expect(resolved?.config.resumeArgs).toContain("--verbose"); expect(resolved?.config.resumeArgs).toContain("--verbose");
expect(resolved?.config.resumeArgs).toContain("--setting-sources"); expect(resolved?.config.resumeArgs).toContain("--setting-sources");
expect(resolved?.config.resumeArgs).toContain("user"); expect(resolved?.config.resumeArgs).toContain("user");
expect(resolved?.config.resumeArgs).not.toContain("--permission-mode"); expect(resolved?.config.resumeArgs).toContain("--allowedTools");
expect(resolved?.config.resumeArgs).toContain("mcp__openclaw__*");
expect(resolved?.config.resumeArgs).toContain("--permission-mode");
expect(resolved?.config.resumeArgs).toContain("bypassPermissions");
expect(resolved?.config.resumeArgs).not.toContain("--dangerously-skip-permissions"); expect(resolved?.config.resumeArgs).not.toContain("--dangerously-skip-permissions");
}); });
it("keeps Claude permission mode unset when OpenClaw exec policy is not YOLO", () => {
const resolved = resolveCliBackendConfig("claude-cli", {
tools: { exec: { security: "allowlist", ask: "on-miss" } },
});
expect(resolved).not.toBeNull();
expect(resolved?.config.args).not.toContain("--permission-mode");
expect(resolved?.config.args).not.toContain("bypassPermissions");
expect(resolved?.config.resumeArgs).not.toContain("--permission-mode");
expect(resolved?.config.resumeArgs).not.toContain("bypassPermissions");
});
it("derives Claude permission mode from per-agent exec policy when an agent id is known", () => {
const cfg = {
tools: { exec: { security: "full", ask: "off" } },
agents: {
list: [
{
id: "reviewer",
tools: { exec: { security: "allowlist", ask: "on-miss" } },
},
{
id: "builder",
tools: { exec: { security: "full", ask: "off" } },
},
],
},
} satisfies OpenClawConfig;
const reviewer = resolveCliBackendConfig("claude-cli", cfg, { agentId: "reviewer" });
const builder = resolveCliBackendConfig("claude-cli", cfg, { agentId: "builder" });
expect(reviewer?.config.args).not.toContain("--permission-mode");
expect(reviewer?.config.resumeArgs).not.toContain("--permission-mode");
expect(builder?.config.args).toContain("--permission-mode");
expect(builder?.config.args).toContain("bypassPermissions");
expect(builder?.config.resumeArgs).toContain("--permission-mode");
expect(builder?.config.resumeArgs).toContain("bypassPermissions");
});
it("uses existing exec policy and raw Claude args as permission overrides", () => {
const safe = resolveCliBackendConfig("claude-cli", {
tools: { exec: { security: "full", ask: "off" } },
agents: {
defaults: {
cliBackends: {
"claude-cli": {
command: "claude",
args: ["-p", "--permission-mode", "default"],
resumeArgs: ["-p", "--permission-mode=default", "--resume", "{sessionId}"],
},
},
},
},
});
const yolo = resolveCliBackendConfig("claude-cli", {
tools: { exec: { security: "deny", ask: "always" } },
agents: {
defaults: {
cliBackends: {
"claude-cli": {
command: "claude",
args: ["-p", "--permission-mode", "bypassPermissions"],
resumeArgs: ["-p", "--permission-mode=bypassPermissions", "--resume", "{sessionId}"],
},
},
},
},
});
expect(safe?.config.args).toContain("default");
expect(safe?.config.args).not.toContain("bypassPermissions");
expect(yolo?.config.args).toContain("--permission-mode");
expect(yolo?.config.args).toContain("bypassPermissions");
});
it("retains default claude safety args when only command is overridden", () => { it("retains default claude safety args when only command is overridden", () => {
const cfg = { const cfg = {
agents: { agents: {
@@ -453,10 +584,12 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
expect(resolved?.config.command).toBe("/usr/local/bin/claude"); expect(resolved?.config.command).toBe("/usr/local/bin/claude");
expect(resolved?.config.args).toContain("--setting-sources"); expect(resolved?.config.args).toContain("--setting-sources");
expect(resolved?.config.args).toContain("user"); expect(resolved?.config.args).toContain("user");
expect(resolved?.config.args).not.toContain("--permission-mode"); expect(resolved?.config.args).toContain("--permission-mode");
expect(resolved?.config.args).toContain("bypassPermissions");
expect(resolved?.config.resumeArgs).toContain("--setting-sources"); expect(resolved?.config.resumeArgs).toContain("--setting-sources");
expect(resolved?.config.resumeArgs).toContain("user"); expect(resolved?.config.resumeArgs).toContain("user");
expect(resolved?.config.resumeArgs).not.toContain("--permission-mode"); expect(resolved?.config.resumeArgs).toContain("--permission-mode");
expect(resolved?.config.resumeArgs).toContain("bypassPermissions");
expect(resolved?.config.env).not.toHaveProperty("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST"); expect(resolved?.config.env).not.toHaveProperty("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_API_TOKEN"); expect(resolved?.config.clearEnv).toContain("ANTHROPIC_API_TOKEN");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_BASE_URL"); expect(resolved?.config.clearEnv).toContain("ANTHROPIC_BASE_URL");
@@ -470,7 +603,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
expect(resolved?.config.clearEnv).toContain("CLAUDE_CODE_USE_COWORK_PLUGINS"); expect(resolved?.config.clearEnv).toContain("CLAUDE_CODE_USE_COWORK_PLUGINS");
}); });
it("drops legacy skip-permissions overrides without inventing bypassPermissions", () => { it("drops legacy skip-permissions overrides without inventing bypassPermissions under safe policy", () => {
const cfg = { const cfg = {
agents: { agents: {
defaults: { defaults: {
@@ -490,6 +623,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
}, },
}, },
}, },
tools: { exec: { security: "allowlist", ask: "on-miss" } },
} satisfies OpenClawConfig; } satisfies OpenClawConfig;
const resolved = resolveCliBackendConfig("claude-cli", cfg); const resolved = resolveCliBackendConfig("claude-cli", cfg);
@@ -587,11 +721,14 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
}); });
it("falls back to user-only setting sources when a custom override leaves the flag without a value", () => { it("falls back to user-only setting sources when a custom override leaves the flag without a value", () => {
const cfg = createClaudeCliOverrideConfig({ const cfg = {
command: "claude", ...createClaudeCliOverrideConfig({
args: ["-p", "--setting-sources", "--output-format", "stream-json"], command: "claude",
resumeArgs: ["-p", "--setting-sources", "--resume", "{sessionId}"], args: ["-p", "--setting-sources", "--output-format", "stream-json"],
}); resumeArgs: ["-p", "--setting-sources", "--resume", "{sessionId}"],
}),
tools: { exec: { security: "allowlist", ask: "on-miss" } },
} satisfies OpenClawConfig;
const resolved = resolveCliBackendConfig("claude-cli", cfg); const resolved = resolveCliBackendConfig("claude-cli", cfg);
@@ -600,12 +737,15 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
expect(resolved?.config.resumeArgs).toEqual(NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS); expect(resolved?.config.resumeArgs).toEqual(NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS);
}); });
it("drops malformed permission-mode overrides without adding bypassPermissions", () => { it("drops malformed permission-mode overrides without adding bypassPermissions under safe policy", () => {
const cfg = createClaudeCliOverrideConfig({ const cfg = {
command: "claude", ...createClaudeCliOverrideConfig({
args: ["-p", "--permission-mode", "--output-format", "stream-json"], command: "claude",
resumeArgs: ["-p", "--permission-mode=--resume", "--resume", "{sessionId}"], args: ["-p", "--permission-mode", "--output-format", "stream-json"],
}); resumeArgs: ["-p", "--permission-mode=--resume", "--resume", "{sessionId}"],
}),
tools: { exec: { security: "allowlist", ask: "on-miss" } },
} satisfies OpenClawConfig;
const resolved = resolveCliBackendConfig("claude-cli", cfg); const resolved = resolveCliBackendConfig("claude-cli", cfg);
@@ -614,7 +754,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
expect(resolved?.config.resumeArgs).toEqual(NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS); expect(resolved?.config.resumeArgs).toEqual(NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS);
}); });
it("leaves permission-mode unset when custom args omit it", () => { it("leaves permission-mode unset when custom args omit it under safe policy", () => {
const cfg = { const cfg = {
agents: { agents: {
defaults: { defaults: {
@@ -634,6 +774,7 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
}, },
}, },
}, },
tools: { exec: { security: "allowlist", ask: "on-miss" } },
} satisfies OpenClawConfig; } satisfies OpenClawConfig;
const resolved = resolveCliBackendConfig("claude-cli", cfg); const resolved = resolveCliBackendConfig("claude-cli", cfg);
@@ -711,6 +852,8 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
"json", "json",
"--setting-sources", "--setting-sources",
"user", "user",
"--permission-mode",
"bypassPermissions",
]); ]);
expect(resolved?.config.resumeArgs).toEqual([ expect(resolved?.config.resumeArgs).toEqual([
"-p", "-p",
@@ -720,6 +863,8 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
"{sessionId}", "{sessionId}",
"--setting-sources", "--setting-sources",
"user", "user",
"--permission-mode",
"bypassPermissions",
]); ]);
expect(resolved?.config.systemPromptArg).toBe("--append-system-prompt"); expect(resolved?.config.systemPromptArg).toBe("--append-system-prompt");
expect(resolved?.config.systemPromptWhen).toBe("first"); expect(resolved?.config.systemPromptWhen).toBe("first");

View File

@@ -5,6 +5,7 @@ import { resolvePluginSetupCliBackend } from "../plugins/setup-registry.js";
import { resolveRuntimeTextTransforms } from "../plugins/text-transforms.runtime.js"; import { resolveRuntimeTextTransforms } from "../plugins/text-transforms.runtime.js";
import type { import type {
CliBackendAuthEpochMode, CliBackendAuthEpochMode,
CliBackendNormalizeConfigContext,
CliBundleMcpMode, CliBundleMcpMode,
CliBackendPlugin, CliBackendPlugin,
PluginTextTransforms, PluginTextTransforms,
@@ -50,7 +51,10 @@ type FallbackCliBackendPolicy = {
bundleMcp: boolean; bundleMcp: boolean;
bundleMcpMode?: CliBundleMcpMode; bundleMcpMode?: CliBundleMcpMode;
baseConfig?: CliBackendConfig; baseConfig?: CliBackendConfig;
normalizeConfig?: (config: CliBackendConfig) => CliBackendConfig; normalizeConfig?: (
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,
) => CliBackendConfig;
transformSystemPrompt?: CliBackendPlugin["transformSystemPrompt"]; transformSystemPrompt?: CliBackendPlugin["transformSystemPrompt"];
textTransforms?: PluginTextTransforms; textTransforms?: PluginTextTransforms;
defaultAuthProfileId?: string; defaultAuthProfileId?: string;
@@ -188,15 +192,23 @@ export function resolveCliBackendLiveTest(provider: string): ResolvedCliBackendL
export function resolveCliBackendConfig( export function resolveCliBackendConfig(
provider: string, provider: string,
cfg?: OpenClawConfig, cfg?: OpenClawConfig,
options: { agentId?: string } = {},
): ResolvedCliBackend | null { ): ResolvedCliBackend | null {
const normalized = normalizeBackendKey(provider); const normalized = normalizeBackendKey(provider);
const normalizeContext: CliBackendNormalizeConfigContext = {
backendId: normalized,
...(options.agentId ? { agentId: options.agentId } : {}),
...(cfg ? { config: cfg } : {}),
};
const runtimeTextTransforms = resolveRuntimeTextTransforms(); const runtimeTextTransforms = resolveRuntimeTextTransforms();
const configured = cfg?.agents?.defaults?.cliBackends ?? {}; const configured = cfg?.agents?.defaults?.cliBackends ?? {};
const override = pickBackendConfig(configured, normalized); const override = pickBackendConfig(configured, normalized);
const registered = resolveRegisteredBackend(normalized); const registered = resolveRegisteredBackend(normalized);
if (registered) { if (registered) {
const merged = mergeBackendConfig(registered.config, override); const merged = mergeBackendConfig(registered.config, override);
const config = registered.normalizeConfig ? registered.normalizeConfig(merged) : merged; const config = registered.normalizeConfig
? registered.normalizeConfig(merged, normalizeContext)
: merged;
const command = config.command?.trim(); const command = config.command?.trim();
if (!command) { if (!command) {
return null; return null;
@@ -224,7 +236,7 @@ export function resolveCliBackendConfig(
return null; return null;
} }
const baseConfig = fallbackPolicy.normalizeConfig const baseConfig = fallbackPolicy.normalizeConfig
? fallbackPolicy.normalizeConfig(fallbackPolicy.baseConfig) ? fallbackPolicy.normalizeConfig(fallbackPolicy.baseConfig, normalizeContext)
: fallbackPolicy.baseConfig; : fallbackPolicy.baseConfig;
const command = baseConfig.command?.trim(); const command = baseConfig.command?.trim();
if (!command) { if (!command) {
@@ -249,7 +261,7 @@ export function resolveCliBackendConfig(
? mergeBackendConfig(fallbackPolicy.baseConfig, override) ? mergeBackendConfig(fallbackPolicy.baseConfig, override)
: override; : override;
const config = fallbackPolicy?.normalizeConfig const config = fallbackPolicy?.normalizeConfig
? fallbackPolicy.normalizeConfig(mergedFallback) ? fallbackPolicy.normalizeConfig(mergedFallback, normalizeContext)
: mergedFallback; : mergedFallback;
const command = config.command?.trim(); const command = config.command?.trim();
if (!command) { if (!command) {

View File

@@ -95,7 +95,9 @@ export async function prepareCliRunContext(
} }
const workspaceDir = resolvedWorkspace; const workspaceDir = resolvedWorkspace;
const backendResolved = resolveCliBackendConfig(params.provider, params.config); const backendResolved = resolveCliBackendConfig(params.provider, params.config, {
agentId: params.agentId,
});
if (!backendResolved) { if (!backendResolved) {
throw new Error(`Unknown CLI backend: ${params.provider}`); throw new Error(`Unknown CLI backend: ${params.provider}`);
} }

View File

@@ -515,7 +515,7 @@ describe("buildAgentSystemPrompt", () => {
workspaceDir: "/tmp/openclaw", workspaceDir: "/tmp/openclaw",
modelAliasLines: [ modelAliasLines: [
"- Opus: anthropic/claude-opus-4-5", "- Opus: anthropic/claude-opus-4-5",
"- Sonnet: anthropic/claude-sonnet-4-5", "- Sonnet: anthropic/claude-sonnet-4-6",
], ],
}); });

View File

@@ -1,6 +1,7 @@
export type { CliBackendConfig } from "../config/types.js"; export type { CliBackendConfig } from "../config/types.js";
export type { export type {
CliBackendAuthEpochMode, CliBackendAuthEpochMode,
CliBackendNormalizeConfigContext,
CliBackendPlugin, CliBackendPlugin,
CliBackendPreparedExecution, CliBackendPreparedExecution,
CliBackendPrepareExecutionContext, CliBackendPrepareExecutionContext,

View File

@@ -35,6 +35,12 @@ export type CliBackendPreparedExecution = {
export type CliBackendAuthEpochMode = "combined" | "profile-only"; export type CliBackendAuthEpochMode = "combined" | "profile-only";
export type CliBackendNormalizeConfigContext = {
config?: OpenClawConfig;
backendId: string;
agentId?: string;
};
/** Plugin-owned CLI backend defaults used by the text-only CLI runner. */ /** Plugin-owned CLI backend defaults used by the text-only CLI runner. */
export type CliBackendPlugin = { export type CliBackendPlugin = {
/** Provider id used in model refs, for example `claude-cli/opus`. */ /** Provider id used in model refs, for example `claude-cli/opus`. */
@@ -78,7 +84,10 @@ export type CliBackendPlugin = {
* Use this for backend-specific compatibility rewrites when old config * Use this for backend-specific compatibility rewrites when old config
* shapes need to stay working. * shapes need to stay working.
*/ */
normalizeConfig?: (config: CliBackendConfig) => CliBackendConfig; normalizeConfig?: (
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,
) => CliBackendConfig;
/** /**
* Backend-owned final system-prompt transform. * Backend-owned final system-prompt transform.
* *

View File

@@ -69,6 +69,7 @@ import type { VideoGenerationProvider } from "../video-generation/types.js";
import type { WizardPrompter } from "../wizard/prompts.js"; import type { WizardPrompter } from "../wizard/prompts.js";
import type { import type {
CliBackendAuthEpochMode, CliBackendAuthEpochMode,
CliBackendNormalizeConfigContext,
CliBackendPreparedExecution, CliBackendPreparedExecution,
CliBackendPrepareExecutionContext, CliBackendPrepareExecutionContext,
CliBackendPlugin, CliBackendPlugin,
@@ -148,6 +149,7 @@ export type {
} from "./conversation-binding.types.js"; } from "./conversation-binding.types.js";
export type { export type {
CliBackendAuthEpochMode, CliBackendAuthEpochMode,
CliBackendNormalizeConfigContext,
CliBackendPreparedExecution, CliBackendPreparedExecution,
CliBackendPrepareExecutionContext, CliBackendPrepareExecutionContext,
CliBackendPlugin, CliBackendPlugin,

View File

@@ -57,8 +57,8 @@ function buildCommonSystemParams(workspaceDir: string) {
os: "Darwin 24.0.0", os: "Darwin 24.0.0",
arch: "arm64", arch: "arm64",
node: process.version, node: process.version,
model: "anthropic/claude-sonnet-4-5", model: "anthropic/claude-sonnet-4-6",
defaultModel: "anthropic/claude-sonnet-4-5", defaultModel: "anthropic/claude-sonnet-4-6",
shell: "zsh", shell: "zsh",
}, },
userTimezone: "America/Los_Angeles", userTimezone: "America/Los_Angeles",

View File

@@ -636,7 +636,7 @@ export function describeCloudflareAiGatewayProviderDiscoveryContract(
baseUrl: "https://gateway.ai.cloudflare.com/v1/acc-123/gw-456/anthropic", baseUrl: "https://gateway.ai.cloudflare.com/v1/acc-123/gw-456/anthropic",
api: "anthropic-messages", api: "anthropic-messages",
apiKey: "CLOUDFLARE_AI_GATEWAY_API_KEY", apiKey: "CLOUDFLARE_AI_GATEWAY_API_KEY",
models: [expect.objectContaining({ id: "claude-sonnet-4-5" })], models: [expect.objectContaining({ id: "claude-sonnet-4-6" })],
}, },
}); });
}); });