use std::fs;
use std::path::{Path, PathBuf};
use embacle::config::CliRunnerType;
use embacle::types::{McpServerConfig, McpTransport};
use serde_json::json;
use super::error::{ErrorType, UhpFailure};
use super::files;
use super::harnesses::{Harness, McpServer, Skill};
pub const PLUGIN_NAME: &str = "harness";
const MANIFEST_PATH: [&str; 2] = [".claude-plugin", "plugin.json"];
const SKILLS_DIR: &str = "skills";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Setting {
Skills,
McpServers,
DisabledTools,
}
impl Setting {
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::Skills => "skills",
Self::McpServers => "mcpServers",
Self::DisabledTools => "disabledTools",
}
}
}
struct Mechanism {
skills: Option<&'static str>,
mcp_servers: bool,
disabled_tools: Option<&'static str>,
}
const fn mechanism(provider: CliRunnerType) -> Mechanism {
match provider {
CliRunnerType::ClaudeCode => Mechanism {
skills: Some("--plugin-dir"),
mcp_servers: true,
disabled_tools: Some("--disallowed-tools"),
},
CliRunnerType::Copilot => Mechanism {
skills: Some("--plugin-dir"),
mcp_servers: true,
disabled_tools: Some("--excluded-tools"),
},
CliRunnerType::CopilotHeadless => Mechanism {
skills: None,
mcp_servers: true,
disabled_tools: None,
},
_ => Mechanism {
skills: None,
mcp_servers: false,
disabled_tools: None,
},
}
}
#[derive(Debug)]
pub struct StagedSkills {
dir: PathBuf,
}
impl StagedSkills {
#[must_use]
pub fn dir(&self) -> &Path {
&self.dir
}
fn write(dir: PathBuf, skills: &[&Skill]) -> Result<Self, UhpFailure> {
let staged = Self { dir };
let manifest: PathBuf = MANIFEST_PATH.iter().collect();
write_file(
&staged.dir.join(manifest),
json!({ "name": PLUGIN_NAME }).to_string().as_bytes(),
)?;
for skill in skills {
if !skill.has_plain_name() {
return Err(invalid_skill(
&skill.name,
"its name is not one path segment",
));
}
let root = staged.dir.join(SKILLS_DIR).join(&skill.name);
for file in &skill.files {
let relative = file
.relative_path()
.ok_or_else(|| invalid_skill(&skill.name, "a member path leaves the bundle"))?;
let bytes = file.bytes().map_err(|_| {
invalid_skill(&skill.name, "a member's content_b64 is not base64")
})?;
write_file(&root.join(relative), &bytes)?;
}
}
Ok(staged)
}
}
impl Drop for StagedSkills {
fn drop(&mut self) {
fs::remove_dir_all(&self.dir).ok();
}
}
fn write_file(path: &Path, bytes: &[u8]) -> Result<(), UhpFailure> {
path.parent()
.map_or(Ok(()), fs::create_dir_all)
.and_then(|()| fs::write(path, bytes))
.map_err(|_| staging_failed())
}
fn staging_failed() -> UhpFailure {
UhpFailure::new(
ErrorType::ServerError,
"skill_staging_failed",
"the harness's skills could not be prepared for this task",
)
}
fn invalid_skill(name: &str, why: &str) -> UhpFailure {
UhpFailure::new(
ErrorType::InvalidRequestError,
"invalid_input",
format!("skill '{name}' cannot be staged for this task: {why}"),
)
.with_detail(json!({ "setting": Setting::Skills.wire_name(), "skill": name }))
}
#[derive(Debug, Default)]
pub struct Applied {
pub args: Vec<String>,
pub mcp_servers: Vec<McpServerConfig>,
pub skills: Option<StagedSkills>,
}
fn mcp_config(server: &McpServer) -> Result<McpServerConfig, UhpFailure> {
let refuse = |why: String| {
UhpFailure::new(ErrorType::InvalidRequestError, "invalid_input", why).with_detail(
json!({ "setting": Setting::McpServers.wire_name(), "server": server.name }),
)
};
let url = server.url.clone().ok_or_else(|| {
refuse(format!(
"MCP server '{}' is enabled but names no url",
server.name
))
})?;
let headers = Vec::new();
let transport = match server.transport.as_deref() {
None | Some("http" | "streamable-http" | "streamable_http") => {
McpTransport::Http { url, headers }
}
Some("sse") => McpTransport::Sse { url, headers },
Some(other) => {
return Err(refuse(format!(
"MCP server '{}' uses transport '{other}'; a harness reaches MCP servers over \
http or sse",
server.name
)))
}
};
Ok(McpServerConfig {
name: server.name.clone(),
transport,
})
}
pub fn apply(
provider: CliRunnerType,
harness: &Harness,
task_id: &str,
) -> Result<Applied, UhpFailure> {
let mechanism = mechanism(provider);
let skills: Vec<&Skill> = harness.skills.iter().filter(|s| s.enabled).collect();
let servers: Vec<&McpServer> = harness.mcp_servers.iter().filter(|s| s.enabled).collect();
let unsupported: Vec<&str> = [
(
Setting::Skills,
!skills.is_empty() && mechanism.skills.is_none(),
),
(
Setting::McpServers,
!servers.is_empty() && !mechanism.mcp_servers,
),
(
Setting::DisabledTools,
!harness.disabled_tools.is_empty() && mechanism.disabled_tools.is_none(),
),
]
.into_iter()
.filter_map(|(setting, refused)| refused.then_some(setting.wire_name()))
.collect();
if !unsupported.is_empty() {
return Err(UhpFailure::new(
ErrorType::InvalidRequestError,
"unsupported_harness_setting",
format!(
"harness '{}' sets {}, which its base '{}' has no way to apply",
harness.name,
unsupported.join(", "),
harness.base
),
)
.with_detail(json!({
"harness_id": harness.id,
"base": harness.base,
"unsupported": unsupported,
})));
}
let mut applied = Applied {
mcp_servers: servers
.into_iter()
.map(mcp_config)
.collect::<Result<_, _>>()?,
..Applied::default()
};
if let Some(flag) = mechanism.disabled_tools {
if !harness.disabled_tools.is_empty() {
applied.args.push(flag.to_owned());
applied.args.extend(harness.disabled_tools.iter().cloned());
}
}
if let Some(flag) = mechanism.skills {
if !skills.is_empty() {
let staged = StagedSkills::write(files::staging_dir(task_id), &skills)?;
let dir = staged.dir().to_str().ok_or_else(staging_failed)?.to_owned();
applied.args.push(flag.to_owned());
applied.args.push(dir);
applied.skills = Some(staged);
}
}
Ok(applied)
}