mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
refactor(desktop): retire codex version gate and bridge npm-install flow
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 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f88d903534
commit
4b9b0639b2
@@ -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
|
||||
|
||||
@@ -19,40 +19,24 @@ fn active_installs() -> &'static std::sync::Mutex<std::collections::HashSet<Stri
|
||||
ACTIVE.get_or_init(|| Mutex::new(HashSet::new()))
|
||||
}
|
||||
|
||||
/// Returns the adapter install commands that `install_acp_runtime_blocking` would
|
||||
/// run for `runtime_id` given a resolved adapter binary at `adapter_path` (or
|
||||
/// `None` if none was found).
|
||||
/// Returns the adapter install commands that `install_acp_runtime_blocking`
|
||||
/// would run given a resolved adapter binary at `adapter_path` (or `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.
|
||||
///
|
||||
/// For the codex **outdated** case the returned sequence is a two-step
|
||||
/// reinstall: first uninstall the old `@zed-industries/codex-acp` package
|
||||
/// (idempotent — exit 0 when absent), then install the new
|
||||
/// `@agentclientprotocol/codex-acp`. This is required because both packages
|
||||
/// install a global binary named `codex-acp`, and npm ≥7 refuses to overwrite
|
||||
/// a bin file owned by a different package with `EEXIST`.
|
||||
///
|
||||
/// For the **missing** case the catalog's `adapter_install_commands` are used
|
||||
/// as-is (no prior package to remove).
|
||||
/// Returns `None` when no install is needed (adapter is present). Returns
|
||||
/// `Some(cmds)` — the catalog's `adapter_install_commands` — when the adapter
|
||||
/// is missing. Bundled bridges (claude, codex) have no install commands, so a
|
||||
/// missing bundled bridge yields an empty plan.
|
||||
///
|
||||
/// This is a pure planning function: it never spawns a process. Tests use it to
|
||||
/// assert the correct install command is selected without touching real npm.
|
||||
pub(crate) fn plan_adapter_install<'c>(
|
||||
runtime_id: &str,
|
||||
adapter_path: Option<&std::path::Path>,
|
||||
adapter_install_commands: &'c [&'c str],
|
||||
) -> Option<Vec<&'c str>> {
|
||||
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<InstallRuntimeResult
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Install adapter if missing (or outdated) and commands are available.
|
||||
// For the codex runtime, "found" is not enough — the resolved binary must also
|
||||
// pass the 1.x version gate. An outdated 0.16.x adapter must be overwritten by
|
||||
// the new npm install so the CODEX_CONFIG spawn contract works correctly.
|
||||
// Phase 2: Install adapter if missing and commands are available. The
|
||||
// bundled bridges (claude, codex) have no adapter install commands — they
|
||||
// ship with the app.
|
||||
let adapter_path = runtime
|
||||
.commands
|
||||
.iter()
|
||||
.find_map(|cmd| crate::managed_agents::resolve_command(cmd));
|
||||
if let Some(cmds) = plan_adapter_install(
|
||||
runtime_id,
|
||||
adapter_path.as_deref(),
|
||||
runtime.adapter_install_commands,
|
||||
) {
|
||||
if let Some(cmds) =
|
||||
plan_adapter_install(adapter_path.as_deref(), runtime.adapter_install_commands)
|
||||
{
|
||||
for cmd in cmds {
|
||||
if is_npm_global_install(cmd) {
|
||||
if let Some(step) = npm_preflight_check("adapter", cmd) {
|
||||
@@ -1196,101 +1177,42 @@ mod tests {
|
||||
assert!(npm_install_target_is_writable(dir.path()));
|
||||
}
|
||||
|
||||
// ── adapter_needs_install (codex version gate) ────────────────────────────
|
||||
// ── plan_adapter_install ──────────────────────────────────────────────────
|
||||
|
||||
/// 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 resolved binary → None (no install, no version probing)
|
||||
/// - Missing binary (None path) → catalog install commands returned
|
||||
#[cfg(unix)]
|
||||
/// - Missing bundled bridge (empty catalog commands) → empty plan
|
||||
#[test]
|
||||
fn test_plan_adapter_install_selects_npm_command_for_outdated_0x_codex_binary() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bin = dir.path().join("codex-acp");
|
||||
// Simulate old 0.16.x: --version exits non-zero (unrecognised flag)
|
||||
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 install_cmds = &["npm install -g @agentclientprotocol/codex-acp"];
|
||||
let plan = plan_adapter_install("codex", Some(&bin), install_cmds);
|
||||
|
||||
assert!(
|
||||
plan.is_some(),
|
||||
"0.x codex adapter must trigger install plan"
|
||||
);
|
||||
let cmds = plan.unwrap();
|
||||
// Outdated arm: must uninstall the old package first, then install new.
|
||||
assert_eq!(
|
||||
cmds,
|
||||
vec![
|
||||
"npm uninstall -g @zed-industries/codex-acp",
|
||||
"npm install -g @agentclientprotocol/codex-acp",
|
||||
],
|
||||
"outdated codex adapter must produce uninstall-then-install sequence; got {cmds:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_plan_adapter_install_returns_none_for_current_1x_codex_binary() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bin = dir.path().join("codex-acp");
|
||||
// Simulate 1.x adapter: outputs version and exits 0
|
||||
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 install_cmds = &["npm install -g @agentclientprotocol/codex-acp"];
|
||||
let plan = plan_adapter_install("codex", Some(&bin), install_cmds);
|
||||
|
||||
fn test_plan_adapter_install_returns_none_for_resolved_binary() {
|
||||
// Any resolved binary means no install needed — the retired codex
|
||||
// version gate must not come back as a reinstall trigger.
|
||||
let install_cmds = &["npm install -g @example/some-acp"];
|
||||
let plan = plan_adapter_install(Some(std::path::Path::new("/usr/bin/true")), install_cmds);
|
||||
assert!(
|
||||
plan.is_none(),
|
||||
"1.x codex adapter must not trigger install plan (no reinstall needed)"
|
||||
"resolved adapter binary must not trigger an install plan"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_adapter_install_returns_catalog_cmds_when_no_adapter_path() {
|
||||
let install_cmds = &["npm install -g @agentclientprotocol/codex-acp"];
|
||||
let plan = plan_adapter_install("codex", None, install_cmds);
|
||||
assert!(plan.is_some(), "missing adapter must trigger install plan");
|
||||
// Missing arm: use the catalog's install commands directly (no prior
|
||||
// package to uninstall — fresh install, not a reinstall).
|
||||
let install_cmds = &["npm install -g @example/some-acp"];
|
||||
let plan = plan_adapter_install(None, install_cmds);
|
||||
assert_eq!(
|
||||
plan.unwrap(),
|
||||
vec!["npm install -g @agentclientprotocol/codex-acp"],
|
||||
"missing codex adapter must use catalog install commands only"
|
||||
plan,
|
||||
Some(vec!["npm install -g @example/some-acp"]),
|
||||
"missing adapter must use catalog install commands"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_plan_adapter_install_non_codex_runtime_never_reinstalls() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// For non-codex runtimes, any resolved binary means no install needed.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let bin = dir.path().join("goose-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 install_cmds = &["npm install -g @block/goose-acp"];
|
||||
let plan = plan_adapter_install("goose", Some(&bin), install_cmds);
|
||||
assert!(
|
||||
plan.is_none(),
|
||||
"non-codex runtime with resolved binary must not trigger reinstall"
|
||||
);
|
||||
fn test_plan_adapter_install_empty_for_missing_bundled_bridge() {
|
||||
// Bundled bridges (claude, codex) carry no install commands: a
|
||||
// missing bundled bridge yields an empty plan (zero install steps).
|
||||
let plan = plan_adapter_install(None, &[]);
|
||||
assert_eq!(plan, Some(vec![]));
|
||||
}
|
||||
|
||||
// ── should_restart_after_install ─────────────────────────────────────────
|
||||
@@ -1361,9 +1283,9 @@ mod tests {
|
||||
assert!(
|
||||
availability_drift(
|
||||
Some(&AcpAvailabilityStatus::Available),
|
||||
Some(AcpAvailabilityStatus::AdapterOutdated),
|
||||
Some(AcpAvailabilityStatus::AdapterMissing),
|
||||
),
|
||||
"Available stamped vs AdapterOutdated current must be detected as drift"
|
||||
"Available stamped vs AdapterMissing current must be detected as drift"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,10 +174,10 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
underlying_cli: Some("claude"),
|
||||
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: &["npm install -g @agentclientprotocol/claude-agent-acp"],
|
||||
adapter_install_commands: &[],
|
||||
install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp",
|
||||
cli_install_hint: "Install the Claude Code CLI via the official install script.",
|
||||
adapter_install_hint: "Install the Claude Code ACP adapter via npm.",
|
||||
adapter_install_hint: "The Claude Code ACP adapter ships with the Buzz desktop app.",
|
||||
skill_dir: Some(".claude/skills"),
|
||||
supports_acp_model_switching: false,
|
||||
model_env_var: None,
|
||||
@@ -205,10 +205,10 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
|
||||
underlying_cli: Some("codex"),
|
||||
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: &["npm install -g @agentclientprotocol/codex-acp"],
|
||||
adapter_install_commands: &[],
|
||||
install_instructions_url: "https://github.com/agentclientprotocol/codex-acp",
|
||||
cli_install_hint: "Install the Codex CLI via the official install script.",
|
||||
adapter_install_hint: "Install the Codex ACP adapter via npm.",
|
||||
adapter_install_hint: "The Codex ACP adapter ships with the Buzz desktop app.",
|
||||
skill_dir: Some(".codex/skills"),
|
||||
supports_acp_model_switching: false,
|
||||
model_env_var: None,
|
||||
@@ -547,11 +547,11 @@ pub fn clear_resolve_cache() {
|
||||
// ── Adapter availability cache (Phase-2 badge fallback) ─────────────────────
|
||||
//
|
||||
// `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
|
||||
// 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`
|
||||
// availability against the *current* availability without re-running command
|
||||
// resolution 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 resolves), so the badge path reads
|
||||
// warm data, and is invalidated by `clear_resolve_cache`
|
||||
// (called on every Doctor install and every `discover_acp_providers` call).
|
||||
|
||||
fn adapter_availability_cache() -> &'static std::sync::Mutex<Option<AcpAvailabilityStatus>> {
|
||||
@@ -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 <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).
|
||||
///
|
||||
/// 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<u64> {
|
||||
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<u64> {
|
||||
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: "<package-name> <major>.<minor>.<patch>"
|
||||
let version_str = stdout.split_whitespace().last()?;
|
||||
let major_str = version_str.split('.').next()?;
|
||||
major_str.parse::<u64>().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<AcpRuntimeCatalogEntry> {
|
||||
.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<AcpRuntimeCatalogEntry> {
|
||||
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}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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 `&[<absolute_path>]`
|
||||
/// 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]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -562,11 +562,9 @@ export function AgentDefinitionDialog({
|
||||
<p className="text-xs text-warning">
|
||||
{selectedRuntime.availability === "adapter_missing"
|
||||
? `${selectedRuntime.label} CLI is installed but the ACP adapter is missing.`
|
||||
: selectedRuntime.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.
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -353,29 +353,6 @@ function RuntimeDetails({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (runtime.availability === "adapter_outdated") {
|
||||
return (
|
||||
<>
|
||||
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
||||
ACP adapter detected but outdated — reinstall required.
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
||||
This updates the machine-global{" "}
|
||||
<code className="rounded bg-muted px-0.5 text-2xs">codex-acp</code>{" "}
|
||||
adapter. Older Buzz releases using the legacy adapter contract may
|
||||
lose community access until{" "}
|
||||
<code className="rounded bg-muted px-0.5 text-2xs">
|
||||
@zed-industries/codex-acp@0.16.0
|
||||
</code>{" "}
|
||||
is restored.
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
||||
{runtime.installHint}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (runtime.availability === "cli_missing") {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -38,8 +38,6 @@ function StatusIcon({
|
||||
return <CheckCircle2 className="h-4 w-4 text-status-added" />;
|
||||
case "adapter_missing":
|
||||
return <AlertTriangle className="h-4 w-4 text-warning" />;
|
||||
case "adapter_outdated":
|
||||
return <AlertTriangle className="h-4 w-4 text-warning" />;
|
||||
case "cli_missing":
|
||||
return <AlertTriangle className="h-4 w-4 text-warning" />;
|
||||
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" ? (
|
||||
<>
|
||||
<p className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
ACP adapter found at{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-2xs">
|
||||
{runtime.binaryPath ?? "unknown path"}
|
||||
</code>{" "}
|
||||
but it is from the deprecated package. Reinstall to enable relay
|
||||
connectivity.
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
This updates the machine-global{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-2xs">
|
||||
codex-acp
|
||||
</code>{" "}
|
||||
adapter. Older Buzz releases using the legacy adapter contract may
|
||||
lose community access until{" "}
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-2xs">
|
||||
@zed-industries/codex-acp@0.16.0
|
||||
</code>{" "}
|
||||
is restored.
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
{runtime.installHint}
|
||||
</p>
|
||||
<InstallActions
|
||||
hasError={installError !== null}
|
||||
isInstalling={isInstalling}
|
||||
onInstall={onInstall}
|
||||
runtime={runtime}
|
||||
/>
|
||||
</>
|
||||
) : runtime.availability === "cli_missing" ? (
|
||||
<>
|
||||
<p className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
|
||||
@@ -557,7 +557,6 @@ export type GitBashPrerequisite = {
|
||||
export type AcpAvailabilityStatus =
|
||||
| "available"
|
||||
| "adapter_missing"
|
||||
| "adapter_outdated"
|
||||
| "cli_missing"
|
||||
| "not_installed";
|
||||
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user