From eab26aca9bdee7c01732821bab12990c459c176b Mon Sep 17 00:00:00 2001 From: Sean Sun <1194458432@qq.com> Date: Sat, 18 Apr 2026 13:59:30 +0800 Subject: [PATCH] extensions/acpx: expose probeAgent config so non-codex ACP stacks stay available Add optional probeAgent field to acpx plugin config, carry through resolveAcpxPluginConfig, forward to AcpxRuntime constructor so users can set plugins.entries.acpx.config.probeAgent to any configured agent id instead of hardcoding codex. Refs #68409 --- CHANGELOG.md | 1 + extensions/acpx/openclaw.plugin.json | 9 ++++++++ extensions/acpx/src/config-schema.ts | 3 +++ extensions/acpx/src/config.test.ts | 31 ++++++++++++++++++++++++++++ extensions/acpx/src/config.ts | 3 +++ extensions/acpx/src/service.test.ts | 31 ++++++++++++++++++++++++++++ extensions/acpx/src/service.ts | 1 + 7 files changed, 79 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62fd06a5e69..4a76e13c8b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai - Providers/OpenAI: lock the auth picker wording for OpenAI API key, Codex browser login, and Codex device pairing so the setup choices no longer imply a mixed Codex/API-key auth path. (#67848) Thanks @tmlxrd. - Agents/BTW: route `/btw` side questions through provider stream registration with the session workspace, so Ollama provider URL construction and workspace-scoped hooks apply correctly. Fixes #68336. (#70413) Thanks @suboss87. - Agents/sessions: make session transcript write locks non-reentrant by default, so same-process transcript writers contend unless a helper explicitly opts into nested lock ownership. +- ACPX/probe: expose an optional `probeAgent` plugin config field so the embedded ACP runtime health probe can target a configured agent (for example `opencode` or `claude`) instead of hardcoding `codex`, and stop marking the entire ACP runtime backend unavailable when the default probe agent is simply not installed or not authenticated. (#68409) Thanks @lyfuci. - Memory search: use sqlite-vec KNN for vector recall while preserving full post-filter result limits in multi-model indexes. Fixes #69666. (#69680) Thanks @aalekh-sarvam. - Providers/OpenAI Codex: stop stale per-agent `openai-codex:default` OAuth profiles from shadowing a newer main-agent identity-scoped profile, and let `openclaw doctor` offer the matching cleanup. (#70393) Thanks @pashpashpash. - ACPX: route OpenClaw ACP bridge commands through the MCP-free runtime path even when the command is wrapped with `env`, has bridge flags, or is resumed from persisted session state, so documented `acpx openclaw` setups no longer fail on per-session MCP injection. (#68741) Thanks @alexlomt. diff --git a/extensions/acpx/openclaw.plugin.json b/extensions/acpx/openclaw.plugin.json index f05107a57fa..416eeb84d83 100644 --- a/extensions/acpx/openclaw.plugin.json +++ b/extensions/acpx/openclaw.plugin.json @@ -46,6 +46,10 @@ "type": "number", "minimum": 0 }, + "probeAgent": { + "type": "string", + "minLength": 1 + }, "mcpServers": { "type": "object", "additionalProperties": { @@ -132,6 +136,11 @@ "help": "Reserved compatibility field for the older embedded ACPX queue-owner path. Accepted for compatibility and logged as ignored.", "advanced": true }, + "probeAgent": { + "label": "Probe Agent", + "help": "Agent id used for the embedded ACP runtime health probe. Defaults to the runtime built-in probe agent (codex). Set this to another configured ACP agent id (for example `opencode` or `claude`) when the default probe agent is not installed or not authenticated, so the whole embedded ACP backend does not get marked unavailable.", + "advanced": true + }, "mcpServers": { "label": "MCP Servers", "help": "Named MCP server definitions to inject into embedded ACP session bootstrap. Each entry needs a command and can include args and env.", diff --git a/extensions/acpx/src/config-schema.ts b/extensions/acpx/src/config-schema.ts index 0b390363d51..306f58f0cc7 100644 --- a/extensions/acpx/src/config-schema.ts +++ b/extensions/acpx/src/config-schema.ts @@ -34,6 +34,7 @@ export type AcpxPluginConfig = { strictWindowsCmdWrapper?: boolean; timeoutSeconds?: number; queueOwnerTtlSeconds?: number; + probeAgent?: string; mcpServers?: Record; agents?: Record; }; @@ -49,6 +50,7 @@ export type ResolvedAcpxPluginConfig = { strictWindowsCmdWrapper: boolean; timeoutSeconds?: number; queueOwnerTtlSeconds: number; + probeAgent?: string; legacyCompatibilityConfig: { strictWindowsCmdWrapper?: boolean; queueOwnerTtlSeconds?: number; @@ -107,6 +109,7 @@ export const AcpxPluginConfigSchema = z.strictObject({ .number({ error: "queueOwnerTtlSeconds must be a number >= 0" }) .min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" }) .optional(), + probeAgent: nonEmptyTrimmedString("probeAgent must be a non-empty string").optional(), mcpServers: z.record(z.string(), McpServerConfigSchema).optional(), agents: z .record( diff --git a/extensions/acpx/src/config.test.ts b/extensions/acpx/src/config.test.ts index 8f25a9df5f8..68aa4a156ab 100644 --- a/extensions/acpx/src/config.test.ts +++ b/extensions/acpx/src/config.test.ts @@ -58,6 +58,37 @@ describe("embedded acpx plugin config", () => { }); }); + it("leaves probeAgent undefined by default so the runtime picks its built-in probe agent", () => { + const resolved = resolveAcpxPluginConfig({ + rawConfig: undefined, + workspaceDir: "/tmp/openclaw-acpx", + }); + + expect(resolved.probeAgent).toBeUndefined(); + }); + + it("carries an explicit probeAgent through to the resolved plugin config, trimmed", () => { + const resolved = resolveAcpxPluginConfig({ + rawConfig: { + probeAgent: " opencode ", + }, + workspaceDir: "/tmp/openclaw-acpx", + }); + + expect(resolved.probeAgent).toBe("opencode"); + }); + + it("rejects an empty probeAgent string", () => { + expect(() => + resolveAcpxPluginConfig({ + rawConfig: { + probeAgent: "", + }, + workspaceDir: "/tmp/openclaw-acpx", + }), + ).toThrow(/probeAgent must be a non-empty string/); + }); + it("injects the built-in plugin-tools MCP server only when explicitly enabled", () => { const resolved = resolveAcpxPluginConfig({ rawConfig: { diff --git a/extensions/acpx/src/config.ts b/extensions/acpx/src/config.ts index c94380aa896..a77a36e117c 100644 --- a/extensions/acpx/src/config.ts +++ b/extensions/acpx/src/config.ts @@ -260,6 +260,8 @@ export function resolveAcpxPluginConfig(params: { ]), ); + const probeAgent = normalized.probeAgent?.trim(); + return { cwd, stateDir, @@ -273,6 +275,7 @@ export function resolveAcpxPluginConfig(params: { normalized.strictWindowsCmdWrapper ?? DEFAULT_STRICT_WINDOWS_CMD_WRAPPER, timeoutSeconds: normalized.timeoutSeconds ?? DEFAULT_ACPX_TIMEOUT_SECONDS, queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds ?? DEFAULT_QUEUE_OWNER_TTL_SECONDS, + probeAgent: probeAgent && probeAgent.length > 0 ? probeAgent : undefined, legacyCompatibilityConfig: { strictWindowsCmdWrapper: normalized.strictWindowsCmdWrapper, queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds, diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index bc95bd1313c..63ef39887d7 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -145,6 +145,37 @@ describe("createAcpxRuntimeService", () => { await service.stop?.(ctx); }); + it("forwards a configured probeAgent to the runtime factory so the probe does not hardcode the default", async () => { + const workspaceDir = await makeTempDir(); + const ctx = createServiceContext(workspaceDir); + const runtime = { + ensureSession: vi.fn(), + runTurn: vi.fn(), + cancel: vi.fn(), + close: vi.fn(), + probeAvailability: vi.fn(async () => {}), + isHealthy: vi.fn(() => true), + doctor: vi.fn(async () => ({ ok: true, message: "ok" })), + }; + const runtimeFactory = vi.fn(() => runtime as never); + const service = createAcpxRuntimeService({ + pluginConfig: { probeAgent: "opencode" }, + runtimeFactory, + }); + + await service.start(ctx); + + expect(runtimeFactory).toHaveBeenCalledWith( + expect.objectContaining({ + pluginConfig: expect.objectContaining({ + probeAgent: "opencode", + }), + }), + ); + + await service.stop?.(ctx); + }); + it("warns when legacy compatibility config is explicitly ignored", async () => { const workspaceDir = await makeTempDir(); const ctx = createServiceContext(workspaceDir); diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index 27175434b9a..576e4072f19 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -53,6 +53,7 @@ function createDefaultRuntime(params: AcpxRuntimeFactoryParams): AcpxRuntimeLike mcpServers: toAcpMcpServers(params.pluginConfig.mcpServers), permissionMode: params.pluginConfig.permissionMode, nonInteractivePermissions: params.pluginConfig.nonInteractivePermissions, + probeAgent: params.pluginConfig.probeAgent, timeoutMs: params.pluginConfig.timeoutSeconds != null ? params.pluginConfig.timeoutSeconds * 1_000