diff --git a/.gitignore b/.gitignore index f23cb358b..bbf535883 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/crates/buzz-acp/src/setup_mode.rs b/crates/buzz-acp/src/setup_mode.rs index 45f943509..f39ac3f00 100644 --- a/crates/buzz-acp/src/setup_mode.rs +++ b/crates/buzz-acp/src/setup_mode.rs @@ -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, diff --git a/desktop/acp-tools.lock.json b/desktop/acp-tools.lock.json index d9430ca33..3eae0934c 100644 --- a/desktop/acp-tools.lock.json +++ b/desktop/acp-tools.lock.json @@ -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" } ] } diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index c8d219a82..f5782b0a2 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -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 diff --git a/desktop/scripts/ensure-acp-tools.sh b/desktop/scripts/ensure-acp-tools.sh index 8477acdc8..59b8be896 100755 --- a/desktop/scripts/ensure-acp-tools.sh +++ b/desktop/scripts/ensure-acp-tools.sh @@ -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 .exe next -to a .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 +.exe next to a .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 (`-`), 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 diff --git a/desktop/scripts/prepare-acp-tools-resource.sh b/desktop/scripts/prepare-acp-tools-resource.sh index 0b8ea5e14..2e731c545 100755 --- a/desktop/scripts/prepare-acp-tools-resource.sh +++ b/desktop/scripts/prepare-acp-tools-resource.sh @@ -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" diff --git a/desktop/scripts/update-acp-tools-lock.mjs b/desktop/scripts/update-acp-tools-lock.mjs index 53403896b..5f6a5ee99 100755 --- a/desktop/scripts/update-acp-tools-lock.mjs +++ b/desktop/scripts/update-acp-tools-lock.mjs @@ -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 ]... [--lock-file ] 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 -// `/-/.tgz` path (the basename is not always derivable -// from name@version — @openai/codex native tarballs carry a platform suffix). +// `/-/.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. diff --git a/desktop/src-tauri/src/commands/agent_config.rs b/desktop/src-tauri/src/commands/agent_config.rs index f6141a53e..6ad2025dc 100644 --- a/desktop/src-tauri/src/commands/agent_config.rs +++ b/desktop/src-tauri/src/commands/agent_config.rs @@ -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: "", diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index 814e08ae7..13958423c 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -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" ); } } diff --git a/desktop/src-tauri/src/managed_agents/acp_tools.rs b/desktop/src-tauri/src/managed_agents/acp_tools.rs index a328ecb3c..472e650fa 100644 --- a/desktop/src-tauri/src/managed_agents/acp_tools.rs +++ b/desktop/src-tauri/src/managed_agents/acp_tools.rs @@ -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> = 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, -} - -#[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 { - 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 { - 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 { 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() { diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs index f5fc7c389..57d5620f1 100644 --- a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs +++ b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs @@ -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: "", diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index b8057fee0..ca605f411 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -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 { 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 { - 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, Option) { 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 { 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 { 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 = probe_args.iter().map(|s| s.to_string()).collect(); let handle = std::thread::spawn(move || { diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index f32be9a99..2ac245449 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -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. diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index d8695624d..dd47973aa 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -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] diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index bc014889b..f915311cc 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -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 diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index e6c42823f..21f1c014c 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -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, } diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx index b8666728c..25cfe9af1 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx @@ -562,7 +562,9 @@ export function AgentDefinitionDialog({

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

) : null; diff --git a/desktop/src/features/agents/ui/personaDialogPickers.test.mjs b/desktop/src/features/agents/ui/personaDialogPickers.test.mjs index de81a5726..5cc49a989 100644 --- a/desktop/src/features/agents/ui/personaDialogPickers.test.mjs +++ b/desktop/src/features/agents/ui/personaDialogPickers.test.mjs @@ -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", diff --git a/desktop/src/features/agents/ui/personaDialogPickers.tsx b/desktop/src/features/agents/ui/personaDialogPickers.tsx index c2c9c9e49..72a96ca99 100644 --- a/desktop/src/features/agents/ui/personaDialogPickers.tsx +++ b/desktop/src/features/agents/ui/personaDialogPickers.tsx @@ -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; } } diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx index ede6aa250..355953808 100644 --- a/desktop/src/features/onboarding/ui/SetupStep.tsx +++ b/desktop/src/features/onboarding/ui/SetupStep.tsx @@ -353,6 +353,19 @@ function RuntimeDetails({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { ); } + if (runtime.availability === "cli_missing") { + return ( + <> +

+ ACP adapter detected; CLI missing. +

+

+ {runtime.installHint} +

+ + ); + } + return ( <>

diff --git a/desktop/src/features/profile/ui/UserProfilePanelPersonaSubmit.test.mjs b/desktop/src/features/profile/ui/UserProfilePanelPersonaSubmit.test.mjs index 2fea5986c..cf21b6f6a 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelPersonaSubmit.test.mjs +++ b/desktop/src/features/profile/ui/UserProfilePanelPersonaSubmit.test.mjs @@ -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.", ); diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index 7e78409b7..ce665157b 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -38,6 +38,8 @@ function StatusIcon({ return ; case "adapter_missing": return ; + case "cli_missing": + return ; case "not_installed": return ; } @@ -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({

) : 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 ? ( +
+

+ CLI:{" "} + {runtime.underlyingCliPath} +

+ {/* The bundled bridge's resource-dir path is noise — the + "Bundled with Buzz." line above covers it. */} + {runtime.adapterBundled ? null : ( +

+ ACP adapter:{" "} + {runtime.binaryPath} +

+ )} +
+ ) : runtime.adapterBundled ? null : ( <>

{runtime.binaryPath} @@ -229,6 +243,34 @@ function RuntimeRow({ runtime={runtime} /> + ) : runtime.availability === "cli_missing" ? ( + <> +

+ {runtime.adapterBundled ? ( + <> + Bundled with Buzz, but the {runtime.label} CLI is not + installed. + + ) : ( + <> + ACP adapter found at{" "} + + {runtime.binaryPath ?? "unknown path"} + {" "} + but the {runtime.label} CLI is not installed. + + )} +

+

+ {runtime.installHint} +

+ + ) : ( <>

diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts index 470b37cc2..9b07fc911 100644 --- a/desktop/src/shared/api/types.ts +++ b/desktop/src/shared/api/types.ts @@ -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. */ diff --git a/desktop/src/shared/lib/configNudge.test.mjs b/desktop/src/shared/lib/configNudge.test.mjs index c38e95aac..8e1603e23 100644 --- a/desktop/src/shared/lib/configNudge.test.mjs +++ b/desktop/src/shared/lib/configNudge.test.mjs @@ -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 diff --git a/desktop/src/shared/lib/configNudge.ts b/desktop/src/shared/lib/configNudge.ts index 5ceb8314d..901bb2273 100644 --- a/desktop/src/shared/lib/configNudge.ts +++ b/desktop/src/shared/lib/configNudge.ts @@ -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": diff --git a/desktop/src/shared/ui/config-nudge-attachment.tsx b/desktop/src/shared/ui/config-nudge-attachment.tsx index b7ebd6ff3..f21e5a329 100644 --- a/desktop/src/shared/ui/config-nudge-attachment.tsx +++ b/desktop/src/shared/ui/config-nudge-attachment.tsx @@ -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": diff --git a/desktop/tests/e2e/doctor-cta-screenshots.spec.ts b/desktop/tests/e2e/doctor-cta-screenshots.spec.ts index e0ab3ffe4..9bd273217 100644 --- a/desktop/tests/e2e/doctor-cta-screenshots.spec.ts +++ b/desktop/tests/e2e/doctor-cta-screenshots.spec.ts @@ -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`, + }); + }); }); diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts index 1c462e53e..9fb09c765 100644 --- a/desktop/tests/e2e/doctor-states.spec.ts +++ b/desktop/tests/e2e/doctor-states.spec.ts @@ -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");