test: add cli backend live matrix metadata

This commit is contained in:
Peter Steinberger
2026-04-07 09:05:05 +01:00
parent a227d1cc65
commit 0af808b457
13 changed files with 300 additions and 106 deletions

View File

@@ -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`)

View File

@@ -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",

View File

@@ -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());
},
});

View File

@@ -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",

View File

@@ -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());
},
});

View File

@@ -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",

View File

@@ -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 <provider>");
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,
),
);

View File

@@ -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" \

View File

@@ -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<string> {
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,

View File

@@ -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 {

View File

@@ -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<string, unknown>): 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 } : {}),
},
},

View File

@@ -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<string>();
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: {

View File

@@ -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.
*