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:
Matt Toohey
2026-07-15 13:43:26 +10:00
co-authored by Claude Fable 5
parent f88d903534
commit 4b9b0639b2
15 changed files with 85 additions and 779 deletions
+17 -24
View File
@@ -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
+36 -114
View File
@@ -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"
);
}
+10 -131
View File
@@ -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 &gt; 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">
-1
View File
@@ -557,7 +557,6 @@ export type GitBashPrerequisite = {
export type AcpAvailabilityStatus =
| "available"
| "adapter_missing"
| "adapter_outdated"
| "cli_missing"
| "not_installed";
+8 -7
View File
@@ -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",
);
});
-2
View File
@@ -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.