coding_agent_hooks/agents/
gemini.rs1use anyhow::Result;
8use serde_json::Value;
9
10use super::{AgentKind, resolve_tool_name};
11use crate::input::ToolUseHookInput;
12use crate::protocol::{HookProtocol, json_str};
13
14pub struct GeminiProtocol;
15
16impl HookProtocol for GeminiProtocol {
17 fn agent(&self) -> AgentKind {
18 AgentKind::Gemini
19 }
20
21 fn parse_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput> {
22 let tool_name = json_str(raw, "tool_name").to_string();
23 let original = tool_name.clone();
24 let resolved = resolve_tool_name(AgentKind::Gemini, &tool_name).to_string();
25
26 Ok(ToolUseHookInput {
27 session_id: json_str(raw, "session_id").to_string(),
28 transcript_path: json_str(raw, "transcript_path").to_string(),
29 cwd: json_str(raw, "cwd").to_string(),
30 permission_mode: "default".to_string(),
31 hook_event_name: json_str(raw, "hook_event_name").to_string(),
32 tool_name: resolved,
33 tool_input: raw
34 .get("tool_input")
35 .cloned()
36 .unwrap_or(Value::Object(serde_json::Map::new())),
37 tool_use_id: None,
38 tool_response: raw.get("tool_response").cloned(),
39 agent: Some(AgentKind::Gemini),
40 original_tool_name: Some(original),
41 })
42 }
43
44 fn format_allow(
46 &self,
47 reason: Option<&str>,
48 _context: Option<&str>,
49 updated_input: Option<Value>,
50 ) -> Value {
51 let mut output = serde_json::json!({ "decision": "allow" });
52 if let Some(r) = reason {
53 output["reason"] = Value::String(r.to_string());
54 }
55 if let Some(ui) = updated_input {
56 output["hookSpecificOutput"] = serde_json::json!({ "tool_input": ui });
57 }
58 output
59 }
60
61 fn format_session_start(&self, context: Option<&str>) -> Value {
62 let mut output = serde_json::json!({ "decision": "allow" });
63 if let Some(ctx) = context {
64 output["hookSpecificOutput"] = serde_json::json!({
65 "hookEventName": "SessionStart",
66 "additionalContext": ctx
67 });
68 }
69 output
70 }
71
72 }
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn parse_gemini_tool_use() {
81 let raw = serde_json::json!({
82 "session_id": "gem-123",
83 "transcript_path": "/tmp/gemini.jsonl",
84 "cwd": "/home/user",
85 "hook_event_name": "BeforeTool",
86 "tool_name": "run_shell_command",
87 "tool_input": {"command": "git status"},
88 "timestamp": "2026-03-26T00:00:00Z"
89 });
90 let input = GeminiProtocol.parse_tool_use(&raw).unwrap();
91 assert_eq!(input.tool_name, "Bash");
92 assert_eq!(
93 input.original_tool_name.as_deref(),
94 Some("run_shell_command")
95 );
96 assert_eq!(input.agent, Some(AgentKind::Gemini));
97 }
98
99 #[test]
100 fn parse_gemini_read_file() {
101 let raw = serde_json::json!({
102 "session_id": "gem-123",
103 "cwd": "/home/user",
104 "hook_event_name": "BeforeTool",
105 "tool_name": "read_file",
106 "tool_input": {"file_path": "/tmp/foo.txt"}
107 });
108 let input = GeminiProtocol.parse_tool_use(&raw).unwrap();
109 assert_eq!(input.tool_name, "Read");
110 }
111
112 #[test]
113 fn format_allow_with_rewrite() {
114 let updated = serde_json::json!({"command": "sandboxed"});
115 let out = GeminiProtocol.format_allow(Some("ok"), None, Some(updated.clone()));
116 assert_eq!(out["hookSpecificOutput"]["tool_input"], updated);
117 }
118
119 #[test]
120 fn format_deny_uses_default() {
121 let out = GeminiProtocol.format_deny("blocked", None);
122 assert_eq!(out["decision"], "deny");
123 assert_eq!(out["reason"], "blocked");
124 }
125
126 #[test]
127 fn format_ask_uses_default() {
128 let out = GeminiProtocol.format_ask(None, None);
129 assert_eq!(out["continue"], true);
130 }
131}