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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/aionui-ai-agent/src/protocol/send_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1659,7 +1659,7 @@ mod tests {

for managed_binary_missing in [
"expected managed Claude ACP platform binary missing: C:\\Users\\user\\AppData\\Roaming\\AionUi\\aionui\\runtime\\acp\\claude-agent-acp\\0.39.0\\win32-x64\\node_modules\\@anthropic-ai\\claude-agent-sdk-win32-x64\\claude.exe",
"expected managed Codex ACP platform binary missing: C:\\Users\\user\\AppData\\Roaming\\AionUi\\aionui\\runtime\\acp\\codex-acp\\0.14.0\\win32-x64\\node_modules\\@zed-industries\\codex-acp-win32-x64\\bin\\codex-acp.exe",
"expected managed Codex ACP platform binary missing: C:\\Users\\user\\AppData\\Roaming\\AionUi\\aionui\\runtime\\acp\\codex-acp\\1.1.2\\win32-x64\\node_modules\\@openai\\codex-win32-x64\\vendor\\x86_64-pc-windows-msvc\\bin\\codex.exe",
] {
assert_classification(
managed_binary_missing,
Expand Down
14 changes: 14 additions & 0 deletions crates/aionui-db/migrations/020_update_codex_acp_package_scope.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Migration 020: Move any remaining builtin Codex ACP rows off the deprecated
-- @zed-industries bridge package. Runtime resolution derives builtin Codex from
-- managed ACP artifacts, where Codex ACP is pinned to
-- @agentclientprotocol/codex-acp@1.1.2, so no npm bridge command should remain
-- persisted in agent_metadata.

UPDATE agent_metadata
SET command = NULL,
args = '[]',
agent_source_info = json_remove(COALESCE(agent_source_info, '{}'), '$.bridge_binary'),
updated_at = CAST(strftime('%s','now') AS INTEGER) * 1000
WHERE agent_source = 'builtin'
AND backend = 'codex'
AND args LIKE '%@zed-industries/codex-acp%';
40 changes: 40 additions & 0 deletions crates/aionui-db/tests/cron_assistant_first_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,46 @@ async fn migration_019_deletes_retired_runtime_client_preferences_only() {
);
}

#[tokio::test]
async fn migration_020_clears_legacy_codex_acp_bridge_without_fixed_id() {
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();

run_migrations_through(&pool, 19).await;

sqlx::query(
"INSERT INTO agent_metadata (
id, name, backend, command, args, agent_type, enabled, agent_source,
agent_source_info, sort_order, created_at, updated_at
) VALUES (
'custom-codex-id', 'Codex CLI', 'codex', 'npx',
'[\"-y\",\"@zed-industries/codex-acp@0.14.0\"]',
'acp', 1, 'builtin', '{\"binary_name\":\"codex\",\"bridge_binary\":\"npx\"}',
3110, 1, 1
)",
)
.execute(&pool)
.await
.unwrap();

run_migration(&pool, 20).await;

let row = sqlx::query(
"SELECT command, args, json_extract(agent_source_info, '$.bridge_binary') AS bridge_binary
FROM agent_metadata
WHERE id = 'custom-codex-id'",
)
.fetch_one(&pool)
.await
.unwrap();
assert!(row.get::<Option<String>, _>("command").is_none());
assert_eq!(row.get::<String, _>("args"), "[]");
assert!(row.get::<Option<String>, _>("bridge_binary").is_none());
}

async fn insert_legacy_cron(
pool: &sqlx::SqlitePool,
id: &str,
Expand Down
97 changes: 63 additions & 34 deletions crates/aionui-runtime/src/acp_tool_runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,17 +653,7 @@ fn validate_platform_binary(
spec: PlatformSpec,
) -> Result<(), ManagedAcpToolError> {
let expected = match tool {
ManagedAcpToolId::CodexAcp => {
let mut path = project_dir
.join("node_modules")
.join(format!("@zed-industries/codex-acp-{}", spec.manifest_key))
.join("bin")
.join("codex-acp");
if spec.manifest_key.starts_with("win32-") {
path.set_extension("exe");
}
path
}
ManagedAcpToolId::CodexAcp => codex_platform_binary_path(project_dir, spec)?,
ManagedAcpToolId::ClaudeAgentAcp => {
let mut path = project_dir
.join("node_modules")
Expand All @@ -687,6 +677,35 @@ fn validate_platform_binary(
}
}

fn codex_platform_binary_path(project_dir: &Path, spec: PlatformSpec) -> Result<PathBuf, ManagedAcpToolError> {
let vendor_triple = match spec.manifest_key {
"darwin-arm64" => "aarch64-apple-darwin",
"darwin-x64" => "x86_64-apple-darwin",
"linux-arm64" => "aarch64-unknown-linux-musl",
"linux-x64" => "x86_64-unknown-linux-musl",
"win32-arm64" => "aarch64-pc-windows-msvc",
"win32-x64" => "x86_64-pc-windows-msvc",
_ => {
return Err(ManagedAcpToolError::invalid(format!(
"unsupported Codex ACP platform {}",
spec.manifest_key
)));
}
};

let mut path = project_dir
.join("node_modules")
.join(format!("@openai/codex-{}", spec.manifest_key))
.join("vendor")
.join(vendor_triple)
.join("bin")
.join("codex");
if spec.manifest_key.starts_with("win32-") {
path.set_extension("exe");
}
Ok(path)
}

async fn validate_dependency_tree(
node_runtime: &crate::ResolvedNodeRuntime,
project_dir: &Path,
Expand Down Expand Up @@ -812,20 +831,32 @@ fn resolve_package_smoke_target(
project_dir: &Path,
package_json: &InstalledPackageJson,
) -> Result<PackageSmokeTarget, ManagedAcpToolError> {
if let Some(entry) = resolve_package_import_entry(&package_json.exports, package_json.main.as_deref()) {
if let Some(entry) = resolve_package_exports_import_entry(&package_json.exports) {
return Ok(PackageSmokeTarget::Import(
package_root(project_dir, &package_json.name).join(entry),
));
}

let bin_entry = resolve_package_bin_entry(package_json.name.as_str(), &package_json.bin)?;
Ok(PackageSmokeTarget::SyntaxCheck(
package_root(project_dir, &package_json.name).join(bin_entry),
))
if let Ok(bin_entry) = resolve_package_bin_entry(package_json.name.as_str(), &package_json.bin) {
return Ok(PackageSmokeTarget::SyntaxCheck(
package_root(project_dir, &package_json.name).join(bin_entry),
));
}

if let Some(entry) = package_json.main.as_deref().filter(|value| !value.is_empty()) {
return Ok(PackageSmokeTarget::Import(
package_root(project_dir, &package_json.name).join(entry),
));
}

Err(ManagedAcpToolError::invalid(format!(
"package {} does not expose a usable smoke-test entry",
package_json.name
)))
}

fn resolve_package_import_entry(exports_field: &serde_json::Value, main_field: Option<&str>) -> Option<String> {
let exports_entry = match exports_field {
fn resolve_package_exports_import_entry(exports_field: &serde_json::Value) -> Option<String> {
match exports_field {
serde_json::Value::String(value) if !value.is_empty() => Some(value.clone()),
serde_json::Value::Object(entries) => entries.get(".").and_then(|root| match root {
serde_json::Value::String(value) if !value.is_empty() => Some(value.clone()),
Expand All @@ -844,9 +875,7 @@ fn resolve_package_import_entry(exports_field: &serde_json::Value, main_field: O
_ => None,
}),
_ => None,
};

exports_entry.or_else(|| main_field.and_then(|value| if value.is_empty() { None } else { Some(value.to_owned()) }))
}
}

fn normalize_slashes(path: &Path) -> String {
Expand Down Expand Up @@ -1074,15 +1103,15 @@ mod tests {

let entrypoint = root
.join("node_modules")
.join("@zed-industries")
.join("@agentclientprotocol")
.join("codex-acp")
.join("bin")
.join("codex-acp.js");
.join("dist")
.join("index.js");
std::fs::create_dir_all(entrypoint.parent().unwrap()).unwrap();
std::fs::write(&entrypoint, "console.log('codex bridge');\n").unwrap();
std::fs::write(
root.join("manifest.json"),
br#"{"entrypoint":"node_modules/@zed-industries/codex-acp/bin/codex-acp.js","path_entries":["node_modules/.bin"]}"#,
br#"{"entrypoint":"node_modules/@agentclientprotocol/codex-acp/dist/index.js","path_entries":["node_modules/.bin"]}"#,
)
.unwrap();

Expand Down Expand Up @@ -1212,15 +1241,15 @@ mod tests {
}

#[test]
fn resolve_package_smoke_target_falls_back_to_bin_check_for_cli_only_package() {
fn resolve_package_smoke_target_prefers_bin_check_for_cli_package_with_main() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path();
let package_json = InstalledPackageJson {
name: "@zed-industries/codex-acp".into(),
name: "@agentclientprotocol/codex-acp".into(),
bin: json!({
"codex-acp": "bin/codex-acp.js",
"codex-acp": "dist/index.js",
}),
main: None,
main: Some("dist/index.js".into()),
exports: serde_json::Value::Null,
};

Expand All @@ -1231,19 +1260,19 @@ mod tests {
PackageSmokeTarget::SyntaxCheck(
project_dir
.join("node_modules")
.join("@zed-industries")
.join("@agentclientprotocol")
.join("codex-acp")
.join("bin")
.join("codex-acp.js")
.join("dist")
.join("index.js")
)
);
}

#[test]
fn package_path_segments_preserve_scoped_package_structure() {
assert_eq!(
package_path_segments("@zed-industries/codex-acp"),
vec!["@zed-industries", "codex-acp"]
package_path_segments("@agentclientprotocol/codex-acp"),
vec!["@agentclientprotocol", "codex-acp"]
);
}

Expand Down
6 changes: 3 additions & 3 deletions crates/aionui-runtime/src/acp_tool_runtime/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl ManagedAcpToolId {

pub fn version(self) -> &'static str {
match self {
Self::CodexAcp => "0.16.0",
Self::CodexAcp => "1.1.2",
Self::ClaudeAgentAcp => "0.39.0",
}
}
Expand All @@ -34,7 +34,7 @@ impl ManagedAcpToolId {

pub fn package_name(self) -> &'static str {
match self {
Self::CodexAcp => "@zed-industries/codex-acp",
Self::CodexAcp => "@agentclientprotocol/codex-acp",
Self::ClaudeAgentAcp => "@agentclientprotocol/claude-agent-acp",
}
}
Expand Down Expand Up @@ -224,7 +224,7 @@ mod tests {

#[test]
fn managed_acp_tool_versions_match_current_pins() {
assert_eq!(ManagedAcpToolId::CodexAcp.version(), "0.16.0");
assert_eq!(ManagedAcpToolId::CodexAcp.version(), "1.1.2");
assert_eq!(ManagedAcpToolId::ClaudeAgentAcp.version(), "0.39.0");
}
}
Expand Down
Loading