use car_registry::supervisor::{AgentSpec, RestartPolicy, Supervisor};
use std::collections::BTreeMap;
use std::path::Path;
pub const ASSISTANT_AGENT_ID: &str = "parslee-core";
pub const LEGACY_ASSISTANT_AGENT_ID: &str = "car-assistant";
pub fn is_assistant_alias(id: &str) -> bool {
id == ASSISTANT_AGENT_ID || id == LEGACY_ASSISTANT_AGENT_ID
}
const ASSISTANT_CAPABILITIES: &[&str] = &[
"chat",
"verified-answers",
"safety-checks",
"memory-routines",
"background-work",
"trust-control",
"approvals",
"approval-channels",
"a2ui",
"continuity",
"notifications",
"connected-services",
"conversation-channels",
"workspace-files",
"governed-host-execution",
"durable-transcript",
"durable-action-fencing",
"personal-context",
"documents",
"creative-output",
];
fn spec(command: String) -> AgentSpec {
AgentSpec {
id: ASSISTANT_AGENT_ID.to_string(),
name: "Parslee Core".to_string(),
command,
args: vec!["do".to_string(), "--serve".to_string()],
cwd: None,
env: BTreeMap::new(),
restart: RestartPolicy::OnFailure,
max_restarts: 5,
backoff_secs: 2,
auto_start: true,
token: String::new(),
capabilities: assistant_capabilities(),
}
}
fn assistant_capabilities() -> Vec<String> {
ASSISTANT_CAPABILITIES
.iter()
.map(|capability| capability.to_string())
.collect()
}
fn merge_assistant_capabilities(existing: &[String]) -> Vec<String> {
let mut merged = existing.to_vec();
for capability in ASSISTANT_CAPABILITIES {
if !merged.iter().any(|value| value == capability) {
merged.push((*capability).to_string());
}
}
merged
}
pub async fn ensure_registered_in(sup: &Supervisor, car_binary: &Path) -> Result<bool, String> {
let already_canonical = sup
.list()
.await
.iter()
.any(|a| a.spec.id == ASSISTANT_AGENT_ID);
if !already_canonical {
if let Some(legacy) = sup
.list()
.await
.iter()
.find(|a| a.spec.id == LEGACY_ASSISTANT_AGENT_ID)
{
let mut migrated = legacy.spec.clone();
migrated.id = ASSISTANT_AGENT_ID.to_string();
sup.upsert(migrated).await.map_err(|e| e.to_string())?;
sup.remove(LEGACY_ASSISTANT_AGENT_ID)
.await
.map_err(|e| e.to_string())?;
migrate_legacy_agent_memgine(sup);
}
} else if sup
.list()
.await
.iter()
.any(|a| a.spec.id == LEGACY_ASSISTANT_AGENT_ID)
{
tracing::warn!(
canonical = ASSISTANT_AGENT_ID,
legacy = LEGACY_ASSISTANT_AGENT_ID,
"both the canonical and legacy flagship agent ids are registered; \
the legacy entry is orphaned and will not receive new dispatches"
);
}
if let Some(existing) = sup
.list()
.await
.iter()
.find(|a| a.spec.id == ASSISTANT_AGENT_ID)
{
let merged_capabilities = merge_assistant_capabilities(&existing.spec.capabilities);
if merged_capabilities != existing.spec.capabilities {
let mut updated = existing.spec.clone();
updated.name = "Parslee Core".to_string();
updated.capabilities = merged_capabilities;
sup.upsert(updated).await.map_err(|e| e.to_string())?;
}
return Ok(false);
}
sup.upsert(spec(car_binary.to_string_lossy().into_owned()))
.await
.map(|_| true)
.map_err(|e| e.to_string())
}
fn migrate_legacy_agent_memgine(sup: &Supervisor) {
let Some(root) = sup.manifest_path().parent() else {
tracing::warn!(
"skipping flagship memgine snapshot migration: manifest path has no parent directory"
);
return;
};
let legacy = agent_memgine_snapshot_path_under(root, LEGACY_ASSISTANT_AGENT_ID);
let canonical = agent_memgine_snapshot_path_under(root, ASSISTANT_AGENT_ID);
migrate_legacy_agent_memgine_at(&legacy, &canonical);
}
fn agent_memgine_snapshot_path_under(root: &Path, agent_id: &str) -> std::path::PathBuf {
root.join("memory")
.join("agents")
.join(format!("{agent_id}.json"))
}
fn migrate_legacy_agent_memgine_at(legacy: &Path, canonical: &Path) {
if canonical.exists() || !legacy.exists() {
return;
}
if let Some(parent) = canonical.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
tracing::warn!(
dir = %parent.display(),
"could not migrate flagship memgine snapshot to the new agent id: {e}"
);
return;
}
}
if let Err(e) = std::fs::rename(legacy, canonical) {
tracing::warn!(
from = %legacy.display(),
to = %canonical.display(),
"could not migrate flagship memgine snapshot to the new agent id: {e}"
);
}
}
pub async fn ensure_registered() -> Result<bool, String> {
let exe =
std::env::current_exe().map_err(|e| format!("cannot resolve current executable: {e}"))?;
let sup = Supervisor::user_default().map_err(|e| e.to_string())?;
ensure_registered_in(&sup, &exe).await
}
pub fn is_manifest_locked(err: &str) -> bool {
err.contains("another supervisor already owns this manifest")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn stand_in_binary() -> PathBuf {
if cfg!(windows) {
let root = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
PathBuf::from(format!(r"{root}\System32\cmd.exe"))
} else {
PathBuf::from("/bin/sh")
}
}
fn other_binary() -> PathBuf {
if cfg!(windows) {
let root = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
PathBuf::from(format!(r"{root}\System32\where.exe"))
} else {
PathBuf::from("/bin/echo")
}
}
#[tokio::test]
async fn register_is_idempotent_and_uses_the_given_binary() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agents.json");
let logs = dir.path().join("logs");
let sup = Supervisor::with_paths(manifest, logs).unwrap();
let car = stand_in_binary();
assert!(ensure_registered_in(&sup, &car).await.unwrap());
assert!(!ensure_registered_in(&sup, &car).await.unwrap());
let listed = sup.list().await;
let entry = listed
.iter()
.find(|a| a.spec.id == ASSISTANT_AGENT_ID)
.expect("assistant registered");
assert_eq!(entry.spec.command, car.to_string_lossy());
assert_eq!(
entry.spec.args,
vec!["do".to_string(), "--serve".to_string()]
);
assert!(entry.spec.auto_start);
assert!(
entry.spec.capabilities.contains(&"chat".to_string()),
"assistant must advertise the chat capability"
);
assert!(
entry.spec.capabilities.len() > 10,
"assistant must advertise the full Parslee Core capability surface"
);
for expected in [
"verified-answers",
"memory-routines",
"approvals",
"approval-channels",
"a2ui",
"continuity",
"notifications",
"connected-services",
"documents",
"creative-output",
] {
assert!(
entry.spec.capabilities.contains(&expected.to_string()),
"assistant capability metadata missing {expected}"
);
}
assert!(!entry.spec.token.is_empty());
}
#[tokio::test]
async fn register_refreshes_stale_builtin_capabilities_without_overwriting_operator_settings() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agents.json");
let logs = dir.path().join("logs");
let sup = Supervisor::with_paths(manifest, logs).unwrap();
let operator_cmd = stand_in_binary();
sup.upsert(AgentSpec {
id: ASSISTANT_AGENT_ID.to_string(),
name: "CAR Assistant".to_string(),
command: operator_cmd.to_string_lossy().into_owned(),
args: vec!["-c".to_string(), "sleep 1".to_string()],
cwd: None,
env: BTreeMap::new(),
restart: RestartPolicy::Never,
max_restarts: 1,
backoff_secs: 9,
auto_start: false,
token: "operator-token".to_string(),
capabilities: vec!["chat".to_string()],
})
.await
.unwrap();
assert!(!ensure_registered_in(&sup, &other_binary()).await.unwrap());
let listed = sup.list().await;
let entry = listed
.iter()
.find(|a| a.spec.id == ASSISTANT_AGENT_ID)
.expect("assistant registered");
assert_eq!(entry.spec.name, "Parslee Core");
assert_eq!(entry.spec.command, operator_cmd.to_string_lossy());
assert_eq!(
entry.spec.args,
vec!["-c".to_string(), "sleep 1".to_string()]
);
assert_eq!(entry.spec.restart, RestartPolicy::Never);
assert_eq!(entry.spec.max_restarts, 1);
assert_eq!(entry.spec.backoff_secs, 9);
assert!(!entry.spec.auto_start);
assert_eq!(entry.spec.token, "operator-token");
assert!(entry
.spec
.capabilities
.contains(&"verified-answers".to_string()));
assert!(entry.spec.capabilities.contains(&"a2ui".to_string()));
assert!(entry
.spec
.capabilities
.contains(&"connected-services".to_string()));
assert!(entry
.spec
.capabilities
.contains(&"creative-output".to_string()));
}
#[tokio::test]
async fn ensure_registered_in_migrates_a_legacy_car_assistant_entry() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agents.json");
let logs = dir.path().join("logs");
let sup = Supervisor::with_paths(manifest, logs).unwrap();
let legacy_cmd = stand_in_binary();
sup.upsert(AgentSpec {
id: LEGACY_ASSISTANT_AGENT_ID.to_string(),
name: "Parslee Core".to_string(),
command: legacy_cmd.to_string_lossy().into_owned(),
args: vec!["do".to_string(), "--serve".to_string()],
cwd: None,
env: BTreeMap::new(),
restart: RestartPolicy::OnFailure,
max_restarts: 5,
backoff_secs: 2,
auto_start: true,
token: "legacy-token".to_string(),
capabilities: vec!["chat".to_string()],
})
.await
.unwrap();
let legacy_memgine =
agent_memgine_snapshot_path_under(dir.path(), LEGACY_ASSISTANT_AGENT_ID);
std::fs::create_dir_all(legacy_memgine.parent().unwrap()).unwrap();
std::fs::write(
&legacy_memgine,
r#"[{"subject":"s","body":"remembered fact"}]"#,
)
.unwrap();
assert!(!ensure_registered_in(&sup, &other_binary()).await.unwrap());
let listed = sup.list().await;
assert!(
listed
.iter()
.all(|a| a.spec.id != LEGACY_ASSISTANT_AGENT_ID),
"the legacy car-assistant entry must not survive migration: {:?}",
listed.iter().map(|a| &a.spec.id).collect::<Vec<_>>()
);
let migrated = listed
.iter()
.find(|a| a.spec.id == ASSISTANT_AGENT_ID)
.expect("legacy entry migrated to the canonical id");
assert_eq!(migrated.spec.command, legacy_cmd.to_string_lossy());
assert_eq!(migrated.spec.token, "legacy-token");
assert!(migrated.spec.auto_start);
assert!(
!legacy_memgine.exists(),
"legacy memgine snapshot should have moved, not been copied"
);
let canonical_memgine = agent_memgine_snapshot_path_under(dir.path(), ASSISTANT_AGENT_ID);
let content = std::fs::read_to_string(&canonical_memgine).unwrap();
assert!(content.contains("remembered fact"));
assert!(is_assistant_alias(ASSISTANT_AGENT_ID));
assert!(is_assistant_alias(LEGACY_ASSISTANT_AGENT_ID));
assert!(!is_assistant_alias("some-other-agent"));
}
#[test]
fn migrate_legacy_agent_memgine_at_is_a_noop_with_no_legacy_file() {
let dir = tempfile::tempdir().unwrap();
let legacy = dir.path().join("legacy.json");
let canonical = dir.path().join("canonical.json");
migrate_legacy_agent_memgine_at(&legacy, &canonical);
assert!(!legacy.exists());
assert!(!canonical.exists());
}
#[test]
fn migrate_legacy_agent_memgine_at_never_overwrites_an_existing_canonical_file() {
let dir = tempfile::tempdir().unwrap();
let legacy = dir.path().join("legacy.json");
let canonical = dir.path().join("canonical.json");
std::fs::write(&legacy, "stale legacy content").unwrap();
std::fs::write(&canonical, "real canonical content").unwrap();
migrate_legacy_agent_memgine_at(&legacy, &canonical);
assert!(legacy.exists());
assert_eq!(
std::fs::read_to_string(&canonical).unwrap(),
"real canonical content"
);
}
#[tokio::test]
async fn ensure_registered_in_leaves_both_entries_alone_when_both_exist() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agents.json");
let logs = dir.path().join("logs");
let sup = Supervisor::with_paths(manifest, logs).unwrap();
let car = stand_in_binary();
sup.upsert(spec(car.to_string_lossy().into_owned()))
.await
.unwrap();
sup.upsert(AgentSpec {
id: LEGACY_ASSISTANT_AGENT_ID.to_string(),
name: "Parslee Core".to_string(),
command: other_binary().to_string_lossy().into_owned(),
args: vec!["do".to_string(), "--serve".to_string()],
cwd: None,
env: BTreeMap::new(),
restart: RestartPolicy::OnFailure,
max_restarts: 5,
backoff_secs: 2,
auto_start: true,
token: "legacy-token".to_string(),
capabilities: vec!["chat".to_string()],
})
.await
.unwrap();
assert!(!ensure_registered_in(&sup, &car).await.unwrap());
let listed = sup.list().await;
assert!(
listed.iter().any(|a| a.spec.id == ASSISTANT_AGENT_ID),
"canonical entry must survive"
);
assert!(
listed
.iter()
.any(|a| a.spec.id == LEGACY_ASSISTANT_AGENT_ID),
"legacy entry is left in place (orphaned, but not deleted out from \
under a process that might be attached under it)"
);
}
}