use crate::{
config::{McPaths, read_settings},
instructions::discover_agents_with_additional_markdown,
providers::{PromptDumpProviderTarget, provider_prompt_dump},
skills::{SkillRoots, discover_skills, filter_enabled_skills},
};
use anyhow::Context;
use std::{io::Write, path::Path};
pub(super) fn run_dump_prompt<W: Write>(
writer: &mut W,
paths: McPaths,
cwd: &Path,
target: PromptDumpProviderTarget,
) -> anyhow::Result<()> {
let settings = read_settings(&paths)?;
let instructions = discover_agents_with_additional_markdown(
&paths.user_agents,
cwd,
&settings.instructions.additional_markdown_paths,
)?;
let mut skill_roots = SkillRoots::from_paths(&paths.root, cwd);
skill_roots.additional_paths = settings.skills.additional_paths.clone();
let discovered_skills = discover_skills(&skill_roots);
let disabled_skills = crate::config::disabled_skill_names_from_settings(&settings);
let skills = filter_enabled_skills(&discovered_skills, &disabled_skills);
let subagent_profile_discovery =
crate::subagents::profiles::discover_subagent_profiles(&paths.subagents);
let agent = crate::agent::runner::new_parent_agent_with_subagent_profiles(
crate::providers::DEFAULT_CODEX_MODEL,
Some(&paths.prompts),
&instructions,
&skills,
&subagent_profile_discovery,
)?;
let system_prompt = strip_eval_context_sections(agent.system_prompt());
let dump = provider_prompt_dump(target, system_prompt.to_string())?;
serde_json::to_writer_pretty(&mut *writer, &dump).context("failed to serialize prompt dump")?;
writer.write_all(b"\n")?;
Ok(())
}
fn strip_eval_context_sections(prompt: &str) -> String {
let mut result = prompt.to_string();
for tag in [
"Subagent-Identities",
"Available-Skills",
"Additional-Context-Files",
] {
result = strip_xml_block(&result, tag);
}
result
}
fn strip_xml_block(input: &str, tag: &str) -> String {
let open = format!("<{tag}>");
let close = format!("</{tag}>");
let Some(start) = input.find(&open) else {
return input.to_string();
};
let Some(end) = input[start..].find(&close) else {
return input.to_string();
};
let end = start + end + close.len();
let mut out = String::with_capacity(input.len());
out.push_str(&input[..start]);
out.push_str(&input[end..]);
while out.contains("\n\n\n\n") {
out = out.replace("\n\n\n\n", "\n\n");
}
out.trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use tempfile::TempDir;
#[test]
fn dump_prompt_outputs_only_system_prompt_and_tools_fields() {
let temp = TempDir::new().unwrap();
let paths = McPaths::from_root(temp.path().join("mc"));
let mut output = Vec::new();
run_dump_prompt(
&mut output,
paths,
temp.path(),
PromptDumpProviderTarget::Anthropic,
)
.unwrap();
let value: Value = serde_json::from_slice(&output).unwrap();
let object = value.as_object().unwrap();
assert_eq!(object.len(), 2);
assert!(
object
.get("system_prompt")
.unwrap()
.as_str()
.unwrap()
.contains("You are")
);
let system_prompt = object.get("system_prompt").unwrap().as_str().unwrap();
assert!(!system_prompt.contains("<Subagent-Identities>"));
assert!(!system_prompt.contains("<Available-Skills>"));
assert!(!system_prompt.contains("<Additional-Context-Files>"));
assert!(object.get("tools").unwrap().as_array().unwrap().len() > 1);
}
#[test]
fn strip_eval_context_sections_removes_all_three_blocks() {
let input = "header\n\n<Subagent-Identities>\nstuff\n</Subagent-Identities>\n\n<Available-Skills>\nmore stuff\n</Available-Skills>\n\nmiddle\n\n<Additional-Context-Files>\nfile stuff\n</Additional-Context-Files>\n\nfooter";
let result = strip_eval_context_sections(input);
assert!(!result.contains("Subagent-Identities"));
assert!(!result.contains("Available-Skills"));
assert!(!result.contains("Additional-Context-Files"));
assert!(result.contains("header"));
assert!(result.contains("middle"));
assert!(result.contains("footer"));
}
#[test]
fn strip_xml_block_leaves_input_untouched_when_tag_absent() {
let input = "nothing to remove here";
assert_eq!(strip_xml_block(input, "Nonexistent"), input);
}
#[cfg(unix)]
#[test]
fn dump_prompt_succeeds_with_insecure_invalid_auth_file() {
use std::fs;
use std::os::unix::fs::PermissionsExt;
let temp = TempDir::new().unwrap();
let paths = McPaths::from_root(temp.path().join("mc"));
fs::create_dir_all(&paths.root).unwrap();
fs::write(&paths.auth_file, "not json").unwrap();
let mut permissions = fs::metadata(&paths.auth_file).unwrap().permissions();
permissions.set_mode(0o644);
fs::set_permissions(&paths.auth_file, permissions).unwrap();
let mut output = Vec::new();
run_dump_prompt(
&mut output,
paths,
temp.path(),
PromptDumpProviderTarget::Anthropic,
)
.unwrap();
let value: Value = serde_json::from_slice(&output).unwrap();
assert!(value.get("system_prompt").unwrap().as_str().is_some());
assert!(value.get("tools").unwrap().as_array().unwrap().len() > 1);
}
#[test]
fn dump_prompt_does_not_create_runtime_dirs_or_session_files() {
let temp = TempDir::new().unwrap();
let paths = McPaths::from_root(temp.path().join("mc"));
let mut output = Vec::new();
run_dump_prompt(
&mut output,
paths.clone(),
temp.path(),
PromptDumpProviderTarget::OpenAi,
)
.unwrap();
assert!(!paths.sessions.exists());
assert!(!paths.settings_file.exists());
assert!(!paths.auth_file.exists());
}
}