use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use serde_json::{Map, Value, json};
use crate::cli::Agent;
pub const FILE_SURFACE_AGENTS: [Agent; 2] = [Agent::Claude, Agent::Codex];
pub fn agent_slug(agent: Agent) -> &'static str {
match agent {
Agent::Claude => "claude",
Agent::Codex => "codex",
Agent::Pi => "pi",
Agent::Grok => "grok",
}
}
pub fn surface_relative_path(agent: Agent) -> &'static str {
match agent {
Agent::Claude => ".claude/settings.json",
Agent::Codex => ".codex/hooks.json",
Agent::Pi => ".pi/settings.json",
Agent::Grok => ".grok/hooks/truth-mirror.json",
}
}
pub const GROK_SKILL_RELATIVE: &str = ".grok/skills/truth-mirror/SKILL.md";
pub const GROK_SKILL_MANAGED_MARKER: &str = "<!-- truth-mirror managed grok skill -->";
pub const GROK_SKILL_SOURCE: &str = r#"---
name: truth-mirror
description: Use at session start, before completion claims, before push, and whenever truth-mirror ledger rejections or memory-skill candidates may be unresolved. Run `truth-mirror reinject --agent grok` and treat non-empty output as mandatory work.
---
<!-- truth-mirror managed grok skill -->
# Truth Mirror Unresolved Findings (Grok)
Grok Build does **not** inject `UserPromptSubmit` (or other passive) hook stdout into the model context. Reinjection is active, not hook-stdout:
```bash
truth-mirror reinject --agent grok
```
If the command prints anything, address or waive those findings before claiming completion or pushing. Do not ignore empty success as "all clear" without having run the command in this session when ledger state may have changed.
Optional status:
```bash
truth-mirror ledger list
truth-mirror status
```
"#;
pub fn grok_skill_path(repo_root: &Path) -> PathBuf {
repo_root.join(GROK_SKILL_RELATIVE)
}
pub fn grok_skill_installed(repo_root: &Path) -> bool {
let path = grok_skill_path(repo_root);
fs::read_to_string(path)
.map(|body| body.contains(GROK_SKILL_MANAGED_MARKER))
.unwrap_or(false)
}
pub fn install_grok_skill(repo_root: &Path) -> Result<()> {
let path = grok_skill_path(repo_root);
reject_symlink_path(repo_root, &path)?;
if path.exists() {
let meta = fs::symlink_metadata(&path)
.with_context(|| format!("statting grok skill {}", path.display()))?;
if !meta.is_file() {
anyhow::bail!(
"refusing to install Grok skill at {} (not a regular file)",
path.display()
);
}
let existing = fs::read_to_string(&path)
.with_context(|| format!("reading existing grok skill {}", path.display()))?;
if !existing.contains(GROK_SKILL_MANAGED_MARKER) {
anyhow::bail!(
"refusing to overwrite foreign Grok skill at {} (missing managed marker); move it aside or pass force via manual edit",
path.display()
);
}
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("creating grok skills dir {}", parent.display()))?;
}
reject_symlink_path(repo_root, &path)?;
fs::write(&path, GROK_SKILL_SOURCE)
.with_context(|| format!("writing grok skill {}", path.display()))?;
Ok(())
}
fn reject_symlink_path(repo_root: &Path, path: &Path) -> Result<()> {
let relative = path.strip_prefix(repo_root).unwrap_or(path);
let mut prefix = repo_root.to_path_buf();
for component in relative.components() {
prefix.push(component);
match fs::symlink_metadata(&prefix) {
Ok(meta) if meta.file_type().is_symlink() => anyhow::bail!(
"refusing to use Grok skill path {} (symlink at {}; possible write-outside-repo attack)",
path.display(),
prefix.display()
),
Ok(_) | Err(_) => {}
}
}
Ok(())
}
pub fn uninstall_grok_skill(repo_root: &Path) -> Result<()> {
let path = grok_skill_path(repo_root);
if path.is_file() {
let existing = fs::read_to_string(&path)
.with_context(|| format!("reading grok skill {}", path.display()))?;
if !existing.contains(GROK_SKILL_MANAGED_MARKER) {
return Ok(());
}
fs::remove_file(&path)
.with_context(|| format!("removing grok skill {}", path.display()))?;
}
let _ = fs::remove_dir(repo_root.join(".grok/skills/truth-mirror"));
let _ = fs::remove_dir(repo_root.join(".grok/skills"));
let _ = fs::remove_dir(repo_root.join(".grok"));
Ok(())
}
pub fn cleanup_empty_grok_dirs(repo_root: &Path) {
let _ = fs::remove_dir(repo_root.join(".grok/hooks"));
let _ = fs::remove_dir(repo_root.join(".grok"));
}
pub fn reinject_command(agent: Agent) -> String {
format!("truth-mirror reinject --agent {}", agent_slug(agent))
}
pub const PI_EXTENSION_RELATIVE: &str = ".pi/extensions/truth-mirror.js";
pub fn pi_extension_path(repo_root: &Path) -> PathBuf {
repo_root.join(PI_EXTENSION_RELATIVE)
}
pub const PI_EXTENSION_SOURCE: &str = r#"// truth-mirror Pi reinjection extension.
// Auto-generated by `truth-mirror install-hooks --pi`. Pi auto-loads this file
// from <repo>/.pi/extensions/ once the project folder is trusted.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);
export default function truthMirror(pi) {
let lastInjected = "";
pi.on("context", async (event) => {
let text = "";
try {
const { stdout } = await run("truth-mirror", ["reinject", "--agent", "pi"], {
cwd: process.cwd(),
});
text = (stdout || "").trim();
} catch {
return; // truth-mirror missing or errored: stay silent.
}
// `context` fires before every LLM call; dedup so findings inject once per change.
if (!text || text === lastInjected) return;
lastInjected = text;
return {
messages: [
...event.messages,
{ role: "user", content: [{ type: "text", text }] },
],
};
});
}
"#;
pub fn install_pi_extension(repo_root: &Path) -> Result<()> {
let path = pi_extension_path(repo_root);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("creating pi extensions dir {}", parent.display()))?;
}
fs::write(&path, PI_EXTENSION_SOURCE)
.with_context(|| format!("writing pi extension {}", path.display()))?;
Ok(())
}
pub fn uninstall_pi_extension(repo_root: &Path) -> Result<()> {
let path = pi_extension_path(repo_root);
if path.is_file() {
fs::remove_file(&path)
.with_context(|| format!("removing pi extension {}", path.display()))?;
}
Ok(())
}
pub const ENFORCE_COMMAND: &str = "gate --pre-tool-use";
fn enforce_command(global_args: &str) -> String {
format!("truth-mirror {global_args}{ENFORCE_COMMAND}")
}
pub fn install_enforcement(repo_root: &Path, agent: Agent, global_args: &str) -> Result<()> {
debug_assert!(is_nested(agent), "enforcement hook is nested-surface only");
let command = enforce_command(global_args);
let path = repo_root.join(surface_relative_path(agent));
let mut root = read_object(&path)?;
remove_own_enforcement(&mut root, "PreToolUse");
let entries = event_array_mut(&mut root, "PreToolUse");
entries.push(json!({ "hooks": [ { "type": "command", "command": command } ] }));
write_object(&path, &root)
}
pub fn uninstall_enforcement(repo_root: &Path, agent: Agent) -> Result<()> {
let path = repo_root.join(surface_relative_path(agent));
if !path.exists() {
return Ok(());
}
let mut root = read_object(&path)?;
remove_own_enforcement(&mut root, "PreToolUse");
if root.is_empty() {
fs::remove_file(&path)
.with_context(|| format!("removing empty surface {}", path.display()))?;
} else {
write_object(&path, &root)?;
}
Ok(())
}
fn is_own_enforcement_command(entry: &Value) -> bool {
entry
.get("command")
.and_then(Value::as_str)
.is_some_and(|value| {
value.split_whitespace().next() == Some("truth-mirror")
&& value.contains(ENFORCE_COMMAND)
})
}
fn event_array_mut<'a>(root: &'a mut Map<String, Value>, event: &str) -> &'a mut Vec<Value> {
let hooks = root
.entry("hooks")
.or_insert_with(|| Value::Object(Map::new()));
if !hooks.is_object() {
*hooks = Value::Object(Map::new());
}
let hooks = hooks.as_object_mut().expect("hooks is object");
let entries = hooks
.entry(event.to_owned())
.or_insert_with(|| Value::Array(Vec::new()));
if !entries.is_array() {
*entries = Value::Array(Vec::new());
}
entries.as_array_mut().expect("event is array")
}
fn remove_own_enforcement(root: &mut Map<String, Value>, event: &str) {
let Some(hooks) = root.get_mut("hooks").and_then(Value::as_object_mut) else {
return;
};
if let Some(groups) = hooks.get_mut(event).and_then(Value::as_array_mut) {
for group in groups.iter_mut() {
if let Some(inner) = group.get_mut("hooks").and_then(Value::as_array_mut) {
inner.retain(|entry| !is_own_enforcement_command(entry));
}
}
groups.retain(|group| {
group
.get("hooks")
.and_then(Value::as_array)
.is_none_or(|inner| !inner.is_empty())
});
if groups.is_empty() {
hooks.remove(event);
}
}
if hooks.is_empty() {
root.remove("hooks");
}
}
fn is_nested(agent: Agent) -> bool {
matches!(agent, Agent::Claude | Agent::Codex | Agent::Grok)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SurfacePlan {
pub agent: Agent,
pub path: PathBuf,
}
impl SurfacePlan {
pub fn for_agent(repo_root: &Path, agent: Agent) -> Self {
Self {
agent,
path: repo_root.join(surface_relative_path(agent)),
}
}
pub fn install(&self) -> Result<()> {
let mut root = read_object(&self.path)?;
install_command(self.agent, &mut root, &reinject_command(self.agent));
write_object(&self.path, &root)
}
pub fn uninstall(&self) -> Result<()> {
if !self.path.exists() {
return Ok(());
}
let mut root = read_object(&self.path)?;
remove_command(self.agent, &mut root, &reinject_command(self.agent));
if root.is_empty() {
fs::remove_file(&self.path)
.with_context(|| format!("removing empty surface {}", self.path.display()))?;
} else {
write_object(&self.path, &root)?;
}
Ok(())
}
pub fn contains_reinject(&self) -> Result<bool> {
if !self.path.exists() {
return Ok(false);
}
let root = read_object(&self.path)?;
Ok(surface_contains(
self.agent,
&root,
&reinject_command(self.agent),
))
}
}
fn read_object(path: &Path) -> Result<Map<String, Value>> {
match fs::read_to_string(path) {
Ok(contents) if contents.trim().is_empty() => Ok(Map::new()),
Ok(contents) => {
let value: Value = serde_json::from_str(&contents)
.with_context(|| format!("parsing existing surface {}", path.display()))?;
match value {
Value::Object(map) => Ok(map),
_ => anyhow::bail!(
"surface {} is not a JSON object; refusing to clobber",
path.display()
),
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Map::new()),
Err(error) => Err(error).with_context(|| format!("reading surface {}", path.display()))?,
}
}
fn write_object(path: &Path, root: &Map<String, Value>) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("creating surface dir {}", parent.display()))?;
}
let mut serialized = serde_json::to_string_pretty(&Value::Object(root.clone()))?;
serialized.push('\n');
fs::write(path, serialized).with_context(|| format!("writing surface {}", path.display()))?;
Ok(())
}
fn install_command(agent: Agent, root: &mut Map<String, Value>, command: &str) {
let entries = user_prompt_submit_mut(agent, root);
if array_contains_command(agent, entries, command) {
return;
}
entries.push(surface_entry(agent, command));
}
fn remove_command(agent: Agent, root: &mut Map<String, Value>, command: &str) {
if is_nested(agent) {
let Some(hooks) = root.get_mut("hooks").and_then(Value::as_object_mut) else {
return;
};
if let Some(groups) = hooks
.get_mut("UserPromptSubmit")
.and_then(Value::as_array_mut)
{
for group in groups.iter_mut() {
if let Some(inner) = group.get_mut("hooks").and_then(Value::as_array_mut) {
inner.retain(|entry| !entry_matches_command(entry, command));
}
}
groups.retain(|group| {
group
.get("hooks")
.and_then(Value::as_array)
.is_none_or(|inner| !inner.is_empty())
});
if groups.is_empty() {
hooks.remove("UserPromptSubmit");
}
}
if hooks.is_empty() {
root.remove("hooks");
}
} else if let Some(entries) = root
.get_mut("UserPromptSubmit")
.and_then(Value::as_array_mut)
{
entries.retain(|entry| !entry_matches_command(entry, command));
if entries.is_empty() {
root.remove("UserPromptSubmit");
}
}
}
fn user_prompt_submit_mut(agent: Agent, root: &mut Map<String, Value>) -> &mut Vec<Value> {
if is_nested(agent) {
let hooks = root
.entry("hooks")
.or_insert_with(|| Value::Object(Map::new()));
if !hooks.is_object() {
*hooks = Value::Object(Map::new());
}
let hooks = hooks.as_object_mut().expect("hooks is object");
let entries = hooks
.entry("UserPromptSubmit")
.or_insert_with(|| Value::Array(Vec::new()));
if !entries.is_array() {
*entries = Value::Array(Vec::new());
}
entries.as_array_mut().expect("UserPromptSubmit is array")
} else {
let entries = root
.entry("UserPromptSubmit")
.or_insert_with(|| Value::Array(Vec::new()));
if !entries.is_array() {
*entries = Value::Array(Vec::new());
}
entries.as_array_mut().expect("UserPromptSubmit is array")
}
}
fn surface_entry(agent: Agent, command: &str) -> Value {
if is_nested(agent) {
json!({ "hooks": [ { "type": "command", "command": command } ] })
} else {
json!({ "command": command })
}
}
fn array_contains_command(agent: Agent, entries: &[Value], command: &str) -> bool {
if is_nested(agent) {
entries.iter().any(|group| {
group
.get("hooks")
.and_then(Value::as_array)
.is_some_and(|inner| inner.iter().any(|e| entry_matches_command(e, command)))
})
} else {
entries.iter().any(|e| entry_matches_command(e, command))
}
}
fn entry_matches_command(entry: &Value, command: &str) -> bool {
entry
.get("command")
.and_then(Value::as_str)
.is_some_and(|value| value == command)
}
pub fn surface_contains(agent: Agent, root: &Map<String, Value>, command: &str) -> bool {
if is_nested(agent) {
root.get("hooks")
.and_then(Value::as_object)
.and_then(|hooks| hooks.get("UserPromptSubmit"))
.and_then(Value::as_array)
.is_some_and(|entries| array_contains_command(agent, entries, command))
} else {
root.get("UserPromptSubmit")
.and_then(Value::as_array)
.is_some_and(|entries| array_contains_command(agent, entries, command))
}
}
#[cfg(test)]
mod tests {
use super::{
Agent, SurfacePlan, install_command, reinject_command, remove_command, surface_contains,
};
use proptest::prelude::*;
use serde_json::{Map, Value, json};
fn install_into(agent: Agent, mut root: Map<String, Value>) -> Map<String, Value> {
install_command(agent, &mut root, &reinject_command(agent));
root
}
#[test]
fn claude_surface_uses_nested_user_prompt_submit() {
let root = install_into(Agent::Claude, Map::new());
let value = Value::Object(root.clone());
let command = value
.pointer("/hooks/UserPromptSubmit/0/hooks/0/command")
.and_then(Value::as_str)
.unwrap();
assert_eq!(command, "truth-mirror reinject --agent claude");
assert!(surface_contains(
Agent::Claude,
&root,
&reinject_command(Agent::Claude)
));
}
#[test]
fn codex_uses_nested_user_prompt_submit_like_claude() {
let root = install_into(Agent::Codex, Map::new());
let value = Value::Object(root.clone());
let command = value
.pointer("/hooks/UserPromptSubmit/0/hooks/0/command")
.and_then(Value::as_str)
.unwrap();
assert_eq!(command, "truth-mirror reinject --agent codex");
assert!(surface_contains(
Agent::Codex,
&root,
&reinject_command(Agent::Codex)
));
}
#[test]
fn grok_skill_install_round_trips() {
let temp = tempfile::tempdir().unwrap();
super::install_grok_skill(temp.path()).unwrap();
let path = super::grok_skill_path(temp.path());
assert!(path.is_file());
let body = std::fs::read_to_string(&path).unwrap();
assert!(body.contains("truth-mirror reinject --agent grok"));
assert!(body.contains(super::GROK_SKILL_MANAGED_MARKER));
assert!(super::grok_skill_installed(temp.path()));
super::uninstall_grok_skill(temp.path()).unwrap();
assert!(!path.is_file());
assert!(!super::grok_skill_installed(temp.path()));
}
#[test]
fn grok_skill_refuses_foreign_file() {
let temp = tempfile::tempdir().unwrap();
let path = super::grok_skill_path(temp.path());
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "hand-written skill without marker").unwrap();
let err = super::install_grok_skill(temp.path()).unwrap_err();
assert!(err.to_string().contains("foreign"));
assert!(!super::grok_skill_installed(temp.path()));
super::uninstall_grok_skill(temp.path()).unwrap();
assert!(path.is_file(), "foreign skill preserved on uninstall");
}
#[cfg(unix)]
#[test]
fn grok_skill_refuses_symlink_path() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().unwrap();
let outside = temp.path().join("outside.md");
std::fs::write(&outside, super::GROK_SKILL_SOURCE).unwrap();
let path = super::grok_skill_path(temp.path());
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
symlink(&outside, &path).unwrap();
let err = super::install_grok_skill(temp.path()).unwrap_err();
assert!(err.to_string().contains("symlink"));
assert_eq!(
std::fs::read_to_string(&outside).unwrap(),
super::GROK_SKILL_SOURCE,
"outside target must not be rewritten"
);
}
#[cfg(unix)]
#[test]
fn grok_skill_refuses_project_ancestor_symlink() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
symlink(outside.path(), temp.path().join(".grok")).unwrap();
let err = super::install_grok_skill(temp.path()).unwrap_err();
assert!(err.to_string().contains("symlink"));
assert!(
!outside.path().join("skills/truth-mirror/SKILL.md").exists(),
"ancestor symlink target must not receive the managed Grok skill"
);
}
#[test]
fn grok_enforcement_uses_nested_hooks_path() {
assert_eq!(
super::surface_relative_path(Agent::Grok),
".grok/hooks/truth-mirror.json"
);
let temp = tempfile::tempdir().unwrap();
super::install_enforcement(temp.path(), Agent::Grok, "").unwrap();
let content =
std::fs::read_to_string(temp.path().join(".grok/hooks/truth-mirror.json")).unwrap();
assert!(content.contains("PreToolUse"));
assert!(content.contains("truth-mirror gate --pre-tool-use"));
assert!(!content.contains("UserPromptSubmit"));
super::uninstall_enforcement(temp.path(), Agent::Grok).unwrap();
super::cleanup_empty_grok_dirs(temp.path());
assert!(!temp.path().join(".grok/hooks/truth-mirror.json").exists());
}
#[test]
fn install_is_idempotent() {
let mut root = install_into(Agent::Claude, Map::new());
install_command(Agent::Claude, &mut root, &reinject_command(Agent::Claude));
let count = Value::Object(root)
.pointer("/hooks/UserPromptSubmit")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap();
assert_eq!(count, 1);
}
#[test]
fn install_preserves_foreign_config() {
let existing: Map<String, Value> = json!({
"model": "sonnet",
"hooks": { "PreToolUse": [ { "matcher": "Bash" } ] }
})
.as_object()
.cloned()
.unwrap();
let root = install_into(Agent::Claude, existing);
let value = Value::Object(root);
assert_eq!(
value.pointer("/model").and_then(Value::as_str),
Some("sonnet")
);
assert!(value.pointer("/hooks/PreToolUse").is_some());
assert!(value.pointer("/hooks/UserPromptSubmit/0").is_some());
}
#[test]
fn uninstall_removes_only_truth_mirror_entries() {
let existing: Map<String, Value> = json!({
"model": "sonnet",
"hooks": {
"UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "other-tool" } ] } ]
}
})
.as_object()
.cloned()
.unwrap();
let mut root = install_into(Agent::Claude, existing);
remove_command(Agent::Claude, &mut root, &reinject_command(Agent::Claude));
let value = Value::Object(root);
assert_eq!(
value.pointer("/model").and_then(Value::as_str),
Some("sonnet")
);
let commands: Vec<&str> = value
.pointer("/hooks/UserPromptSubmit")
.and_then(Value::as_array)
.unwrap()
.iter()
.filter_map(|group| group.pointer("/hooks/0/command").and_then(Value::as_str))
.collect();
assert_eq!(commands, ["other-tool"]);
}
#[test]
fn enforcement_hook_installs_and_coexists_with_reinject() {
let temp = tempfile::tempdir().unwrap();
let plan = SurfacePlan::for_agent(temp.path(), Agent::Claude);
plan.install().unwrap(); super::install_enforcement(temp.path(), Agent::Claude, "").unwrap();
let content = std::fs::read_to_string(&plan.path).unwrap();
assert!(content.contains("UserPromptSubmit"));
assert!(content.contains("PreToolUse"));
assert!(content.contains("truth-mirror gate --pre-tool-use"));
super::uninstall_enforcement(temp.path(), Agent::Claude).unwrap();
let after = std::fs::read_to_string(&plan.path).unwrap();
assert!(after.contains("UserPromptSubmit"));
assert!(!after.contains("PreToolUse"));
}
#[test]
fn reinstalling_enforcement_updates_preserved_flags() {
let temp = tempfile::tempdir().unwrap();
super::install_enforcement(temp.path(), Agent::Codex, "").unwrap();
super::install_enforcement(temp.path(), Agent::Codex, "--config '/abs/x.toml' ").unwrap();
let content = std::fs::read_to_string(temp.path().join(".codex/hooks.json")).unwrap();
assert_eq!(content.matches("gate --pre-tool-use").count(), 1);
assert!(content.contains("--config '/abs/x.toml'"));
}
#[test]
fn enforcement_leaves_foreign_pretooluse_hooks_intact() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join(".codex/hooks.json");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let foreign = "external-auditor gate --pre-tool-use --keep";
std::fs::write(
&path,
format!(
"{{\"hooks\":{{\"PreToolUse\":[{{\"hooks\":[{{\"type\":\"command\",\"command\":\"{foreign}\"}}]}}]}}}}"
),
)
.unwrap();
super::install_enforcement(temp.path(), Agent::Codex, "").unwrap();
let after_install = std::fs::read_to_string(&path).unwrap();
assert!(
after_install.contains(foreign),
"foreign hook survives install"
);
assert!(after_install.contains("truth-mirror gate --pre-tool-use"));
super::uninstall_enforcement(temp.path(), Agent::Codex).unwrap();
let after_uninstall = std::fs::read_to_string(&path).unwrap();
assert!(
after_uninstall.contains(foreign),
"foreign hook survives uninstall"
);
assert!(!after_uninstall.contains("truth-mirror gate --pre-tool-use"));
}
#[test]
fn enforcement_round_trips_for_codex() {
let temp = tempfile::tempdir().unwrap();
super::install_enforcement(temp.path(), Agent::Codex, "").unwrap();
assert!(
std::fs::read_to_string(temp.path().join(".codex/hooks.json"))
.unwrap()
.contains("truth-mirror gate --pre-tool-use")
);
super::uninstall_enforcement(temp.path(), Agent::Codex).unwrap();
assert!(!temp.path().join(".codex/hooks.json").exists());
}
#[test]
fn install_then_uninstall_on_disk_round_trips() {
let temp = tempfile::tempdir().unwrap();
for agent in super::FILE_SURFACE_AGENTS {
let plan = SurfacePlan::for_agent(temp.path(), agent);
plan.install().unwrap();
assert!(plan.contains_reinject().unwrap());
plan.uninstall().unwrap();
assert!(!plan.contains_reinject().unwrap());
assert!(!plan.path.exists());
}
}
proptest! {
#[test]
fn foreign_keys_survive_install_uninstall(
key in "[a-z]{1,8}",
val in "[a-z0-9]{1,8}",
) {
prop_assume!(key != "hooks" && key != "UserPromptSubmit");
let existing: Map<String, Value> = json!({ key.clone(): val.clone() })
.as_object()
.cloned()
.unwrap();
let mut root = existing.clone();
install_command(Agent::Codex, &mut root, &reinject_command(Agent::Codex));
prop_assert!(surface_contains(Agent::Codex, &root, &reinject_command(Agent::Codex)));
remove_command(Agent::Codex, &mut root, &reinject_command(Agent::Codex));
prop_assert!(!surface_contains(Agent::Codex, &root, &reinject_command(Agent::Codex)));
prop_assert_eq!(root.get(&key).and_then(Value::as_str), Some(val.as_str()));
}
}
}