From 81c2a1de26b06bfe9bde85c8f94d5b2a8d2dbdca Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 01:30:39 +0100 Subject: [PATCH] test: add Droid ACP bind Docker lane --- CHANGELOG.md | 3 +++ docs/help/testing-live.md | 7 +++++-- docs/help/testing.md | 2 +- docs/tools/acp-agents.md | 4 ++-- docs/tools/subagents.md | 2 +- package.json | 1 + scripts/lib/live-docker-auth.sh | 5 ++++- scripts/test-docker-all.mjs | 15 +++++++++++++++ scripts/test-live-acp-bind-docker.sh | 22 ++++++++++++++++++---- src/agents/acp-spawn.ts | 2 +- src/gateway/gateway-acp-bind.live.test.ts | 6 +++++- 11 files changed, 56 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 151a39f78bc..1541b60ffe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ Docs: https://docs.openclaw.ai - Providers/LiteLLM: register `litellm` as an image-generation provider so `image_generate model=litellm/...` calls and `agents.defaults.imageGenerationModel.fallbacks` entries resolve through the LiteLLM proxy. Thanks @zqchris. - Codex harness: require Codex app-server `0.125.0` or newer and cover native MCP `PreToolUse`, `PostToolUse`, and `PermissionRequest` payloads through the OpenClaw hook relay. - Agents/Codex: teach prompts and `agents_list` to surface native Codex app-server availability so agents prefer `/codex ...` over Codex ACP unless ACP/acpx is explicit. Thanks @vincentkoc. +- ACPX/Droid: add Factory Droid to the live ACP bind Docker matrix, including + `.factory` settings staging, `FACTORY_API_KEY` forwarding, and the single-agent + `test:docker:live-acp-bind:droid` recipe. ### Fixes diff --git a/docs/help/testing-live.md b/docs/help/testing-live.md index bb3bdaacdda..0514bc47000 100644 --- a/docs/help/testing-live.md +++ b/docs/help/testing-live.md @@ -219,6 +219,7 @@ Notes: - Overrides: - `OPENCLAW_LIVE_ACP_BIND_AGENT=claude` - `OPENCLAW_LIVE_ACP_BIND_AGENT=codex` + - `OPENCLAW_LIVE_ACP_BIND_AGENT=droid` - `OPENCLAW_LIVE_ACP_BIND_AGENT=gemini` - `OPENCLAW_LIVE_ACP_BIND_AGENT=opencode` - `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini` @@ -250,6 +251,7 @@ Single-agent Docker recipes: ```bash pnpm test:docker:live-acp-bind:claude pnpm test:docker:live-acp-bind:codex +pnpm test:docker:live-acp-bind:droid pnpm test:docker:live-acp-bind:gemini pnpm test:docker:live-acp-bind:opencode ``` @@ -258,8 +260,9 @@ Docker notes: - The Docker runner lives at `scripts/test-live-acp-bind-docker.sh`. - By default, it runs the ACP bind smoke against the aggregate live CLI agents in sequence: `claude`, `codex`, then `gemini`. -- Use `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=codex`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=gemini`, or `OPENCLAW_LIVE_ACP_BIND_AGENTS=opencode` to narrow the matrix. -- It sources `~/.profile`, stages the matching CLI auth material into the container, then installs the requested live CLI (`@anthropic-ai/claude-code`, `@openai/codex`, `@google/gemini-cli`, or `opencode-ai`) if missing. The ACP backend itself is the bundled embedded `acpx/runtime` package from the `acpx` plugin. +- Use `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=codex`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=droid`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=gemini`, or `OPENCLAW_LIVE_ACP_BIND_AGENTS=opencode` to narrow the matrix. +- It sources `~/.profile`, stages the matching CLI auth material into the container, then installs the requested live CLI (`@anthropic-ai/claude-code`, `@openai/codex`, Factory Droid via `https://app.factory.ai/cli`, `@google/gemini-cli`, or `opencode-ai`) if missing. The ACP backend itself is the bundled embedded `acpx/runtime` package from the `acpx` plugin. +- The Droid Docker variant stages `~/.factory` for settings, forwards `FACTORY_API_KEY`, and requires that API key because local Factory OAuth/keyring auth is not portable into the container. It uses ACPX's built-in `droid exec --output-format acp` registry entry. - The OpenCode Docker variant is a strict single-agent regression lane. It writes a temporary `OPENCODE_CONFIG_CONTENT` default model from `OPENCLAW_LIVE_ACP_BIND_OPENCODE_MODEL` (default `opencode/kimi-k2.6`) after sourcing `~/.profile`, and `pnpm test:docker:live-acp-bind:opencode` requires a bound assistant transcript instead of accepting the generic post-bind skip. - Direct `acpx` CLI calls are only a manual/workaround path for comparing behavior outside the Gateway. The Docker ACP bind smoke exercises OpenClaw's embedded `acpx` runtime backend. diff --git a/docs/help/testing.md b/docs/help/testing.md index a858bde2de7..7cec063f5e9 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -598,7 +598,7 @@ These Docker runners split into two buckets: The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store: - Direct models: `pnpm test:docker:live-models` (script: `scripts/test-live-models-docker.sh`) -- ACP bind smoke: `pnpm test:docker:live-acp-bind` (script: `scripts/test-live-acp-bind-docker.sh`; covers Claude, Codex, and Gemini by default, with strict OpenCode coverage via `pnpm test:docker:live-acp-bind:opencode`) +- ACP bind smoke: `pnpm test:docker:live-acp-bind` (script: `scripts/test-live-acp-bind-docker.sh`; covers Claude, Codex, and Gemini by default, with strict Droid/OpenCode coverage via `pnpm test:docker:live-acp-bind:droid` and `pnpm test:docker:live-acp-bind:opencode`) - CLI backend smoke: `pnpm test:docker:live-cli-backend` (script: `scripts/test-live-cli-backend-docker.sh`) - Codex app-server harness smoke: `pnpm test:docker:live-codex-harness` (script: `scripts/test-live-codex-harness-docker.sh`) - Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`) diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index 3ee993712bb..0989ca40bc3 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -10,7 +10,7 @@ read_when: title: "ACP agents" --- -[Agent Client Protocol (ACP)](https://agentclientprotocol.com/) sessions let OpenClaw run external coding harnesses (for example Pi, Claude Code, Cursor, Copilot, OpenClaw ACP, OpenCode, Gemini CLI, and other supported ACPX harnesses) through an ACP backend plugin. +[Agent Client Protocol (ACP)](https://agentclientprotocol.com/) sessions let OpenClaw run external coding harnesses (for example Pi, Claude Code, Cursor, Copilot, Droid, OpenClaw ACP, OpenCode, Gemini CLI, and other supported ACPX harnesses) through an ACP backend plugin. If you ask OpenClaw in plain language to bind or control Codex in the current conversation and the bundled `codex` plugin is enabled, OpenClaw should use the native Codex app-server plugin (`/codex bind`, `/codex threads`, `/codex resume`, `/codex steer`, `/codex stop`) instead of ACP. If you ask for `/acp`, ACP, acpx, or an ACP adapter test explicitly, OpenClaw can still route Codex through ACP. Each ACP session spawn is tracked as a [background task](/automation/tasks). @@ -83,7 +83,7 @@ OpenClaw picks `runtime: "acp"`, resolves the harness `agentId`, binds to the cu For `sessions_spawn`, `runtime: "acp"` is advertised only when ACP is enabled, the requester is not sandboxed, and an ACP runtime backend is loaded. It targets -ACP harness ids such as `codex`, `claude`, `gemini`, or `opencode`. Do not pass +ACP harness ids such as `codex`, `claude`, `droid`, `gemini`, or `opencode`. Do not pass a normal OpenClaw config agent id from `agents_list` unless that entry is explicitly configured with `agents.list[].runtime.type="acp"`; otherwise use the default sub-agent runtime. When an OpenClaw agent is configured with diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index a77512a6ae2..23975d94511 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -103,7 +103,7 @@ Tool params: - `task` (required) - `label?` (optional) - `agentId?` (optional; spawn under another agent id if allowed) -- `runtime?` (`subagent|acp`, default `subagent`; `acp` is only for external ACP harnesses such as `claude`, `gemini`, `opencode`, or explicitly requested Codex ACP/acpx, or for `agents.list[]` entries whose `runtime.type` is `acp`) +- `runtime?` (`subagent|acp`, default `subagent`; `acp` is only for external ACP harnesses such as `claude`, `droid`, `gemini`, `opencode`, or explicitly requested Codex ACP/acpx, or for `agents.list[]` entries whose `runtime.type` is `acp`) - `model?` (optional; overrides the sub-agent model; invalid values are skipped and the sub-agent runs on the default model with a warning in the tool result) - `thinking?` (optional; overrides thinking level for the sub-agent run) - `runTimeoutSeconds?` (defaults to `agents.defaults.subagents.runTimeoutSeconds` when set, otherwise `0`; when set, the sub-agent run is aborted after N seconds) diff --git a/package.json b/package.json index 4edd30d858c..370e152989d 100644 --- a/package.json +++ b/package.json @@ -1494,6 +1494,7 @@ "test:docker:live-acp-bind": "bash scripts/test-live-acp-bind-docker.sh", "test:docker:live-acp-bind:claude": "OPENCLAW_LIVE_ACP_BIND_AGENT=claude bash scripts/test-live-acp-bind-docker.sh", "test:docker:live-acp-bind:codex": "OPENCLAW_LIVE_ACP_BIND_AGENT=codex bash scripts/test-live-acp-bind-docker.sh", + "test:docker:live-acp-bind:droid": "OPENCLAW_LIVE_ACP_BIND_AGENT=droid OPENCLAW_LIVE_ACP_BIND_REQUIRE_TRANSCRIPT=1 bash scripts/test-live-acp-bind-docker.sh", "test:docker:live-acp-bind:gemini": "OPENCLAW_LIVE_ACP_BIND_AGENT=gemini bash scripts/test-live-acp-bind-docker.sh", "test:docker:live-acp-bind:opencode": "OPENCLAW_LIVE_ACP_BIND_AGENT=opencode OPENCLAW_LIVE_ACP_BIND_REQUIRE_TRANSCRIPT=1 bash scripts/test-live-acp-bind-docker.sh", "test:docker:live-build": "bash scripts/test-live-build-docker.sh", diff --git a/scripts/lib/live-docker-auth.sh b/scripts/lib/live-docker-auth.sh index f45ab122a01..ed0aff96c65 100644 --- a/scripts/lib/live-docker-auth.sh +++ b/scripts/lib/live-docker-auth.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -OPENCLAW_DOCKER_LIVE_AUTH_ALL=(.gemini .minimax) +OPENCLAW_DOCKER_LIVE_AUTH_ALL=(.factory .gemini .minimax) OPENCLAW_DOCKER_LIVE_AUTH_FILES_ALL=( .codex/auth.json .codex/config.toml @@ -49,6 +49,9 @@ openclaw_live_should_include_auth_dir_for_provider() { local provider provider="$(openclaw_live_trim "${1:-}")" case "$provider" in + droid | factory | factory-droid) + printf '%s\n' ".factory" + ;; gemini | gemini-cli | google-gemini-cli) printf '%s\n' ".gemini" ;; diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index 40fe5dfcaff..a0df9dd0f29 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -25,6 +25,7 @@ const DEFAULT_RESOURCE_LIMITS = { live: 9, "live:claude": 4, "live:codex": 4, + "live:droid": 4, "live:gemini": 4, "live:opencode": 4, npm: 10, @@ -67,6 +68,9 @@ function liveProviderResource(provider) { if (provider === "codex-cli" || provider === "codex") { return "live:codex"; } + if (provider === "droid") { + return "live:droid"; + } if (provider === "google-gemini-cli" || provider === "gemini") { return "live:gemini"; } @@ -318,6 +322,17 @@ const exclusiveLanes = [ weight: 3, }, ), + liveLane( + "live-acp-bind-droid", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:droid", + { + cacheKey: "acp-bind-droid", + provider: "droid", + resources: ["npm"], + timeoutMs: LIVE_ACP_TIMEOUT_MS, + weight: 3, + }, + ), liveLane( "live-acp-bind-gemini", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:gemini", diff --git a/scripts/test-live-acp-bind-docker.sh b/scripts/test-live-acp-bind-docker.sh index f8b42eccd97..86252462a4c 100644 --- a/scripts/test-live-acp-bind-docker.sh +++ b/scripts/test-live-acp-bind-docker.sh @@ -30,10 +30,11 @@ openclaw_live_acp_bind_resolve_auth_provider() { case "${1:-}" in claude) printf '%s\n' "claude-cli" ;; codex) printf '%s\n' "codex-cli" ;; + droid) printf '%s\n' "droid" ;; gemini) printf '%s\n' "google-gemini-cli" ;; opencode) printf '%s\n' "opencode" ;; *) - echo "Unsupported OPENCLAW_LIVE_ACP_BIND agent: ${1:-} (expected claude, codex, gemini, or opencode)" >&2 + echo "Unsupported OPENCLAW_LIVE_ACP_BIND agent: ${1:-} (expected claude, codex, droid, gemini, or opencode)" >&2 return 1 ;; esac @@ -43,6 +44,7 @@ openclaw_live_acp_bind_resolve_agent_command() { case "${1:-}" in claude) printf '%s' "${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND_CLAUDE:-${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}}" ;; codex) printf '%s' "${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND_CODEX:-${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}}" ;; + droid) printf '%s' "${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND_DROID:-${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}}" ;; gemini) printf '%s' "${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND_GEMINI:-${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}}" ;; opencode) printf '%s' "${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND_OPENCODE:-${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}}" ;; *) return 1 ;; @@ -95,9 +97,9 @@ export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" export COREPACK_HOME="${COREPACK_HOME:-$XDG_CACHE_HOME/node/corepack}" export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-$XDG_CACHE_HOME/npm}" export npm_config_cache="$NPM_CONFIG_CACHE" -mkdir -p "$NPM_CONFIG_PREFIX" "$XDG_CACHE_HOME" "$COREPACK_HOME" "$NPM_CONFIG_CACHE" +mkdir -p "$NPM_CONFIG_PREFIX" "$HOME/.local/bin" "$XDG_CACHE_HOME" "$COREPACK_HOME" "$NPM_CONFIG_CACHE" chmod 700 "$XDG_CACHE_HOME" "$COREPACK_HOME" "$NPM_CONFIG_CACHE" || true -export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" +export PATH="$HOME/.local/bin:$NPM_CONFIG_PREFIX/bin:$PATH" if [ "${OPENCLAW_DOCKER_AUTH_PRESTAGED:-0}" != "1" ]; then IFS=',' read -r -a auth_dirs <<<"${OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED:-}" IFS=',' read -r -a auth_files <<<"${OPENCLAW_DOCKER_AUTH_FILES_RESOLVED:-}" @@ -153,6 +155,17 @@ WRAP npm install -g @openai/codex fi ;; + droid) + if ! command -v droid >/dev/null 2>&1; then + curl -fsSL https://app.factory.ai/cli | sh + export PATH="$HOME/.local/bin:$PATH" + fi + droid --version + if [ -z "${FACTORY_API_KEY:-}" ]; then + echo "Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container." >&2 + exit 1 + fi + ;; gemini) mkdir -p "$HOME/.gemini" if [ ! -x "$NPM_CONFIG_PREFIX/bin/gemini" ]; then @@ -197,7 +210,7 @@ for token in "${ACP_AGENT_TOKENS[@]}"; do done if ((${#ACP_AGENTS[@]} == 0)); then - echo "No ACP bind agents selected. Use OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini,opencode." >&2 + echo "No ACP bind agents selected. Use OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,droid,gemini,opencode." >&2 exit 1 fi @@ -283,6 +296,7 @@ for ACP_AGENT in "${ACP_AGENTS[@]}"; do -e OPENCLAW_LIVE_ACP_BIND_ANTHROPIC_API_KEY_OLD="${ANTHROPIC_API_KEY_OLD:-}" \ -e GEMINI_API_KEY \ -e GOOGLE_API_KEY \ + -e FACTORY_API_KEY \ -e OPENAI_API_KEY \ -e OPENCODE_API_KEY \ -e OPENCODE_ZEN_API_KEY \ diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index fd4529d52c0..a30e96b533f 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -413,7 +413,7 @@ function resolveTargetAcpAgentId(params: { error: `agentId "${requested}" is an OpenClaw config agent, not an ACP harness. ` + 'Use runtime="subagent" or omit runtime for OpenClaw config agents. ' + - 'Use runtime="acp" only with external ACP harness ids such as codex, claude, gemini, or opencode, or configure agents.list[].runtime.type="acp" with runtime.acp.agent.', + 'Use runtime="acp" only with external ACP harness ids such as codex, claude, droid, gemini, or opencode, or configure agents.list[].runtime.type="acp" with runtime.acp.agent.', }; } return { ok: true, agentId: requested }; diff --git a/src/gateway/gateway-acp-bind.live.test.ts b/src/gateway/gateway-acp-bind.live.test.ts index 3baa4104ca9..47745253c24 100644 --- a/src/gateway/gateway-acp-bind.live.test.ts +++ b/src/gateway/gateway-acp-bind.live.test.ts @@ -38,7 +38,7 @@ const CONNECT_TIMEOUT_MS = 90_000; const LIVE_TIMEOUT_MS = 240_000; const DEFAULT_LIVE_CODEX_MODEL = "gpt-5.5"; const DEFAULT_LIVE_PARENT_MODEL = "openai/gpt-5.4"; -type LiveAcpAgent = "claude" | "codex" | "gemini" | "opencode"; +type LiveAcpAgent = "claude" | "codex" | "droid" | "gemini" | "opencode"; function createSlackCurrentConversationBindingRegistry() { return createTestRegistry([ @@ -76,6 +76,9 @@ function normalizeAcpAgent(raw: string | undefined): LiveAcpAgent { if (normalized === "codex") { return "codex"; } + if (normalized === "droid") { + return "droid"; + } if (normalized === "opencode") { return "opencode"; } @@ -141,6 +144,7 @@ function logLiveStep(message: string): void { function shouldRequireBoundAssistantTranscript(liveAgent: LiveAcpAgent): boolean { return ( + liveAgent === "droid" || liveAgent === "opencode" || isTruthyEnvValue(process.env.OPENCLAW_LIVE_ACP_BIND_REQUIRE_TRANSCRIPT) );