Skip to main content

coding_agent_hooks/agents/
copilot.rs

1//! GitHub Copilot CLI hook protocol implementation.
2//!
3//! Copilot uses "approve"/"deny" decisions.
4
5use anyhow::Result;
6use serde_json::Value;
7
8use super::{AgentKind, resolve_tool_name};
9use crate::input::ToolUseHookInput;
10use crate::protocol::{HookProtocol, json_str};
11
12pub struct CopilotProtocol;
13
14impl HookProtocol for CopilotProtocol {
15    fn agent(&self) -> AgentKind {
16        AgentKind::Copilot
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::Copilot, &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(raw, "cwd").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: raw
32                .get("tool_input")
33                .cloned()
34                .unwrap_or(Value::Object(serde_json::Map::new())),
35            tool_use_id: None,
36            tool_response: raw.get("tool_response").cloned(),
37            agent: Some(AgentKind::Copilot),
38            original_tool_name: Some(original),
39        })
40    }
41
42    // Copilot uses "approve"/"deny"
43    fn format_allow(
44        &self,
45        reason: Option<&str>,
46        _context: Option<&str>,
47        _updated_input: Option<Value>,
48    ) -> Value {
49        let mut output = serde_json::json!({ "decision": "approve" });
50        if let Some(r) = reason {
51            output["reason"] = Value::String(r.to_string());
52        }
53        output
54    }
55
56    fn format_deny(&self, reason: &str, _context: Option<&str>) -> Value {
57        serde_json::json!({ "decision": "deny", "reason": reason })
58    }
59
60    fn format_ask(&self, _reason: Option<&str>, _context: Option<&str>) -> Value {
61        serde_json::json!({ "decision": "approve" })
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn parse_copilot_bash() {
71        let raw = serde_json::json!({
72            "session_id": "cp-123",
73            "cwd": "/home/user",
74            "hook_event_name": "preToolUse",
75            "tool_name": "bash",
76            "tool_input": {"command": "git status"}
77        });
78        let input = CopilotProtocol.parse_tool_use(&raw).unwrap();
79        assert_eq!(input.tool_name, "Bash");
80    }
81
82    #[test]
83    fn format_allow_copilot() {
84        assert_eq!(
85            CopilotProtocol.format_allow(None, None, None)["decision"],
86            "approve"
87        );
88    }
89
90    #[test]
91    fn format_deny_copilot() {
92        assert_eq!(CopilotProtocol.format_deny("no", None)["decision"], "deny");
93    }
94}