use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CatalogueHarness {
pub id: &'static str,
pub display_name: &'static str,
pub program: &'static str,
pub args: &'static [&'static str],
pub install_hint: &'static str,
}
impl CatalogueHarness {
#[must_use]
pub fn launch(&self) -> String {
if self.args.is_empty() {
return self.program.to_owned();
}
format!("{} {}", self.program, self.args.join(" "))
}
#[must_use]
pub fn available_in(&self, path: Option<&str>) -> bool {
program_resolves(Path::new(self.program), path)
}
#[must_use]
pub fn available(&self) -> bool {
self.available_in(
std::env::var_os(PATH_VARIABLE)
.as_ref()
.and_then(|value| value.to_str()),
)
}
}
const PATH_VARIABLE: &str = "PATH";
pub const CATALOGUE: &[CatalogueHarness] = &[
CatalogueHarness {
id: "claude-code",
display_name: "Claude Code",
program: "npx",
args: &["@agentclientprotocol/claude-agent-acp"],
install_hint: "Install Node.js so `npx` is on the server's PATH; the ACP adapter itself is \
fetched by npx on first use. Log Claude Code in on the server host (`claude \
login`) — nothing here can enter a credential.",
},
CatalogueHarness {
id: "codex",
display_name: "Codex",
program: "npx",
args: &["@agentclientprotocol/codex-acp"],
install_hint: "Install Node.js so `npx` is on the server's PATH; the ACP adapter itself is \
fetched by npx on first use. Log Codex in on the server host — nothing here \
can enter a credential.",
},
CatalogueHarness {
id: "gemini-cli",
display_name: "Gemini CLI",
program: "npx",
args: &["@google/gemini-cli", "--acp"],
install_hint: "Install Node.js so `npx` is on the server's PATH; the Gemini CLI itself is \
fetched by npx on first use. Log it in on the server host — nothing here \
can enter a credential.",
},
CatalogueHarness {
id: "opencode",
display_name: "OpenCode",
program: "opencode",
args: &["acp"],
install_hint: "Install OpenCode so `opencode` is on the server's PATH \
(https://opencode.ai), then log it in on the server host — nothing here can \
enter a credential.",
},
CatalogueHarness {
id: "pi",
display_name: "Pi",
program: "npx",
args: &["-y", "pi-acp"],
install_hint: "Install Node.js so `npx` is on the server's PATH; the `pi-acp` adapter is \
fetched by npx on first use and drives the `pi` coding agent, which must be \
installed on the server host at 0.80.4 or newer and logged in there — nothing \
here can enter a credential. The adapter accepts MCP servers without wiring \
them into pi, so the Aion tools do not reach this harness.",
},
];
#[must_use]
pub fn harness(id: &str) -> Option<&'static CatalogueHarness> {
harness_in(CATALOGUE, id)
}
#[must_use]
pub fn harness_in(
catalogue: &'static [CatalogueHarness],
id: &str,
) -> Option<&'static CatalogueHarness> {
catalogue.iter().find(|entry| entry.id == id)
}
#[must_use]
pub fn ids() -> String {
ids_of(CATALOGUE)
}
#[must_use]
pub fn ids_of(catalogue: &'static [CatalogueHarness]) -> String {
catalogue
.iter()
.map(|entry| entry.id)
.collect::<Vec<_>>()
.join(", ")
}
fn program_resolves(program: &Path, path: Option<&str>) -> bool {
if program.components().count() > 1 || program.is_absolute() {
return is_executable_file(program);
}
let Some(path) = path else {
return false;
};
path.split(PATH_SEPARATOR)
.filter(|directory| !directory.is_empty())
.any(|directory| is_executable_file(&PathBuf::from(directory).join(program)))
}
#[cfg(windows)]
const PATH_SEPARATOR: char = ';';
#[cfg(not(windows))]
const PATH_SEPARATOR: char = ':';
#[cfg(unix)]
fn is_executable_file(candidate: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(candidate)
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn is_executable_file(candidate: &Path) -> bool {
std::fs::metadata(candidate).is_ok_and(|metadata| metadata.is_file())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_entry_renders_the_line_the_adapters_own_table_documents() {
let rendered: Vec<String> = CATALOGUE.iter().map(CatalogueHarness::launch).collect();
assert_eq!(
rendered,
vec![
"npx @agentclientprotocol/claude-agent-acp".to_owned(),
"npx @agentclientprotocol/codex-acp".to_owned(),
"npx @google/gemini-cli --acp".to_owned(),
"opencode acp".to_owned(),
"npx -y pi-acp".to_owned(),
],
"the catalogue's launch lines are the harness table's own invocations, verbatim"
);
}
#[test]
fn every_entry_carries_an_id_a_name_and_a_hint_and_no_two_share_an_id() {
let mut seen: Vec<&str> = Vec::new();
for entry in CATALOGUE {
assert!(!entry.id.is_empty(), "a catalogue entry has no id");
assert!(
!entry.display_name.is_empty(),
"catalogue entry `{}` has no display name",
entry.id
);
assert!(
!entry.install_hint.is_empty(),
"catalogue entry `{}` has no install hint, so a surface could offer it, refuse \
it, and say nothing about what to install",
entry.id
);
assert!(
!seen.contains(&entry.id),
"catalogue id `{}` is declared twice; a session record names a harness by id",
entry.id
);
seen.push(entry.id);
}
assert!(
CATALOGUE.len() >= 5,
"the catalogue ships Claude Code, Codex, Gemini CLI, OpenCode and Pi"
);
}
#[test]
fn a_lookup_answers_by_id_and_a_name_nobody_ships_is_a_miss() {
assert_eq!(
harness("claude-code").map(|entry| entry.program),
Some("npx")
);
assert!(
harness("claude").is_none(),
"a near-miss id must not resolve: a session would record a harness this build cannot \
launch"
);
assert!(
ids().contains("claude-code"),
"the refusal names what is offered"
);
}
#[test]
fn availability_is_measured_against_the_path_it_is_asked_about() -> Result<(), std::io::Error> {
let directory = tempfile::tempdir()?;
let program = directory.path().join("opencode");
std::fs::write(&program, b"#!/bin/sh\nexit 0\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755))?;
}
let entry = harness("opencode").ok_or_else(|| {
std::io::Error::other("the catalogue must carry the opencode entry this pin is about")
})?;
let planted = directory.path().to_string_lossy().into_owned();
assert!(
entry.available_in(Some(&planted)),
"the positive control: the program is on the PATH that was asked about"
);
assert!(
!entry.available_in(Some("")),
"an empty PATH resolves nothing"
);
assert!(
!entry.available_in(None),
"a launching context with no PATH at all resolves no bare program name"
);
Ok(())
}
#[test]
fn a_file_without_the_execute_bit_does_not_count_as_available() -> Result<(), std::io::Error> {
let directory = tempfile::tempdir()?;
std::fs::write(directory.path().join("opencode"), b"not executable\n")?;
let entry = harness("opencode").ok_or_else(|| {
std::io::Error::other("the catalogue must carry the opencode entry this pin is about")
})?;
let planted = directory.path().to_string_lossy().into_owned();
#[cfg(unix)]
assert!(
!entry.available_in(Some(&planted)),
"a file that cannot be executed is not a harness that can be launched; reporting it \
available would move the failure to the spawn, where the operator gets `Permission \
denied` and no hint"
);
#[cfg(not(unix))]
let _ = planted;
Ok(())
}
}