coding_agent_hooks/agents/
codex.rs1use anyhow::Result;
6use serde_json::Value;
7
8use super::{AgentKind, resolve_tool_name};
9use crate::input::ToolUseHookInput;
10use crate::protocol::{HookProtocol, json_str, json_str_any, json_value_any};
11
12pub struct CodexProtocol;
13
14impl HookProtocol for CodexProtocol {
15 fn agent(&self) -> AgentKind {
16 AgentKind::Codex
17 }
18
19 fn parse_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput> {
20 let tool_name = json_str(raw, "tool_name").to_string();
21 let original = tool_name.clone();
22 let resolved = resolve_tool_name(AgentKind::Codex, &tool_name).to_string();
23
24 Ok(ToolUseHookInput {
25 session_id: json_str(raw, "session_id").to_string(),
26 transcript_path: json_str(raw, "transcript_path").to_string(),
27 cwd: json_str_any(raw, &["cwd", "working_directory"]).to_string(),
28 permission_mode: "default".to_string(),
29 hook_event_name: json_str(raw, "hook_event_name").to_string(),
30 tool_name: resolved,
31 tool_input: json_value_any(raw, &["tool_input", "input"])
32 .unwrap_or(Value::Object(serde_json::Map::new())),
33 tool_use_id: None,
34 tool_response: raw.get("tool_response").cloned(),
35 agent: Some(AgentKind::Codex),
36 original_tool_name: Some(original),
37 })
38 }
39
40 fn format_allow(
42 &self,
43 reason: Option<&str>,
44 _context: Option<&str>,
45 updated_input: Option<Value>,
46 ) -> Value {
47 let mut output = serde_json::json!({ "decision": "proceed" });
48 if let Some(r) = reason {
49 output["reason"] = Value::String(r.to_string());
50 }
51 if let Some(ui) = updated_input {
52 output["decision"] = Value::String("modify".to_string());
53 output["tool_input"] = ui;
54 }
55 output
56 }
57
58 fn format_deny(&self, reason: &str, _context: Option<&str>) -> Value {
59 serde_json::json!({
60 "decision": "block",
61 "message": reason
62 })
63 }
64
65 fn format_ask(&self, _reason: Option<&str>, _context: Option<&str>) -> Value {
66 serde_json::json!({ "decision": "proceed" })
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn parse_codex_shell() {
76 let raw = serde_json::json!({
77 "session_id": "codex-123",
78 "cwd": "/home/user",
79 "hook_event_name": "PreToolUse",
80 "tool_name": "shell",
81 "tool_input": {"command": "git status"}
82 });
83 let input = CodexProtocol.parse_tool_use(&raw).unwrap();
84 assert_eq!(input.tool_name, "Bash");
85 assert_eq!(input.original_tool_name.as_deref(), Some("shell"));
86 }
87
88 #[test]
89 fn format_allow_codex() {
90 assert_eq!(
91 CodexProtocol.format_allow(None, None, None)["decision"],
92 "proceed"
93 );
94 }
95
96 #[test]
97 fn format_deny_codex() {
98 assert_eq!(CodexProtocol.format_deny("no", None)["decision"], "block");
99 }
100
101 #[test]
102 fn format_modify_codex() {
103 let ui = serde_json::json!({"command": "sandboxed"});
104 let out = CodexProtocol.format_allow(None, None, Some(ui));
105 assert_eq!(out["decision"], "modify");
106 }
107}