diff --git a/crates/aionui-ai-agent/src/registry.rs b/crates/aionui-ai-agent/src/registry.rs index 1d3d70d22..16d5fe07f 100644 --- a/crates/aionui-ai-agent/src/registry.rs +++ b/crates/aionui-ai-agent/src/registry.rs @@ -1644,7 +1644,7 @@ mod tests { // when none of the CLIs are installed on the test host. let reg = registry().await; let all = reg.list_all_including_hidden().await; - assert_eq!(all.len(), 43, "seed rows: 42 pre-existing + antigravity"); + assert_eq!(all.len(), 44, "seed rows: 42 pre-existing + antigravity + minimax-code"); } #[tokio::test] @@ -1772,7 +1772,7 @@ mod tests { .unwrap_or_else(|error| panic!("missing release lock for {backend}: {error}")); locked += 1; } - assert_eq!(locked, 12); + assert_eq!(locked, 13); } /// On a host that has *none* of the seeded CLIs installed, the @@ -1807,7 +1807,7 @@ mod tests { let reg = registry().await; let all = reg.list_all_including_hidden().await; let count = |t: AgentType| all.iter().filter(|m| m.agent_type == t).count(); - assert_eq!(count(AgentType::Acp), 39); + assert_eq!(count(AgentType::Acp), 40); assert_eq!(count(AgentType::Nanobot), 1); assert_eq!(count(AgentType::OpenclawGateway), 1); assert_eq!(count(AgentType::Aionrs), 1); @@ -2029,7 +2029,7 @@ mod tests { async fn diagnostic_snapshot_pairs_rows_with_reasons() { let reg = registry().await; let snapshot = reg.diagnostic_snapshot().await; - assert_eq!(snapshot.len(), 43, "every row appears once"); + assert_eq!(snapshot.len(), 44, "every row appears once"); for (meta, reason) in &snapshot { match (meta.available, reason) { diff --git a/crates/aionui-assets/assets/logos/acp-registry/minimax-code.svg b/crates/aionui-assets/assets/logos/acp-registry/minimax-code.svg new file mode 100644 index 000000000..8b33c7709 --- /dev/null +++ b/crates/aionui-assets/assets/logos/acp-registry/minimax-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/aionui-assets/src/service.rs b/crates/aionui-assets/src/service.rs index afa5363cf..a56ce7b12 100644 --- a/crates/aionui-assets/src/service.rs +++ b/crates/aionui-assets/src/service.rs @@ -128,6 +128,7 @@ mod tests { "grok", "kilo", "mimo-code", + "minimax-code", "nova", "omp", "sigit", diff --git a/crates/aionui-db/migrations/044_add_minimax_code_builtin_agent.sql b/crates/aionui-db/migrations/044_add_minimax_code_builtin_agent.sql new file mode 100644 index 000000000..ff9af5d3c --- /dev/null +++ b/crates/aionui-db/migrations/044_add_minimax_code_builtin_agent.sql @@ -0,0 +1,70 @@ +-- Add MiniMax Code as a builtin ACP agent (Registry-listed, npx distribution). +-- +-- Identity: the public Registry card is "MiniMax Code" (https://agent.minimax.io); +-- the npm package is @minimax-ai/code (bin `mcode`); a live ACP `initialize` +-- reports agentInfo {name:"minimax-code", title:"MiniMax Code"}. `backend` is +-- taken from the product name plus the agent's own reported name, following the +-- `mimo-code` precedent (029) for " Code" products. It is not a copy of +-- the CDN JSON lookup id, which merely coincides. +-- +-- `binary_name` is `mcode`: the official README installs the product with +-- `npm install -g @minimax-ai/code`, which places `mcode` on PATH, and +-- `mcode --version` is documented, so the default PATH probe applies. +-- `mcode acp` is the vendor's documented ACP server (README "ACP clients"); +-- the Registry declares the same `acp` argument. AionCore launches the +-- Registry npx distribution; the exact version is pinned in +-- crates/aionui-runtime/resources/acp-registry-npx-lock.json, never here. +-- +-- Probe evidence (2026-09-14, @minimax-ai/code@0.2.7, clean temporary HOME): +-- initialize ok, protocolVersion 1; session/new -> -32000 +-- "Authentication required: Run `mcode login` and try again." +-- agent_capabilities is the snake_case form of what initialize returned +-- (contract: migration 003 header). auth_methods stays NULL: initialize +-- advertised none, and a blob must not be synthesized. +-- +-- yolo_id stays NULL. The 0.2.7 ACP server exposes exactly two session modes, +-- `default` and `plan` (source: run-acp-command chunk, availableModes). The +-- Ask / Auto / Full access permission levels are a `_permission`-category +-- CONFIG OPTION (`permissionMode` = default | auto | bypassPermissions), not a +-- session mode, and AionCore resolves yolo_id through `session/set_mode`, so +-- storing `bypassPermissions` here would send an unsupported mode id. +-- +-- native_skills_dirs stays NULL. The 0.2.7 source scans only +-- `$MINIMAX_DATA_DIR/skills` (a user data directory) and the bundled +-- `assets/skills`; there is no project-relative skills directory to declare. +-- +-- behavior_policy omits `supports_team`: migration 033 retired the negative +-- form, and team capability is derived from backend + probed capabilities. +-- Post-030 seed shape: builtin rows use agent_id = id and user_id NULL. +INSERT INTO agent_metadata + (id, agent_id, icon, name, backend, agent_type, agent_source, agent_source_info, + enabled, command, args, env, native_skills_dirs, behavior_policy, yolo_id, + agent_capabilities, sort_order, created_at, updated_at) +VALUES + ('ec619063', 'ec619063', '/api/assets/logos/acp-registry/minimax-code.svg', 'MiniMax Code', + 'minimax-code', 'acp', 'builtin', '{"binary_name":"mcode","bridge_binary":"npx"}', + 1, 'npx', '["-y","@minimax-ai/code","acp"]', '[]', + NULL, + '{"supports_side_question":false}', + NULL, + '{"load_session":true,"mcp_capabilities":{"http":true,"sse":true},"prompt_capabilities":{"image":false,"audio":false,"embedded_context":false},"session_capabilities":{"list":{},"fork":{},"resume":{},"close":{}}}', + 3340, + unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000) +ON CONFLICT(id) DO UPDATE SET + agent_id = excluded.agent_id, + icon = excluded.icon, + name = excluded.name, + description = NULL, + backend = excluded.backend, + agent_type = excluded.agent_type, + agent_source = excluded.agent_source, + agent_source_info = excluded.agent_source_info, + enabled = excluded.enabled, + command = excluded.command, + args = excluded.args, + env = excluded.env, + native_skills_dirs = excluded.native_skills_dirs, + behavior_policy = excluded.behavior_policy, + yolo_id = excluded.yolo_id, + sort_order = excluded.sort_order, + updated_at = unixepoch('now','subsec')*1000; diff --git a/crates/aionui-db/src/repository/sqlite_agent_metadata.rs b/crates/aionui-db/src/repository/sqlite_agent_metadata.rs index a1f4715e0..7e3e90fa0 100644 --- a/crates/aionui-db/src/repository/sqlite_agent_metadata.rs +++ b/crates/aionui-db/src/repository/sqlite_agent_metadata.rs @@ -863,7 +863,11 @@ mod tests { let (repo, _db) = setup().await; let rows = repo.list_all().await.unwrap(); // 39 ACP vendors + 2 non-ACP builtins + 1 internal = 42. - assert_eq!(rows.len(), 43, "seed rows: 42 pre-existing + antigravity"); + assert_eq!( + rows.len(), + 44, + "seed rows: 42 pre-existing + antigravity + minimax-code" + ); assert!( rows.iter() .any(|r| r.name == "Claude Code" && r.agent_source == "builtin") diff --git a/crates/aionui-db/tests/minimax_code_builtin_agent_migration.rs b/crates/aionui-db/tests/minimax_code_builtin_agent_migration.rs new file mode 100644 index 000000000..de67c53cb --- /dev/null +++ b/crates/aionui-db/tests/minimax_code_builtin_agent_migration.rs @@ -0,0 +1,153 @@ +//! Migration 044 seeds MiniMax Code as a Registry-listed builtin ACP agent. +//! +//! The assertions pin the fields that silently change behaviour if wrong: the +//! launch argv (the npx bridge pins its exact version from the release lock at +//! spawn time), the product CLI that availability detection looks for, and the +//! two handshake columns that a re-seed must never clobber. + +use aionui_db::{IAgentMetadataRepository, SqliteAgentMetadataRepository, init_database_memory}; + +const BACKEND: &str = "minimax-code"; + +#[tokio::test] +async fn seeds_minimax_code_as_a_bridged_registry_builtin() { + let db = init_database_memory().await.unwrap(); + let repo = SqliteAgentMetadataRepository::new(db.pool().clone()); + + let row = repo + .find_builtin_by_backend(BACKEND) + .await + .unwrap() + .expect("minimax-code is seeded by migration 044"); + + assert_eq!(row.id, "ec619063"); + assert_eq!(row.user_id, None, "builtin rows are machine-level, user_id stays NULL"); + assert_eq!(row.name, "MiniMax Code"); + assert_eq!(row.agent_type, "acp"); + assert_eq!(row.agent_source, "builtin"); + assert!(row.enabled); + assert_eq!( + row.icon.as_deref(), + Some("/api/assets/logos/acp-registry/minimax-code.svg"), + "icon is keyed by the local backend, not the Registry lookup id" + ); + + // The Registry npx distribution is the ACP connection; the release lock + // pins its exact version at launch, so the stored argv carries none. + assert_eq!(row.command.as_deref(), Some("npx")); + assert_eq!(row.args.as_deref(), Some(r#"["-y","@minimax-ai/code","acp"]"#)); + assert_eq!(row.env.as_deref(), Some("[]")); + + // PATH detection looks for the product CLI the vendor installs (`mcode`), + // while the bridge that speaks ACP is npx. + let source: serde_json::Value = + serde_json::from_str(row.agent_source_info.as_deref().expect("agent_source_info")).unwrap(); + assert_eq!(source["binary_name"], "mcode"); + assert_eq!(source["bridge_binary"], "npx"); + assert!( + source.get("registry_json_id").is_none() && source.get("package_name").is_none(), + "agent_source_info must not carry Registry identity fields: {source}" + ); +} + +/// `initialize` was probed at integration time, so `agent_capabilities` is +/// seeded; `initialize` advertised no auth methods, so `auth_methods` stays NULL +/// rather than carrying a synthesized blob. Neither column may be listed in the +/// migration's `ON CONFLICT DO UPDATE` set — a re-seed must not reset what a +/// live handshake later teaches this install. +#[tokio::test] +async fn seeds_probed_capabilities_and_leaves_unprobed_fields_null() { + let db = init_database_memory().await.unwrap(); + let repo = SqliteAgentMetadataRepository::new(db.pool().clone()); + + let row = repo + .find_builtin_by_backend(BACKEND) + .await + .unwrap() + .expect("minimax-code is seeded"); + + let caps: serde_json::Value = + serde_json::from_str(row.agent_capabilities.as_deref().expect("agent_capabilities seeded")).unwrap(); + assert_eq!(caps["load_session"], true); + assert_eq!( + caps["mcp_capabilities"]["http"], true, + "Team must route it to the MCP transport" + ); + assert_eq!(caps["mcp_capabilities"]["sse"], true); + assert_eq!(caps["prompt_capabilities"]["image"], false); + assert!(caps["session_capabilities"].get("resume").is_some()); + assert!( + row.agent_capabilities.as_deref().unwrap().contains("load_session") + && !row.agent_capabilities.as_deref().unwrap().contains("loadSession"), + "handshake columns are stored snake_case (migration 003 contract)" + ); + + assert_eq!( + row.auth_methods, None, + "initialize advertised no auth methods; nothing is synthesized" + ); + assert_eq!( + row.yolo_id, None, + "its ACP session modes are default/plan only; bypassPermissions is a config option" + ); + assert_eq!( + row.native_skills_dirs, None, + "no project-relative skills directory is documented or in source" + ); + + let policy: serde_json::Value = + serde_json::from_str(row.behavior_policy.as_deref().expect("behavior_policy")).unwrap(); + assert_eq!(policy["supports_side_question"], false); + assert!( + policy.get("supports_team").is_none(), + "no negative team flag (retired by 033)" + ); + assert!( + policy.get("team_capable_override").is_none(), + "team_capable_override was retired by 033" + ); +} + +/// The lock manifest is the only place the Registry version lives. The entry +/// must exist under the local backend key, name the stable package, and pin an +/// exact semver — a tag such as `latest` would defeat the release lock. +#[test] +fn minimax_code_is_pinned_in_the_npx_release_lock() { + let lock = include_str!("../../aionui-runtime/resources/acp-registry-npx-lock.json"); + let parsed: serde_json::Value = serde_json::from_str(lock).unwrap(); + + let entry = parsed["agents"] + .get(BACKEND) + .unwrap_or_else(|| panic!("{BACKEND} must be pinned in the npx release lock")); + assert_eq!(entry["package"], "@minimax-ai/code"); + assert_eq!( + entry["registry_json_id"], "minimax-code", + "lookup alias lives in the lock, not in metadata" + ); + + let version = entry["version"].as_str().expect("version is a string"); + let parts: Vec<&str> = version.split('.').collect(); + assert!( + parts.len() == 3 + && parts + .iter() + .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit())), + "version must be an exact semver, got {version:?}" + ); +} + +/// Bad path: the product CLI name and the npm scope are not backends. A lookup +/// by either must miss, so nothing can accidentally seed a second row under an +/// alias and split the agent's identity. +#[tokio::test] +async fn aliases_are_not_registered_as_backends() { + let db = init_database_memory().await.unwrap(); + let repo = SqliteAgentMetadataRepository::new(db.pool().clone()); + + for alias in ["mcode", "minimax", "minimax-ai", "@minimax-ai/code"] { + assert!( + repo.find_builtin_by_backend(alias).await.unwrap().is_none(), + "{alias} must not resolve to a builtin row" + ); + } +} diff --git a/crates/aionui-db/tests/team_capability_criteria_migration.rs b/crates/aionui-db/tests/team_capability_criteria_migration.rs index 30fa30d0f..97b1fc558 100644 --- a/crates/aionui-db/tests/team_capability_criteria_migration.rs +++ b/crates/aionui-db/tests/team_capability_criteria_migration.rs @@ -111,7 +111,7 @@ async fn probed_registry_agents_carry_seeded_auth_methods() { assert!(!raw.contains("/home/"), "{backend} auth_methods leaks a host path"); } - for backend in ["cortex-code", "dimcode", "poolside", "vtcode", "junie"] { + for backend in ["cortex-code", "dimcode", "poolside", "vtcode", "junie", "minimax-code"] { let row = repo .find_builtin_by_backend(backend) .await @@ -137,7 +137,7 @@ async fn probed_registry_agents_carry_seeded_mcp_capabilities() { // (backend, http, sse) — `None` means the agent advertises no usable // mcp_capabilities (absent or empty object), which keeps Team on the CLI // transport for it. Covers EVERY agent added by migrations 025/029/031. - let cases: [(&str, Option<(bool, bool)>); 20] = [ + let cases: [(&str, Option<(bool, bool)>); 21] = [ // npx distributions (025 + 029 + 031) ("autohand", Some((true, true))), ("deepagents", Some((false, false))), @@ -147,6 +147,8 @@ async fn probed_registry_agents_carry_seeded_mcp_capabilities() { ("grok", Some((true, true))), ("kilo", Some((true, true))), ("mimo-code", Some((true, true))), + // 044 (Registry npx, probed 2026-09-14 at @minimax-ai/code@0.2.7) + ("minimax-code", Some((true, true))), ("nova", Some((true, true))), ("sigit", Some((false, false))), // direct CLI launch (031 seeded it on npx; 039 moved it off the bridge) diff --git a/crates/aionui-runtime/resources/acp-registry-npx-lock.json b/crates/aionui-runtime/resources/acp-registry-npx-lock.json index f68a0f54c..f11653e61 100644 --- a/crates/aionui-runtime/resources/acp-registry-npx-lock.json +++ b/crates/aionui-runtime/resources/acp-registry-npx-lock.json @@ -45,6 +45,11 @@ "package": "@mimo-ai/cli", "version": "0.1.9" }, + "minimax-code": { + "registry_json_id": "minimax-code", + "package": "@minimax-ai/code", + "version": "0.2.7" + }, "nova": { "registry_json_id": "nova", "package": "@compass-ai/nova", diff --git a/crates/aionui-runtime/src/registry_npx_lock.rs b/crates/aionui-runtime/src/registry_npx_lock.rs index d3d782978..f0f78be41 100644 --- a/crates/aionui-runtime/src/registry_npx_lock.rs +++ b/crates/aionui-runtime/src/registry_npx_lock.rs @@ -130,7 +130,7 @@ mod tests { #[test] fn every_lock_entry_has_an_exact_version() { let lock = registry_npx_lock().unwrap(); - assert_eq!(lock.agents.len(), 12); + assert_eq!(lock.agents.len(), 13); for package in lock.agents.values() { assert!(semver::Version::parse(&package.version).is_ok()); }