1use apm_core::wrapper::path_guard::{PathGuard, canonicalize_lenient};
2use std::io::Read;
3use std::path::{Path, PathBuf};
4
5pub fn run() {
17 let mut stdin_buf = String::new();
18 if std::io::stdin().read_to_string(&mut stdin_buf).is_err() {
19 std::process::exit(0);
20 }
21
22 let payload: serde_json::Value = match serde_json::from_str(&stdin_buf) {
23 Ok(v) => v,
24 Err(_) => {
25 std::process::exit(0);
27 }
28 };
29
30 let tool_name = match payload.get("tool_name").and_then(|v| v.as_str()) {
31 Some(n) => n,
32 None => std::process::exit(0),
33 };
34
35 if !matches!(tool_name, "Edit" | "Write" | "Bash") {
37 std::process::exit(0);
38 }
39
40 let worktree_str = match std::env::var("APM_TICKET_WORKTREE") {
42 Ok(v) if !v.is_empty() => v,
43 _ => std::process::exit(0), };
45 let worktree = Path::new(&worktree_str);
46
47 let apm_bin = std::env::var("APM_BIN").unwrap_or_default();
48 let sys_file = std::env::var("APM_SYSTEM_PROMPT_FILE").unwrap_or_default();
49 let msg_file = std::env::var("APM_USER_MESSAGE_FILE").unwrap_or_default();
50
51 let isolation = load_isolation_config(worktree);
53
54 let mut write_protected: Vec<PathBuf> = Vec::new();
56 if !apm_bin.is_empty() {
57 write_protected.push(canonicalize_lenient(Path::new(&apm_bin)));
58 }
59 if !sys_file.is_empty() {
60 write_protected.push(canonicalize_lenient(Path::new(&sys_file)));
61 }
62 if !msg_file.is_empty() {
63 write_protected.push(canonicalize_lenient(Path::new(&msg_file)));
64 }
65
66 let guard = match PathGuard::new(worktree, &isolation.read_allow, &write_protected) {
67 Ok(g) => g,
68 Err(_) => std::process::exit(0), };
70
71 let tool_input = match payload.get("tool_input") {
72 Some(v) => v,
73 None => std::process::exit(0),
74 };
75
76 let result = match tool_name {
77 "Edit" | "Write" => {
78 let file_path = match tool_input.get("file_path").and_then(|v| v.as_str()) {
79 Some(p) => p,
80 None => std::process::exit(0),
81 };
82 guard.check_write(Path::new(file_path))
83 }
84 "Bash" => {
85 let command = match tool_input.get("command").and_then(|v| v.as_str()) {
86 Some(c) => c,
87 None => std::process::exit(0),
88 };
89 guard.check_bash(command)
90 }
91 _ => std::process::exit(0),
92 };
93
94 match result {
95 Ok(()) => std::process::exit(0),
96 Err(msg) => {
97 #[allow(clippy::print_stdout)]
98 {
99 println!("{}", msg);
100 }
101 std::process::exit(2);
102 }
103 }
104}
105
106fn load_isolation_config(worktree: &Path) -> apm_core::config::IsolationConfig {
109 let mut dir = worktree;
110 loop {
111 let config_path = dir.join(".apm").join("config.toml");
112 if config_path.exists() {
113 if let Ok(config) = apm_core::config::Config::load(dir) {
114 return config.isolation;
115 }
116 }
117 match dir.parent() {
118 Some(parent) => dir = parent,
119 None => break,
120 }
121 }
122 apm_core::config::IsolationConfig::default()
123}