mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): bundle only the ACP bridges, not the harness CLIs
Switch the bundled ACP tooling from full-CLI bundling to bridge-only bundling. The staging scripts now install the bridge JS trees with `npm --omit=optional`, which skips the SDK/codex platform packages that vendor the native claude/codex CLIs — the bundled resource drops from ~586MB to ~56MB of pure JS, and ad-hoc codesigning of vendored Mach-O binaries is no longer needed. The bridges instead run the user's own harness CLI: at spawn time the desktop resolves `claude`/`codex` from PATH and exports CLAUDE_CODE_EXECUTABLE / CODEX_PATH (neither bridge falls back to PATH itself), driven by a new `bridge_cli_env_var` catalog field. A value already present in the desktop's environment wins, and per-agent env overrides still apply afterwards. Because the app once again depends on a user-installed CLI, this reinstates the machinery that 55a80e5c retired: the CliMissing availability gate, PATH-based auth probes, curl CLI install commands, and the Doctor's CLI-missing copy and CLI-path row. The Doctor's "bundled" badge now reads an explicit `adapter_ships_with_app` catalog field, since inferring it from empty install-command lists breaks once claude/codex regain CLI install commands. The lock drops the native*/npmOs/npmCpu/npmLibc fields (trees are platform-independent, but stay per-target so pins can be bumped independently), install validation asserts no platform package slipped into the tree, and freshness stamps gain STAMP_INSTALL_MODE=bridge-only so stale full-CLI caches are reinstalled rather than reused. The harness-clis.json manifest and its resolution path are removed; the prepare script deletes the stale file from previously staged resource dirs. Verified: desktop cargo tests (1421), buzz-acp tests, clippy + fmt on both, tsc, biome, desktop unit tests (2791), doctor-states + doctor-cta-screenshots Playwright specs, file-size/px guards, and end-to-end staging on aarch64-apple-darwin (bridge wrappers report 0.58.1/1.1.2, no Mach-O in the tree, idempotent re-run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
64eae442e3
commit
b2ce7a4a01
@@ -43,7 +43,6 @@ desktop/src-tauri/resources/acp/bin/*
|
||||
!desktop/src-tauri/resources/acp/bin/.gitkeep
|
||||
desktop/src-tauri/resources/acp/node/
|
||||
desktop/src-tauri/resources/acp/node-runtime.json
|
||||
desktop/src-tauri/resources/acp/harness-clis.json
|
||||
|
||||
# sqlx offline query data (generated, not portable)
|
||||
.sqlx/
|
||||
|
||||
@@ -67,8 +67,6 @@ pub(crate) enum AcpAvailabilityStatus {
|
||||
/// still parse.
|
||||
AdapterOutdated,
|
||||
/// CLI binary missing; ACP adapter may be present.
|
||||
/// Retired on current desktops (the bundled bridges vendor their own
|
||||
/// CLI); kept so payloads from older app versions still parse.
|
||||
CliMissing,
|
||||
/// Neither adapter nor CLI found.
|
||||
NotInstalled,
|
||||
|
||||
+10
-94
@@ -9,20 +9,12 @@
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "aarch64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "arm64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-darwin-arm64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-darwin-arm64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-lrfJ4eVtzfPkCpbSkBOGSMQCBbvmW6nbPzgHE4IwMN3scZlpuFMUFqh2aaJa/X2SAcWD9H2S0t2WWvSRgM7BjA==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
"claudeCodeVersion": "2.1.205"
|
||||
},
|
||||
{
|
||||
"id": "claude-acp",
|
||||
@@ -33,21 +25,12 @@
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "aarch64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "arm64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-linux-arm64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-linux-arm64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-CXzySK3PV3EizCRPXnxPqeaAtgrBFDnMFOVpMe36oC3U16yDb1b1tAJGqZi/7uFrVvAiaXvnSFxhUWnDDSaO+A==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
"claudeCodeVersion": "2.1.205"
|
||||
},
|
||||
{
|
||||
"id": "claude-acp",
|
||||
@@ -58,20 +41,12 @@
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "x86_64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "x64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-darwin-x64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-darwin-x64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-G6ETPmL5mNzJ2DFsWxG3jmsmrXgZX1N2ZCJvxaGUUpjTsKZJ4Tup1cWYvcd/m7o5fYZmx9REmgzTwsAIc1fdPQ==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
"claudeCodeVersion": "2.1.205"
|
||||
},
|
||||
{
|
||||
"id": "claude-acp",
|
||||
@@ -82,20 +57,12 @@
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "x86_64-pc-windows-msvc",
|
||||
"npmOs": "win32",
|
||||
"npmCpu": "x64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-win32-x64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-win32-x64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-kg2kkXyeSoFLruO3Ic2IruLxzBR0xCUtmlJHdWi3SYW7JhAKNJg4fcrdJsWcardmEw23Y2UDGDJbRyxqSVx6wg==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude.exe"
|
||||
"claudeCodeVersion": "2.1.205"
|
||||
},
|
||||
{
|
||||
"id": "claude-acp",
|
||||
@@ -106,21 +73,12 @@
|
||||
"integrity": "sha512-F1/W6EJdoYbrEUluRUknx0Nn0MAKDOkn2C/9YcP/joVkmdFUGTAxlGDpwdYu239TOkpc8Qm4+ffGsQjPZdryTg==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/claude-agent-acp/-/claude-agent-acp-0.58.1.tgz",
|
||||
"target": "x86_64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "x64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@anthropic-ai/claude-agent-sdk",
|
||||
"dependencyVersion": "0.3.205",
|
||||
"dependencyIntegrity": "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.205.tgz",
|
||||
"claudeCodeVersion": "2.1.205",
|
||||
"nativePackage": "@anthropic-ai/claude-agent-sdk-linux-x64",
|
||||
"nativePackageName": "@anthropic-ai/claude-agent-sdk-linux-x64",
|
||||
"nativeVersion": "0.3.205",
|
||||
"nativeIntegrity": "sha512-siS+1iNqBSlGFZZvJY6+mhzZ/6/ec/TbX9GMuwmTF0E6fxGhIIp797jJxR1q8r6FAq7d39mEoRNhC0Ffo60uNQ==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.205.tgz",
|
||||
"nativeExecutable": "claude"
|
||||
"claudeCodeVersion": "2.1.205"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
@@ -131,19 +89,11 @@
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "aarch64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "arm64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-darwin-arm64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-darwin-arm64",
|
||||
"nativeIntegrity": "sha512-dABeDK+ATqMG54MGBd3VjpKfh5EOoqx9PKVQB2QYDaEXx3F6CdUCXue5QIMfr4OxziUj8pUcLAQyd+KFqiTUFw==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1-darwin-arm64.tgz",
|
||||
"nativeExecutable": "vendor/aarch64-apple-darwin/bin/codex"
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
@@ -154,20 +104,11 @@
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "aarch64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "arm64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-linux-arm64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-linux-arm64",
|
||||
"nativeIntegrity": "sha512-451o15+XtaXCCb35t/KCyyPqXHnTPxPxtdqEYOnE3e4sH5AfnI/uVJwfdjOksMG6vRLy6R+fLvSDOMguRFLmQw==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1-linux-arm64.tgz",
|
||||
"nativeExecutable": "vendor/aarch64-unknown-linux-musl/bin/codex"
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
@@ -178,19 +119,11 @@
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "x86_64-apple-darwin",
|
||||
"npmOs": "darwin",
|
||||
"npmCpu": "x64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-darwin-x64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-darwin-x64",
|
||||
"nativeIntegrity": "sha512-K2g3Q3tNxzFhV0SuzO6HcsYK7EQrp/o4HyeReyhkwVrwwUPoYwyIbB0IRjHIiDzRhbKriDccid2iyF5aPqdTcg==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1-darwin-x64.tgz",
|
||||
"nativeExecutable": "vendor/x86_64-apple-darwin/bin/codex"
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
@@ -201,19 +134,11 @@
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "x86_64-pc-windows-msvc",
|
||||
"npmOs": "win32",
|
||||
"npmCpu": "x64",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-win32-x64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-win32-x64",
|
||||
"nativeIntegrity": "sha512-qv2HOp6v/nVP31p5I5GxYyL0wa79PMzim1+W9CKSV0UldjFV9AMbualA8PeXcYhbvvh9Y1UASXxwjuQdlyfAvw==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1-win32-x64.tgz",
|
||||
"nativeExecutable": "vendor/x86_64-pc-windows-msvc/bin/codex.exe"
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz"
|
||||
},
|
||||
{
|
||||
"id": "codex-acp",
|
||||
@@ -224,20 +149,11 @@
|
||||
"integrity": "sha512-qE/R1WdqJJ9OFHsHGvbmVmS2j9iCMZzpWT3g2XIViXrGHu1fLOALLINBIlW+WzKDllCh131aB6cqcIWSt0otbw==",
|
||||
"tarball": "https://registry.npmjs.org/@agentclientprotocol/codex-acp/-/codex-acp-1.1.2.tgz",
|
||||
"target": "x86_64-unknown-linux-gnu",
|
||||
"npmOs": "linux",
|
||||
"npmCpu": "x64",
|
||||
"npmLibc": "glibc",
|
||||
"nodeEngine": ">=22",
|
||||
"dependencyPackage": "@openai/codex",
|
||||
"dependencyVersion": "0.144.1",
|
||||
"dependencyIntegrity": "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw==",
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz",
|
||||
"nativePackage": "@openai/codex-linux-x64",
|
||||
"nativePackageName": "@openai/codex",
|
||||
"nativeVersion": "0.144.1-linux-x64",
|
||||
"nativeIntegrity": "sha512-HNGVI+BulrOaC/0IzBvd6EL62j7LrlbFKibrhw6hZjjCjAeUYzRB2jB4qDzXN1NfqDi6Xrvniof3kwbwab24lg==",
|
||||
"nativeTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1-linux-x64.tgz",
|
||||
"nativeExecutable": "vendor/x86_64-unknown-linux-musl/bin/codex"
|
||||
"dependencyTarball": "https://registry.npmjs.org/@openai/codex/-/codex-0.144.1.tgz"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -125,7 +125,9 @@ const overrides = new Map([
|
||||
// the availability_drift half of needs_restart deleted (the bundled bridge
|
||||
// can't drift out-of-band); ratcheted to bank the deletions (main's
|
||||
// team-instructions spawn-hash growth stays).
|
||||
["src-tauri/src/managed_agents/runtime.rs", 2087],
|
||||
// bridge-only bundling: spawn-time CLAUDE_CODE_EXECUTABLE/CODEX_PATH export
|
||||
// pointing the bundled bridge at the user's CLI (+25 lines incl. rationale).
|
||||
["src-tauri/src/managed_agents/runtime.rs", 2112],
|
||||
// config-bridge setup-payload env-boundary fix adds readiness wiring in
|
||||
// spawn_agent_child; load-bearing security fix, queued to split.
|
||||
["src-tauri/src/managed_agents/config_bridge/reader.rs", 1016],
|
||||
@@ -168,7 +170,9 @@ const overrides = new Map([
|
||||
// bundle-acps: codex version-gate retirement removes the AdapterOutdated
|
||||
// probe from cli_login_requirements and its gate tests, both obsolete now
|
||||
// that the bridges ship bundled; ratcheting 1765 -> 1599 to bank the headroom.
|
||||
["src-tauri/src/managed_agents/readiness.rs", 1599],
|
||||
// bridge-only bundling: bridge_cli_env_var + adapter_ships_with_app fields
|
||||
// in the make_cli_runtime stub (+2 lines).
|
||||
["src-tauri/src/managed_agents/readiness.rs", 1601],
|
||||
// applyWorkspace reposDir parameter plus the validateReposDir binding,
|
||||
// threaded through Tauri invokes for configurable repos_dir, plus the
|
||||
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
|
||||
@@ -231,11 +235,11 @@ const overrides = new Map([
|
||||
// "adapter_outdated" availability retired with the codex version gate (-1 line).
|
||||
// bundled-adapter-doctor-copy: adapterBundled field on
|
||||
// AcpRuntimeCatalogEntry (+2 lines).
|
||||
// bundled-cli-probes: "cli_missing" availability retired; +4 doc lines on
|
||||
// AcpAvailabilityStatus explaining why the state no longer exists.
|
||||
// acp-dead-machinery retirement: nodeRequired field deleted;
|
||||
// ratcheted 1075 -> 1073.
|
||||
["src/shared/api/types.ts", 1073],
|
||||
// acp-dead-machinery retirement: nodeRequired field deleted.
|
||||
// bridge-only bundling: the "cli_missing" availability literal returns —
|
||||
// the bundled bridges run the user's claude/codex CLI again; ratcheted
|
||||
// to bank the node_required-era headroom.
|
||||
["src/shared/api/types.ts", 1069],
|
||||
// readiness-gate: PersonaDialog.tsx threads computeLocalModeGate +
|
||||
// requiredCredentialEnvKeys + RequiredFieldLabel so the "New agent" dialog
|
||||
// shows required markers and credential amber rows (parity with
|
||||
@@ -299,13 +303,14 @@ const overrides = new Map([
|
||||
// claude-code-acp-fallback-retirement: the legacy command moved from the
|
||||
// resolution sweep to identity-only aliases; +5 comment lines documenting
|
||||
// the commands/aliases split that move makes load-bearing.
|
||||
// bundled-cli-probes: resolve_probe_binary (bundled-CLI-first probe
|
||||
// resolution) + classify_runtime/underlying_cli doc rewrites for the
|
||||
// CliMissing retirement (+8 lines).
|
||||
// acp-dead-machinery retirement: adapter-availability cache +
|
||||
// availability_drift, runtime_needs_npm/is_npm_global_install, and the
|
||||
// node_required computation deleted; ratcheted 1267 -> 1166.
|
||||
["src-tauri/src/managed_agents/discovery.rs", 1166],
|
||||
// bridge-only bundling: CliMissing classification and user-CLI probe
|
||||
// resolution return with the cli_missing gate; bridge_cli_env_var +
|
||||
// adapter_ships_with_app catalog fields (docs + 4 initializers) and the
|
||||
// restored curl/PowerShell CLI install commands (+15 lines).
|
||||
["src-tauri/src/managed_agents/discovery.rs", 1181],
|
||||
// rebase over codex-acp-package-swap: its version-probe tests union with the
|
||||
// doctor-install-reliability nvm/login-shell/semver tests — each side alone
|
||||
// stayed under the 1000 default; the union exceeds it.
|
||||
@@ -320,11 +325,10 @@ const overrides = new Map([
|
||||
// bundle-acps: version-gate retirement deletes the probe/availability test
|
||||
// sections; ratcheting 1271 -> 1067 to bank the deletions (main's Windows
|
||||
// Doctor test growth keeps this above the 1000 default).
|
||||
// bundled-cli-probes: CliMissing test reworked to assert Available when the
|
||||
// adapter resolves without an underlying CLI (+2 comment lines); main's
|
||||
// Windows install-command tests inverted to assert the bundled runtimes
|
||||
// expose no install commands (-13 lines).
|
||||
["src-tauri/src/managed_agents/discovery/tests.rs", 1056],
|
||||
// bridge-only bundling: classifies_cli_missing returns with the CliMissing
|
||||
// gate + bridge_cli_env_vars catalog mapping test (+24 lines), and main's
|
||||
// Windows install-command tests return with the restored CLI installers.
|
||||
["src-tauri/src/managed_agents/discovery/tests.rs", 1093],
|
||||
// identity-import-keyring: the identity resolution state machine's behavioral
|
||||
// matrix (46 tests over FakeIdentityStore — probe × marker × file cells,
|
||||
// adoption / read-back-corruption / marker-failure arms, recovery-mode
|
||||
@@ -460,7 +464,9 @@ const overrides = new Map([
|
||||
// is_safe_to_reveal allowlist + baked_env_thinking_effort_is_unmasked test.
|
||||
// +1: doctor-install-reliability: login_hint: None added to goose_runtime test stub.
|
||||
// +1: doctor-install-reliability review fixes: auth_probe_args: None added to stub.
|
||||
["src-tauri/src/commands/agent_config.rs", 1021],
|
||||
// +2: bridge-only bundling: bridge_cli_env_var + adapter_ships_with_app
|
||||
// added to the runtime test stub (union with main's Windows stub field).
|
||||
["src-tauri/src/commands/agent_config.rs", 1022],
|
||||
// codex-install-auto-restart review-fixes: should_restart_after_install
|
||||
// takes pid_alive:bool (pure predicate, no OS-dependent call); 3 racy
|
||||
// cache tests replaced with 6 pure availability_drift predicate tests;
|
||||
@@ -486,7 +492,9 @@ const overrides = new Map([
|
||||
// groups, and the availability_drift tests deleted; ratcheted
|
||||
// 1576 -> 1184 to bank the deletions (main's Windows install-shell
|
||||
// machinery and tests stay).
|
||||
["src-tauri/src/commands/agent_discovery.rs", 1184],
|
||||
// bridge-only bundling: runtime_adapter_is_bundled reads the explicit
|
||||
// adapter_ships_with_app catalog field (+2 doc lines).
|
||||
["src-tauri/src/commands/agent_discovery.rs", 1187],
|
||||
// draft-persistence predicate: submit-time `loadDraft` check + inline comment
|
||||
// + deps-array entry in submitMessage closes the never-persisted-boundary
|
||||
// defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to
|
||||
|
||||
@@ -16,10 +16,13 @@ Installs the ACP bridge tools pinned in acp-tools.lock.json into the shared
|
||||
Buzz dev cache. The lockfile is target-specific; only entries matching the
|
||||
requested target are prepared. Each tool is installed as a vendored npm
|
||||
package tree with a small executable wrapper, validated against the locked
|
||||
versions and integrity hashes. Unix targets get a bash wrapper shim; Windows
|
||||
targets get the compiled buzz-acp-node-launcher staged as <binary>.exe next
|
||||
to a <binary>.shim.json manifest (built with cargo, override with
|
||||
ACP_NODE_LAUNCHER_EXE).
|
||||
versions and integrity hashes. Installs are bridge-only: --omit=optional
|
||||
skips the SDK/codex platform packages that vendor native CLIs, so the trees
|
||||
stay pure JS and the desktop app points each bridge at the user's own
|
||||
claude/codex CLI at spawn time. Unix targets get a bash wrapper shim;
|
||||
Windows targets get the compiled buzz-acp-node-launcher staged as
|
||||
<binary>.exe next to a <binary>.shim.json manifest (built with cargo,
|
||||
override with ACP_NODE_LAUNCHER_EXE).
|
||||
|
||||
Environment variables:
|
||||
ACP_TOOLS_LOCK_FILE lockfile path (default: desktop/acp-tools.lock.json)
|
||||
@@ -111,18 +114,10 @@ for (const entry of entries) {
|
||||
"version",
|
||||
"integrity",
|
||||
"tarball",
|
||||
"npmOs",
|
||||
"npmCpu",
|
||||
"dependencyPackage",
|
||||
"dependencyVersion",
|
||||
"dependencyIntegrity",
|
||||
"dependencyTarball",
|
||||
"nativePackage",
|
||||
"nativePackageName",
|
||||
"nativeVersion",
|
||||
"nativeIntegrity",
|
||||
"nativeTarball",
|
||||
"nativeExecutable",
|
||||
]) {
|
||||
requireString(entry, field);
|
||||
}
|
||||
@@ -161,14 +156,9 @@ validate_npm_install() {
|
||||
local dependency_package="$5"
|
||||
local dependency_version="$6"
|
||||
local dependency_integrity="$7"
|
||||
local native_package="$8"
|
||||
local native_package_name="$9"
|
||||
local native_version="${10}"
|
||||
local native_integrity="${11}"
|
||||
local native_executable="${12}"
|
||||
local claude_code_version="${13}"
|
||||
local claude_code_version="$8"
|
||||
|
||||
node - "$install_dir" "$package" "$version" "$integrity" "$dependency_package" "$dependency_version" "$dependency_integrity" "$native_package" "$native_package_name" "$native_version" "$native_integrity" "$native_executable" "$claude_code_version" <<'NODE'
|
||||
node - "$install_dir" "$package" "$version" "$integrity" "$dependency_package" "$dependency_version" "$dependency_integrity" "$claude_code_version" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -180,11 +170,6 @@ const [
|
||||
dependencyPackageName,
|
||||
expectedDependencyVersion,
|
||||
expectedDependencyIntegrity,
|
||||
nativePackageName,
|
||||
expectedNativePackageName,
|
||||
expectedNativeVersion,
|
||||
expectedNativeIntegrity,
|
||||
nativeExecutable,
|
||||
expectedClaudeCodeVersion,
|
||||
] = process.argv.slice(2);
|
||||
|
||||
@@ -242,23 +227,25 @@ assertEqual(
|
||||
`${dependencyPackageName} integrity`,
|
||||
);
|
||||
|
||||
const nativePackageJson = readJson(packagePath(nativePackageName, "package.json"));
|
||||
assertEqual(
|
||||
nativePackageJson.name,
|
||||
expectedNativePackageName,
|
||||
`${nativePackageName} package name`,
|
||||
);
|
||||
assertEqual(
|
||||
nativePackageJson.version,
|
||||
expectedNativeVersion,
|
||||
`${nativePackageName} version`,
|
||||
);
|
||||
fs.accessSync(packagePath(nativePackageName, nativeExecutable), fs.constants.X_OK);
|
||||
assertEqual(
|
||||
packageLockEntry(lock, nativePackageName).integrity,
|
||||
expectedNativeIntegrity,
|
||||
`${nativePackageName} integrity`,
|
||||
// Bridge-only invariant: --omit=optional must have skipped the platform
|
||||
// packages that vendor a native CLI. They install as scope siblings of the
|
||||
// dependency package (`<dependencyPackage>-<platform>`), so any such
|
||||
// directory means a native CLI slipped into the bundle.
|
||||
const dependencySegments = dependencyPackageName.split("/");
|
||||
const dependencyBasename = dependencySegments.at(-1);
|
||||
const scopeDir = path.join(
|
||||
installDir,
|
||||
"node_modules",
|
||||
...dependencySegments.slice(0, -1),
|
||||
);
|
||||
const vendoredPlatformPackages = fs
|
||||
.readdirSync(scopeDir)
|
||||
.filter((name) => name.startsWith(`${dependencyBasename}-`));
|
||||
if (vendoredPlatformPackages.length > 0) {
|
||||
throw new Error(
|
||||
`bridge-only install unexpectedly vendored native CLI package(s): ${vendoredPlatformPackages.join(", ")}`,
|
||||
);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
@@ -272,31 +259,21 @@ for (const entry of entries) {
|
||||
entry.version,
|
||||
entry.integrity,
|
||||
entry.tarball,
|
||||
entry.npmOs,
|
||||
entry.npmCpu,
|
||||
entry.npmLibc ?? "",
|
||||
entry.nodeEngine ?? ">=22",
|
||||
entry.dependencyPackage,
|
||||
entry.dependencyVersion,
|
||||
entry.dependencyIntegrity,
|
||||
entry.dependencyTarball,
|
||||
entry.nativePackage,
|
||||
entry.nativePackageName,
|
||||
entry.nativeVersion,
|
||||
entry.nativeIntegrity,
|
||||
entry.nativeTarball,
|
||||
entry.nativeExecutable,
|
||||
entry.claudeCodeVersion ?? "",
|
||||
].join("\x1f"));
|
||||
}
|
||||
' "$lock_entries" | while IFS=$'\x1f' read -r id binary package version integrity tarball npm_os npm_cpu npm_libc node_engine dependency_package dependency_version dependency_integrity dependency_tarball native_package native_package_name native_version native_integrity native_tarball native_executable claude_code_version; do
|
||||
' "$lock_entries" | while IFS=$'\x1f' read -r id binary package version integrity tarball node_engine dependency_package dependency_version dependency_integrity dependency_tarball claude_code_version; do
|
||||
[[ -n "$id" ]] || continue
|
||||
|
||||
tool_dir="$cache_root/$target/$id/$version"
|
||||
install_dir="$tool_dir/npm"
|
||||
package_dir="$install_dir/node_modules/$package"
|
||||
entrypoint="$package_dir/dist/index.js"
|
||||
native_binary="$install_dir/node_modules/$native_package/$native_executable"
|
||||
staged_bin="$bin_dir/$(acp_staged_binary_name "$binary" "$target")"
|
||||
# Windows shims embed a bin-dir-relative entrypoint: under Git Bash the
|
||||
# absolute cache path is POSIX-style (/c/Users/...), which the native
|
||||
@@ -306,10 +283,13 @@ for (const entry of entries) {
|
||||
# must live next to it, not in the per-version tool_dir: a per-version stamp
|
||||
# stays self-consistent after a lock revert and would skip re-staging.
|
||||
stamp="$staged_bin.stamp"
|
||||
if [[ -x "$staged_bin" && -f "$stamp" && -f "$entrypoint" && -x "$native_binary" ]]; then
|
||||
if [[ -x "$staged_bin" && -f "$stamp" && -f "$entrypoint" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$stamp"
|
||||
if [[ "${STAMP_PACKAGE:-}" == "$package" && "${STAMP_VERSION:-}" == "$version" && "${STAMP_INTEGRITY:-}" == "$integrity" && "${STAMP_DEPENDENCY_PACKAGE:-}" == "$dependency_package" && "${STAMP_DEPENDENCY_VERSION:-}" == "$dependency_version" && "${STAMP_DEPENDENCY_INTEGRITY:-}" == "$dependency_integrity" && "${STAMP_NATIVE_PACKAGE:-}" == "$native_package" && "${STAMP_NATIVE_PACKAGE_NAME:-}" == "$native_package_name" && "${STAMP_NATIVE_VERSION:-}" == "$native_version" && "${STAMP_NATIVE_INTEGRITY:-}" == "$native_integrity" && "${STAMP_NATIVE_EXECUTABLE:-}" == "$native_executable" ]]; then
|
||||
# STAMP_INSTALL_MODE distinguishes bridge-only trees from caches staged by
|
||||
# the retired full-CLI bundling (whose stamps lack the marker): those trees
|
||||
# still vendor the native CLIs and must be reinstalled, not reused.
|
||||
if [[ "${STAMP_INSTALL_MODE:-}" == "bridge-only" && "${STAMP_PACKAGE:-}" == "$package" && "${STAMP_VERSION:-}" == "$version" && "${STAMP_INTEGRITY:-}" == "$integrity" && "${STAMP_DEPENDENCY_PACKAGE:-}" == "$dependency_package" && "${STAMP_DEPENDENCY_VERSION:-}" == "$dependency_version" && "${STAMP_DEPENDENCY_INTEGRITY:-}" == "$dependency_integrity" ]]; then
|
||||
# The npm tree is fresh, but the compiled launcher tracks the crate,
|
||||
# not the lock, so the stamp cannot see it change — refresh it every
|
||||
# run (the copy no-ops when already identical).
|
||||
@@ -323,50 +303,37 @@ for (const entry of entries) {
|
||||
echo "Installing ACP tool $id $version from npm for $target..." >&2
|
||||
rm -rf "$install_dir"
|
||||
mkdir -p "$install_dir" "$bin_dir"
|
||||
npm_args=(
|
||||
install
|
||||
--prefix "$install_dir"
|
||||
--omit=dev
|
||||
--include=optional
|
||||
--ignore-scripts
|
||||
--no-audit
|
||||
--no-fund
|
||||
--os "$npm_os"
|
||||
--cpu "$npm_cpu"
|
||||
)
|
||||
if [[ -n "$npm_libc" ]]; then
|
||||
npm_args+=(--libc "$npm_libc")
|
||||
fi
|
||||
npm_args+=("$package@$version")
|
||||
npm "${npm_args[@]}" >&2
|
||||
# --omit=optional is what makes the install bridge-only: the native
|
||||
# claude/codex CLIs ship as optional platform packages of the pinned
|
||||
# dependency, and skipping them keeps the tree pure JS.
|
||||
npm install \
|
||||
--prefix "$install_dir" \
|
||||
--omit=dev \
|
||||
--omit=optional \
|
||||
--ignore-scripts \
|
||||
--no-audit \
|
||||
--no-fund \
|
||||
"$package@$version" >&2
|
||||
|
||||
validate_npm_install "$install_dir" "$package" "$version" "$integrity" "$dependency_package" "$dependency_version" "$dependency_integrity" "$native_package" "$native_package_name" "$native_version" "$native_integrity" "$native_executable" "$claude_code_version"
|
||||
validate_npm_install "$install_dir" "$package" "$version" "$integrity" "$dependency_package" "$dependency_version" "$dependency_integrity" "$claude_code_version"
|
||||
if acp_target_is_windows "$target"; then
|
||||
write_windows_node_launcher "$staged_bin" "$launcher_exe" "$entrypoint_from_bin_dir" "$node_engine"
|
||||
else
|
||||
write_node_wrapper "$staged_bin" "$entrypoint" "$node_engine"
|
||||
fi
|
||||
{
|
||||
printf 'STAMP_INSTALL_MODE=bridge-only\n'
|
||||
printf 'STAMP_TARGET=%q\n' "$target"
|
||||
printf 'STAMP_PACKAGE=%q\n' "$package"
|
||||
printf 'STAMP_VERSION=%q\n' "$version"
|
||||
printf 'STAMP_INTEGRITY=%q\n' "$integrity"
|
||||
printf 'STAMP_TARBALL=%q\n' "$tarball"
|
||||
printf 'STAMP_NPM_OS=%q\n' "$npm_os"
|
||||
printf 'STAMP_NPM_CPU=%q\n' "$npm_cpu"
|
||||
printf 'STAMP_NPM_LIBC=%q\n' "$npm_libc"
|
||||
printf 'STAMP_NODE_ENGINE=%q\n' "$node_engine"
|
||||
printf 'STAMP_DEPENDENCY_PACKAGE=%q\n' "$dependency_package"
|
||||
printf 'STAMP_DEPENDENCY_VERSION=%q\n' "$dependency_version"
|
||||
printf 'STAMP_DEPENDENCY_INTEGRITY=%q\n' "$dependency_integrity"
|
||||
printf 'STAMP_DEPENDENCY_TARBALL=%q\n' "$dependency_tarball"
|
||||
printf 'STAMP_CLAUDE_CODE_VERSION=%q\n' "$claude_code_version"
|
||||
printf 'STAMP_NATIVE_PACKAGE=%q\n' "$native_package"
|
||||
printf 'STAMP_NATIVE_PACKAGE_NAME=%q\n' "$native_package_name"
|
||||
printf 'STAMP_NATIVE_VERSION=%q\n' "$native_version"
|
||||
printf 'STAMP_NATIVE_INTEGRITY=%q\n' "$native_integrity"
|
||||
printf 'STAMP_NATIVE_TARBALL=%q\n' "$native_tarball"
|
||||
printf 'STAMP_NATIVE_EXECUTABLE=%q\n' "$native_executable"
|
||||
printf 'STAMP_BINARY=%q\n' "$binary"
|
||||
} > "$stamp"
|
||||
done
|
||||
|
||||
@@ -67,15 +67,11 @@ node_runtime_manifest="$resource_root/node-runtime.json"
|
||||
rm -f "$node_runtime_manifest"
|
||||
node_runtime_entries=()
|
||||
|
||||
# Manifest of the native harness CLIs vendored inside the bundled bridges
|
||||
# (e.g. `claude` inside the claude-agent-sdk native package, `codex` inside
|
||||
# @openai/codex). The app resolves auth probes against these pinned binaries
|
||||
# instead of user installs. Kept OUT of resources/acp/bin on purpose: that
|
||||
# dir is the highest-priority segment of the agent-spawn PATH, and staging
|
||||
# `claude`/`codex` there would shadow the user's CLIs inside every session.
|
||||
harness_cli_manifest="$resource_root/harness-clis.json"
|
||||
rm -f "$harness_cli_manifest"
|
||||
harness_cli_entries=()
|
||||
# Stale artifact of the retired full-CLI bundling: the harness CLI manifest
|
||||
# pointed auth probes at CLIs vendored inside the bundles. Bridge-only
|
||||
# bundles carry no CLIs, so remove any leftover manifest from an earlier
|
||||
# stage — it is unread, but it would otherwise ride into the app bundle.
|
||||
rm -f "$resource_root/harness-clis.json"
|
||||
|
||||
# Ad-hoc signing failure is a warning, not a hard stop: an unsignable Mach-O
|
||||
# fragment that never executes should not sink the stage, and release builds
|
||||
@@ -93,7 +89,7 @@ codesign_if_darwin() {
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS=$'\t' read -r id binary package version node_engine native_package native_executable; do
|
||||
while IFS=$'\t' read -r id binary package version node_engine; do
|
||||
[[ -n "$id" ]] || continue
|
||||
install_dir="$cache_root/$target/$id/$version/npm"
|
||||
entrypoint="$install_dir/node_modules/$package/dist/index.js"
|
||||
@@ -118,29 +114,10 @@ while IFS=$'\t' read -r id binary package version node_engine native_package nat
|
||||
write_node_wrapper "$resource_bin_dir/$binary" "../node/$id/node_modules/$package/dist/index.js" "$node_engine"
|
||||
fi
|
||||
node_runtime_entries+=("$id"$'\t'"$binary"$'\t'"$node_engine"$'\t'"$(acp_required_node_major "$node_engine")")
|
||||
# Record the vendored native harness CLI (relative to the acp resource
|
||||
# root) for the auth-probe manifest. Fail loudly if the lock names one
|
||||
# that is not in the staged tree — a silent miss would quietly send auth
|
||||
# probes back to unpinned user installs.
|
||||
if [[ -n "$native_package" && -n "$native_executable" ]]; then
|
||||
cli_relpath="node/$id/node_modules/$native_package/$native_executable"
|
||||
cli_abspath="$resource_root/$cli_relpath"
|
||||
if [[ ! -f "$cli_abspath" ]]; then
|
||||
echo "Locked native harness CLI missing from staged tree: $cli_relpath" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$cli_abspath"
|
||||
# The manifest keys CLIs by the bare probe name the app resolves
|
||||
# ("claude", "codex"), so the Windows vendored executables drop their
|
||||
# .exe suffix here.
|
||||
cli_name="$(basename "$native_executable")"
|
||||
cli_name="${cli_name%.exe}"
|
||||
harness_cli_entries+=("$id"$'\t'"$cli_name"$'\t'"$cli_relpath")
|
||||
fi
|
||||
# Ad-hoc sign every Mach-O in the staged package, not just the main CLIs:
|
||||
# the codex native package also vendors executables like rg and zsh, and
|
||||
# unsigned nested Mach-Os are killed by Gatekeeper. Darwin only, so Linux
|
||||
# staging skips the file(1) scan.
|
||||
# Ad-hoc sign every Mach-O in the staged package. Bridge-only trees are
|
||||
# pure JS, so this scan normally finds nothing — kept because unsigned
|
||||
# nested Mach-Os are killed by Gatekeeper, and a future dependency could
|
||||
# reintroduce one. Darwin only, so Linux staging skips the file(1) scan.
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
while IFS= read -r -d '' candidate; do
|
||||
if file -b "$candidate" | grep -q "Mach-O"; then
|
||||
@@ -157,15 +134,7 @@ for (const entry of data.tools ?? []) {
|
||||
if (entry.source !== "npm") {
|
||||
throw new Error(`Unsupported ACP tool source: ${entry.source}`);
|
||||
}
|
||||
console.log([
|
||||
entry.id,
|
||||
entry.binary,
|
||||
entry.package,
|
||||
entry.version,
|
||||
entry.nodeEngine ?? ">=22",
|
||||
entry.nativePackage ?? "",
|
||||
entry.nativeExecutable ?? "",
|
||||
].join("\t"));
|
||||
console.log([entry.id, entry.binary, entry.package, entry.version, entry.nodeEngine ?? ">=22"].join("\t"));
|
||||
}
|
||||
NODE
|
||||
)
|
||||
@@ -186,20 +155,4 @@ fs.writeFileSync(manifestFile, `${JSON.stringify({ tools }, null, 2)}\n`);
|
||||
echo "Wrote ACP Node runtime manifest: $node_runtime_manifest"
|
||||
fi
|
||||
|
||||
# One manifest entry per vendored native harness CLI, keyed by the bare CLI
|
||||
# name the app's auth probes use (`claude`, `codex`). Paths are relative to
|
||||
# the acp resource root (the bin dir's parent).
|
||||
if ((${#harness_cli_entries[@]} > 0)); then
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const [manifestFile, ...entries] = process.argv.slice(1);
|
||||
const clis = entries.map((line) => {
|
||||
const [id, cli, path] = line.split("\t");
|
||||
return { id, cli, path };
|
||||
});
|
||||
fs.writeFileSync(manifestFile, `${JSON.stringify({ clis }, null, 2)}\n`);
|
||||
' "$harness_cli_manifest" ${harness_cli_entries[@]+"${harness_cli_entries[@]}"}
|
||||
echo "Wrote ACP harness CLI manifest: $harness_cli_manifest"
|
||||
fi
|
||||
|
||||
echo "Staged ACP tools resource: $resource_bin_dir"
|
||||
|
||||
@@ -21,13 +21,18 @@ const SUPPORTED_TARGETS = [
|
||||
// Zed package.
|
||||
const CODEX_ACP_PACKAGE = "@agentclientprotocol/codex-acp";
|
||||
|
||||
// Bridge-only bundling: only the bridge JS trees are pinned and installed
|
||||
// (`npm install --omit=optional` skips the SDK/codex platform packages that
|
||||
// vendor the native claude/codex CLIs). The desktop app points each bridge at
|
||||
// the user's own CLI via CLAUDE_CODE_EXECUTABLE / CODEX_PATH at spawn time.
|
||||
// The trees are therefore platform-independent, but the lock stays per-target
|
||||
// so a single target's pins can still be bumped or dropped independently.
|
||||
const TOOL_SPECS = [
|
||||
{
|
||||
id: "claude-acp",
|
||||
binary: "claude-agent-acp",
|
||||
package: "@agentclientprotocol/claude-agent-acp",
|
||||
dependencyPackage: "@anthropic-ai/claude-agent-sdk",
|
||||
nativePackageKey: "claudeAgentSdk",
|
||||
includeClaudeCodeVersion: true,
|
||||
},
|
||||
{
|
||||
@@ -35,75 +40,9 @@ const TOOL_SPECS = [
|
||||
binary: "codex-acp",
|
||||
package: CODEX_ACP_PACKAGE,
|
||||
dependencyPackage: "@openai/codex",
|
||||
nativePackageKey: "openaiCodex",
|
||||
},
|
||||
];
|
||||
|
||||
const NPM_TARGET_CONFIG = {
|
||||
"aarch64-apple-darwin": {
|
||||
npmOs: "darwin",
|
||||
npmCpu: "arm64",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-darwin-arm64",
|
||||
openaiCodex: "@openai/codex-darwin-arm64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/aarch64-apple-darwin/bin/codex",
|
||||
},
|
||||
},
|
||||
"x86_64-apple-darwin": {
|
||||
npmOs: "darwin",
|
||||
npmCpu: "x64",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-darwin-x64",
|
||||
openaiCodex: "@openai/codex-darwin-x64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/x86_64-apple-darwin/bin/codex",
|
||||
},
|
||||
},
|
||||
"aarch64-unknown-linux-gnu": {
|
||||
npmOs: "linux",
|
||||
npmCpu: "arm64",
|
||||
npmLibc: "glibc",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-linux-arm64",
|
||||
openaiCodex: "@openai/codex-linux-arm64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/aarch64-unknown-linux-musl/bin/codex",
|
||||
},
|
||||
},
|
||||
"x86_64-unknown-linux-gnu": {
|
||||
npmOs: "linux",
|
||||
npmCpu: "x64",
|
||||
npmLibc: "glibc",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-linux-x64",
|
||||
openaiCodex: "@openai/codex-linux-x64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude",
|
||||
openaiCodex: "vendor/x86_64-unknown-linux-musl/bin/codex",
|
||||
},
|
||||
},
|
||||
"x86_64-pc-windows-msvc": {
|
||||
npmOs: "win32",
|
||||
npmCpu: "x64",
|
||||
nativePackages: {
|
||||
claudeAgentSdk: "@anthropic-ai/claude-agent-sdk-win32-x64",
|
||||
openaiCodex: "@openai/codex-win32-x64",
|
||||
},
|
||||
nativeExecutables: {
|
||||
claudeAgentSdk: "claude.exe",
|
||||
openaiCodex: "vendor/x86_64-pc-windows-msvc/bin/codex.exe",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Tarball URLs in the lock are informational — ensure-acp-tools.sh installs
|
||||
// via the ambient npm registry and validates the registry-agnostic sha512
|
||||
// integrity. Normalize them to the public registry so regenerating against an
|
||||
@@ -118,9 +57,11 @@ function usage() {
|
||||
console.log(`Usage: desktop/scripts/update-acp-tools-lock.mjs [--target <triple>]... [--lock-file <path>]
|
||||
|
||||
Queries npm for the latest release of each supported ACP bridge tool and
|
||||
writes acp-tools.lock.json. Fails loudly when a package or one of its
|
||||
per-target native dependencies cannot be resolved — never silently pins an
|
||||
older version.
|
||||
writes acp-tools.lock.json. Only the bridge JS packages are pinned — the
|
||||
SDK/codex platform packages that vendor native CLIs are optional dependencies
|
||||
the install omits; the app runs the user's own claude/codex CLI. Fails loudly
|
||||
when a package or its pinned dependency cannot be resolved — never silently
|
||||
pins an older version.
|
||||
|
||||
A --target run regenerates only the selected targets; the existing lock's
|
||||
entries for every other target are preserved verbatim, so a partial bump
|
||||
@@ -203,8 +144,8 @@ function requireString(value, label) {
|
||||
}
|
||||
|
||||
// Rebuild the tarball URL on PUBLIC_NPM_REGISTRY, keeping the registry's
|
||||
// `<package>/-/<basename>.tgz` path (the basename is not always derivable
|
||||
// from name@version — @openai/codex native tarballs carry a platform suffix).
|
||||
// `<package>/-/<basename>.tgz` path rather than deriving the basename from
|
||||
// name@version — registries own that naming, not us.
|
||||
function normalizeTarballUrl(tarball, packageName, label) {
|
||||
const separator = "/-/";
|
||||
const separatorIndex = tarball.lastIndexOf(separator);
|
||||
@@ -257,27 +198,7 @@ function pickLatestMatch(metadata, label) {
|
||||
);
|
||||
}
|
||||
|
||||
function parseNpmAliasSpec(spec, fallbackPackage) {
|
||||
if (!spec.startsWith("npm:")) {
|
||||
return { packageName: fallbackPackage, version: spec };
|
||||
}
|
||||
const aliased = spec.slice("npm:".length);
|
||||
const versionSeparator = aliased.lastIndexOf("@");
|
||||
if (versionSeparator <= 0) {
|
||||
throw new Error(`Unsupported npm alias spec: ${spec}`);
|
||||
}
|
||||
return {
|
||||
packageName: aliased.slice(0, versionSeparator),
|
||||
version: aliased.slice(versionSeparator + 1),
|
||||
};
|
||||
}
|
||||
|
||||
async function lockToolForTarget(tool, target) {
|
||||
const npmTarget = NPM_TARGET_CONFIG[target];
|
||||
if (!npmTarget) {
|
||||
throw new Error(`No npm target mapping for ${target}`);
|
||||
}
|
||||
|
||||
const packageName = tool.package;
|
||||
const packageMetadata = await npmView(`${packageName}@latest`, [
|
||||
"name",
|
||||
@@ -308,9 +229,6 @@ async function lockToolForTarget(tool, target) {
|
||||
integrity: packageInfo.integrity,
|
||||
tarball: packageInfo.tarball,
|
||||
target,
|
||||
npmOs: npmTarget.npmOs,
|
||||
npmCpu: npmTarget.npmCpu,
|
||||
...(npmTarget.npmLibc ? { npmLibc: npmTarget.npmLibc } : {}),
|
||||
nodeEngine: packageMetadata.engines?.node ?? ">=22",
|
||||
};
|
||||
|
||||
@@ -323,7 +241,6 @@ async function lockToolForTarget(tool, target) {
|
||||
"name",
|
||||
"version",
|
||||
"dist",
|
||||
"optionalDependencies",
|
||||
"claudeCodeVersion",
|
||||
]),
|
||||
`${tool.dependencyPackage}@${dependencyRange}`,
|
||||
@@ -344,46 +261,7 @@ async function lockToolForTarget(tool, target) {
|
||||
entry.claudeCodeVersion = dependencyMetadata.claudeCodeVersion ?? null;
|
||||
}
|
||||
|
||||
const nativePackage = requireString(
|
||||
npmTarget.nativePackages?.[tool.nativePackageKey],
|
||||
`${target} native package for ${tool.nativePackageKey}`,
|
||||
);
|
||||
const nativeExecutable = requireString(
|
||||
npmTarget.nativeExecutables?.[tool.nativePackageKey],
|
||||
`${target} native executable for ${tool.nativePackageKey}`,
|
||||
);
|
||||
const nativeSpec = requireString(
|
||||
dependencyMetadata.optionalDependencies?.[nativePackage],
|
||||
`${tool.dependencyPackage}@${dependencyVersion} optional dependency ${nativePackage}`,
|
||||
);
|
||||
const nativeAlias = parseNpmAliasSpec(nativeSpec, nativePackage);
|
||||
const nativeMetadata = await npmView(
|
||||
`${nativeAlias.packageName}@${nativeAlias.version}`,
|
||||
["name", "version", "dist"],
|
||||
);
|
||||
const nativeVersion = requireString(
|
||||
nativeMetadata.version,
|
||||
`${nativeAlias.packageName}@${nativeAlias.version} version`,
|
||||
);
|
||||
if (nativeVersion !== nativeAlias.version) {
|
||||
throw new Error(
|
||||
`${nativeAlias.packageName}@${nativeAlias.version} resolved to ${nativeVersion}`,
|
||||
);
|
||||
}
|
||||
const nativeInfo = packageDist(
|
||||
nativeMetadata,
|
||||
`${nativeAlias.packageName}@${nativeVersion}`,
|
||||
);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
nativePackage,
|
||||
nativePackageName: nativeMetadata.name ?? nativePackage,
|
||||
nativeVersion,
|
||||
nativeIntegrity: nativeInfo.integrity,
|
||||
nativeTarball: nativeInfo.tarball,
|
||||
nativeExecutable,
|
||||
};
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Entries preserved from the existing lock when --target selects a subset.
|
||||
|
||||
@@ -581,9 +581,11 @@ mod tests {
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: None,
|
||||
bridge_cli_env_var: None,
|
||||
cli_install_commands: &[],
|
||||
cli_install_commands_windows: &[],
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: false,
|
||||
install_instructions_url: "",
|
||||
cli_install_hint: "",
|
||||
adapter_install_hint: "",
|
||||
|
||||
@@ -89,11 +89,13 @@ fn adapter_verification_step(
|
||||
}
|
||||
|
||||
/// A runtime's adapter ships with the Buzz desktop app when its catalog entry
|
||||
/// carries no install commands at all — neither CLI nor adapter. Goose has a
|
||||
/// says so (`adapter_ships_with_app`). Inferring this from empty install
|
||||
/// commands broke once the bridge-only bundles brought back CLI install
|
||||
/// commands for claude/codex — their bridges are still bundled. Goose has a
|
||||
/// curl CLI installer (and its CLI *is* its adapter), so a failed goose verify
|
||||
/// must not claim the adapter is bundled and point at reinstalling Buzz.
|
||||
fn runtime_adapter_is_bundled(runtime: &crate::managed_agents::KnownAcpRuntime) -> bool {
|
||||
runtime.cli_install_commands.is_empty() && runtime.adapter_install_commands.is_empty()
|
||||
runtime.adapter_ships_with_app
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1166,7 +1168,8 @@ mod tests {
|
||||
.unwrap_or_else(|| panic!("{id} must be in the catalog"));
|
||||
assert!(
|
||||
runtime_adapter_is_bundled(runtime),
|
||||
"{id} carries no install commands — its adapter ships with Buzz"
|
||||
"{id}'s adapter ships with Buzz (bundled bridge or sidecar), \
|
||||
even though its user CLI may have install commands"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,6 @@ const ACP_TOOLS_RESOURCE_DIR: &str = "resources/acp/bin";
|
||||
/// Node runtime manifest staged by `desktop/scripts/prepare-acp-tools-resource.sh`
|
||||
/// next to the bundled bin dir.
|
||||
const NODE_RUNTIME_MANIFEST_FILE: &str = "node-runtime.json";
|
||||
/// Harness CLI manifest staged next to the bin dir: one entry per native CLI
|
||||
/// vendored inside a bundled bridge (`claude` inside the claude-agent-sdk
|
||||
/// native package, `codex` inside @openai/codex), with paths relative to the
|
||||
/// acp resource root.
|
||||
const HARNESS_CLI_MANIFEST_FILE: &str = "harness-clis.json";
|
||||
|
||||
static BUNDLED_ACP_TOOLS_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
|
||||
@@ -84,52 +79,6 @@ pub(in crate::managed_agents) fn node_runtime_manifest_path(bin_dir: &Path) -> O
|
||||
.map(|dir| dir.join(NODE_RUNTIME_MANIFEST_FILE))
|
||||
}
|
||||
|
||||
/// On-disk shape of `resources/acp/harness-clis.json`.
|
||||
#[derive(serde::Deserialize)]
|
||||
struct HarnessCliManifest {
|
||||
#[serde(default)]
|
||||
clis: Vec<HarnessCliEntry>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct HarnessCliEntry {
|
||||
cli: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
/// Resolve the pinned native harness CLI (`claude`, `codex`) vendored inside
|
||||
/// a bundled bridge, via the staged `harness-clis.json` manifest. This is the
|
||||
/// same binary the bridge itself runs, so auth probes against it can never
|
||||
/// drift from what agent sessions see. Bare command names only, mirroring
|
||||
/// [`command_in_bundled_dir`]. Deliberately separate from the bin dir: these
|
||||
/// CLIs must never join the agent-spawn PATH, where they would shadow the
|
||||
/// user's (possibly newer) installs inside every session.
|
||||
pub(in crate::managed_agents) fn bundled_harness_cli(cli: &str) -> Option<PathBuf> {
|
||||
let bin_dir = bundled_acp_tools_dir()?;
|
||||
bundled_harness_cli_in_root(bin_dir.parent()?, cli)
|
||||
}
|
||||
|
||||
fn bundled_harness_cli_in_root(acp_root: &Path, cli: &str) -> Option<PathBuf> {
|
||||
if command_looks_like_path(cli) {
|
||||
return None;
|
||||
}
|
||||
let manifest = std::fs::read_to_string(acp_root.join(HARNESS_CLI_MANIFEST_FILE)).ok()?;
|
||||
let manifest: HarnessCliManifest = serde_json::from_str(&manifest).ok()?;
|
||||
let entry = manifest.clis.into_iter().find(|entry| entry.cli == cli)?;
|
||||
let relative = PathBuf::from(entry.path);
|
||||
// Manifest paths are acp-root-relative by contract; anything absolute or
|
||||
// escaping the root is malformed and must not resolve.
|
||||
let escapes = relative.is_absolute()
|
||||
|| relative
|
||||
.components()
|
||||
.any(|component| matches!(component, std::path::Component::ParentDir));
|
||||
if escapes {
|
||||
return None;
|
||||
}
|
||||
let candidate = acp_root.join(relative);
|
||||
is_executable_file(&candidate).then_some(candidate)
|
||||
}
|
||||
|
||||
fn command_in_dir(dir: &Path, command: &str) -> Option<PathBuf> {
|
||||
if command_looks_like_path(command) {
|
||||
return None;
|
||||
@@ -152,10 +101,7 @@ fn bundled_acp_tools_dir_from_parts(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
bundled_acp_tools_dir_from_parts, bundled_harness_cli_in_root, command_in_dir,
|
||||
path_is_in_dir,
|
||||
};
|
||||
use super::{bundled_acp_tools_dir_from_parts, command_in_dir, path_is_in_dir};
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -255,74 +201,6 @@ mod tests {
|
||||
assert!(command_in_dir(temp.path(), "custom/codex-acp").is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn bundled_harness_cli_resolves_manifest_relative_path() {
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let vendored = temp.path().join("node/claude-acp/node_modules/sdk-native");
|
||||
fs::create_dir_all(&vendored).expect("vendored dir");
|
||||
let cli = vendored.join("claude");
|
||||
fs::write(&cli, "#!/bin/sh\n").expect("write cli");
|
||||
fs::set_permissions(&cli, fs::Permissions::from_mode(0o755)).expect("chmod cli");
|
||||
fs::write(
|
||||
temp.path().join("harness-clis.json"),
|
||||
r#"{"clis":[{"id":"claude-acp","cli":"claude","path":"node/claude-acp/node_modules/sdk-native/claude"}]}"#,
|
||||
)
|
||||
.expect("write manifest");
|
||||
|
||||
assert_eq!(
|
||||
bundled_harness_cli_in_root(temp.path(), "claude").as_deref(),
|
||||
Some(cli.as_path()),
|
||||
);
|
||||
assert!(
|
||||
bundled_harness_cli_in_root(temp.path(), "codex").is_none(),
|
||||
"a CLI absent from the manifest must not resolve"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_harness_cli_without_manifest_is_none() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
assert!(bundled_harness_cli_in_root(temp.path(), "claude").is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn bundled_harness_cli_rejects_escaping_paths_and_path_like_names() {
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let outside = temp.path().join("outside");
|
||||
fs::write(&outside, "#!/bin/sh\n").expect("write outside");
|
||||
fs::set_permissions(&outside, fs::Permissions::from_mode(0o755)).expect("chmod outside");
|
||||
let root = temp.path().join("acp");
|
||||
fs::create_dir_all(&root).expect("acp root");
|
||||
fs::write(
|
||||
root.join("harness-clis.json"),
|
||||
format!(
|
||||
r#"{{"clis":[{{"id":"a","cli":"escape","path":"../outside"}},{{"id":"b","cli":"absolute","path":"{}"}}]}}"#,
|
||||
outside.display()
|
||||
),
|
||||
)
|
||||
.expect("write manifest");
|
||||
|
||||
assert!(
|
||||
bundled_harness_cli_in_root(&root, "escape").is_none(),
|
||||
"a ..-escaping manifest path must not resolve"
|
||||
);
|
||||
assert!(
|
||||
bundled_harness_cli_in_root(&root, "absolute").is_none(),
|
||||
"an absolute manifest path must not resolve"
|
||||
);
|
||||
// Path-like probe names bypass the manifest entirely — they name a
|
||||
// specific binary and must fall through to regular resolution.
|
||||
assert!(bundled_harness_cli_in_root(&root, "some/claude").is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn command_in_dir_skips_non_executable_files() {
|
||||
|
||||
@@ -37,9 +37,11 @@ fn test_runtime() -> &'static KnownAcpRuntime {
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: None,
|
||||
bridge_cli_env_var: None,
|
||||
cli_install_commands: &[],
|
||||
cli_install_commands_windows: &[],
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: false,
|
||||
install_instructions_url: "",
|
||||
cli_install_hint: "",
|
||||
adapter_install_hint: "",
|
||||
@@ -612,9 +614,11 @@ fn buzz_agent_runtime() -> &'static KnownAcpRuntime {
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: None,
|
||||
bridge_cli_env_var: None,
|
||||
cli_install_commands: &[],
|
||||
cli_install_commands_windows: &[],
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: false,
|
||||
install_instructions_url: "",
|
||||
cli_install_hint: "",
|
||||
adapter_install_hint: "",
|
||||
|
||||
@@ -21,11 +21,16 @@ pub(crate) struct KnownAcpRuntime {
|
||||
pub mcp_command: Option<&'static str>,
|
||||
/// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent.
|
||||
pub mcp_hooks: bool,
|
||||
/// CLI binary whose presence distinguishes `AdapterMissing` from
|
||||
/// `NotInstalled` when the adapter is absent. `None` for the bundled
|
||||
/// bridges (claude, codex): they ship their own vendored CLI, so the
|
||||
/// user's install neither gates availability nor serves auth probes.
|
||||
/// CLI binary that indicates partial install (e.g. `"claude"` when `claude-agent-acp` is missing).
|
||||
pub underlying_cli: Option<&'static str>,
|
||||
/// Env var through which the bundled ACP bridge finds the user's
|
||||
/// `underlying_cli` binary. The bridge-only bundles vendor no CLI: the
|
||||
/// claude bridge has no PATH fallback (`CLAUDE_CODE_EXECUTABLE` or the
|
||||
/// omitted SDK native package), and the codex bridge's `CODEX_PATH`
|
||||
/// fallback hard-requires the omitted platform package — so spawn exports
|
||||
/// the resolved user CLI here. `None` for runtimes whose adapter needs no
|
||||
/// separate CLI handle.
|
||||
pub bridge_cli_env_var: Option<&'static str>,
|
||||
/// Shell commands to install the runtime CLI itself (run sequentially).
|
||||
pub cli_install_commands: &'static [&'static str],
|
||||
/// Windows-specific CLI install commands (e.g. PowerShell installers).
|
||||
@@ -34,6 +39,13 @@ pub(crate) struct KnownAcpRuntime {
|
||||
pub cli_install_commands_windows: &'static [&'static str],
|
||||
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
|
||||
pub adapter_install_commands: &'static [&'static str],
|
||||
/// Whether the ACP adapter binary ships inside the Buzz desktop app (the
|
||||
/// bundled bridges and the buzz-agent sidecar) rather than being
|
||||
/// user-installed. Drives the post-install verification hint: a bundled
|
||||
/// adapter that fails to resolve means a broken Buzz installation
|
||||
/// ("Reinstall Buzz"), not a failed install step. False for goose, whose
|
||||
/// adapter is the user-installed goose CLI itself.
|
||||
pub adapter_ships_with_app: bool,
|
||||
/// Link to docs/repo for manual instructions.
|
||||
pub install_instructions_url: &'static str,
|
||||
/// Human-readable hint about installing the CLI binary.
|
||||
@@ -147,9 +159,11 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: Some("goose"),
|
||||
bridge_cli_env_var: None,
|
||||
cli_install_commands: &["curl -fsSL https://github.com/block-open-source/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash"],
|
||||
cli_install_commands_windows: &[], // goose install script is already Windows-aware
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: false,
|
||||
install_instructions_url: "https://block.github.io/goose/",
|
||||
cli_install_hint: "Install Goose via the official install script.",
|
||||
adapter_install_hint: "",
|
||||
@@ -179,12 +193,14 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
avatar_url: CLAUDE_CODE_AVATAR_URL,
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli: None,
|
||||
cli_install_commands: &[],
|
||||
cli_install_commands_windows: &[],
|
||||
underlying_cli: Some("claude"),
|
||||
bridge_cli_env_var: Some("CLAUDE_CODE_EXECUTABLE"),
|
||||
cli_install_commands: &["curl -fsSL https://claude.ai/install.sh | bash"],
|
||||
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://claude.ai/install.ps1 | iex\""],
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: true,
|
||||
install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp",
|
||||
cli_install_hint: "",
|
||||
cli_install_hint: "Install the Claude Code CLI via the official install script.",
|
||||
adapter_install_hint: "The Claude Code ACP adapter ships with the Buzz desktop app. Reinstall Buzz to restore it.",
|
||||
skill_dir: Some(".claude/skills"),
|
||||
supports_acp_model_switching: false,
|
||||
@@ -199,7 +215,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
max_tokens_env_var: None,
|
||||
context_limit_env_var: None,
|
||||
required_normalized_fields: &[],
|
||||
login_hint: Some("Run the Claude CLI to complete authentication (install it first if needed)."),
|
||||
login_hint: Some("Run the Claude CLI to complete authentication."),
|
||||
auth_probe_args: Some(&["claude", "auth", "status"]),
|
||||
},
|
||||
KnownAcpRuntime {
|
||||
@@ -210,12 +226,14 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
avatar_url: CODEX_AVATAR_URL,
|
||||
mcp_command: Some("buzz-dev-mcp"),
|
||||
mcp_hooks: false,
|
||||
underlying_cli: None,
|
||||
cli_install_commands: &[],
|
||||
cli_install_commands_windows: &[],
|
||||
underlying_cli: Some("codex"),
|
||||
bridge_cli_env_var: Some("CODEX_PATH"),
|
||||
cli_install_commands: &["curl -fsSL https://chatgpt.com/codex/install.sh | sh"],
|
||||
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://chatgpt.com/codex/install.ps1 | iex\""],
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: true,
|
||||
install_instructions_url: "https://github.com/agentclientprotocol/codex-acp",
|
||||
cli_install_hint: "",
|
||||
cli_install_hint: "Install the Codex CLI via the official install script.",
|
||||
adapter_install_hint: "The Codex ACP adapter ships with the Buzz desktop app. Reinstall Buzz to restore it.",
|
||||
skill_dir: Some(".codex/skills"),
|
||||
supports_acp_model_switching: false,
|
||||
@@ -230,7 +248,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
max_tokens_env_var: None,
|
||||
context_limit_env_var: None,
|
||||
required_normalized_fields: &[],
|
||||
login_hint: Some("Run `codex login` to authenticate (install the Codex CLI first if needed)."),
|
||||
login_hint: Some("Run `codex login` to authenticate."),
|
||||
// Verified: `codex login status` exits 0 when logged in, non-zero otherwise.
|
||||
auth_probe_args: Some(&["codex", "login", "status"]),
|
||||
},
|
||||
@@ -243,9 +261,11 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
mcp_command: Some("buzz-dev-mcp"),
|
||||
mcp_hooks: true,
|
||||
underlying_cli: None,
|
||||
bridge_cli_env_var: None,
|
||||
cli_install_commands: &[],
|
||||
cli_install_commands_windows: &[],
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: true,
|
||||
install_instructions_url: "https://github.com/block/buzz",
|
||||
cli_install_hint: "Ships with the Buzz desktop app.",
|
||||
adapter_install_hint: "",
|
||||
@@ -876,16 +896,6 @@ pub(crate) fn find_command(command: &str) -> Option<PathBuf> {
|
||||
resolve_command(command)
|
||||
}
|
||||
|
||||
/// Resolve the binary for a CLI auth probe (`claude`, `codex`): the pinned
|
||||
/// CLI vendored inside the bundled bridge wins — it is the exact binary agent
|
||||
/// sessions run and reads the same credential store — falling back to the
|
||||
/// user's install for builds without staged bundle resources. Not routed
|
||||
/// through `resolve_command`: probe binaries must stay out of the resolve
|
||||
/// cache and off the agent-spawn PATH.
|
||||
pub(crate) fn resolve_probe_binary(cli: &str) -> Option<PathBuf> {
|
||||
super::acp_tools::bundled_harness_cli(cli).or_else(|| resolve_command(cli))
|
||||
}
|
||||
|
||||
/// Run a CLI auth probe with a 10-second process-level timeout.
|
||||
///
|
||||
/// Spawns the probe CLI as a child process. Stdout and stderr are drained on
|
||||
@@ -1003,21 +1013,25 @@ pub fn missing_command_message(command: &str, role: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// A resolving adapter is available, full stop — whether the runtime is
|
||||
/// usable beyond that is an auth question (`auth_status`), not an install
|
||||
/// question. The retired `CliMissing` state gated availability on a user
|
||||
/// CLI install the bundled bridges no longer need.
|
||||
pub(crate) fn classify_runtime(
|
||||
adapter_result: Option<(&str, PathBuf)>,
|
||||
underlying_cli: Option<&str>,
|
||||
underlying_cli_found: bool,
|
||||
) -> (AcpAvailabilityStatus, Option<String>, Option<String>) {
|
||||
if let Some((cmd, path)) = adapter_result {
|
||||
(
|
||||
AcpAvailabilityStatus::Available,
|
||||
Some(cmd.to_string()),
|
||||
Some(path.display().to_string()),
|
||||
)
|
||||
if underlying_cli.is_some() && !underlying_cli_found {
|
||||
(
|
||||
AcpAvailabilityStatus::CliMissing,
|
||||
Some(cmd.to_string()),
|
||||
Some(path.display().to_string()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
AcpAvailabilityStatus::Available,
|
||||
Some(cmd.to_string()),
|
||||
Some(path.display().to_string()),
|
||||
)
|
||||
}
|
||||
} else if underlying_cli.is_some() && underlying_cli_found {
|
||||
(AcpAvailabilityStatus::AdapterMissing, None, None)
|
||||
} else {
|
||||
@@ -1068,6 +1082,7 @@ pub fn discover_acp_runtimes() -> Vec<AcpRuntimeCatalogEntry> {
|
||||
let adapter_hint = runtime.adapter_install_hint;
|
||||
let install_hint = match availability {
|
||||
AcpAvailabilityStatus::Available => cli_hint.to_string(),
|
||||
AcpAvailabilityStatus::CliMissing => cli_hint.to_string(),
|
||||
AcpAvailabilityStatus::AdapterMissing => adapter_hint.to_string(),
|
||||
AcpAvailabilityStatus::NotInstalled => {
|
||||
if !cli_hint.is_empty() && !adapter_hint.is_empty() {
|
||||
@@ -1114,8 +1129,8 @@ pub fn discover_acp_runtimes() -> Vec<AcpRuntimeCatalogEntry> {
|
||||
return None;
|
||||
}
|
||||
let probe_args = partial.runtime.auth_probe_args?;
|
||||
// Probe the bundled CLI when the app ships one, else the user's.
|
||||
let binary_path = resolve_probe_binary(probe_args[0])?;
|
||||
// Need the resolved binary path for the CLI (e.g. the actual `claude` binary).
|
||||
let binary_path = resolve_command(probe_args[0])?;
|
||||
let probe_args_owned: Vec<String> = probe_args.iter().map(|s| s.to_string()).collect();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
|
||||
@@ -176,19 +176,42 @@ fn classifies_not_installed_when_no_underlying_cli() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_available_when_adapter_found_even_without_underlying_cli() {
|
||||
// The retired CliMissing gate must not come back: a resolving adapter is
|
||||
// available regardless of whether an underlying CLI is on the user PATH.
|
||||
fn classifies_cli_missing_when_adapter_found_but_cli_absent() {
|
||||
let (status, cmd, path) = classify_runtime(
|
||||
Some(("codex-acp", PathBuf::from("/opt/homebrew/bin/codex-acp"))),
|
||||
Some("codex"),
|
||||
false,
|
||||
);
|
||||
assert_eq!(status, AcpAvailabilityStatus::Available);
|
||||
assert_eq!(status, AcpAvailabilityStatus::CliMissing);
|
||||
assert_eq!(cmd.as_deref(), Some("codex-acp"));
|
||||
assert_eq!(path.as_deref(), Some("/opt/homebrew/bin/codex-acp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_cli_env_vars_map_bundled_bridges_to_the_user_cli() {
|
||||
// The bridge-only bundles vendor no CLI, so spawn must export exactly
|
||||
// these env vars for the bridges to find the user's install. Pinned here
|
||||
// because a drifted mapping fails silently — sessions die at bridge
|
||||
// startup, not at build time.
|
||||
let expectations = [
|
||||
("claude", Some("CLAUDE_CODE_EXECUTABLE")),
|
||||
("codex", Some("CODEX_PATH")),
|
||||
("goose", None),
|
||||
("buzz-agent", None),
|
||||
];
|
||||
for (id, expected) in expectations {
|
||||
let runtime = crate::managed_agents::known_acp_runtime_exact(id)
|
||||
.unwrap_or_else(|| panic!("{id} must be in the catalog"));
|
||||
assert_eq!(runtime.bridge_cli_env_var, expected, "runtime {id}");
|
||||
if runtime.bridge_cli_env_var.is_some() {
|
||||
assert!(
|
||||
runtime.underlying_cli.is_some(),
|
||||
"{id}: a bridge CLI env var is meaningless without an underlying CLI to resolve"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn persona_with_runtime(id: &str, runtime: Option<&str>) -> crate::managed_agents::AgentDefinition {
|
||||
crate::managed_agents::AgentDefinition {
|
||||
id: id.to_string(),
|
||||
@@ -870,51 +893,65 @@ fn test_command_basenames_dotted_name_no_extra_candidates() {
|
||||
|
||||
// ── Phase B: cli_install_commands_for_os ────────────────────────────────────
|
||||
|
||||
/// Claude and Codex vendor their CLIs inside the bundled ACP packages —
|
||||
/// the curl-pipe install commands are retired with the cli_missing gate.
|
||||
/// Claude and Codex have non-empty default cli_install_commands (install.sh) —
|
||||
/// bridge-only bundling means the user's own CLI must be installable again.
|
||||
#[test]
|
||||
fn test_claude_and_codex_have_no_cli_install_commands() {
|
||||
fn test_claude_and_codex_have_cli_install_commands() {
|
||||
let claude = super::known_acp_runtime_exact("claude").unwrap();
|
||||
let codex = super::known_acp_runtime_exact("codex").unwrap();
|
||||
assert!(
|
||||
claude.cli_install_commands.is_empty(),
|
||||
"claude CLI ships inside the bundled adapter — must not have cli install commands"
|
||||
!claude.cli_install_commands.is_empty(),
|
||||
"claude must have cli install commands"
|
||||
);
|
||||
assert!(
|
||||
codex.cli_install_commands.is_empty(),
|
||||
"codex CLI ships inside the bundled adapter — must not have cli install commands"
|
||||
!codex.cli_install_commands.is_empty(),
|
||||
"codex must have cli install commands"
|
||||
);
|
||||
}
|
||||
|
||||
/// cli_install_commands_for_os is empty for claude and codex on every platform.
|
||||
/// cli_install_commands_for_os returns a non-empty slice for claude and codex.
|
||||
#[test]
|
||||
fn test_cli_install_commands_for_os_empty_for_claude_codex() {
|
||||
fn test_cli_install_commands_for_os_non_empty_for_claude_codex() {
|
||||
let claude = super::known_acp_runtime_exact("claude").unwrap();
|
||||
let codex = super::known_acp_runtime_exact("codex").unwrap();
|
||||
assert!(
|
||||
claude.cli_install_commands_for_os().is_empty(),
|
||||
"claude must not have install commands on any platform"
|
||||
!claude.cli_install_commands_for_os().is_empty(),
|
||||
"claude must have install commands on every platform"
|
||||
);
|
||||
assert!(
|
||||
codex.cli_install_commands_for_os().is_empty(),
|
||||
"codex must not have install commands on any platform"
|
||||
!codex.cli_install_commands_for_os().is_empty(),
|
||||
"codex must have install commands on every platform"
|
||||
);
|
||||
}
|
||||
|
||||
/// On Windows, the bundled runtimes still expose no install commands, and
|
||||
/// goose keeps its platform-neutral commands.
|
||||
/// On Windows, Claude and Codex select the PowerShell install commands.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn test_cli_install_commands_for_os_on_windows() {
|
||||
fn test_cli_install_commands_for_os_selects_powershell_on_windows() {
|
||||
let claude = super::known_acp_runtime_exact("claude").unwrap();
|
||||
let codex = super::known_acp_runtime_exact("codex").unwrap();
|
||||
|
||||
// Windows must select the PowerShell commands, not the curl|bash ones.
|
||||
let claude_cmds = claude.cli_install_commands_for_os();
|
||||
let codex_cmds = codex.cli_install_commands_for_os();
|
||||
|
||||
assert_ne!(
|
||||
claude_cmds, claude.cli_install_commands,
|
||||
"Windows must NOT use the default curl|bash commands for claude"
|
||||
);
|
||||
assert_ne!(
|
||||
codex_cmds, codex.cli_install_commands,
|
||||
"Windows must NOT use the default curl|bash commands for codex"
|
||||
);
|
||||
|
||||
// Verify they are the PowerShell installers.
|
||||
assert!(
|
||||
claude.cli_install_commands_for_os().is_empty(),
|
||||
"claude ships bundled — no Windows install commands"
|
||||
claude_cmds.iter().any(|c| c.contains("powershell")),
|
||||
"claude Windows install must use powershell; got: {claude_cmds:?}"
|
||||
);
|
||||
assert!(
|
||||
codex.cli_install_commands_for_os().is_empty(),
|
||||
"codex ships bundled — no Windows install commands"
|
||||
codex_cmds.iter().any(|c| c.contains("powershell")),
|
||||
"codex Windows install must use powershell; got: {codex_cmds:?}"
|
||||
);
|
||||
|
||||
// Goose and buzz-agent must NOT use Windows-specific commands.
|
||||
|
||||
@@ -46,7 +46,7 @@ use crate::managed_agents::{
|
||||
agent_env::baked_build_env,
|
||||
config_bridge::read_goose_file_config,
|
||||
discovery::{
|
||||
classify_runtime, find_command, known_acp_runtime, resolve_probe_binary, KnownAcpRuntime,
|
||||
classify_runtime, find_command, known_acp_runtime, resolve_command, KnownAcpRuntime,
|
||||
},
|
||||
env_vars::merged_user_env,
|
||||
global_config::GlobalAgentConfig,
|
||||
@@ -502,10 +502,7 @@ fn cli_login_requirements(
|
||||
.iter()
|
||||
.find_map(|cmd| find_command(cmd).map(|path| (*cmd, path)));
|
||||
|
||||
// Check whether the underlying CLI is on PATH — only set for runtimes
|
||||
// whose CLI is a separate install (not the bundled claude/codex bridges,
|
||||
// which vendor their own); it distinguishes AdapterMissing from
|
||||
// NotInstalled below.
|
||||
// Check whether the underlying CLI itself (e.g. "claude", "codex") is on PATH.
|
||||
let underlying_cli_found = runtime
|
||||
.underlying_cli
|
||||
.map(|cli| find_command(cli).is_some())
|
||||
@@ -516,12 +513,10 @@ fn cli_login_requirements(
|
||||
|
||||
match availability {
|
||||
AcpAvailabilityStatus::Available => {
|
||||
// Adapter present — probe login status against the bundled CLI
|
||||
// when the app ships one (the same pinned binary agent sessions
|
||||
// run), else the user's install resolved via the full login-shell
|
||||
// PATH so the probe works in a packaged macOS DMG where the GUI
|
||||
// PATH lacks npm/homebrew.
|
||||
let Some(binary_path) = resolve_probe_binary(probe_args[0]) else {
|
||||
// Both adapter and CLI are present — probe login status.
|
||||
// Resolve via the full login-shell PATH so the probe works in a
|
||||
// packaged macOS DMG where the GUI PATH lacks npm/homebrew.
|
||||
let Some(binary_path) = resolve_command(probe_args[0]) else {
|
||||
// Unexpectedly not resolvable (race or PATH edge case).
|
||||
return vec![Requirement::CliLogin {
|
||||
probe_args: probe_args.iter().map(|s| s.to_string()).collect(),
|
||||
@@ -945,9 +940,11 @@ mod tests {
|
||||
mcp_command: None,
|
||||
mcp_hooks: false,
|
||||
underlying_cli,
|
||||
bridge_cli_env_var: None,
|
||||
cli_install_commands: &[],
|
||||
cli_install_commands_windows: &[],
|
||||
adapter_install_commands: &[],
|
||||
adapter_ships_with_app: false,
|
||||
install_instructions_url: "",
|
||||
cli_install_hint: "",
|
||||
adapter_install_hint: "",
|
||||
@@ -1046,11 +1043,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_login_requirements_probe_runs_even_without_underlying_cli() {
|
||||
fn cli_login_requirements_cli_missing_emits_cli_missing() {
|
||||
// Adapter present (use the running test binary as a portable stand-in),
|
||||
// underlying CLI absent. The retired CliMissing gate would have skipped
|
||||
// the probe; now the adapter alone means Available, the probe runs
|
||||
// (here: exit 0 → logged in) and no requirement is emitted.
|
||||
// underlying CLI absent.
|
||||
// → CliMissing state → no probe run → CliLogin{CliMissing}.
|
||||
let exe = present_binary_str();
|
||||
let rt = make_cli_runtime(
|
||||
static_commands(vec![exe]), // adapter found via absolute path
|
||||
@@ -1058,9 +1054,19 @@ mod tests {
|
||||
);
|
||||
let reqs = cli_login_requirements(&[exe, "--list"], "install the CLI", &rt);
|
||||
assert!(
|
||||
reqs.is_empty(),
|
||||
"adapter present must probe login regardless of the user CLI; got {reqs:?}"
|
||||
!reqs.is_empty(),
|
||||
"CLI missing must produce a CliLogin requirement"
|
||||
);
|
||||
if let Requirement::CliLogin {
|
||||
ref availability, ..
|
||||
} = reqs[0]
|
||||
{
|
||||
assert_eq!(
|
||||
*availability,
|
||||
crate::managed_agents::AcpAvailabilityStatus::CliMissing,
|
||||
"adapter present, CLI absent → CliMissing"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1579,6 +1579,31 @@ pub fn spawn_agent_child(
|
||||
command.env("MCP_HOOK_SERVERS", "*");
|
||||
}
|
||||
|
||||
// Point the bundled bridge at the user's harness CLI. The bridge-only
|
||||
// bundles vendor no CLI: the claude bridge only looks at
|
||||
// CLAUDE_CODE_EXECUTABLE (or the omitted SDK native package) and the
|
||||
// codex bridge's CODEX_PATH fallback hard-requires the omitted platform
|
||||
// package — neither falls back to PATH. A value already present in the
|
||||
// desktop's own environment is inherited untouched (it names a binary the
|
||||
// user picked), and a per-agent env var applied below overrides this one.
|
||||
// Resolution failure is non-fatal here: readiness classifies the runtime
|
||||
// CliMissing and the agent enters setup mode instead of the pool.
|
||||
if let Some((cli, env_var)) =
|
||||
runtime_meta.and_then(|r| r.underlying_cli.zip(r.bridge_cli_env_var))
|
||||
{
|
||||
if std::env::var_os(env_var).is_none() {
|
||||
match resolve_command(cli) {
|
||||
Some(path) => {
|
||||
command.env(env_var, &path);
|
||||
}
|
||||
None => eprintln!(
|
||||
"buzz-desktop: {cli} not found for {env_var}; \
|
||||
bridge sessions will fail until it is installed"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Readiness check: set setup-payload if agent is not ready ─────────────
|
||||
//
|
||||
// Build the effective env the agent would have at start-time, run the
|
||||
|
||||
@@ -523,14 +523,12 @@ pub struct ManagedAgentLogResponse {
|
||||
pub log_path: String,
|
||||
}
|
||||
|
||||
/// The retired `CliMissing` variant (adapter present, user CLI absent) is
|
||||
/// gone: the bundled bridges vendor their own CLI, so a resolving adapter is
|
||||
/// available regardless of user installs — sign-in state is `AuthStatus`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AcpAvailabilityStatus {
|
||||
Available,
|
||||
AdapterMissing,
|
||||
CliMissing,
|
||||
NotInstalled,
|
||||
}
|
||||
|
||||
|
||||
@@ -562,7 +562,9 @@ export function AgentDefinitionDialog({
|
||||
<p className="text-xs text-warning">
|
||||
{selectedRuntime.availability === "adapter_missing"
|
||||
? `${selectedRuntime.label} CLI is installed but the ACP adapter is missing.`
|
||||
: `${selectedRuntime.label} is not installed.`}{" "}
|
||||
: selectedRuntime.availability === "cli_missing"
|
||||
? `${selectedRuntime.label} ACP adapter is installed but the CLI is missing.`
|
||||
: `${selectedRuntime.label} is not installed.`}{" "}
|
||||
Visit Settings > Doctor to set it up.
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
@@ -97,7 +97,7 @@ test("getDefaultPersonaRuntime falls back to goose when buzz-agent is unavailabl
|
||||
test("getDefaultPersonaRuntime returns first available when neither buzz-agent nor goose is available", () => {
|
||||
const runtimes = [
|
||||
makeRuntime("buzz-agent", "adapter_missing"),
|
||||
makeRuntime("goose", "not_installed"),
|
||||
makeRuntime("goose", "cli_missing"),
|
||||
makeRuntime("claude"),
|
||||
];
|
||||
const result = getDefaultPersonaRuntime(runtimes);
|
||||
@@ -111,7 +111,7 @@ test("getDefaultPersonaRuntime returns null for an empty list", () => {
|
||||
test("getDefaultPersonaRuntime returns null when no runtime is available", () => {
|
||||
const runtimes = [
|
||||
makeRuntime("buzz-agent", "not_installed"),
|
||||
makeRuntime("goose", "adapter_missing"),
|
||||
makeRuntime("goose", "cli_missing"),
|
||||
];
|
||||
assert.equal(getDefaultPersonaRuntime(runtimes), null);
|
||||
});
|
||||
@@ -168,7 +168,11 @@ test("getPersonaModelOptions for buzz-agent with no provider returns default mod
|
||||
// is non-null (so the UI surfaces the reason) for each unavailability reason.
|
||||
|
||||
test("formatModelDiscoveryErrorStatus returns a non-null status for runtime unavailable errors", () => {
|
||||
for (const availability of ["adapter_missing", "not_installed"]) {
|
||||
for (const availability of [
|
||||
"adapter_missing",
|
||||
"cli_missing",
|
||||
"not_installed",
|
||||
]) {
|
||||
const status = formatModelDiscoveryErrorStatus(
|
||||
new Error(`Runtime not available: ${availability}`),
|
||||
"anthropic",
|
||||
|
||||
@@ -383,9 +383,11 @@ export function formatRuntimeOptionLabel(runtime: AcpRuntimeCatalogEntry) {
|
||||
const suffix =
|
||||
runtime.availability === "adapter_missing"
|
||||
? " (adapter missing)"
|
||||
: runtime.availability === "not_installed"
|
||||
? " (not installed)"
|
||||
: "";
|
||||
: runtime.availability === "cli_missing"
|
||||
? " (CLI missing)"
|
||||
: runtime.availability === "not_installed"
|
||||
? " (not installed)"
|
||||
: "";
|
||||
return `${runtime.label}${suffix}`;
|
||||
}
|
||||
|
||||
@@ -395,10 +397,12 @@ function runtimeAvailabilitySortRank(
|
||||
switch (availability) {
|
||||
case "available":
|
||||
return 0;
|
||||
case "not_installed":
|
||||
case "cli_missing":
|
||||
return 1;
|
||||
case "adapter_missing":
|
||||
case "not_installed":
|
||||
return 2;
|
||||
case "adapter_missing":
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -353,6 +353,19 @@ function RuntimeDetails({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (runtime.availability === "cli_missing") {
|
||||
return (
|
||||
<>
|
||||
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
||||
ACP adapter detected; CLI missing.
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
||||
{runtime.installHint}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
||||
|
||||
@@ -108,7 +108,7 @@ test("validateLinkedAgentRuntimeEdit rejects unavailable linked-agent runtime ch
|
||||
input: updateInput({ runtime: "claude" }),
|
||||
managedAgent: agent(),
|
||||
previousPersona: persona({ runtime: "goose" }),
|
||||
runtimes: [runtime({ availability: "not_installed", command: null })],
|
||||
runtimes: [runtime({ availability: "cli_missing", command: null })],
|
||||
}),
|
||||
"Claude Code is not available. Install it before saving this linked agent.",
|
||||
);
|
||||
|
||||
@@ -38,6 +38,8 @@ function StatusIcon({
|
||||
return <CheckCircle2 className="h-4 w-4 text-status-added" />;
|
||||
case "adapter_missing":
|
||||
return <AlertTriangle className="h-4 w-4 text-warning" />;
|
||||
case "cli_missing":
|
||||
return <AlertTriangle className="h-4 w-4 text-warning" />;
|
||||
case "not_installed":
|
||||
return <Circle className="h-4 w-4 text-muted-foreground/50" />;
|
||||
}
|
||||
@@ -136,7 +138,8 @@ function RuntimeRow({
|
||||
"flex min-h-16 items-start gap-3 px-4 py-3 text-sm",
|
||||
runtime.availability === "available"
|
||||
? "bg-background/60"
|
||||
: runtime.availability === "adapter_missing"
|
||||
: runtime.availability === "adapter_missing" ||
|
||||
runtime.availability === "cli_missing"
|
||||
? "bg-amber-500/5"
|
||||
: "bg-muted/20",
|
||||
)}
|
||||
@@ -173,12 +176,23 @@ function RuntimeRow({
|
||||
</code>
|
||||
</p>
|
||||
) : null}
|
||||
{/* The bundled bridge's resource-dir path is noise — the
|
||||
"Bundled with Buzz." line above covers it. The
|
||||
user-CLI path row is retired with the cli_missing gate: the
|
||||
bundled bridges vendor their own CLI, so no runtime reports a
|
||||
separate CLI path anymore. */}
|
||||
{runtime.adapterBundled ? null : (
|
||||
{runtime.underlyingCliPath &&
|
||||
runtime.underlyingCliPath !== runtime.binaryPath ? (
|
||||
<div className="mt-1 space-y-0.5">
|
||||
<p className="break-all font-mono text-2xs text-muted-foreground/80">
|
||||
<span className="text-muted-foreground">CLI:</span>{" "}
|
||||
{runtime.underlyingCliPath}
|
||||
</p>
|
||||
{/* The bundled bridge's resource-dir path is noise — the
|
||||
"Bundled with Buzz." line above covers it. */}
|
||||
{runtime.adapterBundled ? null : (
|
||||
<p className="break-all font-mono text-2xs text-muted-foreground/80">
|
||||
<span className="text-muted-foreground">ACP adapter:</span>{" "}
|
||||
{runtime.binaryPath}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : runtime.adapterBundled ? null : (
|
||||
<>
|
||||
<p className="mt-1 break-all font-mono text-2xs text-muted-foreground/80">
|
||||
{runtime.binaryPath}
|
||||
@@ -229,6 +243,34 @@ function RuntimeRow({
|
||||
runtime={runtime}
|
||||
/>
|
||||
</>
|
||||
) : runtime.availability === "cli_missing" ? (
|
||||
<>
|
||||
<p className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
{runtime.adapterBundled ? (
|
||||
<>
|
||||
Bundled with Buzz, but the {runtime.label} CLI is not
|
||||
installed.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
ACP adapter found at{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-2xs">
|
||||
{runtime.binaryPath ?? "unknown path"}
|
||||
</code>{" "}
|
||||
but the {runtime.label} CLI is not installed.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
{runtime.installHint}
|
||||
</p>
|
||||
<InstallActions
|
||||
hasError={installError !== null}
|
||||
isInstalling={isInstalling}
|
||||
onInstall={onInstall}
|
||||
runtime={runtime}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
|
||||
@@ -554,14 +554,10 @@ export type GitBashPrerequisite = {
|
||||
installHint: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The retired "cli_missing" state (adapter present, user CLI absent) is gone:
|
||||
* the bundled bridges vendor their own CLI, so a resolving adapter is
|
||||
* available regardless of user installs — sign-in state lives in AuthStatus.
|
||||
*/
|
||||
export type AcpAvailabilityStatus =
|
||||
| "available"
|
||||
| "adapter_missing"
|
||||
| "cli_missing"
|
||||
| "not_installed";
|
||||
|
||||
/** Authentication/login status for a CLI-based ACP runtime. */
|
||||
|
||||
@@ -103,30 +103,6 @@ test("extractConfigNudge rejects retired adapter_outdated availability", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("extractConfigNudge rejects retired cli_missing availability", () => {
|
||||
// The cli_missing gate was retired when auth probes moved to the CLIs
|
||||
// vendored inside the bundled bridges — a user CLI install no longer gates
|
||||
// availability. Stale nudge JSON emitted by an older app version must not
|
||||
// parse into a card the current UI has no rendering for.
|
||||
const payload = {
|
||||
agent_name: "Fizz",
|
||||
agent_pubkey: FIZZ_PUBKEY,
|
||||
requirements: [
|
||||
{
|
||||
surface: "cli_login",
|
||||
probe_args: ["claude", "auth", "status"],
|
||||
setup_copy: "install the Claude CLI",
|
||||
availability: "cli_missing",
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(
|
||||
extractConfigNudge(withSentinel("prose", payload)),
|
||||
null,
|
||||
"retired cli_missing availability must be rejected by the validator",
|
||||
);
|
||||
});
|
||||
|
||||
test("extractConfigNudge returns null for cli_login without availability", () => {
|
||||
// availability is required — old-format payloads (no availability field)
|
||||
// must not parse so stale nudge JSON from before the Doctor-CTA update
|
||||
|
||||
@@ -32,7 +32,8 @@ export type ConfigNudgeRequirement =
|
||||
* Determines which message and CTA the nudge card shows:
|
||||
* - "available" → tooling installed, needs login
|
||||
* - "adapter_missing" → CLI installed but ACP adapter missing
|
||||
* - "not_installed" → no adapter found
|
||||
* - "cli_missing" → ACP adapter installed but CLI missing
|
||||
* - "not_installed" → neither adapter nor CLI found
|
||||
*/
|
||||
availability: AcpAvailabilityStatus;
|
||||
}
|
||||
@@ -138,11 +139,9 @@ function isConfigNudgeRequirement(v: unknown): v is ConfigNudgeRequirement {
|
||||
Array.isArray(r.probe_args) &&
|
||||
r.probe_args.every((a) => typeof a === "string") &&
|
||||
typeof r.setup_copy === "string" &&
|
||||
// Retired literals ("adapter_outdated", "cli_missing") emitted by
|
||||
// older app versions are rejected here so stale nudge JSON cannot
|
||||
// render a card the current UI has no branch for.
|
||||
(r.availability === "available" ||
|
||||
r.availability === "adapter_missing" ||
|
||||
r.availability === "cli_missing" ||
|
||||
r.availability === "not_installed")
|
||||
);
|
||||
case "git_bash":
|
||||
|
||||
@@ -103,6 +103,8 @@ function cliLoginMessage(
|
||||
switch (req.availability) {
|
||||
case "not_installed":
|
||||
return `${harness} isn't installed`;
|
||||
case "cli_missing":
|
||||
return `${harness} CLI is missing`;
|
||||
case "adapter_missing":
|
||||
return `${harness} ACP adapter isn't installed`;
|
||||
case "available":
|
||||
|
||||
@@ -257,7 +257,44 @@ test.describe("doctor CTA nudge card screenshots", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The former 04-cli-login-cli-missing-state test retired with the
|
||||
// cli_missing gate: the validator now rejects that availability literal,
|
||||
// so no card renders for it (see configNudge.test.mjs).
|
||||
/**
|
||||
* 04 — cli_missing state: ACP adapter present but underlying CLI absent.
|
||||
* Shows "claude CLI is missing" copy.
|
||||
*/
|
||||
test("04-cli-login-cli-missing-state", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [
|
||||
{
|
||||
pubkey: AGENT_PUBKEY,
|
||||
name: AGENT_NAME,
|
||||
status: "stopped" as const,
|
||||
channelNames: ["general"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const content = makeNudgeSentinel(AGENT_NAME, AGENT_PUBKEY, [
|
||||
{
|
||||
surface: "cli_login",
|
||||
probe_args: ["claude"],
|
||||
setup_copy: "install the Claude CLI",
|
||||
availability: "cli_missing",
|
||||
},
|
||||
]);
|
||||
|
||||
await injectNudgeAndNavigate(page, content);
|
||||
|
||||
const card = page.locator("[data-config-nudge]").last();
|
||||
await expect(card).toBeVisible({ timeout: 10_000 });
|
||||
await expect(card.getByText(/CLI is missing/)).toBeVisible();
|
||||
|
||||
await card.scrollIntoViewIfNeeded();
|
||||
await settleAnimations(page);
|
||||
|
||||
await card.screenshot({
|
||||
path: `${SHOTS}/04-cli-login-cli-missing-state.png`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,9 +48,6 @@ const BUZZ_AGENT_AVAILABLE = {
|
||||
/**
|
||||
* Claude available and logged in — used as a neutral entry when claude is not
|
||||
* the runtime under test, and as the base for the auth states being tested.
|
||||
* No `underlying_cli_path` and no auto-install: the bundled bridge vendors
|
||||
* its own CLI, so the backend reports neither for claude since the
|
||||
* cli_missing gate was retired.
|
||||
*/
|
||||
const CLAUDE_AVAILABLE_LOGGED_IN = {
|
||||
id: "claude",
|
||||
@@ -64,8 +61,8 @@ const CLAUDE_AVAILABLE_LOGGED_IN = {
|
||||
install_hint: "",
|
||||
install_instructions_url:
|
||||
"https://github.com/agentclientprotocol/claude-agent-acp",
|
||||
can_auto_install: false,
|
||||
underlying_cli_path: null,
|
||||
can_auto_install: true,
|
||||
underlying_cli_path: "/usr/local/bin/claude",
|
||||
auth_status: { status: "logged_in" },
|
||||
};
|
||||
|
||||
@@ -149,6 +146,7 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
availability: "available",
|
||||
command: "codex-acp",
|
||||
binary_path: "/usr/local/bin/codex-acp",
|
||||
underlying_cli_path: "/usr/local/bin/codex",
|
||||
auth_status: { status: "logged_out" },
|
||||
login_hint: "Run `codex login` to authenticate.",
|
||||
},
|
||||
@@ -416,8 +414,8 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
/**
|
||||
* 09 — available runtime whose adapter is the bridge bundled with the app:
|
||||
* the row says "Bundled with Buzz" instead of rendering the
|
||||
* resource-dir path, and no CLI path renders — the bundled bridge vendors
|
||||
* its own CLI, so the user-CLI row retired with the cli_missing gate.
|
||||
* resource-dir path; the user's CLI path still renders (the bridge-only
|
||||
* bundle runs the user's claude CLI).
|
||||
*/
|
||||
test("09-bundled-adapter", async ({ page }) => {
|
||||
const bundledPath =
|
||||
@@ -441,7 +439,7 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
const row = page.getByTestId("doctor-runtime-claude");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).toContainText("Bundled with Buzz.");
|
||||
await expect(row).not.toContainText("CLI:");
|
||||
await expect(row).toContainText("/usr/local/bin/claude");
|
||||
await expect(row).not.toContainText(bundledPath);
|
||||
await expect(row).not.toContainText("installed on PATH");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user