1use std::path::PathBuf;
17
18use super::common;
19use crate::engine::CommandShell;
20use crate::engine::ConfiguredHandler;
21use crate::engine::command_runner::CommandRunResult;
22use crate::engine::dispatcher;
23use crate::engine::output_parser;
24use crate::schema::PermissionRequestCommandInput;
25use crate::schema::SubagentCommandInputFields;
26use codex_protocol::ThreadId;
27use codex_protocol::protocol::HookCompletedEvent;
28use codex_protocol::protocol::HookEventName;
29use codex_protocol::protocol::HookOutputEntry;
30use codex_protocol::protocol::HookOutputEntryKind;
31use codex_protocol::protocol::HookRunStatus;
32use codex_protocol::protocol::HookRunSummary;
33use serde_json::Value;
34
35#[derive(Debug, Clone)]
36pub struct PermissionRequestRequest {
37 pub session_id: ThreadId,
38 pub turn_id: String,
39 pub subagent: Option<common::SubagentHookContext>,
40 pub cwd: PathBuf,
41 pub transcript_path: Option<PathBuf>,
42 pub model: String,
43 pub permission_mode: String,
44 pub tool_name: String,
45 pub matcher_aliases: Vec<String>,
46 pub run_id_suffix: String,
47 pub tool_input: Value,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum PermissionRequestDecision {
52 Allow,
53 Deny { message: String },
54}
55
56#[derive(Debug)]
57pub struct PermissionRequestOutcome {
58 pub hook_events: Vec<HookCompletedEvent>,
59 pub decision: Option<PermissionRequestDecision>,
60}
61
62#[derive(Debug, Default, PartialEq, Eq)]
63struct PermissionRequestHandlerData {
64 decision: Option<PermissionRequestDecision>,
65}
66
67pub(crate) fn preview(
68 handlers: &[ConfiguredHandler],
69 request: &PermissionRequestRequest,
70) -> Vec<HookRunSummary> {
71 let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
72 dispatcher::select_handlers_for_matcher_inputs(
73 handlers,
74 HookEventName::PermissionRequest,
75 &matcher_inputs,
76 )
77 .into_iter()
78 .map(|handler| {
79 common::hook_run_for_tool_use(
80 dispatcher::running_summary(&handler),
81 &request.run_id_suffix,
82 )
83 })
84 .collect()
85}
86
87pub(crate) async fn run(
88 handlers: &[ConfiguredHandler],
89 shell: &CommandShell,
90 request: PermissionRequestRequest,
91) -> PermissionRequestOutcome {
92 let matcher_inputs = common::matcher_inputs(&request.tool_name, &request.matcher_aliases);
93 let matched = dispatcher::select_handlers_for_matcher_inputs(
94 handlers,
95 HookEventName::PermissionRequest,
96 &matcher_inputs,
97 );
98 if matched.is_empty() {
99 return PermissionRequestOutcome {
100 hook_events: Vec::new(),
101 decision: None,
102 };
103 }
104
105 let input_json = match serde_json::to_string(&build_command_input(&request)) {
106 Ok(input_json) => input_json,
107 Err(error) => {
108 let hook_events = common::serialization_failure_hook_events_for_tool_use(
109 matched,
110 Some(request.turn_id.clone()),
111 format!("failed to serialize permission request hook input: {error}"),
112 &request.run_id_suffix,
113 );
114 return PermissionRequestOutcome {
115 hook_events,
116 decision: None,
117 };
118 }
119 };
120
121 let results = dispatcher::execute_handlers(
122 shell,
123 matched,
124 input_json,
125 request.cwd.as_path(),
126 Some(request.turn_id.clone()),
127 parse_completed,
128 )
129 .await;
130
131 let decision = resolve_permission_request_decision(
134 results
135 .iter()
136 .filter_map(|result| result.data.decision.as_ref()),
137 );
138
139 PermissionRequestOutcome {
140 hook_events: results
141 .into_iter()
142 .map(|result| {
143 common::hook_completed_for_tool_use(result.completed, &request.run_id_suffix)
144 })
145 .collect(),
146 decision,
147 }
148}
149
150fn resolve_permission_request_decision<'a>(
154 decisions: impl IntoIterator<Item = &'a PermissionRequestDecision>,
155) -> Option<PermissionRequestDecision> {
156 let mut resolved_allow = None;
157 for decision in decisions {
158 match decision {
159 PermissionRequestDecision::Allow => {
160 resolved_allow = Some(PermissionRequestDecision::Allow);
161 }
162 PermissionRequestDecision::Deny { message } => {
163 return Some(PermissionRequestDecision::Deny {
164 message: message.clone(),
165 });
166 }
167 }
168 }
169 resolved_allow
170}
171
172fn build_command_input(request: &PermissionRequestRequest) -> PermissionRequestCommandInput {
173 let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
174 PermissionRequestCommandInput {
175 session_id: request.session_id.to_string(),
176 turn_id: request.turn_id.clone(),
177 agent_id: subagent.agent_id,
178 agent_type: subagent.agent_type,
179 transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
180 cwd: request.cwd.display().to_string(),
181 hook_event_name: "PermissionRequest".to_string(),
182 model: request.model.clone(),
183 permission_mode: request.permission_mode.clone(),
184 tool_name: request.tool_name.clone(),
185 tool_input: request.tool_input.clone(),
186 }
187}
188
189fn parse_completed(
190 handler: &ConfiguredHandler,
191 run_result: CommandRunResult,
192 turn_id: Option<String>,
193) -> dispatcher::ParsedHandler<PermissionRequestHandlerData> {
194 let mut entries = Vec::new();
195 let mut status = HookRunStatus::Completed;
196 let mut decision = None;
197
198 match run_result.error.as_deref() {
199 Some(error) => {
200 status = HookRunStatus::Failed;
201 entries.push(HookOutputEntry {
202 kind: HookOutputEntryKind::Error,
203 text: error.to_string(),
204 });
205 }
206 None => match run_result.exit_code {
207 Some(0) => {
208 let trimmed_stdout = run_result.stdout.trim();
209 if trimmed_stdout.is_empty() {
210 } else if let Some(parsed) =
211 output_parser::parse_permission_request(&run_result.stdout)
212 {
213 if let Some(system_message) = parsed.universal.system_message {
214 entries.push(HookOutputEntry {
215 kind: HookOutputEntryKind::Warning,
216 text: system_message,
217 });
218 }
219 if let Some(invalid_reason) = parsed.invalid_reason {
220 status = HookRunStatus::Failed;
221 entries.push(HookOutputEntry {
222 kind: HookOutputEntryKind::Error,
223 text: invalid_reason,
224 });
225 } else if let Some(parsed_decision) = parsed.decision {
226 match parsed_decision {
227 output_parser::PermissionRequestDecision::Allow => {
228 decision = Some(PermissionRequestDecision::Allow);
229 }
230 output_parser::PermissionRequestDecision::Deny { message } => {
231 status = HookRunStatus::Blocked;
232 entries.push(HookOutputEntry {
233 kind: HookOutputEntryKind::Feedback,
234 text: message.clone(),
235 });
236 decision = Some(PermissionRequestDecision::Deny { message });
237 }
238 }
239 }
240 } else if output_parser::looks_like_json(&run_result.stdout) {
241 status = HookRunStatus::Failed;
242 entries.push(HookOutputEntry {
243 kind: HookOutputEntryKind::Error,
244 text: "hook returned invalid permission-request JSON output".to_string(),
245 });
246 }
247 }
248 Some(2) => {
249 if let Some(message) = common::trimmed_non_empty(&run_result.stderr) {
250 status = HookRunStatus::Blocked;
251 entries.push(HookOutputEntry {
252 kind: HookOutputEntryKind::Feedback,
253 text: message.clone(),
254 });
255 decision = Some(PermissionRequestDecision::Deny { message });
256 } else {
257 status = HookRunStatus::Failed;
258 entries.push(HookOutputEntry {
259 kind: HookOutputEntryKind::Error,
260 text: "PermissionRequest hook exited with code 2 but did not write a denial reason to stderr".to_string(),
261 });
262 }
263 }
264 Some(exit_code) => {
265 status = HookRunStatus::Failed;
266 entries.push(HookOutputEntry {
267 kind: HookOutputEntryKind::Error,
268 text: format!("hook exited with code {exit_code}"),
269 });
270 }
271 None => {
272 status = HookRunStatus::Failed;
273 entries.push(HookOutputEntry {
274 kind: HookOutputEntryKind::Error,
275 text: "hook exited without a status code".to_string(),
276 });
277 }
278 },
279 }
280
281 let completed = HookCompletedEvent {
282 turn_id,
283 run: dispatcher::completed_summary(handler, &run_result, status, entries),
284 };
285
286 dispatcher::ParsedHandler {
287 completed,
288 data: PermissionRequestHandlerData { decision },
289 completion_order: 0,
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use pretty_assertions::assert_eq;
296
297 use super::PermissionRequestDecision;
298 use super::resolve_permission_request_decision;
299
300 #[test]
301 fn permission_request_deny_overrides_earlier_allow() {
302 let decisions = [
303 PermissionRequestDecision::Allow,
304 PermissionRequestDecision::Deny {
305 message: "repo deny".to_string(),
306 },
307 ];
308
309 assert_eq!(
310 resolve_permission_request_decision(decisions.iter()),
311 Some(PermissionRequestDecision::Deny {
312 message: "repo deny".to_string(),
313 })
314 );
315 }
316
317 #[test]
318 fn permission_request_returns_allow_when_no_handler_denies() {
319 let decisions = [
320 PermissionRequestDecision::Allow,
321 PermissionRequestDecision::Allow,
322 ];
323
324 assert_eq!(
325 resolve_permission_request_decision(decisions.iter()),
326 Some(PermissionRequestDecision::Allow)
327 );
328 }
329
330 #[test]
331 fn permission_request_returns_none_when_no_handler_decides() {
332 let decisions = Vec::<PermissionRequestDecision>::new();
333
334 assert_eq!(resolve_permission_request_decision(decisions.iter()), None);
335 }
336}