From 0af808b457ec71e6a4791a85597deb1d4cd9665d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 09:05:05 +0100 Subject: [PATCH] test: add cli backend live matrix metadata --- docs/help/testing.md | 18 +++- extensions/anthropic/cli-backend.ts | 9 ++ extensions/google/setup-api.ts | 11 +- extensions/openai/cli-backend.ts | 21 ++-- extensions/openai/setup-api.ts | 11 ++ package.json | 9 ++ scripts/print-cli-backend-live-metadata.ts | 64 +++++++++++ scripts/test-live-cli-backend-docker.sh | 42 ++++++-- src/agents/cli-backends.ts | 24 +++++ .../gateway-cli-backend.live-helpers.ts | 47 +------- src/gateway/gateway-cli-backend.live.test.ts | 36 +++---- src/plugins/setup-registry.ts | 100 +++++++++++++++++- src/plugins/types.ts | 14 +++ 13 files changed, 300 insertions(+), 106 deletions(-) create mode 100644 extensions/openai/setup-api.ts create mode 100644 scripts/print-cli-backend-live-metadata.ts diff --git a/docs/help/testing.md b/docs/help/testing.md index 3c19481acd3..d22cddca39f 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -253,17 +253,17 @@ openclaw models list openclaw models list --json ``` -## Live: CLI backend smoke (Codex CLI or other local CLIs) +## Live: CLI backend smoke (Claude, Codex, Gemini, or other local CLIs) - Test: `src/gateway/gateway-cli-backend.live.test.ts` - Goal: validate the Gateway + agent pipeline using a local CLI backend, without touching your default config. +- Backend-specific smoke defaults live with the owning extension's `cli-backend.ts` definition. - Enable: - `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly) - `OPENCLAW_LIVE_CLI_BACKEND=1` - Defaults: - - Model: `codex-cli/gpt-5.4` - - Command: `codex` - - Args: `["exec","--json","--color","never","--sandbox","read-only","--skip-git-repo-check"]` + - Default provider/model: `claude-cli/claude-sonnet-4-6` + - Command/args/image behavior come from the owning CLI backend plugin metadata. - Overrides (optional): - `OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.4"` - `OPENCLAW_LIVE_CLI_BACKEND_COMMAND="/full/path/to/codex"` @@ -287,11 +287,19 @@ Docker recipe: pnpm test:docker:live-cli-backend ``` +Single-provider Docker recipes: + +```bash +pnpm test:docker:live-cli-backend:claude +pnpm test:docker:live-cli-backend:codex +pnpm test:docker:live-cli-backend:gemini +``` + Notes: - The Docker runner lives at `scripts/test-live-cli-backend-docker.sh`. - It runs the live CLI-backend smoke inside the repo Docker image as the non-root `node` user. -- For `codex-cli`, it installs the Linux `@openai/codex` package into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`). +- It resolves CLI smoke metadata from the owning extension, then installs the matching Linux CLI package (`@anthropic-ai/claude-code`, `@openai/codex`, or `@google/gemini-cli`) into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`). ## Live: ACP bind smoke (`/acp spawn ... --bind here`) diff --git a/extensions/anthropic/cli-backend.ts b/extensions/anthropic/cli-backend.ts index cd648ad37a4..d2150bace11 100644 --- a/extensions/anthropic/cli-backend.ts +++ b/extensions/anthropic/cli-backend.ts @@ -5,6 +5,7 @@ import { } from "openclaw/plugin-sdk/cli-backend"; import { CLAUDE_CLI_BACKEND_ID, + CLAUDE_CLI_DEFAULT_MODEL_REF, CLAUDE_CLI_CLEAR_ENV, CLAUDE_CLI_HOST_MANAGED_ENV, CLAUDE_CLI_MODEL_ALIASES, @@ -15,6 +16,14 @@ import { export function buildAnthropicCliBackend(): CliBackendPlugin { return { id: CLAUDE_CLI_BACKEND_ID, + liveTest: { + defaultModelRef: CLAUDE_CLI_DEFAULT_MODEL_REF, + defaultImageProbe: true, + docker: { + npmPackage: "@anthropic-ai/claude-code", + binaryName: "claude", + }, + }, bundleMcp: true, config: { command: "claude", diff --git a/extensions/google/setup-api.ts b/extensions/google/setup-api.ts index 869eb280378..7af0a515594 100644 --- a/extensions/google/setup-api.ts +++ b/extensions/google/setup-api.ts @@ -1,18 +1,11 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { normalizeGoogleProviderConfig } from "./api.js"; +import { buildGoogleGeminiCliBackend } from "./cli-backend.js"; export default definePluginEntry({ id: "google", name: "Google Setup", description: "Lightweight Google setup hooks", register(api) { - api.registerProvider({ - id: "google", - label: "Google AI Studio", - hookAliases: ["google-antigravity", "google-vertex"], - auth: [], - normalizeConfig: ({ provider, providerConfig }) => - normalizeGoogleProviderConfig(provider, providerConfig), - }); + api.registerCliBackend(buildGoogleGeminiCliBackend()); }, }); diff --git a/extensions/openai/cli-backend.ts b/extensions/openai/cli-backend.ts index 4f21e0e48af..2a86a51121f 100644 --- a/extensions/openai/cli-backend.ts +++ b/extensions/openai/cli-backend.ts @@ -4,9 +4,19 @@ import { CLI_RESUME_WATCHDOG_DEFAULTS, } from "openclaw/plugin-sdk/cli-backend"; +const CODEX_CLI_DEFAULT_MODEL_REF = "codex-cli/gpt-5.4"; + export function buildOpenAICodexCliBackend(): CliBackendPlugin { return { id: "codex-cli", + liveTest: { + defaultModelRef: CODEX_CLI_DEFAULT_MODEL_REF, + defaultImageProbe: true, + docker: { + npmPackage: "@openai/codex", + binaryName: "codex", + }, + }, config: { command: "codex", args: [ @@ -18,16 +28,7 @@ export function buildOpenAICodexCliBackend(): CliBackendPlugin { "workspace-write", "--skip-git-repo-check", ], - resumeArgs: [ - "exec", - "resume", - "{sessionId}", - "--color", - "never", - "--sandbox", - "workspace-write", - "--skip-git-repo-check", - ], + resumeArgs: ["exec", "resume", "{sessionId}", "--dangerously-bypass-approvals-and-sandbox"], output: "jsonl", resumeOutput: "text", input: "arg", diff --git a/extensions/openai/setup-api.ts b/extensions/openai/setup-api.ts new file mode 100644 index 00000000000..4d41fff3771 --- /dev/null +++ b/extensions/openai/setup-api.ts @@ -0,0 +1,11 @@ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { buildOpenAICodexCliBackend } from "./cli-backend.js"; + +export default definePluginEntry({ + id: "openai", + name: "OpenAI Setup", + description: "Lightweight OpenAI setup hooks", + register(api) { + api.registerCliBackend(buildOpenAICodexCliBackend()); + }, +}); diff --git a/package.json b/package.json index 81197a7212e..79e14755d72 100644 --- a/package.json +++ b/package.json @@ -1183,8 +1183,17 @@ "test:docker:live-acp-bind:gemini": "OPENCLAW_LIVE_ACP_BIND_AGENT=gemini bash scripts/test-live-acp-bind-docker.sh", "test:docker:live-build": "bash scripts/test-live-build-docker.sh", "test:docker:live-cli-backend": "bash scripts/test-live-cli-backend-docker.sh", + "test:docker:live-cli-backend:claude": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6 bash scripts/test-live-cli-backend-docker.sh", + "test:docker:live-cli-backend:codex": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4 bash scripts/test-live-cli-backend-docker.sh", + "test:docker:live-cli-backend:gemini": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-cli-backend-docker.sh", "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh", + "test:docker:live-gateway:claude": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=claude-cli OPENCLAW_LIVE_GATEWAY_MODELS=claude-cli/claude-sonnet-4-6 bash scripts/test-live-gateway-models-docker.sh", + "test:docker:live-gateway:codex": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=codex-cli OPENCLAW_LIVE_GATEWAY_MODELS=codex-cli/gpt-5.4 bash scripts/test-live-gateway-models-docker.sh", + "test:docker:live-gateway:gemini": "OPENCLAW_LIVE_GATEWAY_PROVIDERS=google-gemini-cli OPENCLAW_LIVE_GATEWAY_MODELS=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-gateway-models-docker.sh", "test:docker:live-models": "bash scripts/test-live-models-docker.sh", + "test:docker:live-models:claude": "OPENCLAW_LIVE_PROVIDERS=claude-cli OPENCLAW_LIVE_MODELS=claude-cli/claude-sonnet-4-6 bash scripts/test-live-models-docker.sh", + "test:docker:live-models:codex": "OPENCLAW_LIVE_PROVIDERS=codex-cli OPENCLAW_LIVE_MODELS=codex-cli/gpt-5.4 bash scripts/test-live-models-docker.sh", + "test:docker:live-models:gemini": "OPENCLAW_LIVE_PROVIDERS=google-gemini-cli OPENCLAW_LIVE_MODELS=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-models-docker.sh", "test:docker:mcp-channels": "bash scripts/e2e/mcp-channels-docker.sh", "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh", "test:docker:openwebui": "bash scripts/e2e/openwebui-docker.sh", diff --git a/scripts/print-cli-backend-live-metadata.ts b/scripts/print-cli-backend-live-metadata.ts new file mode 100644 index 00000000000..e8327ceb010 --- /dev/null +++ b/scripts/print-cli-backend-live-metadata.ts @@ -0,0 +1,64 @@ +import { resolveCliBackendConfig, resolveCliBackendLiveTest } from "../src/agents/cli-backends.js"; + +const provider = process.argv[2]?.trim().toLowerCase(); + +if (!provider) { + console.error("usage: node scripts/print-cli-backend-live-metadata.ts "); + process.exit(1); +} + +async function loadFallbackBackend(id: string) { + switch (id) { + case "claude-cli": { + const mod = await import("../extensions/anthropic/cli-backend.ts"); + return mod.buildAnthropicCliBackend(); + } + case "codex-cli": { + const mod = await import("../extensions/openai/cli-backend.ts"); + return mod.buildOpenAICodexCliBackend(); + } + case "google-gemini-cli": { + const mod = await import("../extensions/google/cli-backend.ts"); + return mod.buildGoogleGeminiCliBackend(); + } + default: + return null; + } +} + +const resolved = resolveCliBackendConfig(provider); +const liveTest = resolveCliBackendLiveTest(provider); +const fallbackBackend = + !resolved || !liveTest?.defaultModelRef ? await loadFallbackBackend(provider) : null; +const backendConfig = resolved?.config ?? fallbackBackend?.config; +const backendLiveTest = + liveTest ?? + (fallbackBackend + ? { + defaultModelRef: fallbackBackend.liveTest?.defaultModelRef, + defaultImageProbe: fallbackBackend.liveTest?.defaultImageProbe === true, + dockerNpmPackage: fallbackBackend.liveTest?.docker?.npmPackage, + dockerBinaryName: fallbackBackend.liveTest?.docker?.binaryName, + } + : null); + +process.stdout.write( + JSON.stringify( + { + provider, + command: backendConfig?.command, + args: backendConfig?.args, + clearEnv: backendConfig?.clearEnv ?? [], + imageArg: backendConfig?.imageArg, + imageMode: backendConfig?.imageMode, + systemPromptWhen: backendConfig?.systemPromptWhen ?? "never", + bundleMcp: resolved?.bundleMcp === true || fallbackBackend?.bundleMcp === true, + defaultModelRef: backendLiveTest?.defaultModelRef, + defaultImageProbe: backendLiveTest?.defaultImageProbe === true, + dockerNpmPackage: backendLiveTest?.dockerNpmPackage, + dockerBinaryName: backendLiveTest?.dockerBinaryName, + }, + null, + 2, + ), +); diff --git a/scripts/test-live-cli-backend-docker.sh b/scripts/test-live-cli-backend-docker.sh index 90a9fecc9b9..254c11a8d60 100644 --- a/scripts/test-live-cli-backend-docker.sh +++ b/scripts/test-live-cli-backend-docker.sh @@ -9,14 +9,29 @@ CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}" PROFILE_FILE="${OPENCLAW_PROFILE_FILE:-$HOME/.profile}" CLI_TOOLS_DIR="${OPENCLAW_DOCKER_CLI_TOOLS_DIR:-$HOME/.cache/openclaw/docker-cli-tools}" -DEFAULT_MODEL="claude-cli/claude-sonnet-4-6" -CLI_MODEL="${OPENCLAW_LIVE_CLI_BACKEND_MODEL:-$DEFAULT_MODEL}" +DEFAULT_PROVIDER="${OPENCLAW_DOCKER_CLI_BACKEND_PROVIDER:-claude-cli}" +CLI_MODEL="${OPENCLAW_LIVE_CLI_BACKEND_MODEL:-}" CLI_PROVIDER="${CLI_MODEL%%/*}" CLI_DISABLE_MCP_CONFIG="${OPENCLAW_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG:-}" if [[ -z "$CLI_PROVIDER" || "$CLI_PROVIDER" == "$CLI_MODEL" ]]; then - CLI_PROVIDER="claude-cli" + CLI_PROVIDER="$DEFAULT_PROVIDER" fi + +CLI_METADATA_JSON="$(node --import tsx "$ROOT_DIR/scripts/print-cli-backend-live-metadata.ts" "$CLI_PROVIDER")" +read_metadata_field() { + local field="$1" + node -e 'const data = JSON.parse(process.argv[1]); const field = process.argv[2]; const value = data?.[field]; if (value == null) process.exit(1); process.stdout.write(typeof value === "string" ? value : JSON.stringify(value));' \ + "$CLI_METADATA_JSON" \ + "$field" +} + +DEFAULT_MODEL="$(read_metadata_field defaultModelRef 2>/dev/null || printf '%s' 'claude-cli/claude-sonnet-4-6')" +CLI_MODEL="${CLI_MODEL:-$DEFAULT_MODEL}" +CLI_DEFAULT_COMMAND="$(read_metadata_field command 2>/dev/null || true)" +CLI_DOCKER_NPM_PACKAGE="$(read_metadata_field dockerNpmPackage 2>/dev/null || true)" +CLI_DOCKER_BINARY_NAME="$(read_metadata_field dockerBinaryName 2>/dev/null || true)" + if [[ "$CLI_PROVIDER" == "claude-cli" && -z "$CLI_DISABLE_MCP_CONFIG" ]]; then CLI_DISABLE_MCP_CONFIG="0" fi @@ -103,13 +118,19 @@ if ((${#auth_files[@]} > 0)); then done fi provider="${OPENCLAW_DOCKER_CLI_BACKEND_PROVIDER:-claude-cli}" +default_command="${OPENCLAW_DOCKER_CLI_BACKEND_COMMAND_DEFAULT:-}" +docker_package="${OPENCLAW_DOCKER_CLI_BACKEND_NPM_PACKAGE:-}" +binary_name="${OPENCLAW_DOCKER_CLI_BACKEND_BINARY_NAME:-}" +if [ -z "$binary_name" ] && [ -n "$default_command" ]; then + binary_name="$(basename "$default_command")" +fi +if [ -z "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND:-}" ] && [ -n "$binary_name" ]; then + export OPENCLAW_LIVE_CLI_BACKEND_COMMAND="$HOME/.npm-global/bin/$binary_name" +fi +if [ -n "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND:-}" ] && [ ! -x "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND}" ] && [ -n "$docker_package" ]; then + npm_config_prefix="$HOME/.npm-global" npm install -g "$docker_package" +fi if [ "$provider" = "claude-cli" ]; then - if [ -z "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND:-}" ]; then - export OPENCLAW_LIVE_CLI_BACKEND_COMMAND="$HOME/.npm-global/bin/claude" - fi - if [ ! -x "${OPENCLAW_LIVE_CLI_BACKEND_COMMAND}" ]; then - npm_config_prefix="$HOME/.npm-global" npm install -g @anthropic-ai/claude-code - fi real_claude="$HOME/.npm-global/bin/claude-real" if [ ! -x "$real_claude" ] && [ -x "$HOME/.npm-global/bin/claude" ]; then mv "$HOME/.npm-global/bin/claude" "$real_claude" @@ -177,6 +198,9 @@ docker run --rm -t \ -e OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED="$AUTH_DIRS_CSV" \ -e OPENCLAW_DOCKER_AUTH_FILES_RESOLVED="$AUTH_FILES_CSV" \ -e OPENCLAW_DOCKER_CLI_BACKEND_PROVIDER="$CLI_PROVIDER" \ + -e OPENCLAW_DOCKER_CLI_BACKEND_COMMAND_DEFAULT="$CLI_DEFAULT_COMMAND" \ + -e OPENCLAW_DOCKER_CLI_BACKEND_NPM_PACKAGE="$CLI_DOCKER_NPM_PACKAGE" \ + -e OPENCLAW_DOCKER_CLI_BACKEND_BINARY_NAME="$CLI_DOCKER_BINARY_NAME" \ -e OPENCLAW_LIVE_TEST=1 \ -e OPENCLAW_LIVE_CLI_BACKEND=1 \ -e OPENCLAW_LIVE_CLI_BACKEND_MODEL="$CLI_MODEL" \ diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index a40c75356df..71931658dc1 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -11,6 +11,13 @@ export type ResolvedCliBackend = { pluginId?: string; }; +export type ResolvedCliBackendLiveTest = { + defaultModelRef?: string; + defaultImageProbe: boolean; + dockerNpmPackage?: string; + dockerBinaryName?: string; +}; + export function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig { const normalizeConfig = resolveFallbackCliBackendPolicy("claude-cli")?.normalizeConfig; return normalizeConfig ? normalizeConfig(config) : config; @@ -118,6 +125,23 @@ export function resolveCliBackendIds(cfg?: OpenClawConfig): Set { return ids; } +export function resolveCliBackendLiveTest(provider: string): ResolvedCliBackendLiveTest | null { + const normalized = normalizeBackendKey(provider); + const entry = + resolvePluginSetupCliBackend({ backend: normalized }) ?? + resolveRuntimeCliBackends().find((backend) => normalizeBackendKey(backend.id) === normalized); + if (!entry) { + return null; + } + const backend = "backend" in entry ? entry.backend : entry; + return { + defaultModelRef: backend.liveTest?.defaultModelRef, + defaultImageProbe: backend.liveTest?.defaultImageProbe === true, + dockerNpmPackage: backend.liveTest?.docker?.npmPackage, + dockerBinaryName: backend.liveTest?.docker?.binaryName, + }; +} + export function resolveCliBackendConfig( provider: string, cfg?: OpenClawConfig, diff --git a/src/gateway/gateway-cli-backend.live-helpers.ts b/src/gateway/gateway-cli-backend.live-helpers.ts index a3e10f78d04..638128f7c1e 100644 --- a/src/gateway/gateway-cli-backend.live-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-helpers.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import { expect } from "vitest"; +import { resolveCliBackendLiveTest } from "../agents/cli-backends.js"; import { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, @@ -24,50 +25,6 @@ import { extractPayloadText } from "./test-helpers.agent-results.js"; const execFileAsync = promisify(execFile); const CLI_GATEWAY_CONNECT_TIMEOUT_MS = 30_000; -export const DEFAULT_CLAUDE_ARGS = [ - "-p", - "--output-format", - "stream-json", - "--include-partial-messages", - "--verbose", - "--setting-sources", - "user", - "--permission-mode", - "bypassPermissions", -]; - -export const DEFAULT_CODEX_ARGS = [ - "exec", - "--json", - "--color", - "never", - "--sandbox", - "read-only", - "--skip-git-repo-check", -]; - -export const DEFAULT_CLEAR_ENV = [ - "ANTHROPIC_API_KEY", - "ANTHROPIC_API_KEY_OLD", - "ANTHROPIC_AUTH_TOKEN", - "ANTHROPIC_BASE_URL", - "ANTHROPIC_UNIX_SOCKET", - "CLAUDE_CONFIG_DIR", - "CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR", - "CLAUDE_CODE_ENTRYPOINT", - "CLAUDE_CODE_OAUTH_REFRESH_TOKEN", - "CLAUDE_CODE_OAUTH_SCOPES", - "CLAUDE_CODE_OAUTH_TOKEN", - "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR", - "CLAUDE_CODE_PLUGIN_CACHE_DIR", - "CLAUDE_CODE_PLUGIN_SEED_DIR", - "CLAUDE_CODE_REMOTE", - "CLAUDE_CODE_USE_COWORK_PLUGINS", - "CLAUDE_CODE_USE_BEDROCK", - "CLAUDE_CODE_USE_FOUNDRY", - "CLAUDE_CODE_USE_VERTEX", -]; - export type BootstrapWorkspaceContext = { expectedInjectedFiles: string[]; workspaceDir: string; @@ -147,7 +104,7 @@ export function shouldRunCliImageProbe(providerId: string): boolean { if (raw) { return isTruthyEnvValue(raw); } - return providerId === "claude-cli"; + return resolveCliBackendLiveTest(providerId)?.defaultImageProbe === true; } export function matchesCliBackendReply(text: string, expected: string): boolean { diff --git a/src/gateway/gateway-cli-backend.live.test.ts b/src/gateway/gateway-cli-backend.live.test.ts index be6215db9d8..daefe479dd3 100644 --- a/src/gateway/gateway-cli-backend.live.test.ts +++ b/src/gateway/gateway-cli-backend.live.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { resolveCliBackendConfig, resolveCliBackendLiveTest } from "../agents/cli-backends.js"; import { isLiveTestEnabled } from "../agents/live-test-helpers.js"; import { parseModelRef } from "../agents/model-selection.js"; import { clearRuntimeConfigSnapshot, type OpenClawConfig } from "../config/config.js"; @@ -11,9 +12,6 @@ import { applyCliBackendLiveEnv, createBootstrapWorkspace, ensurePairedTestGatewayClientIdentity, - DEFAULT_CLAUDE_ARGS, - DEFAULT_CLEAR_ENV, - DEFAULT_CODEX_ARGS, getFreeGatewayPort, matchesCliBackendReply, parseImageMode, @@ -36,7 +34,9 @@ const CLI_RESUME = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_RESUME const CLI_DEBUG = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_DEBUG); const describeLive = LIVE && CLI_LIVE ? describe : describe.skip; -const DEFAULT_MODEL = "claude-cli/claude-sonnet-4-6"; +const DEFAULT_PROVIDER = "claude-cli"; +const DEFAULT_MODEL = + resolveCliBackendLiveTest(DEFAULT_PROVIDER)?.defaultModelRef ?? "claude-cli/claude-sonnet-4-6"; const CLI_BACKEND_LIVE_TIMEOUT_MS = 420_000; function logCliBackendLiveStep(step: string, details?: Record): void { @@ -77,22 +77,10 @@ describeLive("gateway live (cli backend)", () => { const providerId = parsed.provider; const modelKey = `${providerId}/${parsed.model}`; + const backendResolved = resolveCliBackendConfig(providerId); const enableCliImageProbe = shouldRunCliImageProbe(providerId); logCliBackendLiveStep("model-selected", { providerId, modelKey, enableCliImageProbe }); - const providerDefaults = - providerId === "claude-cli" - ? { - command: "claude", - args: DEFAULT_CLAUDE_ARGS, - } - : providerId === "codex-cli" - ? { - command: "codex", - args: DEFAULT_CODEX_ARGS, - imageArg: "--image", - imageMode: "repeat" as const, - } - : null; + const providerDefaults = backendResolved?.config; const cliCommand = process.env.OPENCLAW_LIVE_CLI_BACKEND_COMMAND ?? providerDefaults?.command; if (!cliCommand) { @@ -114,7 +102,9 @@ describeLive("gateway live (cli backend)", () => { parseJsonStringArray( "OPENCLAW_LIVE_CLI_BACKEND_CLEAR_ENV", process.env.OPENCLAW_LIVE_CLI_BACKEND_CLEAR_ENV, - ) ?? (providerId === "claude-cli" ? DEFAULT_CLEAR_ENV : []); + ) ?? + providerDefaults?.clearEnv ?? + []; const filteredCliClearEnv = cliClearEnv.filter((name) => !preservedEnv.has(name)); const preservedCliEnv = Object.fromEntries( [...preservedEnv] @@ -136,11 +126,11 @@ describeLive("gateway live (cli backend)", () => { const stateDir = path.join(tempDir, "state"); await fs.mkdir(stateDir, { recursive: true }); process.env.OPENCLAW_STATE_DIR = stateDir; - const bootstrapWorkspace = - providerId === "claude-cli" ? await createBootstrapWorkspace(tempDir) : null; + const bundleMcp = backendResolved?.bundleMcp === true; + const bootstrapWorkspace = bundleMcp ? await createBootstrapWorkspace(tempDir) : null; const disableMcpConfig = process.env.OPENCLAW_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG !== "0"; let cliArgs = baseCliArgs; - if (providerId === "claude-cli" && disableMcpConfig) { + if (bundleMcp && disableMcpConfig) { const mcpConfigPath = path.join(tempDir, "claude-mcp.json"); await fs.writeFile(mcpConfigPath, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`); cliArgs = withMcpConfigOverrides(baseCliArgs, mcpConfigPath); @@ -176,7 +166,7 @@ describeLive("gateway live (cli backend)", () => { args: cliArgs, clearEnv: filteredCliClearEnv.length > 0 ? filteredCliClearEnv : undefined, env: Object.keys(preservedCliEnv).length > 0 ? preservedCliEnv : undefined, - systemPromptWhen: providerId === "claude-cli" ? "first" : "never", + systemPromptWhen: providerDefaults?.systemPromptWhen ?? "never", ...(cliImageArg ? { imageArg: cliImageArg, imageMode: cliImageMode } : {}), }, }, diff --git a/src/plugins/setup-registry.ts b/src/plugins/setup-registry.ts index ae1f256dae3..053b16288f6 100644 --- a/src/plugins/setup-registry.ts +++ b/src/plugins/setup-registry.ts @@ -142,9 +142,15 @@ function resolveSetupApiPath(rootDir: string): string | null { } const bundledExtensionDir = path.basename(rootDir); - const repoRoot = path.resolve(path.dirname(CURRENT_MODULE_PATH), "..", ".."); - const sourceExtensionRoot = path.join(repoRoot, "extensions", bundledExtensionDir); - if (sourceExtensionRoot !== rootDir) { + const repoRootCandidates = [ + path.resolve(path.dirname(CURRENT_MODULE_PATH), "..", ".."), + process.cwd(), + ]; + for (const repoRoot of repoRootCandidates) { + const sourceExtensionRoot = path.join(repoRoot, "extensions", bundledExtensionDir); + if (sourceExtensionRoot === rootDir) { + continue; + } const sourceFallback = findSetupApi(sourceExtensionRoot); if (sourceFallback) { return sourceFallback; @@ -215,7 +221,7 @@ export function resolvePluginSetupRegistry(params?: { }); for (const record of manifestRegistry.plugins) { - const setupSource = resolveSetupApiPath(record.rootDir); + const setupSource = record.setupSource ?? resolveSetupApiPath(record.rootDir); if (!setupSource) { continue; } @@ -411,9 +417,93 @@ export function resolvePluginSetupCliBackend(params: { env?: NodeJS.ProcessEnv; }): SetupCliBackendEntry | undefined { const normalized = normalizeProviderId(params.backend); - return resolvePluginSetupRegistry(params).cliBackends.find( + const direct = resolvePluginSetupRegistry(params).cliBackends.find( (entry) => normalizeProviderId(entry.backend.id) === normalized, ); + if (direct) { + return direct; + } + + const env = params.env ?? process.env; + const discovery = discoverOpenClawPlugins({ + workspaceDir: params.workspaceDir, + env, + cache: true, + }); + const manifestRegistry = loadPluginManifestRegistry({ + workspaceDir: params.workspaceDir, + env, + cache: true, + candidates: discovery.candidates, + diagnostics: discovery.diagnostics, + }); + const record = manifestRegistry.plugins.find((entry) => + entry.cliBackends.some((backendId) => normalizeProviderId(backendId) === normalized), + ); + if (!record) { + return undefined; + } + + const setupSource = record.setupSource ?? resolveSetupApiPath(record.rootDir); + if (!setupSource) { + return undefined; + } + + let mod: OpenClawPluginModule; + try { + mod = getJiti(setupSource)(setupSource) as OpenClawPluginModule; + } catch { + return undefined; + } + const resolved = resolveRegister((mod as { default?: OpenClawPluginModule }).default ?? mod); + if (!resolved.register) { + return undefined; + } + if (resolved.definition?.id && resolved.definition.id !== record.id) { + return undefined; + } + + let matchedBackend: CliBackendPlugin | undefined; + const localBackendKeys = new Set(); + const api = buildPluginApi({ + id: record.id, + name: record.name ?? record.id, + version: record.version, + description: record.description, + source: setupSource, + rootDir: record.rootDir, + registrationMode: "setup-only", + config: {} as OpenClawConfig, + runtime: EMPTY_RUNTIME, + logger: NOOP_LOGGER, + resolvePath: (input) => input, + handlers: { + registerProvider() {}, + registerConfigMigration() {}, + registerAutoEnableProbe() {}, + registerCliBackend(backend) { + const key = normalizeProviderId(backend.id); + if (localBackendKeys.has(key)) { + return; + } + localBackendKeys.add(key); + if (key === normalized) { + matchedBackend = backend; + } + }, + }, + }); + + try { + const result = resolved.register(api); + if (result && typeof result.then === "function") { + return undefined; + } + } catch { + return undefined; + } + + return matchedBackend ? { pluginId: record.id, backend: matchedBackend } : undefined; } export function runPluginSetupConfigMigrations(params: { diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 64f9b6eaf36..e6237eea686 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -2023,6 +2023,20 @@ export type CliBackendPlugin = { id: string; /** Default backend config before user overrides from `agents.defaults.cliBackends`. */ config: CliBackendConfig; + /** + * Optional live-smoke metadata owned by the backend plugin. + * + * Keep provider-specific test wiring here instead of scattering it across + * Docker wrappers, docs, and gateway live tests. + */ + liveTest?: { + defaultModelRef?: string; + defaultImageProbe?: boolean; + docker?: { + npmPackage?: string; + binaryName?: string; + }; + }; /** * Whether OpenClaw should inject bundle MCP config for this backend. *