From a4a00aa1da3c67c0374d58a43889fe781f3d3aa3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 15:09:01 +0000 Subject: [PATCH] feat: pluginize cli inference backends --- CHANGELOG.md | 1 + docs/.generated/plugin-sdk-api-baseline.json | 28 ++- docs/.generated/plugin-sdk-api-baseline.jsonl | 12 +- docs/cli/gateway.md | 3 +- docs/cli/index.md | 3 +- docs/gateway/cli-backends.md | 34 +++- docs/plugins/architecture.md | 17 +- docs/plugins/building-plugins.md | 27 +-- docs/plugins/sdk-overview.md | 14 ++ extensions/anthropic/cli-backend.ts | 112 +++++++++++ extensions/anthropic/index.ts | 2 + extensions/google/cli-backend.ts | 35 ++++ extensions/google/index.ts | 2 + extensions/lobster/src/lobster-tool.test.ts | 1 + extensions/openai/cli-backend.ts | 48 +++++ extensions/openai/index.ts | 2 + package.json | 4 + scripts/lib/plugin-sdk-entrypoints.json | 1 + src/agents/cli-backends.test.ts | 50 ++++- src/agents/cli-backends.ts | 183 +++--------------- src/agents/cli-runner.ts | 10 +- src/agents/cli-runner/bundle-mcp.test.ts | 18 +- src/agents/cli-runner/bundle-mcp.ts | 4 +- src/agents/model-selection.ts | 8 +- .../gateway-cli/run.option-collisions.test.ts | 16 +- src/cli/gateway-cli/run.ts | 13 +- src/commands/auth-choice-legacy.ts | 26 +++ .../channel-setup/plugin-install.test.ts | 1 + ...oard-non-interactive.provider-auth.test.ts | 1 + .../local/auth-choice.ts | 13 +- src/commands/onboard.ts | 21 +- src/plugin-sdk/cli-backend.ts | 6 + src/plugin-sdk/index.ts | 2 + src/plugins/captured-registration.ts | 7 + src/plugins/cli-backends.runtime.ts | 13 ++ .../contracts/registry.contract.test.ts | 16 ++ src/plugins/contracts/registry.ts | 4 + src/plugins/contracts/shape.contract.test.ts | 1 + src/plugins/hooks.test-helpers.ts | 1 + src/plugins/loader.test.ts | 33 ++++ src/plugins/loader.ts | 1 + .../provider-auth-choice-preference.ts | 12 +- src/plugins/registry-empty.ts | 1 + src/plugins/registry.ts | 48 +++++ src/plugins/status.test-helpers.ts | 1 + src/plugins/status.test.ts | 20 ++ src/plugins/status.ts | 2 + src/plugins/types.ts | 26 ++- test/helpers/extensions/plugin-api.ts | 1 + 49 files changed, 657 insertions(+), 248 deletions(-) create mode 100644 extensions/anthropic/cli-backend.ts create mode 100644 extensions/google/cli-backend.ts create mode 100644 extensions/openai/cli-backend.ts create mode 100644 src/plugin-sdk/cli-backend.ts create mode 100644 src/plugins/cli-backends.runtime.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d9a326a910..474b16ae864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Plugins/runtime: expose `runHeartbeatOnce` in the plugin runtime `system` namespace so plugins can trigger a single heartbeat cycle with an explicit delivery target override (e.g. `heartbeat: { target: "last" }`). (#40299) Thanks @loveyana. - Agents/compaction: preserve the post-compaction AGENTS refresh on stale-usage preflight compaction for both immediate replies and queued followups. (#49479) Thanks @jared596. - Agents/compaction: surface safeguard-specific cancel reasons and relabel benign manual `/compact` no-op cases as skipped instead of failed. (#51072) Thanks @afurm. +- Plugins/CLI backends: move bundled Claude CLI, Codex CLI, and Gemini CLI inference defaults onto the plugin surface, add bundled Gemini CLI backend support, and replace `gateway run --claude-cli-logs` with generic `--cli-backend-logs` while keeping the old flag as a compatibility alias. ### Fixes diff --git a/docs/.generated/plugin-sdk-api-baseline.json b/docs/.generated/plugin-sdk-api-baseline.json index a9f7b174eb2..5555f58af86 100644 --- a/docs/.generated/plugin-sdk-api-baseline.json +++ b/docs/.generated/plugin-sdk-api-baseline.json @@ -257,6 +257,24 @@ "path": "src/config/types.openclaw.ts" } }, + { + "declaration": "export type CliBackendConfig = CliBackendConfig;", + "exportName": "CliBackendConfig", + "kind": "type", + "source": { + "line": 47, + "path": "src/config/types.agent-defaults.ts" + } + }, + { + "declaration": "export type CliBackendPlugin = CliBackendPlugin;", + "exportName": "CliBackendPlugin", + "kind": "type", + "source": { + "line": 1292, + "path": "src/plugins/types.ts" + } + }, { "declaration": "export type CompiledConfiguredBinding = CompiledConfiguredBinding;", "exportName": "CompiledConfiguredBinding", @@ -415,7 +433,7 @@ "exportName": "OpenClawPluginApi", "kind": "type", "source": { - "line": 1314, + "line": 1336, "path": "src/plugins/types.ts" } }, @@ -3414,7 +3432,7 @@ "exportName": "OpenClawPluginApi", "kind": "type", "source": { - "line": 1314, + "line": 1336, "path": "src/plugins/types.ts" } }, @@ -3441,7 +3459,7 @@ "exportName": "OpenClawPluginDefinition", "kind": "type", "source": { - "line": 1296, + "line": 1318, "path": "src/plugins/types.ts" } }, @@ -3920,7 +3938,7 @@ "exportName": "OpenClawPluginApi", "kind": "type", "source": { - "line": 1314, + "line": 1336, "path": "src/plugins/types.ts" } }, @@ -3947,7 +3965,7 @@ "exportName": "OpenClawPluginDefinition", "kind": "type", "source": { - "line": 1296, + "line": 1318, "path": "src/plugins/types.ts" } }, diff --git a/docs/.generated/plugin-sdk-api-baseline.jsonl b/docs/.generated/plugin-sdk-api-baseline.jsonl index f4ac7c006b0..e970df4a770 100644 --- a/docs/.generated/plugin-sdk-api-baseline.jsonl +++ b/docs/.generated/plugin-sdk-api-baseline.jsonl @@ -27,6 +27,8 @@ {"declaration":"export type ChannelSetupWizardAllowFromEntry = ChannelSetupWizardAllowFromEntry;","entrypoint":"index","exportName":"ChannelSetupWizardAllowFromEntry","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":154,"sourcePath":"src/channels/plugins/setup-wizard.ts"} {"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"index","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":100,"sourcePath":"src/channels/plugins/types.core.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"ClawdbotConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} +{"declaration":"export type CliBackendConfig = CliBackendConfig;","entrypoint":"index","exportName":"CliBackendConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/config/types.agent-defaults.ts"} +{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1292,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type CompiledConfiguredBinding = CompiledConfiguredBinding;","entrypoint":"index","exportName":"CompiledConfiguredBinding","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":38,"sourcePath":"src/channels/plugins/binding-types.ts"} {"declaration":"export type ConfiguredBindingConversation = ConversationRef;","entrypoint":"index","exportName":"ConfiguredBindingConversation","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/binding-types.ts"} {"declaration":"export type ConfiguredBindingResolution = ConfiguredBindingResolution;","entrypoint":"index","exportName":"ConfiguredBindingResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":49,"sourcePath":"src/channels/plugins/binding-types.ts"} @@ -44,7 +46,7 @@ {"declaration":"export type ImageGenerationSourceImage = ImageGenerationSourceImage;","entrypoint":"index","exportName":"ImageGenerationSourceImage","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":14,"sourcePath":"src/image-generation/types.ts"} {"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} -{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1336,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"index","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"} @@ -375,10 +377,10 @@ {"declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":112,"sourcePath":"src/gateway/server-methods/types.ts"} {"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} -{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1336,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1296,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1318,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":103,"sourcePath":"src/plugins/types.ts"} @@ -431,10 +433,10 @@ {"declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"plugin-entry","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/agents/tools/common.ts"} {"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"} -{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1336,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"} -{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1296,"sourcePath":"src/plugins/types.ts"} +{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1318,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"} {"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":966,"sourcePath":"src/plugins/types.ts"} diff --git a/docs/cli/gateway.md b/docs/cli/gateway.md index d79bb2d4b08..028260efee1 100644 --- a/docs/cli/gateway.md +++ b/docs/cli/gateway.md @@ -55,7 +55,8 @@ Notes: - `--reset`: reset dev config + credentials + sessions + workspace (requires `--dev`). - `--force`: kill any existing listener on the selected port before starting. - `--verbose`: verbose logs. -- `--claude-cli-logs`: only show claude-cli logs in the console (and enable its stdout/stderr). +- `--cli-backend-logs`: only show CLI backend logs in the console (and enable stdout/stderr). +- `--claude-cli-logs`: deprecated alias for `--cli-backend-logs`. - `--ws-log `: websocket log style (default `auto`). - `--compact`: alias for `--ws-log compact`. - `--raw-stream`: log raw model stream events to jsonl. diff --git a/docs/cli/index.md b/docs/cli/index.md index 0eee66d1f35..78d7c8b5ac0 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -780,7 +780,8 @@ Options: - `--reset` (reset dev config + credentials + sessions + workspace) - `--force` (kill existing listener on port) - `--verbose` -- `--claude-cli-logs` +- `--cli-backend-logs` +- `--claude-cli-logs` (deprecated alias) - `--ws-log ` - `--compact` (alias for `--ws-log compact`) - `--raw-stream` diff --git a/docs/gateway/cli-backends.md b/docs/gateway/cli-backends.md index f76a6046b60..37defaa0678 100644 --- a/docs/gateway/cli-backends.md +++ b/docs/gateway/cli-backends.md @@ -22,13 +22,14 @@ want “always works” text responses without relying on external APIs. ## Beginner-friendly quick start -You can use Claude Code CLI **without any config** (OpenClaw ships a built-in default): +You can use Claude Code CLI **without any config** (the bundled Anthropic plugin +registers a default backend): ```bash openclaw agent --message "hi" --model claude-cli/opus-4.6 ``` -Codex CLI also works out of the box: +Codex CLI also works out of the box (via the bundled OpenAI plugin): ```bash openclaw agent --message "hi" --model codex-cli/gpt-5.4 @@ -180,9 +181,9 @@ Input modes: - `input: "stdin"` sends the prompt via stdin. - If the prompt is very long and `maxPromptArgChars` is set, stdin is used. -## Defaults (built-in) +## Defaults (plugin-owned) -OpenClaw ships a default for `claude-cli`: +The bundled Anthropic plugin registers a default for `claude-cli`: - `command: "claude"` - `args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"]` @@ -193,19 +194,38 @@ OpenClaw ships a default for `claude-cli`: - `systemPromptWhen: "first"` - `sessionMode: "always"` -OpenClaw also ships a default for `codex-cli`: +The bundled OpenAI plugin also registers a default for `codex-cli`: - `command: "codex"` -- `args: ["exec","--json","--color","never","--sandbox","read-only","--skip-git-repo-check"]` -- `resumeArgs: ["exec","resume","{sessionId}","--color","never","--sandbox","read-only","--skip-git-repo-check"]` +- `args: ["exec","--json","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]` +- `resumeArgs: ["exec","resume","{sessionId}","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]` - `output: "jsonl"` - `resumeOutput: "text"` - `modelArg: "--model"` - `imageArg: "--image"` - `sessionMode: "existing"` +The bundled Google plugin also registers a default for `google-gemini-cli`: + +- `command: "gemini"` +- `args: ["--prompt", "--output-format", "json"]` +- `resumeArgs: ["--resume", "{sessionId}", "--prompt", "--output-format", "json"]` +- `modelArg: "--model"` +- `sessionMode: "existing"` +- `sessionIdFields: ["session_id", "sessionId"]` + Override only if needed (common: absolute `command` path). +## Plugin-owned defaults + +CLI backend defaults are now part of the plugin surface: + +- Plugins register them with `api.registerCliBackend(...)`. +- The backend `id` becomes the provider prefix in model refs. +- User config in `agents.defaults.cliBackends.` still overrides the plugin default. +- Backend-specific config cleanup stays plugin-owned through the optional + `normalizeConfig` hook. + ## Limitations - **No OpenClaw tools** (the CLI backend never receives tool calls). Some CLIs diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 94afe6c5846..0d6131af2fb 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -27,14 +27,15 @@ This page covers the internal architecture of the OpenClaw plugin system. Capabilities are the public **native plugin** model inside OpenClaw. Every native OpenClaw plugin registers against one or more capability types: -| Capability | Registration method | Example plugins | -| ------------------- | --------------------------------------------- | ------------------------- | -| Text inference | `api.registerProvider(...)` | `openai`, `anthropic` | -| Speech | `api.registerSpeechProvider(...)` | `elevenlabs`, `microsoft` | -| Media understanding | `api.registerMediaUnderstandingProvider(...)` | `openai`, `google` | -| Image generation | `api.registerImageGenerationProvider(...)` | `openai`, `google` | -| Web search | `api.registerWebSearchProvider(...)` | `google` | -| Channel / messaging | `api.registerChannel(...)` | `msteams`, `matrix` | +| Capability | Registration method | Example plugins | +| --------------------- | --------------------------------------------- | ------------------------- | +| Text inference | `api.registerProvider(...)` | `openai`, `anthropic` | +| CLI inference backend | `api.registerCliBackend(...)` | `openai`, `anthropic` | +| Speech | `api.registerSpeechProvider(...)` | `elevenlabs`, `microsoft` | +| Media understanding | `api.registerMediaUnderstandingProvider(...)` | `openai`, `google` | +| Image generation | `api.registerImageGenerationProvider(...)` | `openai`, `google` | +| Web search | `api.registerWebSearchProvider(...)` | `google` | +| Channel / messaging | `api.registerChannel(...)` | `msteams`, `matrix` | A plugin that registers zero capabilities but provides hooks, tools, or services is a **legacy hook-only** plugin. That pattern is still fully supported. diff --git a/docs/plugins/building-plugins.md b/docs/plugins/building-plugins.md index aee08d067b4..fe896bda4f7 100644 --- a/docs/plugins/building-plugins.md +++ b/docs/plugins/building-plugins.md @@ -128,19 +128,20 @@ and provider plugins have dedicated guides linked above. A single plugin can register any number of capabilities via the `api` object: -| Capability | Registration method | Detailed guide | -| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------- | -| Text inference (LLM) | `api.registerProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins) | -| Channel / messaging | `api.registerChannel(...)` | [Channel Plugins](/plugins/sdk-channel-plugins) | -| Speech (TTS/STT) | `api.registerSpeechProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | -| Media understanding | `api.registerMediaUnderstandingProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | -| Image generation | `api.registerImageGenerationProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | -| Web search | `api.registerWebSearchProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | -| Agent tools | `api.registerTool(...)` | Below | -| Custom commands | `api.registerCommand(...)` | [Entry Points](/plugins/sdk-entrypoints) | -| Event hooks | `api.registerHook(...)` | [Entry Points](/plugins/sdk-entrypoints) | -| HTTP routes | `api.registerHttpRoute(...)` | [Internals](/plugins/architecture#gateway-http-routes) | -| CLI subcommands | `api.registerCli(...)` | [Entry Points](/plugins/sdk-entrypoints) | +| Capability | Registration method | Detailed guide | +| --------------------- | --------------------------------------------- | ------------------------------------------------------------------------------- | +| Text inference (LLM) | `api.registerProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins) | +| CLI inference backend | `api.registerCliBackend(...)` | [CLI Backends](/gateway/cli-backends) | +| Channel / messaging | `api.registerChannel(...)` | [Channel Plugins](/plugins/sdk-channel-plugins) | +| Speech (TTS/STT) | `api.registerSpeechProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | +| Media understanding | `api.registerMediaUnderstandingProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | +| Image generation | `api.registerImageGenerationProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | +| Web search | `api.registerWebSearchProvider(...)` | [Provider Plugins](/plugins/sdk-provider-plugins#step-5-add-extra-capabilities) | +| Agent tools | `api.registerTool(...)` | Below | +| Custom commands | `api.registerCommand(...)` | [Entry Points](/plugins/sdk-entrypoints) | +| Event hooks | `api.registerHook(...)` | [Entry Points](/plugins/sdk-entrypoints) | +| HTTP routes | `api.registerHttpRoute(...)` | [Internals](/plugins/architecture#gateway-http-routes) | +| CLI subcommands | `api.registerCli(...)` | [Entry Points](/plugins/sdk-entrypoints) | For the full registration API, see [SDK Overview](/plugins/sdk-overview#registration-api). diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index eda2085cc50..08836a87bd9 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -66,6 +66,7 @@ subpaths is in `scripts/lib/plugin-sdk-entrypoints.json`. | Subpath | Key exports | | --- | --- | + | `plugin-sdk/cli-backend` | CLI backend defaults + watchdog constants | | `plugin-sdk/provider-auth` | `createProviderApiKeyAuthMethod`, `ensureApiKeyFromOptionEnvOrPrompt`, `upsertAuthProfile` | | `plugin-sdk/provider-models` | `normalizeModelCompat` | | `plugin-sdk/provider-catalog` | Catalog type re-exports | @@ -114,6 +115,7 @@ methods: | Method | What it registers | | --------------------------------------------- | ------------------------------ | | `api.registerProvider(...)` | Text inference (LLM) | +| `api.registerCliBackend(...)` | Local CLI inference backend | | `api.registerChannel(...)` | Messaging channel | | `api.registerSpeechProvider(...)` | Text-to-speech / STT synthesis | | `api.registerMediaUnderstandingProvider(...)` | Image/audio/video analysis | @@ -138,6 +140,18 @@ methods: | `api.registerService(service)` | Background service | | `api.registerInteractiveHandler(registration)` | Interactive handler | +### CLI backend registration + +`api.registerCliBackend(...)` lets a plugin own the default config for a local +AI CLI backend such as `claude-cli` or `codex-cli`. + +- The backend `id` becomes the provider prefix in model refs like `claude-cli/opus`. +- The backend `config` uses the same shape as `agents.defaults.cliBackends.`. +- User config still wins. OpenClaw merges `agents.defaults.cliBackends.` over the + plugin default before running the CLI. +- Use `normalizeConfig` when a backend needs compatibility rewrites after merge + (for example normalizing old flag shapes). + ### Exclusive slots | Method | What it registers | diff --git a/extensions/anthropic/cli-backend.ts b/extensions/anthropic/cli-backend.ts new file mode 100644 index 00000000000..a8542a96864 --- /dev/null +++ b/extensions/anthropic/cli-backend.ts @@ -0,0 +1,112 @@ +import type { CliBackendPlugin, CliBackendConfig } from "openclaw/plugin-sdk/cli-backend"; +import { + CLI_FRESH_WATCHDOG_DEFAULTS, + CLI_RESUME_WATCHDOG_DEFAULTS, +} from "openclaw/plugin-sdk/cli-backend"; + +const CLAUDE_MODEL_ALIASES: Record = { + opus: "opus", + "opus-4.6": "opus", + "opus-4.5": "opus", + "opus-4": "opus", + "claude-opus-4-6": "opus", + "claude-opus-4-5": "opus", + "claude-opus-4": "opus", + sonnet: "sonnet", + "sonnet-4.6": "sonnet", + "sonnet-4.5": "sonnet", + "sonnet-4.1": "sonnet", + "sonnet-4.0": "sonnet", + "claude-sonnet-4-6": "sonnet", + "claude-sonnet-4-5": "sonnet", + "claude-sonnet-4-1": "sonnet", + "claude-sonnet-4-0": "sonnet", + haiku: "haiku", + "haiku-3.5": "haiku", + "claude-haiku-3-5": "haiku", +}; + +const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions"; +const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode"; +const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions"; + +function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined { + if (!args) { + return args; + } + const normalized: string[] = []; + let sawLegacySkip = false; + let hasPermissionMode = false; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) { + sawLegacySkip = true; + continue; + } + if (arg === CLAUDE_PERMISSION_MODE_ARG) { + hasPermissionMode = true; + normalized.push(arg); + const maybeValue = args[i + 1]; + if (typeof maybeValue === "string") { + normalized.push(maybeValue); + i += 1; + } + continue; + } + if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) { + hasPermissionMode = true; + } + normalized.push(arg); + } + if (sawLegacySkip && !hasPermissionMode) { + normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE); + } + return normalized; +} + +function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig { + return { + ...config, + args: normalizeClaudePermissionArgs(config.args), + resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs), + }; +} + +export function buildAnthropicCliBackend(): CliBackendPlugin { + return { + id: "claude-cli", + bundleMcp: true, + config: { + command: "claude", + args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"], + resumeArgs: [ + "-p", + "--output-format", + "json", + "--permission-mode", + "bypassPermissions", + "--resume", + "{sessionId}", + ], + output: "json", + input: "arg", + modelArg: "--model", + modelAliases: CLAUDE_MODEL_ALIASES, + sessionArg: "--session-id", + sessionMode: "always", + sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"], + systemPromptArg: "--append-system-prompt", + systemPromptMode: "append", + systemPromptWhen: "first", + clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"], + reliability: { + watchdog: { + fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, + resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, + }, + }, + serialize: true, + }, + normalizeConfig: normalizeClaudeBackendConfig, + }; +} diff --git a/extensions/anthropic/index.ts b/extensions/anthropic/index.ts index 4a499ced761..06724dc8981 100644 --- a/extensions/anthropic/index.ts +++ b/extensions/anthropic/index.ts @@ -27,6 +27,7 @@ import { } from "openclaw/plugin-sdk/provider-auth"; import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-models"; import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage"; +import { buildAnthropicCliBackend } from "./cli-backend.js"; import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js"; const PROVIDER_ID = "anthropic"; @@ -316,6 +317,7 @@ export default definePluginEntry({ name: "Anthropic Provider", description: "Bundled Anthropic provider plugin", register(api) { + api.registerCliBackend(buildAnthropicCliBackend()); api.registerProvider({ id: PROVIDER_ID, label: "Anthropic", diff --git a/extensions/google/cli-backend.ts b/extensions/google/cli-backend.ts new file mode 100644 index 00000000000..d6ed1ec9b5f --- /dev/null +++ b/extensions/google/cli-backend.ts @@ -0,0 +1,35 @@ +import type { CliBackendPlugin } from "openclaw/plugin-sdk/cli-backend"; +import { + CLI_FRESH_WATCHDOG_DEFAULTS, + CLI_RESUME_WATCHDOG_DEFAULTS, +} from "openclaw/plugin-sdk/cli-backend"; + +const GEMINI_MODEL_ALIASES: Record = { + pro: "gemini-3.1-pro-preview", + flash: "gemini-3.1-flash-preview", + "flash-lite": "gemini-3.1-flash-lite-preview", +}; + +export function buildGoogleGeminiCliBackend(): CliBackendPlugin { + return { + id: "google-gemini-cli", + config: { + command: "gemini", + args: ["--prompt", "--output-format", "json"], + resumeArgs: ["--resume", "{sessionId}", "--prompt", "--output-format", "json"], + output: "json", + input: "arg", + modelArg: "--model", + modelAliases: GEMINI_MODEL_ALIASES, + sessionMode: "existing", + sessionIdFields: ["session_id", "sessionId"], + reliability: { + watchdog: { + fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, + resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, + }, + }, + serialize: true, + }, + }; +} diff --git a/extensions/google/index.ts b/extensions/google/index.ts index eb578942a9f..ef88a5e8ddf 100644 --- a/extensions/google/index.ts +++ b/extensions/google/index.ts @@ -5,6 +5,7 @@ import { applyGoogleGeminiModelDefault, } from "openclaw/plugin-sdk/provider-models"; import { createGoogleThinkingPayloadWrapper } from "openclaw/plugin-sdk/provider-stream"; +import { buildGoogleGeminiCliBackend } from "./cli-backend.js"; import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js"; import { buildGoogleImageGenerationProvider } from "./image-generation-provider.js"; import { googleMediaUnderstandingProvider } from "./media-understanding-provider.js"; @@ -48,6 +49,7 @@ export default definePluginEntry({ wrapStreamFn: (ctx) => createGoogleThinkingPayloadWrapper(ctx.streamFn, ctx.thinkingLevel), isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId), }); + api.registerCliBackend(buildGoogleGeminiCliBackend()); registerGoogleGeminiCliProvider(api); api.registerImageGenerationProvider(buildGoogleImageGenerationProvider()); api.registerMediaUnderstandingProvider(googleMediaUnderstandingProvider); diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index ff78b3728d3..7acfb015865 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -44,6 +44,7 @@ function fakeApi(overrides: Partial = {}): OpenClawPluginApi registerGatewayMethod() {}, registerCli() {}, registerService() {}, + registerCliBackend() {}, registerProvider() {}, registerSpeechProvider() {}, registerMediaUnderstandingProvider() {}, diff --git a/extensions/openai/cli-backend.ts b/extensions/openai/cli-backend.ts new file mode 100644 index 00000000000..4f21e0e48af --- /dev/null +++ b/extensions/openai/cli-backend.ts @@ -0,0 +1,48 @@ +import type { CliBackendPlugin } from "openclaw/plugin-sdk/cli-backend"; +import { + CLI_FRESH_WATCHDOG_DEFAULTS, + CLI_RESUME_WATCHDOG_DEFAULTS, +} from "openclaw/plugin-sdk/cli-backend"; + +export function buildOpenAICodexCliBackend(): CliBackendPlugin { + return { + id: "codex-cli", + config: { + command: "codex", + args: [ + "exec", + "--json", + "--color", + "never", + "--sandbox", + "workspace-write", + "--skip-git-repo-check", + ], + resumeArgs: [ + "exec", + "resume", + "{sessionId}", + "--color", + "never", + "--sandbox", + "workspace-write", + "--skip-git-repo-check", + ], + output: "jsonl", + resumeOutput: "text", + input: "arg", + modelArg: "--model", + sessionIdFields: ["thread_id"], + sessionMode: "existing", + imageArg: "--image", + imageMode: "repeat", + reliability: { + watchdog: { + fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, + resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, + }, + }, + serialize: true, + }, + }; +} diff --git a/extensions/openai/index.ts b/extensions/openai/index.ts index fb048d29243..2be15ff5b60 100644 --- a/extensions/openai/index.ts +++ b/extensions/openai/index.ts @@ -1,4 +1,5 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { buildOpenAICodexCliBackend } from "./cli-backend.js"; import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js"; import { openaiCodexMediaUnderstandingProvider, @@ -13,6 +14,7 @@ export default definePluginEntry({ name: "OpenAI Provider", description: "Bundled OpenAI provider plugins", register(api) { + api.registerCliBackend(buildOpenAICodexCliBackend()); api.registerProvider(buildOpenAIProvider()); api.registerProvider(buildOpenAICodexProviderPlugin()); api.registerSpeechProvider(buildOpenAISpeechProvider()); diff --git a/package.json b/package.json index 74f14c75df2..2fffbc15dd0 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,10 @@ "types": "./dist/plugin-sdk/cli-runtime.d.ts", "default": "./dist/plugin-sdk/cli-runtime.js" }, + "./plugin-sdk/cli-backend": { + "types": "./dist/plugin-sdk/cli-backend.d.ts", + "default": "./dist/plugin-sdk/cli-backend.js" + }, "./plugin-sdk/hook-runtime": { "types": "./dist/plugin-sdk/hook-runtime.d.ts", "default": "./dist/plugin-sdk/hook-runtime.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index f67c51a49a2..8144c4a9701 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -37,6 +37,7 @@ "gateway-runtime", "github-copilot-token", "cli-runtime", + "cli-backend", "hook-runtime", "process-runtime", "windows-spawn", diff --git a/src/agents/cli-backends.test.ts b/src/agents/cli-backends.test.ts index 6dde78797cb..e8b3f871066 100644 --- a/src/agents/cli-backends.test.ts +++ b/src/agents/cli-backends.test.ts @@ -1,7 +1,34 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; +import { buildAnthropicCliBackend } from "../../extensions/anthropic/cli-backend.js"; +import { buildGoogleGeminiCliBackend } from "../../extensions/google/cli-backend.js"; +import { buildOpenAICodexCliBackend } from "../../extensions/openai/cli-backend.js"; import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; import { resolveCliBackendConfig } from "./cli-backends.js"; +beforeEach(() => { + const registry = createEmptyPluginRegistry(); + registry.cliBackends = [ + { + pluginId: "anthropic", + backend: buildAnthropicCliBackend(), + source: "test", + }, + { + pluginId: "openai", + backend: buildOpenAICodexCliBackend(), + source: "test", + }, + { + pluginId: "google", + backend: buildGoogleGeminiCliBackend(), + source: "test", + }, + ]; + setActivePluginRegistry(registry); +}); + describe("resolveCliBackendConfig reliability merge", () => { it("defaults codex-cli to workspace-write for fresh and resume runs", () => { const resolved = resolveCliBackendConfig("codex-cli"); @@ -166,3 +193,24 @@ describe("resolveCliBackendConfig claude-cli defaults", () => { expect(resolved?.config.resumeArgs).not.toContain("bypassPermissions"); }); }); + +describe("resolveCliBackendConfig google-gemini-cli defaults", () => { + it("uses Gemini CLI json args and existing-session resume mode", () => { + const resolved = resolveCliBackendConfig("google-gemini-cli"); + + expect(resolved).not.toBeNull(); + expect(resolved?.bundleMcp).toBe(false); + expect(resolved?.config.args).toEqual(["--prompt", "--output-format", "json"]); + expect(resolved?.config.resumeArgs).toEqual([ + "--resume", + "{sessionId}", + "--prompt", + "--output-format", + "json", + ]); + expect(resolved?.config.modelArg).toBe("--model"); + expect(resolved?.config.sessionMode).toBe("existing"); + expect(resolved?.config.sessionIdFields).toEqual(["session_id", "sessionId"]); + expect(resolved?.config.modelAliases?.pro).toBe("gemini-3.1-pro-preview"); + }); +}); diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index 1b19c4a5087..cce4829851a 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -1,110 +1,13 @@ import type { OpenClawConfig } from "../config/config.js"; import type { CliBackendConfig } from "../config/types.js"; -import { - CLI_FRESH_WATCHDOG_DEFAULTS, - CLI_RESUME_WATCHDOG_DEFAULTS, -} from "./cli-watchdog-defaults.js"; +import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js"; import { normalizeProviderId } from "./model-selection.js"; export type ResolvedCliBackend = { id: string; config: CliBackendConfig; -}; - -const CLAUDE_MODEL_ALIASES: Record = { - opus: "opus", - "opus-4.6": "opus", - "opus-4.5": "opus", - "opus-4": "opus", - "claude-opus-4-6": "opus", - "claude-opus-4-5": "opus", - "claude-opus-4": "opus", - sonnet: "sonnet", - "sonnet-4.6": "sonnet", - "sonnet-4.5": "sonnet", - "sonnet-4.1": "sonnet", - "sonnet-4.0": "sonnet", - "claude-sonnet-4-6": "sonnet", - "claude-sonnet-4-5": "sonnet", - "claude-sonnet-4-1": "sonnet", - "claude-sonnet-4-0": "sonnet", - haiku: "haiku", - "haiku-3.5": "haiku", - "claude-haiku-3-5": "haiku", -}; - -const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions"; -const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode"; -const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions"; - -const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { - command: "claude", - args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"], - resumeArgs: [ - "-p", - "--output-format", - "json", - "--permission-mode", - "bypassPermissions", - "--resume", - "{sessionId}", - ], - output: "json", - input: "arg", - modelArg: "--model", - modelAliases: CLAUDE_MODEL_ALIASES, - sessionArg: "--session-id", - sessionMode: "always", - sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"], - systemPromptArg: "--append-system-prompt", - systemPromptMode: "append", - systemPromptWhen: "first", - clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"], - reliability: { - watchdog: { - fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, - resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, - }, - }, - serialize: true, -}; - -const DEFAULT_CODEX_BACKEND: CliBackendConfig = { - command: "codex", - args: [ - "exec", - "--json", - "--color", - "never", - "--sandbox", - "workspace-write", - "--skip-git-repo-check", - ], - resumeArgs: [ - "exec", - "resume", - "{sessionId}", - "--color", - "never", - "--sandbox", - "workspace-write", - "--skip-git-repo-check", - ], - output: "jsonl", - resumeOutput: "text", - input: "arg", - modelArg: "--model", - sessionIdFields: ["thread_id"], - sessionMode: "existing", - imageArg: "--image", - imageMode: "repeat", - reliability: { - watchdog: { - fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, - resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, - }, - }, - serialize: true, + bundleMcp: boolean; + pluginId?: string; }; function normalizeBackendKey(key: string): string { @@ -123,6 +26,11 @@ function pickBackendConfig( return undefined; } +function resolveRegisteredBackend(provider: string) { + const normalized = normalizeBackendKey(provider); + return resolveRuntimeCliBackends().find((entry) => normalizeBackendKey(entry.id) === normalized); +} + function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig { if (!override) { return { ...base }; @@ -160,53 +68,11 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig) }; } -function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined { - if (!args) { - return args; - } - const normalized: string[] = []; - let sawLegacySkip = false; - let hasPermissionMode = false; - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; - if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) { - sawLegacySkip = true; - continue; - } - if (arg === CLAUDE_PERMISSION_MODE_ARG) { - hasPermissionMode = true; - normalized.push(arg); - const maybeValue = args[i + 1]; - if (typeof maybeValue === "string") { - normalized.push(maybeValue); - i += 1; - } - continue; - } - if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) { - hasPermissionMode = true; - } - normalized.push(arg); - } - if (sawLegacySkip && !hasPermissionMode) { - normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE); - } - return normalized; -} - -function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig { - return { - ...config, - args: normalizeClaudePermissionArgs(config.args), - resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs), - }; -} - export function resolveCliBackendIds(cfg?: OpenClawConfig): Set { - const ids = new Set([ - normalizeBackendKey("claude-cli"), - normalizeBackendKey("codex-cli"), - ]); + const ids = new Set(); + for (const backend of resolveRuntimeCliBackends()) { + ids.add(normalizeBackendKey(backend.id)); + } const configured = cfg?.agents?.defaults?.cliBackends ?? {}; for (const key of Object.keys(configured)) { ids.add(normalizeBackendKey(key)); @@ -221,23 +87,20 @@ export function resolveCliBackendConfig( const normalized = normalizeBackendKey(provider); const configured = cfg?.agents?.defaults?.cliBackends ?? {}; const override = pickBackendConfig(configured, normalized); - - if (normalized === "claude-cli") { - const merged = mergeBackendConfig(DEFAULT_CLAUDE_BACKEND, override); - const config = normalizeClaudeBackendConfig(merged); + const registered = resolveRegisteredBackend(normalized); + if (registered) { + const merged = mergeBackendConfig(registered.config, override); + const config = registered.normalizeConfig ? registered.normalizeConfig(merged) : merged; const command = config.command?.trim(); if (!command) { return null; } - return { id: normalized, config: { ...config, command } }; - } - if (normalized === "codex-cli") { - const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override); - const command = merged.command?.trim(); - if (!command) { - return null; - } - return { id: normalized, config: { ...merged, command } }; + return { + id: normalized, + config: { ...config, command }, + bundleMcp: registered.bundleMcp === true, + pluginId: registered.pluginId, + }; } if (!override) { @@ -247,5 +110,5 @@ export function resolveCliBackendConfig( if (!command) { return null; } - return { id: normalized, config: { ...override, command } }; + return { id: normalized, config: { ...override, command }, bundleMcp: false }; } diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index fc83c34907a..5b7d30352f2 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -50,7 +50,9 @@ import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js"; import { buildSystemPromptReport } from "./system-prompt-report.js"; import { redactRunIdentifier, resolveRunWorkspaceDir } from "./workspace-run.js"; -const log = createSubsystemLogger("agent/claude-cli"); +const log = createSubsystemLogger("agent/cli-backend"); +const CLI_BACKEND_LOG_OUTPUT_ENV = "OPENCLAW_CLI_BACKEND_LOG_OUTPUT"; +const LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV = "OPENCLAW_CLAUDE_CLI_LOG_OUTPUT"; export async function runCliAgent(params: { sessionId: string; @@ -97,7 +99,7 @@ export async function runCliAgent(params: { throw new Error(`Unknown CLI backend: ${params.provider}`); } const preparedBackend = await prepareCliBundleMcpConfig({ - backendId: backendResolved.id, + enabled: backendResolved.bundleMcp, backend: backendResolved.config, workspaceDir, config: params.config, @@ -264,7 +266,9 @@ export async function runCliAgent(params: { log.info( `cli exec: provider=${params.provider} model=${normalizedModel} promptChars=${params.prompt.length}`, ); - const logOutputText = isTruthyEnvValue(process.env.OPENCLAW_CLAUDE_CLI_LOG_OUTPUT); + const logOutputText = + isTruthyEnvValue(process.env[CLI_BACKEND_LOG_OUTPUT_ENV]) || + isTruthyEnvValue(process.env[LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV]); if (logOutputText) { const logArgs: string[] = []; for (let i = 0; i < args.length; i += 1) { diff --git a/src/agents/cli-runner/bundle-mcp.test.ts b/src/agents/cli-runner/bundle-mcp.test.ts index fae294ab951..5446b884c85 100644 --- a/src/agents/cli-runner/bundle-mcp.test.ts +++ b/src/agents/cli-runner/bundle-mcp.test.ts @@ -15,7 +15,7 @@ afterEach(async () => { }); describe("prepareCliBundleMcpConfig", () => { - it("injects a merged --mcp-config overlay for claude-cli", async () => { + it("injects a merged --mcp-config overlay for bundle-MCP-enabled backends", async () => { const env = captureEnv(["HOME"]); try { const homeDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-home-"); @@ -33,7 +33,7 @@ describe("prepareCliBundleMcpConfig", () => { }; const prepared = await prepareCliBundleMcpConfig({ - backendId: "claude-cli", + enabled: true, backend: { command: "node", args: ["./fake-claude.mjs"], @@ -57,4 +57,18 @@ describe("prepareCliBundleMcpConfig", () => { env.restore(); } }); + + it("leaves args untouched when bundle MCP is disabled", async () => { + const prepared = await prepareCliBundleMcpConfig({ + enabled: false, + backend: { + command: "node", + args: ["./fake-cli.mjs"], + }, + workspaceDir: "/tmp/openclaw-bundle-mcp-disabled", + }); + + expect(prepared.backend.args).toEqual(["./fake-cli.mjs"]); + expect(prepared.cleanup).toBeUndefined(); + }); }); diff --git a/src/agents/cli-runner/bundle-mcp.ts b/src/agents/cli-runner/bundle-mcp.ts index 96aeb867869..f6eae9ae059 100644 --- a/src/agents/cli-runner/bundle-mcp.ts +++ b/src/agents/cli-runner/bundle-mcp.ts @@ -63,13 +63,13 @@ function injectMcpConfigArgs(args: string[] | undefined, mcpConfigPath: string): } export async function prepareCliBundleMcpConfig(params: { - backendId: string; + enabled: boolean; backend: CliBackendConfig; workspaceDir: string; config?: OpenClawConfig; warn?: (message: string) => void; }): Promise { - if (params.backendId !== "claude-cli") { + if (!params.enabled) { return { backend: params.backend }; } diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 48f3af0de78..d9b487b77b0 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -6,6 +6,7 @@ import { toAgentModelListLike, } from "../config/model-input.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js"; import { sanitizeForLog } from "../terminal/ansi.js"; import { resolveAgentConfig, @@ -81,10 +82,9 @@ export { export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean { const normalized = normalizeProviderId(provider); - if (normalized === "claude-cli") { - return true; - } - if (normalized === "codex-cli") { + if ( + resolveRuntimeCliBackends().some((backend) => normalizeProviderId(backend.id) === normalized) + ) { return true; } const backends = cfg?.agents?.defaults?.cliBackends ?? {}; diff --git a/src/cli/gateway-cli/run.option-collisions.test.ts b/src/cli/gateway-cli/run.option-collisions.test.ts index 165d94902aa..874f7a5cd5e 100644 --- a/src/cli/gateway-cli/run.option-collisions.test.ts +++ b/src/cli/gateway-cli/run.option-collisions.test.ts @@ -8,6 +8,7 @@ const startGatewayServer = vi.fn(async (_port: number, _opts?: unknown) => ({ })); const setGatewayWsLogStyle = vi.fn((_style: string) => undefined); const setVerbose = vi.fn((_enabled: boolean) => undefined); +const setConsoleSubsystemFilter = vi.fn((_filters: string[]) => undefined); const forceFreePortAndWait = vi.fn(async (_port: number, _opts: unknown) => ({ killed: [], waitedMs: 0, @@ -81,7 +82,7 @@ vi.mock("../../infra/ports.js", () => ({ })); vi.mock("../../logging/console.js", () => ({ - setConsoleSubsystemFilter: () => undefined, + setConsoleSubsystemFilter: (filters: string[]) => setConsoleSubsystemFilter(filters), setConsoleTimestampPrefix: () => undefined, })); @@ -133,6 +134,7 @@ describe("gateway run option collisions", () => { startGatewayServer.mockClear(); setGatewayWsLogStyle.mockClear(); setVerbose.mockClear(); + setConsoleSubsystemFilter.mockClear(); forceFreePortAndWait.mockClear(); waitForPortBindable.mockClear(); ensureDevGatewayConfig.mockClear(); @@ -182,6 +184,18 @@ describe("gateway run option collisions", () => { ); }); + it.each([ + ["--cli-backend-logs", "generic flag"], + ["--claude-cli-logs", "deprecated alias"], + ])("enables CLI backend log filtering via %s (%s)", async (flag) => { + delete process.env.OPENCLAW_CLI_BACKEND_LOG_OUTPUT; + + await runGatewayCli(["gateway", "run", flag, "--allow-unconfigured"]); + + expect(setConsoleSubsystemFilter).toHaveBeenCalledWith(["agent/cli-backend"]); + expect(process.env.OPENCLAW_CLI_BACKEND_LOG_OUTPUT).toBe("1"); + }); + it("starts gateway when token mode has no configured token (startup bootstrap path)", async () => { await runGatewayCli(["gateway", "run", "--allow-unconfigured"]); diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index e814cfd60e3..c6c6dde43b4 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -48,6 +48,7 @@ type GatewayRunOpts = { allowUnconfigured?: boolean; force?: boolean; verbose?: boolean; + cliBackendLogs?: boolean; claudeCliLogs?: boolean; wsLog?: unknown; compact?: boolean; @@ -78,6 +79,7 @@ const GATEWAY_RUN_BOOLEAN_KEYS = [ "reset", "force", "verbose", + "cliBackendLogs", "claudeCliLogs", "compact", "rawStream", @@ -171,9 +173,9 @@ async function runGatewayCommand(opts: GatewayRunOpts) { setConsoleTimestampPrefix(true); setVerbose(Boolean(opts.verbose)); - if (opts.claudeCliLogs) { - setConsoleSubsystemFilter(["agent/claude-cli"]); - process.env.OPENCLAW_CLAUDE_CLI_LOG_OUTPUT = "1"; + if (opts.cliBackendLogs || opts.claudeCliLogs) { + setConsoleSubsystemFilter(["agent/cli-backend"]); + process.env.OPENCLAW_CLI_BACKEND_LOG_OUTPUT = "1"; } const wsLogRaw = (opts.compact ? "compact" : opts.wsLog) as string | undefined; const wsLogStyle: GatewayWsLogStyle = @@ -519,10 +521,11 @@ export function addGatewayRunCommand(cmd: Command): Command { .option("--force", "Kill any existing listener on the target port before starting", false) .option("--verbose", "Verbose logging to stdout/stderr", false) .option( - "--claude-cli-logs", - "Only show claude-cli logs in the console (includes stdout/stderr)", + "--cli-backend-logs", + "Only show CLI backend logs in the console (includes stdout/stderr)", false, ) + .option("--claude-cli-logs", "Deprecated alias for --cli-backend-logs", false) .option("--ws-log