Skip to main content

coding_agent_hooks/agents/
opencode.rs

1//! OpenCode hook protocol implementation.
2//!
3//! OpenCode uses a JS plugin API; the extension wrapper translates to
4//! JSON stdin/stdout calls. Field names differ (tool, args, sessionID, directory).
5
6use anyhow::Result;
7use serde_json::Value;
8
9use super::{AgentKind, resolve_tool_name};
10use crate::input::ToolUseHookInput;
11use crate::protocol::{HookProtocol, json_str_any, json_value_any};
12
13pub struct OpenCodeProtocol;
14
15impl HookProtocol for OpenCodeProtocol {
16    fn agent(&self) -> AgentKind {
17        AgentKind::OpenCode
18    }
19
20    fn parse_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput> {
21        let tool_name = json_str_any(raw, &["tool_name", "tool"]).to_string();
22        let original = tool_name.clone();
23        let resolved = resolve_tool_name(AgentKind::OpenCode, &tool_name).to_string();
24
25        Ok(ToolUseHookInput {
26            session_id: json_str_any(raw, &["session_id", "sessionID"]).to_string(),
27            transcript_path: String::new(),
28            cwd: json_str_any(raw, &["cwd", "directory"]).to_string(),
29            permission_mode: "default".to_string(),
30            hook_event_name: json_str_any(raw, &["hook_event_name", "event"]).to_string(),
31            tool_name: resolved,
32            tool_input: json_value_any(raw, &["tool_input", "args"])
33                .unwrap_or(Value::Object(serde_json::Map::new())),
34            tool_use_id: None,
35            tool_response: raw.get("tool_response").cloned(),
36            agent: Some(AgentKind::OpenCode),
37            original_tool_name: Some(original),
38        })
39    }
40
41    // OpenCode uses "action" field and "ask" for passthrough
42    fn format_allow(
43        &self,
44        _reason: Option<&str>,
45        _context: Option<&str>,
46        updated_input: Option<Value>,
47    ) -> Value {
48        let mut output = serde_json::json!({ "action": "allow" });
49        if let Some(ui) = updated_input {
50            output["args"] = ui;
51        }
52        output
53    }
54
55    fn format_deny(&self, reason: &str, _context: Option<&str>) -> Value {
56        serde_json::json!({ "action": "deny", "reason": reason })
57    }
58
59    fn format_ask(&self, _reason: Option<&str>, _context: Option<&str>) -> Value {
60        serde_json::json!({ "action": "ask" })
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn parse_opencode_bash() {
70        let raw = serde_json::json!({
71            "tool": "bash",
72            "sessionID": "oc-123",
73            "directory": "/home/user",
74            "args": {"command": "ls"}
75        });
76        let input = OpenCodeProtocol.parse_tool_use(&raw).unwrap();
77        assert_eq!(input.tool_name, "Bash");
78        assert_eq!(input.session_id, "oc-123");
79        assert_eq!(input.cwd, "/home/user");
80    }
81
82    #[test]
83    fn format_deny_opencode() {
84        assert_eq!(OpenCodeProtocol.format_deny("no", None)["action"], "deny");
85    }
86}