use aion_integration_acp::catalogue;
use serde::Serialize;
use super::sessions::{AssistantSessionError, AssistantSessions};
use crate::namespace::CallerIdentity;
use crate::namespace::grants::GRANT_WORDS;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantHarnessDescriptor {
pub name: String,
pub kind: String,
pub accounts: Vec<String>,
pub available: bool,
pub install_hint: Option<String>,
pub launch: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantToolsDescriptor {
pub aion: bool,
pub assistant: AssistantOwnToolsDescriptor,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantOwnToolsDescriptor {
pub server: String,
pub route: String,
pub tools: Vec<String>,
pub handed_over: bool,
pub unavailable_reason: Option<String>,
pub token: AssistantSessionTokenDescriptor,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantSessionTokenDescriptor {
pub kind: String,
pub minted_by: String,
pub scope: String,
pub description: String,
}
impl AssistantSessionTokenDescriptor {
#[must_use]
pub fn current() -> Self {
Self {
kind: crate::assistant::mcp::SESSION_TOKEN_KIND.to_owned(),
minted_by: TOKEN_MINTED_BY.to_owned(),
scope: TOKEN_SCOPE.to_owned(),
description: crate::assistant::mcp::SESSION_TOKEN_DESCRIPTION.to_owned(),
}
}
}
const TOKEN_MINTED_BY: &str = "server";
const TOKEN_SCOPE: &str = "session";
const NO_DIALABLE_ADDRESS: &str = "this server cannot state an address an agent could dial back on (`server.listen_address` \
names port 0, whose real port is only known after bind), so no MCP server of ours is handed \
to a session's agent and it cannot read what is on the operator's screen";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantGrantDescriptor {
pub name: String,
pub held: bool,
pub description: String,
}
const HARNESS_KIND: &str = "acp";
const ASSISTANT_TOOL_SERVER_NAME: &str = "assistant";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssistantDescriptor {
pub harnesses: Vec<AssistantHarnessDescriptor>,
pub default_harness: Option<String>,
pub tools: AssistantToolsDescriptor,
pub sessions_enabled: bool,
pub sessions_disabled_reason: Option<String>,
pub grants: Vec<AssistantGrantDescriptor>,
}
pub async fn describe(
sessions: &AssistantSessions,
caller: &CallerIdentity,
) -> Result<AssistantDescriptor, AssistantSessionError> {
let config = sessions.config();
let availability = sessions.availability();
let default_harness = sessions.last_harness_pick(caller.subject()).await?;
Ok(AssistantDescriptor {
harnesses: catalogue::CATALOGUE
.iter()
.map(|entry| AssistantHarnessDescriptor {
name: entry.id.to_owned(),
kind: HARNESS_KIND.to_owned(),
accounts: config.account_names(entry.id),
available: entry.available(),
install_hint: (!entry.available()).then(|| entry.install_hint.to_owned()),
launch: entry.launch(),
})
.collect(),
default_harness,
tools: AssistantToolsDescriptor {
aion: sessions.hands_over_general_mcp(),
assistant: AssistantOwnToolsDescriptor {
server: ASSISTANT_TOOL_SERVER_NAME.to_owned(),
route: crate::assistant::mcp::ASSISTANT_MCP_PATH.to_owned(),
tools: crate::assistant::mcp::SESSION_TOOL_NAMES
.iter()
.map(|name| (*name).to_owned())
.collect(),
handed_over: sessions.hands_over_assistant_tools(),
unavailable_reason: (!sessions.hands_over_assistant_tools())
.then(|| NO_DIALABLE_ADDRESS.to_owned()),
token: AssistantSessionTokenDescriptor::current(),
},
},
sessions_enabled: availability.is_available(),
sessions_disabled_reason: availability.reason().map(ToOwned::to_owned),
grants: GRANT_WORDS
.iter()
.map(|grant| AssistantGrantDescriptor {
name: grant.word().to_owned(),
held: grant.granted_for(caller),
description: grant.description().to_owned(),
})
.collect(),
})
}