use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use ureq::http::Uri;
use super::plan::text;
use super::types::Error;
pub(super) const LAUNCHER: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/embedded/plugin/scripts/mcp.sh"
));
pub(super) const NO_AGENT: &str = concat!(
"No agent is set in the bundle. ",
"Claude Code asks for the agent id and key file when the plugin is enabled. ",
"Codex needs HAEMATITE_LYS_AGENT and a proof in its environment. ",
"Without them the plugin stops with haematite_mcp_agent_missing."
);
#[derive(Debug, PartialEq, Eq)]
pub(super) struct Agent {
issuer: String,
id: String,
key: PathBuf,
}
impl Agent {
pub(super) fn parse(
issuer: Option<PathBuf>,
id: Option<PathBuf>,
key: Option<PathBuf>,
) -> Result<Option<Self>, Error> {
if issuer.is_none() && id.is_none() && key.is_none() {
return Ok(None);
}
let issuer = issuer.unwrap_or_else(|| "https://lys.ablative.com.au".into());
let (Some(issuer), Some(id), Some(key)) = (Some(issuer), id, key) else {
return Err(Error::new(
"bundle_lys_agent_incomplete",
"--lys-agent and --key-file must be given together; --lys-issuer defaults to the central origin",
));
};
let issuer = text(&issuer)?;
let origin: Uri = issuer.parse().map_err(|_| invalid("invalid Lys origin"))?;
let authority = origin
.authority()
.ok_or_else(|| invalid("invalid Lys origin"))?;
let local = matches!(authority.host(), "127.0.0.1" | "localhost");
if !issuer.is_ascii()
|| issuer.contains('#')
|| authority.as_str().contains('@')
|| !(origin.scheme_str() == Some("https")
|| (origin.scheme_str() == Some("http") && local))
|| origin.query().is_some()
|| !matches!(origin.path(), "" | "/")
{
return Err(invalid(
"Lys issuer must be an HTTPS origin, or HTTP on loopback",
));
}
let id = text(&id)?;
if id.is_empty()
|| id.len() > 128
|| !id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
{
return Err(invalid("--lys-agent must be a plain agent id"));
}
let key = std::fs::canonicalize(key)
.map_err(|error| Error::new("bundle_lys_key_invalid", &error.kind()))?;
if !key.is_file() {
return Err(invalid("--key-file must name a regular file"));
}
text(&key)?;
Ok(Some(Self {
issuer: issuer.to_owned(),
id: id.to_owned(),
key,
}))
}
pub(super) fn environment(&self, bundle: &Path) -> Result<Value, Error> {
Ok(json!({
"HAEMATITE_BIN": text(&bundle.join("bin/haematite"))?,
"HAEMATITE_URL": "https://haematite.ablative.com.au/mcp",
"HAEMATITE_LYS_ISSUER": self.issuer,
"HAEMATITE_LYS_AGENT": self.id,
"HAEMATITE_KEY_FILE": text(&self.key)?
}))
}
}
fn invalid(message: &str) -> Error {
Error::new("bundle_lys_agent_invalid", message)
}