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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/aionui-ai-agent/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions crates/aionui-assets/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ mod tests {
"grok",
"kilo",
"mimo-code",
"minimax-code",
"nova",
"omp",
"sigit",
Expand Down
70 changes: 70 additions & 0 deletions crates/aionui-db/migrations/044_add_minimax_code_builtin_agent.sql
Original file line number Diff line number Diff line change
@@ -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 "<vendor> 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;
6 changes: 5 additions & 1 deletion crates/aionui-db/src/repository/sqlite_agent_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
153 changes: 153 additions & 0 deletions crates/aionui-db/tests/minimax_code_builtin_agent_migration.rs
Original file line number Diff line number Diff line change
@@ -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"
);
}
}
6 changes: 4 additions & 2 deletions crates/aionui-db/tests/team_capability_criteria_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))),
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions crates/aionui-runtime/resources/acp-registry-npx-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion crates/aionui-runtime/src/registry_npx_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Loading