mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
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:
committed by
GitHub
parent
999caf530b
commit
f523bbfcd1
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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" })],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user