Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ const overrides = new Map([
// entries.push block collapsed into the helper.
// +6: legacy Goose Windows install dir (%USERPROFILE%\goose) probed in
// common_binary_paths so pre-#2680 standalone installs are discoverable.
["src-tauri/src/managed_agents/discovery.rs", 1841],
// +19: codex-acp minimum-version gate — MIN_CODEX_ACP_VERSION plus the strict
// three-component parse in probe_codex_acp_version, so an outdated 1.x adapter
// is offered a reinstall instead of classifying as Available on major alone.
["src-tauri/src/managed_agents/discovery.rs", 1860],
// BYOH — save_custom_harness_to_dir (backup-swap atomic write) + save_and_warm /
// delete_and_warm (persist-mutex serialization for concurrent-safe registry
// refresh, B-6). Also: id/collision/load/registry tests (from the file base) +
Expand Down Expand Up @@ -391,7 +394,11 @@ const overrides = new Map([
// Available both-present AND adapter-present/CLI-absent — the selectability
// regression guard), bound to an injectable resolver so the tests stay
// PATH-independent.
["src-tauri/src/managed_agents/discovery/tests.rs", 1871],
// +51: codex-acp minimum-version gate — probe_codex_acp_version assertions carry
// the full (major, minor, patch) triple instead of a bare major, plus
// below-the-floor and uncomparable-version (partial / prerelease) classification
// regressions for the fail-closed parse.
["src-tauri/src/managed_agents/discovery/tests.rs", 1922],
// 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
Expand Down Expand Up @@ -633,7 +640,9 @@ const overrides = new Map([
// with backoff, output truncation) extracted to agent_discovery/install_exec.rs
// alongside its tests, matching the managed_node.rs / post_install_verification.rs
// split. The entries above describe the file's history, not its current shape.
["src-tauri/src/commands/agent_discovery.rs", 1808],
// +27: codex-acp minimum-version gate — test_plan_adapter_install_updates_older_
// 1x_codex_binary pins that a 1.x adapter below the floor still plans a reinstall.
["src-tauri/src/commands/agent_discovery.rs", 1835],
// 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
Expand Down
37 changes: 32 additions & 5 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ fn active_installs() -> &'static std::sync::Mutex<std::collections::HashSet<Stri
/// `None` if none was found).
///
/// Returns `None` when no install is needed (adapter is present and current).
/// Returns `Some(cmds)` when the adapter is missing or (for codex) outdated.
/// Returns `Some(cmds)` when the adapter is missing or (for codex) below its
/// minimum supported version.
///
/// For the codex **outdated** case the returned sequence is a two-step
/// reinstall: first uninstall the old `@zed-industries/codex-acp` package
Expand Down Expand Up @@ -1152,7 +1153,8 @@ mod tests {
/// plan_adapter_install is the pure install-plan seam used by
/// install_acp_runtime_blocking. These tests verify:
/// - A 0.x binary (AdapterOutdated) → uninstall-then-install sequence returned
/// - A 1.x binary (Available) → None (no reinstall)
/// - A current 1.x binary (Available) → None (no reinstall)
/// - A 1.x binary below the floor → install plan returned
/// - Missing binary (None path) → catalog install commands returned
#[cfg(unix)]
#[test]
Expand Down Expand Up @@ -1192,10 +1194,10 @@ mod tests {

let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("codex-acp");
// Simulate 1.x adapter: outputs version and exits 0
// Simulate the minimum supported adapter version.
std::fs::write(
&bin,
"#!/bin/sh\necho '@agentclientprotocol/codex-acp 1.1.2'\nexit 0\n",
"#!/bin/sh\necho '@agentclientprotocol/codex-acp 1.1.7'\nexit 0\n",
)
.expect("write script");
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755))
Expand All @@ -1206,7 +1208,32 @@ mod tests {

assert!(
plan.is_none(),
"1.x codex adapter must not trigger install plan (no reinstall needed)"
"current codex adapter must not trigger install plan (no reinstall needed)"
);
}

#[cfg(unix)]
#[test]
fn test_plan_adapter_install_updates_older_1x_codex_binary() {
use std::os::unix::fs::PermissionsExt;

let dir = tempfile::tempdir().unwrap();
let bin = dir.path().join("codex-acp");
// A 1.x adapter below MIN_CODEX_ACP_VERSION must still be reinstalled.
std::fs::write(
&bin,
"#!/bin/sh\necho '@agentclientprotocol/codex-acp 1.1.5'\nexit 0\n",
)
.expect("write script");
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755))
.expect("chmod script");

let install_cmds = &["npm install -g @agentclientprotocol/codex-acp"];
let plan = plan_adapter_install("codex", Some(&bin), install_cmds, Some("/usr/bin:/bin"));

assert!(
plan.is_some(),
"older 1.x codex adapter must trigger update plan"
);
}

Expand Down
69 changes: 44 additions & 25 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ pub fn clear_resolve_cache() {
//
// `build_managed_agent_summary` needs to compare the spawn-time adapter
// availability against the *current* availability without triggering a live
// `probe_codex_acp_major_version` subprocess on every poll cycle. This cache
// `probe_codex_acp_version` subprocess on every poll cycle. This cache
// stores the last availability status of the codex-acp binary at its resolved
// path. It is warmed by `discover_acp_runtimes` (which already probes), so
// the badge path reads warm data, and is invalidated by `clear_resolve_cache`
Expand Down Expand Up @@ -1163,15 +1163,30 @@ pub(crate) fn classify_runtime(
}
}

/// Probe the major version of a `codex-acp` binary by running `--version`.
/// The oldest `codex-acp` version supported by Buzz managed agents.
///
/// Older 1.x adapters are detected successfully, but can still bundle a Codex runtime
/// that does not reliably give `buzz` CLI subprocesses outbound relay access.
///
/// Bump policy: raise this only when a newer adapter fixes a defect that breaks managed
/// agents, and only to a version already published on npm — every user below the floor is
/// offered a reinstall on their next discovery pass.
pub(crate) const MIN_CODEX_ACP_VERSION: (u64, u64, u64) = (1, 1, 7);

/// Probe the full version of a `codex-acp` binary by running `--version`.
///
/// The 1.x adapter (`@agentclientprotocol/codex-acp`) outputs
/// `@agentclientprotocol/codex-acp <major>.<minor>.<patch>` 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).
/// Returns the `(major, minor, patch)` triple on success, `None` on any failure
/// (non-zero exit, unparseable output, timeout, or missing binary).
///
/// The parse is deliberately strict: exactly three numeric dot-separated components.
/// Partial versions (`1.2`) and prerelease tags (`1.2.0-rc1`) return `None` and so
/// classify as [`AcpAvailabilityStatus::AdapterOutdated`] — failing closed offers a
/// reinstall rather than running an adapter whose version cannot be compared.
///
/// 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
Expand All @@ -1180,16 +1195,16 @@ pub(crate) fn classify_runtime(
/// 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<u64> {
probe_codex_acp_major_version_with_path(
pub(crate) fn probe_codex_acp_version(binary_path: &Path) -> Option<(u64, u64, u64)> {
probe_codex_acp_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(
pub(crate) fn probe_codex_acp_version_with_path(
binary_path: &Path,
augmented_path: Option<&str>,
) -> Option<u64> {
) -> Option<(u64, u64, u64)> {
use std::io::{Read as _, Seek as _, SeekFrom};
use std::time::{Duration, Instant};
const VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
Expand Down Expand Up @@ -1245,30 +1260,35 @@ pub(crate) fn probe_codex_acp_major_version_with_path(
let stdout = String::from_utf8_lossy(&buf);
// Output format: "<package-name> <major>.<minor>.<patch>"
let version_str = stdout.split_whitespace().last()?;
let major_str = version_str.split('.').next()?;
major_str.parse::<u64>().ok()
let mut components = version_str.split('.');
let major = components.next()?.parse::<u64>().ok()?;
let minor = components.next()?.parse::<u64>().ok()?;
let patch = components.next()?.parse::<u64>().ok()?;
if components.next().is_some() {
return None;
}
Some((major, minor, patch))
}

/// 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`.
/// and exits non-zero — that probe failure yields `AdapterOutdated`. An adapter is
/// available only when its version is at least [`MIN_CODEX_ACP_VERSION`].
///
/// 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,
match probe_codex_acp_version(path) {
Some(version) if version >= MIN_CODEX_ACP_VERSION => AcpAvailabilityStatus::Available,
_ => AcpAvailabilityStatus::AdapterOutdated,
}
}

/// Returns `true` when the codex-acp binary at `path` is outdated (major version < 1)
/// or cannot be probed using `augmented_path`. Thin wrapper around
/// [`codex_adapter_is_outdated_with_path`].
/// Returns `true` when the codex-acp binary at `path` is below
/// [`MIN_CODEX_ACP_VERSION`] or cannot be probed using `augmented_path`. Thin wrapper
/// around [`codex_adapter_is_outdated_with_path`].
#[cfg(test)]
pub(crate) fn codex_adapter_is_outdated(path: &Path) -> bool {
codex_adapter_is_outdated_with_path(
Expand All @@ -1277,15 +1297,15 @@ pub(crate) fn codex_adapter_is_outdated(path: &Path) -> bool {
)
}

/// Returns `true` when the codex-acp binary at `path` is outdated (major version < 1)
/// or cannot be probed with the supplied PATH.
/// Returns `true` when the codex-acp binary at `path` is below
/// [`MIN_CODEX_ACP_VERSION`] or cannot be probed with the supplied PATH.
pub(crate) fn codex_adapter_is_outdated_with_path(
path: &Path,
augmented_path: Option<&str>,
) -> bool {
!matches!(
probe_codex_acp_major_version_with_path(path, augmented_path),
Some(major) if major >= 1
probe_codex_acp_version_with_path(path, augmented_path),
Some(version) if version >= MIN_CODEX_ACP_VERSION
)
}

Expand All @@ -1308,9 +1328,8 @@ fn discover_acp_runtime_phase1(runtime: &'static KnownAcpRuntime) -> PartialEntr
let (mut 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.
// For codex-acp: when the adapter resolves as Available, probe its full
// version. An adapter below MIN_CODEX_ACP_VERSION is treated as outdated.
if runtime.id == "codex"
&& availability == AcpAvailabilityStatus::Available
&& command.as_deref() == Some("codex-acp")
Expand Down
Loading
Loading