From 4b9b0639b2e992cdc30b6891c474a582c76ab423 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 13 Jul 2026 13:54:22 +1000 Subject: [PATCH] refactor(desktop): retire codex version gate and bridge npm-install flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the ACP bridges (@agentclientprotocol/claude-agent-acp, @agentclientprotocol/codex-acp) bundled with the app at pinned versions and resolved ahead of user installs, three pieces of machinery are dead code for those two runtimes: - The codex 0.16.x version gate: probe_codex_acp_major_version, codex_adapter_availability / codex_adapter_is_outdated, and the AdapterOutdated availability status (Rust enum variant plus the frontend "adapter_outdated" union member and all its UI branches). The bundled adapter is always the pinned 1.x package, so probing for the deprecated @zed-industries/codex-acp package has no trigger left. - The EEXIST uninstall-then-reinstall two-step in plan_adapter_install, which existed only to swap the deprecated codex package for the new one. The plan is now the simple missing -> catalog-commands mapping, and the function drops its runtime_id parameter. - The in-app `npm install -g` flow for the two bundled bridges: their catalog entries now carry empty adapter_install_commands and a hint that the adapter ships with the Buzz desktop app. Goose keeps its npm install flow untouched — its adapter is not bundled. cli_login_requirements now classifies availability purely via classify_runtime (which resolves bundle-first), replacing the codex version-probe special case. The configNudge validator rejects the retired "adapter_outdated" literal, so stale nudge JSON emitted by an older app version cannot render a card the current UI has no branch for (regression test inverted accordingly). This is a Buzz-specific retirement enabled by the bundling series; the donor series (block/builderbot apps/staged, berd) had no equivalent version gate to remove. File-size ledger entries for discovery.rs (1178 -> 1056), discovery/tests.rs (1029 -> 825), and readiness.rs (1754 -> 1583) are ratcheted down to bank the deletions, per the note left in the bundled-resolution commit. Verification: - cargo test --lib (desktop/src-tauri): 1381 passed, 0 failed - cargo clippy --lib --tests -D warnings: clean - cargo fmt --check: clean - tsc --noEmit: clean; biome check src: no new diagnostics - pnpm test (desktop): 2744 passed, 0 failed - node scripts/check-file-sizes.mjs: pass with ratcheted limits Co-Authored-By: Claude Fable 5 Signed-off-by: Matt Toohey --- desktop/scripts/check-file-sizes.mjs | 41 ++-- .../src-tauri/src/commands/agent_discovery.rs | 150 +++---------- .../src-tauri/src/managed_agents/discovery.rs | 141 +----------- .../src/managed_agents/discovery/tests.rs | 212 +----------------- .../discovery/tests/codex_version.rs | 48 ---- .../src-tauri/src/managed_agents/readiness.rs | 170 +------------- desktop/src-tauri/src/managed_agents/types.rs | 2 - .../agents/ui/AgentDefinitionDialog.tsx | 8 +- .../agents/ui/personaDialogPickers.tsx | 14 +- .../src/features/onboarding/ui/SetupStep.tsx | 23 -- .../settings/ui/DoctorSettingsPanel.tsx | 35 --- desktop/src/shared/api/types.ts | 1 - desktop/src/shared/lib/configNudge.test.mjs | 15 +- desktop/src/shared/lib/configNudge.ts | 2 - .../src/shared/ui/config-nudge-attachment.tsx | 2 - 15 files changed, 85 insertions(+), 779 deletions(-) delete mode 100644 desktop/src-tauri/src/managed_agents/discovery/tests/codex_version.rs diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index c0c6e9989..59428ac19 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -152,12 +152,6 @@ const overrides = new Map([ // +18: CliConfigInvalid requirement surface for config-parse probe classification — // new Requirement variant + updated cli_login_requirements + 3 new probe-layer tests. // Load-bearing UX fix (bad config → clear diagnostic, not "run codex login"). - // codex-acp-package-swap: AdapterOutdated version-probe in cli_login_requirements - // (+22 lines). Load-bearing — blocks login gate for deprecated 0.16.x adapter. - // code-reviewer fix-round: codex readiness gate tests — 2 new tests for - // outdated-adapter and garbage-version-output paths through the codex id gate - // (+140 lines: make_codex_runtime helper, PATH_MUTEX serializer, 2 test fns). - // Load-bearing test coverage; queued to split with the file generally. // +1: pub(crate) mod cli_probe declaration for doctor auth probe access. // +3: auth_probe_args: None + login_hint: None added to make_cli_runtime and // make_codex_runtime stubs (new KnownAcpRuntime fields). @@ -167,7 +161,10 @@ const overrides = new Map([ // Windows Doctor install fix: cli_install_commands_windows field added to test stubs. // team-instructions-first-class: ManagedAgentRecord fixture gains the new // team_id field (+1 line). - ["src-tauri/src/managed_agents/readiness.rs", 1765], + // 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], // 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 @@ -208,7 +205,6 @@ const overrides = new Map([ // split with the rest of this file. ["src/shared/api/tauri.ts", 1340], // doctor-npm-eacces-preflight: hint field added to InstallStepResult (+1 line). - // codex-acp-package-swap: "adapter_outdated" variant added to AcpAvailabilityStatus (+1 line). // doctor-install-reliability: AuthStatus tagged union + nodeRequired/authStatus/ // loginHint fields on AcpRuntimeCatalogEntry (+14 lines). Load-bearing new feature. // agent-lifecycle-fixes: GlobalAgentConfigSaveResult type grows with @@ -223,8 +219,9 @@ const overrides = new Map([ // field. // bundle-acps: NodeRuntimeCheck + NodeRuntimeRequirement types for the // bundled-bridge Node.js doctor check (+23 lines on rebase union with - // main's Git Bash / signout-wipe / team-instructions type growth). - ["src/shared/api/types.ts", 1070], + // main's Git Bash / signout-wipe / team-instructions type growth); + // "adapter_outdated" availability retired with the codex version gate (-1 line). + ["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 @@ -258,14 +255,6 @@ const overrides = new Map([ // agent-config-propagation: the agent_command_override decision family // (divergent / create-time / update-time / apply) moved to // discovery/overrides.rs; ratcheting 802 -> 685 to bank the headroom. - // codex-acp-package-swap: probe_codex_acp_major_version (+24 lines) + - // AdapterOutdated version-gate in discover_acp_runtimes (+22 lines). Both - // load-bearing — required to detect the deprecated 0.16.x adapter and - // prevent silent relay breakage after the spawn-contract change. - // codex-acp-package-swap follow-up: tempfile-based bounded stdout read - // (+18 lines), codex_adapter_availability/is_outdated helpers (+16 lines), - // cross-platform probe contract. All load-bearing — required for correct - // probe behaviour on Windows and descendant-process edge cases. // doctor-install-reliability: refreshable login_shell_path cache, // find_nvm_default_bin + parse_semver_tag helpers, auth probe cache + // probe_auth_status/cached_auth_status, runtime_needs_npm, probe_args_for, @@ -286,11 +275,12 @@ const overrides = new Map([ // +13: fetch_login_shell_path_inner Windows guard (POSIX PATH → None). // resolve_git_bash made pub(crate) for Windows test access. // +1: login_shell_candidates doc comment expanded for resolve_bash_path. - // bundle-acps: bundled ACP bridge check at the top of the resolution sweep - // (+4 lines over the Windows baseline). Temporary — the codex version-gate - // retirement later in the same series deletes far more from this file and - // ratchets this back down. - ["src-tauri/src/managed_agents/discovery.rs", 1371], + // bundle-acps: bundled ACP bridge check at the top of the resolution sweep, + // then the codex version-gate retirement (probe_codex_acp_major_version, + // codex_adapter_availability/is_outdated, AdapterOutdated arm) made + // obsolete by pinned bundling; ratcheting 1371 -> 1250 to bank the + // deletions (main's Windows Doctor install growth stays). + ["src-tauri/src/managed_agents/discovery.rs", 1250], // 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. @@ -302,7 +292,10 @@ const overrides = new Map([ // +32: deterministic .cmd resolver + no-registry + install_shell_from tests. // team-instructions-first-class: record_with test fixture gained the new // ManagedAgentRecord.team_id field (+1 line) alongside persona_team_dir. - ["src-tauri/src/managed_agents/discovery/tests.rs", 1271], + // 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). + ["src-tauri/src/managed_agents/discovery/tests.rs", 1067], // 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 diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index 052bc44ff..c6c31c09c 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -19,40 +19,24 @@ fn active_installs() -> &'static std::sync::Mutex( - runtime_id: &str, adapter_path: Option<&std::path::Path>, adapter_install_commands: &'c [&'c str], ) -> Option> { match adapter_path { - // Adapter present and current — no install needed. - Some(_) if runtime_id != "codex" => None, - Some(path) if !crate::managed_agents::codex_adapter_is_outdated(path) => None, - // Codex adapter is outdated: uninstall the old package first so npm - // doesn't hit EEXIST on the shared `codex-acp` bin-link, then install. - Some(_) => Some(vec![ - "npm uninstall -g @zed-industries/codex-acp", - "npm install -g @agentclientprotocol/codex-acp", - ]), + // Adapter present — no install needed. + Some(_) => None, // Adapter missing: use the catalog's install commands directly. None => Some(adapter_install_commands.to_vec()), } @@ -177,19 +161,16 @@ fn install_acp_runtime_blocking(runtime_id: &str) -> Result &'static std::sync::Mutex> { @@ -1096,114 +1096,6 @@ pub(crate) fn classify_runtime( } } -/// Probe the major version of a `codex-acp` binary by running `--version`. -/// -/// The 1.x adapter (`@agentclientprotocol/codex-acp`) outputs -/// `@agentclientprotocol/codex-acp ..` on stdout and exits 0. -/// The old 0.16.x adapter (`@zed-industries/codex-acp`) is a Rust binary that does -/// not recognise `--version` and exits non-zero. -/// -/// Returns the major version on success, `None` on any failure (non-zero exit, -/// unparseable output, timeout, or missing binary). -/// -/// The probe is bounded by a 5-second deadline. The child is polled with -/// [`std::process::Child::try_wait`] (the repo's standard deadline pattern) and -/// killed if it does not exit in time. -/// -/// Stdout is redirected to a temporary file rather than a pipe, so forked -/// descendants cannot hold EOF open. Reads from a regular file return EOF at its -/// current write position regardless of inherited file descriptors, cross-platform. -pub(crate) fn probe_codex_acp_major_version(binary_path: &Path) -> Option { - probe_codex_acp_major_version_with_path( - binary_path, - crate::managed_agents::readiness::cli_probe::augmented_path().as_deref(), - ) -} -pub(crate) fn probe_codex_acp_major_version_with_path( - binary_path: &Path, - augmented_path: Option<&str>, -) -> Option { - use std::io::{Read as _, Seek as _, SeekFrom}; - use std::time::{Duration, Instant}; - const VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(5); - - // A regular file returns EOF at its current size even when a descendant - // inherits its descriptor, bounding the post-exit read cross-platform. - let mut tmp = tempfile::tempfile().ok()?; - - let mut command = Command::new(binary_path); - command.arg("--version"); - if let Some(path) = augmented_path { - command.env("PATH", path); - } - let mut child = command - .stdout(tmp.try_clone().ok()?) - .stderr(std::process::Stdio::null()) - .spawn() - .ok()?; - - // Poll until the deadline rather than blocking on stdout EOF. - let deadline = Instant::now() + VERSION_PROBE_TIMEOUT; - let exit_status = loop { - match child.try_wait() { - Ok(Some(status)) => break status, - Ok(None) => { - if Instant::now() >= deadline { - let _ = child.kill(); - let _ = child.wait(); - return None; - } - std::thread::sleep(Duration::from_millis(50)); - } - Err(_) => { - let _ = child.kill(); - let _ = child.wait(); - return None; - } - } - }; - - if !exit_status.success() { - return None; - } - - // Read at most 4 KiB from the regular file without blocking. - tmp.seek(SeekFrom::Start(0)).ok()?; - let mut buf = Vec::with_capacity(128); - let _ = (&mut tmp as &mut dyn std::io::Read) - .take(4096) - .read_to_end(&mut buf); - - let stdout = String::from_utf8_lossy(&buf); - // Output format: " .." - let version_str = stdout.split_whitespace().last()?; - let major_str = version_str.split('.').next()?; - major_str.parse::().ok() -} - -/// Classifies a resolved codex-acp binary path as [`AcpAvailabilityStatus::Available`] -/// or [`AcpAvailabilityStatus::AdapterOutdated`]. -/// -/// The 0.16.x adapter (`@zed-industries/codex-acp`) does not recognise `--version` -/// and exits non-zero — that probe failure yields `AdapterOutdated`. The 1.x adapter -/// (`@agentclientprotocol/codex-acp`) prints its version and exits 0; major ≥ 1 -/// yields `Available`. -/// -/// Used by `discover_acp_runtimes`, `cli_login_requirements`, and -/// `install_acp_runtime_blocking` so the version-gate logic is not duplicated. -pub(crate) fn codex_adapter_availability(path: &Path) -> AcpAvailabilityStatus { - match probe_codex_acp_major_version(path) { - Some(major) if major >= 1 => AcpAvailabilityStatus::Available, - _ => AcpAvailabilityStatus::AdapterOutdated, - } -} - -/// Returns `true` when the codex-acp binary at `path` is outdated (major version < 1) -/// or cannot be probed. Thin wrapper around [`codex_adapter_availability`]. -pub(crate) fn codex_adapter_is_outdated(path: &Path) -> bool { - codex_adapter_availability(path) == AcpAvailabilityStatus::AdapterOutdated -} - /// Intermediate struct built before the (potentially slow) auth probe phase. struct PartialEntry { runtime: &'static KnownAcpRuntime, @@ -1224,21 +1116,9 @@ pub fn discover_acp_runtimes() -> Vec { .underlying_cli .map(|cli| find_command(cli).is_some()) .unwrap_or(false); - let (mut availability, command, binary_path) = + let (availability, command, binary_path) = classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found); - // For codex-acp: when the adapter resolves as Available, probe the - // version. An adapter with major version < 1 is treated as outdated — - // the CODEX_CONFIG spawn contract requires 1.x. - if runtime.id == "codex" - && availability == AcpAvailabilityStatus::Available - && command.as_deref() == Some("codex-acp") - { - if let Some(path_str) = &binary_path { - availability = codex_adapter_availability(&PathBuf::from(path_str)); - } - } - // Warm the adapter-availability cache for the badge fallback. // The cache is scoped to the codex runtime; other runtimes leave it // unchanged. Invalidated by `clear_resolve_cache`. @@ -1265,7 +1145,6 @@ pub fn discover_acp_runtimes() -> Vec { AcpAvailabilityStatus::Available => cli_hint.to_string(), AcpAvailabilityStatus::CliMissing => cli_hint.to_string(), AcpAvailabilityStatus::AdapterMissing => adapter_hint.to_string(), - AcpAvailabilityStatus::AdapterOutdated => adapter_hint.to_string(), AcpAvailabilityStatus::NotInstalled => { if !cli_hint.is_empty() && !adapter_hint.is_empty() { format!("{cli_hint} {adapter_hint}") diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 608b511fa..7ce9fa593 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -2,13 +2,11 @@ use std::path::PathBuf; use super::overrides::{divergent_agent_command_override, update_time_agent_command_override}; use super::{ - apply_agent_command_update, classify_runtime, codex_adapter_availability, - codex_adapter_is_outdated, create_time_agent_command_override, default_agent_command, - effective_agent_command, find_nvm_default_bin, find_via_login_shell, + apply_agent_command_update, classify_runtime, create_time_agent_command_override, + default_agent_command, effective_agent_command, find_nvm_default_bin, find_via_login_shell, is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, normalize_agent_args, - parse_semver_tag, probe_codex_acp_major_version, record_agent_command, - refresh_login_shell_path, BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, - GOOSE_AVATAR_URL, + parse_semver_tag, record_agent_command, refresh_login_shell_path, BUZZ_AGENT_AVATAR_URL, + CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL, }; use crate::managed_agents::AcpAvailabilityStatus; @@ -607,208 +605,6 @@ fn apply_agent_command_update_concrete_pin_keeps_materialized_runtime() { assert_eq!(record_agent_command(&record, &personas), "codex-acp"); } -// ── probe_codex_acp_major_version ───────────────────────────────────────────── - -#[cfg(unix)] -#[test] -fn probe_codex_acp_major_version_parses_1x_output() { - use std::os::unix::fs::PermissionsExt; - - // Simulate `@agentclientprotocol/codex-acp 1.1.2` output (1.x adapter) - let dir = std::env::temp_dir().join(format!("buzz-probe-1x-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&dir).expect("create temp dir"); - let bin = dir.join("codex-acp"); - std::fs::write( - &bin, - "#!/bin/sh\necho '@agentclientprotocol/codex-acp 1.1.2'\nexit 0\n", - ) - .expect("write script"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).expect("chmod script"); - - let major = probe_codex_acp_major_version(&bin); - let _ = std::fs::remove_dir_all(dir); - - assert_eq!(major, Some(1), "1.x adapter must return major version 1"); -} - -mod codex_version; - -#[cfg(unix)] -#[test] -fn probe_codex_acp_major_version_returns_none_for_nonzero_exit() { - use std::os::unix::fs::PermissionsExt; - - // Simulate old 0.16.x adapter: `--version` is unrecognised, exits non-zero - let dir = std::env::temp_dir().join(format!("buzz-probe-0x-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&dir).expect("create temp dir"); - let bin = dir.join("codex-acp"); - std::fs::write(&bin, "#!/bin/sh\nexit 1\n").expect("write script"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).expect("chmod script"); - - let major = probe_codex_acp_major_version(&bin); - let _ = std::fs::remove_dir_all(dir); - - assert_eq!( - major, None, - "old 0.16.x adapter (non-zero exit) must return None" - ); -} - -#[cfg(unix)] -#[test] -fn probe_codex_acp_major_version_returns_none_for_missing_binary() { - let path = std::path::Path::new("/nonexistent/path/codex-acp-does-not-exist"); - let major = probe_codex_acp_major_version(path); - assert_eq!(major, None, "missing binary must return None"); -} - -// ── codex_adapter_availability / codex_adapter_is_outdated ─────────────────── -// -// Outcome-level classification: verify helpers map probe results to the correct -// AcpAvailabilityStatus and boolean without duplicating version-gate logic. - -#[cfg(unix)] -#[test] -fn codex_adapter_availability_available_for_1x_binary() { - use std::os::unix::fs::PermissionsExt; - - let dir = std::env::temp_dir().join(format!("buzz-avail-1x-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&dir).expect("create temp dir"); - let bin = dir.join("codex-acp"); - std::fs::write( - &bin, - "#!/bin/sh\necho '@agentclientprotocol/codex-acp 1.1.2'\nexit 0\n", - ) - .expect("write script"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).expect("chmod script"); - - let status = codex_adapter_availability(&bin); - let _ = std::fs::remove_dir_all(dir); - - assert_eq!( - status, - AcpAvailabilityStatus::Available, - "1.x adapter must classify as Available" - ); -} - -#[cfg(unix)] -#[test] -fn codex_adapter_availability_outdated_for_0x_binary() { - use std::os::unix::fs::PermissionsExt; - - // Simulate old 0.16.x: `--version` exits non-zero (unrecognised flag) - let dir = std::env::temp_dir().join(format!("buzz-avail-0x-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&dir).expect("create temp dir"); - let bin = dir.join("codex-acp"); - std::fs::write(&bin, "#!/bin/sh\nexit 1\n").expect("write script"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).expect("chmod script"); - - let status = codex_adapter_availability(&bin); - let _ = std::fs::remove_dir_all(dir); - - assert_eq!( - status, - AcpAvailabilityStatus::AdapterOutdated, - "0.x adapter (non-zero exit) must classify as AdapterOutdated" - ); -} - -#[cfg(unix)] -#[test] -fn codex_adapter_availability_outdated_for_missing_binary() { - let path = std::path::Path::new("/nonexistent/codex-acp-probe-test"); - assert_eq!( - codex_adapter_availability(path), - AcpAvailabilityStatus::AdapterOutdated, - "missing binary must classify as AdapterOutdated" - ); - // Thin wrapper consistency - assert!( - codex_adapter_is_outdated(path), - "missing binary must be classified as outdated via thin wrapper" - ); -} - -#[cfg(unix)] -#[test] -fn probe_codex_acp_major_version_returns_none_for_hung_direct_child() { - use std::os::unix::fs::PermissionsExt; - use std::time::Instant; - - // Simulate a process that writes version to stdout then blocks forever. - // The probe reads stdout only after the child exits, so it will time out. - // `exec sleep 300` replaces the shell so killing the child reaps `sleep` too. - let dir = std::env::temp_dir().join(format!("buzz-probe-hung-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&dir).expect("create temp dir"); - let bin = dir.join("codex-acp"); - std::fs::write( - &bin, - "#!/bin/sh\nprintf '@agentclientprotocol/codex-acp 1.1.2\\n'\nexec sleep 300\n", - ) - .expect("write script"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).expect("chmod script"); - - let start = Instant::now(); - let major = probe_codex_acp_major_version(&bin); - let elapsed = start.elapsed(); - let _ = std::fs::remove_dir_all(dir); - - assert_eq!( - major, None, - "hung binary must return None (timeout kills child)" - ); - // The timeout is 5 s; give a 10 s margin for parallel pre-push suites. - assert!( - elapsed.as_secs() < 15, - "probe must complete within timeout bound; elapsed: {elapsed:?}" - ); -} - -#[cfg(unix)] -#[test] -fn probe_codex_acp_major_version_returns_version_when_descendant_holds_pipe_open() { - use std::os::unix::fs::PermissionsExt; - use std::time::Instant; - - // Simulate a process that forks a background child which inherits stdout - // and stays alive, while the parent writes version and exits 0. - // - // The probe writes the child's stdout to a temp file, then reads from the - // file after the parent process exits. Because the file has reached EOF - // (the parent closed its write end), read_to_end() returns immediately - // without waiting for the descendant to close its inherited fd. - // - // `(exec sleep 60 &)` forks a subshell that execs `sleep 60`; the subshell - // inherits the parent's stdout fd and keeps it open. - let dir = std::env::temp_dir().join(format!("buzz-probe-descendant-{}", uuid::Uuid::new_v4())); - std::fs::create_dir_all(&dir).expect("create temp dir"); - let bin = dir.join("codex-acp"); - std::fs::write( - &bin, - "#!/bin/sh\necho '@agentclientprotocol/codex-acp 1.1.2'\n(exec sleep 60 &)\nexit 0\n", - ) - .expect("write script"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).expect("chmod script"); - - let start = Instant::now(); - let major = probe_codex_acp_major_version(&bin); - let elapsed = start.elapsed(); - let _ = std::fs::remove_dir_all(dir); - - // Must return within ~1 s: non-blocking read, no waiting for descendant. - // Give a 9 s margin for parallel pre-push suites. - assert!( - elapsed.as_secs() < 10, - "probe must not block on descendant pipe; elapsed: {elapsed:?}" - ); - assert_eq!( - major, - Some(1), - "1.x version must be parsed even when descendant holds pipe open" - ); -} - // ── parse_semver_tag ────────────────────────────────────────────────────────── #[test] diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests/codex_version.rs b/desktop/src-tauri/src/managed_agents/discovery/tests/codex_version.rs deleted file mode 100644 index 5886a4399..000000000 --- a/desktop/src-tauri/src/managed_agents/discovery/tests/codex_version.rs +++ /dev/null @@ -1,48 +0,0 @@ -use super::super::probe_codex_acp_major_version_with_path; - -#[cfg(unix)] -#[test] -fn probe_codex_acp_major_version_uses_augmented_path_for_env_shebang_interpreter() { - use std::fs; - use std::os::unix::fs::PermissionsExt; - let temp = tempfile::tempdir().expect("temp dir"); - let script_dir = temp.path().join("script-bin"); - let interpreter_dir = temp.path().join("interpreter-bin"); - let empty_path_dir = temp.path().join("empty-bin"); - fs::create_dir_all(&script_dir).expect("script dir"); - fs::create_dir_all(&interpreter_dir).expect("interpreter dir"); - fs::create_dir_all(&empty_path_dir).expect("empty path dir"); - - let interpreter_path = interpreter_dir.join("node"); - fs::write( - &interpreter_path, - "#!/bin/sh\necho '@agentclientprotocol/codex-acp 1.1.2'\n", - ) - .expect("write interpreter"); - fs::set_permissions(&interpreter_path, fs::Permissions::from_mode(0o755)) - .expect("chmod interpreter"); - - let shim_path = script_dir.join("codex-acp"); - fs::write(&shim_path, "#!/usr/bin/env node\n").expect("write shim"); - fs::set_permissions(&shim_path, fs::Permissions::from_mode(0o755)).expect("chmod shim"); - - let scrubbed_path = std::env::join_paths([empty_path_dir.as_path()]) - .expect("join scrubbed PATH") - .to_string_lossy() - .into_owned(); - assert_eq!( - probe_codex_acp_major_version_with_path(&shim_path, Some(&scrubbed_path)), - None, - "with a scrubbed PATH, /usr/bin/env should not find node" - ); - - let augmented_path = std::env::join_paths([interpreter_dir.as_path()]) - .expect("join augmented PATH") - .to_string_lossy() - .into_owned(); - assert_eq!( - probe_codex_acp_major_version_with_path(&shim_path, Some(&augmented_path)), - Some(1), - "the injected augmented PATH should allow /usr/bin/env to find node" - ); -} diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index 6a5af8925..05f001e24 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -39,7 +39,6 @@ //! UI display only. use std::collections::BTreeMap; -use std::path::Path; use serde::{Deserialize, Serialize}; @@ -47,8 +46,7 @@ use crate::managed_agents::{ agent_env::baked_build_env, config_bridge::read_goose_file_config, discovery::{ - classify_runtime, codex_adapter_availability, find_command, known_acp_runtime, - resolve_command, KnownAcpRuntime, + classify_runtime, find_command, known_acp_runtime, resolve_command, KnownAcpRuntime, }, env_vars::merged_user_env, global_config::GlobalAgentConfig, @@ -510,26 +508,9 @@ fn cli_login_requirements( .map(|cli| find_command(cli).is_some()) .unwrap_or(false); - let (availability, cmd, adapter_path) = + let (availability, _cmd, _adapter_path) = classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found); - // For codex-acp: if the adapter resolved as Available, probe the version. - // An adapter with major version < 1 is the deprecated package and must be - // treated as outdated (blocks login probe — the agent can't reach the relay). - // Guard on `cmd == "codex-acp"` to match the discovery path and avoid - // probing when the runtime resolves via an alias command. - let availability = if runtime.id == "codex" - && availability == AcpAvailabilityStatus::Available - && cmd.as_deref() == Some("codex-acp") - { - adapter_path - .as_deref() - .map(|path_str| codex_adapter_availability(Path::new(path_str))) - .unwrap_or(availability) - } else { - availability - }; - match availability { AcpAvailabilityStatus::Available => { // Both adapter and CLI are present — probe login status. @@ -1132,153 +1113,6 @@ mod tests { } } - // ── codex readiness version gate ─────────────────────────────────────── - - /// Build a minimal `KnownAcpRuntime` for testing the codex version gate. - /// `adapter_commands` are the exact strings passed to `find_command` — use - /// `&["codex-acp"]` when the binary is on PATH, or `&[]` - /// when resolving via absolute path. `underlying_cli` is a portable - /// stand-in so the adapter is not misclassified as `CliMissing`. - fn make_codex_runtime( - adapter_commands: &'static [&'static str], - underlying_cli: Option<&'static str>, - ) -> KnownAcpRuntime { - KnownAcpRuntime { - id: "codex", - label: "Codex", - commands: adapter_commands, - aliases: &[], - avatar_url: "", - mcp_command: None, - mcp_hooks: false, - underlying_cli, - cli_install_commands: &[], - cli_install_commands_windows: &[], - adapter_install_commands: &[], - install_instructions_url: "", - cli_install_hint: "", - adapter_install_hint: "", - skill_dir: None, - supports_acp_model_switching: false, - config_file_path: None, - config_file_format: None, - model_env_var: None, - provider_env_var: None, - provider_locked: false, - default_env: &[], - supports_acp_native_config: false, - thinking_env_var: None, - max_tokens_env_var: None, - context_limit_env_var: None, - required_normalized_fields: &[], - login_hint: None, - auth_probe_args: None, - } - } - - /// Build a temp dir containing a `codex-acp` script with the given body, - /// prepend it to PATH, and clear the resolve cache. Returns the temp dir - /// and the original PATH string for restoration. - #[cfg(unix)] - fn setup_temp_codex_acp(script_body: &str) -> (tempfile::TempDir, String) { - use std::os::unix::fs::PermissionsExt; - - let dir = tempfile::tempdir().expect("create temp dir"); - let bin = dir.path().join("codex-acp"); - std::fs::write(&bin, script_body).expect("write script"); - std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)) - .expect("chmod script"); - - let original_path = std::env::var("PATH").unwrap_or_default(); - let new_path = format!("{}:{}", dir.path().display(), original_path); - std::env::set_var("PATH", &new_path); - crate::managed_agents::clear_resolve_cache(); - - (dir, original_path) - } - - /// Restore PATH and clear the resolve cache after a PATH-mutating test. - #[cfg(unix)] - fn restore_path(original: &str) { - std::env::set_var("PATH", original); - crate::managed_agents::clear_resolve_cache(); - } - - /// Codex readiness: outdated adapter (exits non-zero) → AdapterOutdated, - /// login probe skipped. - #[cfg(unix)] - #[test] - fn cli_login_requirements_codex_outdated_adapter_emits_adapter_outdated() { - let _guard = crate::managed_agents::lock_path_mutex(); - - let (dir, orig) = setup_temp_codex_acp("#!/bin/sh\nexit 1\n"); - let exe = present_binary_str(); - // underlying_cli = running test binary (always present, never probed) - let rt = make_codex_runtime(&["codex-acp"], Some(exe)); - let reqs = cli_login_requirements( - &[exe, "--buzz-probe-must-not-run-xyz"], - "run `codex login`", - &rt, - ); - - restore_path(&orig); - drop(dir); - - assert!( - !reqs.is_empty(), - "outdated codex adapter must produce a requirement; got {reqs:?}" - ); - if let Requirement::CliLogin { - ref availability, .. - } = reqs[0] - { - assert_eq!( - *availability, - crate::managed_agents::AcpAvailabilityStatus::AdapterOutdated, - "0.x codex adapter must yield AdapterOutdated; got {availability:?}" - ); - } else { - panic!("expected CliLogin requirement; got {:?}", reqs[0]); - } - } - - /// Codex readiness: adapter exits 0 but output is not a parseable version - /// → AdapterOutdated (garbage output treated as outdated, same as non-zero). - #[cfg(unix)] - #[test] - fn cli_login_requirements_codex_garbage_version_output_emits_adapter_outdated() { - let _guard = crate::managed_agents::lock_path_mutex(); - - let (dir, orig) = setup_temp_codex_acp("#!/bin/sh\necho 'not a version string'\nexit 0\n"); - let exe = present_binary_str(); - let rt = make_codex_runtime(&["codex-acp"], Some(exe)); - let reqs = cli_login_requirements( - &[exe, "--buzz-probe-must-not-run-xyz"], - "run `codex login`", - &rt, - ); - - restore_path(&orig); - drop(dir); - - assert!( - !reqs.is_empty(), - "garbage version output must produce a requirement; got {reqs:?}" - ); - if let Requirement::CliLogin { - ref availability, .. - } = reqs[0] - { - assert_eq!( - *availability, - crate::managed_agents::AcpAvailabilityStatus::AdapterOutdated, - "unparseable version output must yield AdapterOutdated; got {availability:?}" - ); - } else { - panic!("expected CliLogin requirement; got {:?}", reqs[0]); - } - } - // ── custom/unknown command ───────────────────────────────────────────── #[test] diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 9cb235476..36131fde6 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -534,8 +534,6 @@ pub struct ManagedAgentLogResponse { pub enum AcpAvailabilityStatus { Available, AdapterMissing, - /// Adapter binary is present but is from the deprecated package (< 1.0). Reinstall required. - AdapterOutdated, CliMissing, NotInstalled, } diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx index 28b51b1ef..25cfe9af1 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx @@ -562,11 +562,9 @@ export function AgentDefinitionDialog({

{selectedRuntime.availability === "adapter_missing" ? `${selectedRuntime.label} CLI is installed but the ACP adapter is missing.` - : selectedRuntime.availability === "adapter_outdated" - ? `${selectedRuntime.label} ACP adapter is outdated — reinstall to continue.` - : selectedRuntime.availability === "cli_missing" - ? `${selectedRuntime.label} ACP adapter is installed but the CLI 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.tsx b/desktop/src/features/agents/ui/personaDialogPickers.tsx index 07cc6ae2f..72a96ca99 100644 --- a/desktop/src/features/agents/ui/personaDialogPickers.tsx +++ b/desktop/src/features/agents/ui/personaDialogPickers.tsx @@ -383,13 +383,11 @@ export function formatRuntimeOptionLabel(runtime: AcpRuntimeCatalogEntry) { const suffix = runtime.availability === "adapter_missing" ? " (adapter missing)" - : runtime.availability === "adapter_outdated" - ? " (adapter outdated)" - : runtime.availability === "cli_missing" - ? " (CLI missing)" - : runtime.availability === "not_installed" - ? " (not installed)" - : ""; + : runtime.availability === "cli_missing" + ? " (CLI missing)" + : runtime.availability === "not_installed" + ? " (not installed)" + : ""; return `${runtime.label}${suffix}`; } @@ -405,8 +403,6 @@ function runtimeAvailabilitySortRank( return 2; case "adapter_missing": return 3; - case "adapter_outdated": - return 3; } } diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx index bd59f0124..355953808 100644 --- a/desktop/src/features/onboarding/ui/SetupStep.tsx +++ b/desktop/src/features/onboarding/ui/SetupStep.tsx @@ -353,29 +353,6 @@ function RuntimeDetails({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { ); } - if (runtime.availability === "adapter_outdated") { - return ( - <> -

- ACP adapter detected but outdated — reinstall required. -

-

- This updates the machine-global{" "} - codex-acp{" "} - adapter. Older Buzz releases using the legacy adapter contract may - lose community access until{" "} - - @zed-industries/codex-acp@0.16.0 - {" "} - is restored. -

-

- {runtime.installHint} -

- - ); - } - if (runtime.availability === "cli_missing") { return ( <> diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index 5350a52a2..2c70a453c 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -38,8 +38,6 @@ function StatusIcon({ return ; case "adapter_missing": return ; - case "adapter_outdated": - return ; case "cli_missing": return ; case "not_installed": @@ -183,7 +181,6 @@ function RuntimeRow({ runtime.availability === "available" ? "bg-background/60" : runtime.availability === "adapter_missing" || - runtime.availability === "adapter_outdated" || runtime.availability === "cli_missing" ? "bg-amber-500/5" : "bg-muted/20", @@ -283,38 +280,6 @@ function RuntimeRow({ runtime={runtime} /> - ) : runtime.availability === "adapter_outdated" ? ( - <> -

- ACP adapter found at{" "} - - {runtime.binaryPath ?? "unknown path"} - {" "} - but it is from the deprecated package. Reinstall to enable relay - connectivity. -

-

- This updates the machine-global{" "} - - codex-acp - {" "} - adapter. Older Buzz releases using the legacy adapter contract may - lose community access until{" "} - - @zed-industries/codex-acp@0.16.0 - {" "} - is restored. -

-

- {runtime.installHint} -

- - ) : runtime.availability === "cli_missing" ? ( <>

diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts index 997b70933..a959c558a 100644 --- a/desktop/src/shared/api/types.ts +++ b/desktop/src/shared/api/types.ts @@ -557,7 +557,6 @@ export type GitBashPrerequisite = { export type AcpAvailabilityStatus = | "available" | "adapter_missing" - | "adapter_outdated" | "cli_missing" | "not_installed"; diff --git a/desktop/src/shared/lib/configNudge.test.mjs b/desktop/src/shared/lib/configNudge.test.mjs index f616d7829..8e1603e23 100644 --- a/desktop/src/shared/lib/configNudge.test.mjs +++ b/desktop/src/shared/lib/configNudge.test.mjs @@ -79,10 +79,11 @@ test("extractConfigNudge parses cli_login requirement", () => { assert.deepEqual(extractConfigNudge(withSentinel("prose", payload)), payload); }); -test("extractConfigNudge parses cli_login with adapter_outdated availability", () => { - // Backend emits availability: "adapter_outdated" when codex-acp is old (0.16.x). - // isConfigNudgeRequirement must accept this literal — regression test for the - // validator omission that caused it to be silently rejected. +test("extractConfigNudge rejects retired adapter_outdated availability", () => { + // The codex version gate was retired when the ACP bridges started shipping + // bundled with the app — "adapter_outdated" is no longer a valid + // 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: "Codex", agent_pubkey: CODEX_PUBKEY, @@ -95,10 +96,10 @@ test("extractConfigNudge parses cli_login with adapter_outdated availability", ( }, ], }; - assert.deepEqual( + assert.equal( extractConfigNudge(withSentinel("prose", payload)), - payload, - "adapter_outdated availability must be accepted by the validator", + null, + "retired adapter_outdated availability must be rejected by the validator", ); }); diff --git a/desktop/src/shared/lib/configNudge.ts b/desktop/src/shared/lib/configNudge.ts index 7893236c2..901bb2273 100644 --- a/desktop/src/shared/lib/configNudge.ts +++ b/desktop/src/shared/lib/configNudge.ts @@ -32,7 +32,6 @@ 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 - * - "adapter_outdated" → ACP adapter present but from deprecated package; reinstall required * - "cli_missing" → ACP adapter installed but CLI missing * - "not_installed" → neither adapter nor CLI found */ @@ -142,7 +141,6 @@ function isConfigNudgeRequirement(v: unknown): v is ConfigNudgeRequirement { typeof r.setup_copy === "string" && (r.availability === "available" || r.availability === "adapter_missing" || - r.availability === "adapter_outdated" || r.availability === "cli_missing" || r.availability === "not_installed") ); diff --git a/desktop/src/shared/ui/config-nudge-attachment.tsx b/desktop/src/shared/ui/config-nudge-attachment.tsx index 4be3e6398..f21e5a329 100644 --- a/desktop/src/shared/ui/config-nudge-attachment.tsx +++ b/desktop/src/shared/ui/config-nudge-attachment.tsx @@ -107,8 +107,6 @@ function cliLoginMessage( return `${harness} CLI is missing`; case "adapter_missing": return `${harness} ACP adapter isn't installed`; - case "adapter_outdated": - return `${harness} ACP adapter is outdated — reinstall required`; case "available": // Tooling is present but authentication is needed — fall back to // the backend-supplied copy which has the exact login command.