Skip to main content

opencode_sdk/types/
permission.rs

1//! Permission types for opencode_rs.
2//!
3//! Matches TypeScript PermissionNext schema from permission/next.ts.
4
5use serde::{Deserialize, Serialize};
6
7/// Permission action (allow, deny, ask).
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "lowercase")]
10pub enum PermissionAction {
11    Allow,
12    Deny,
13    Ask,
14}
15
16/// A permission rule in a ruleset.
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "camelCase")]
19pub struct PermissionRule {
20    /// Permission type (e.g., "file.read", "bash.execute").
21    pub permission: String,
22    /// Pattern to match (glob or regex).
23    pub pattern: String,
24    /// Action to take when matched.
25    pub action: PermissionAction,
26}
27
28/// A ruleset is a list of permission rules.
29pub type Ruleset = Vec<PermissionRule>;
30
31/// Reference to a tool invocation for permission context.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "camelCase")]
34pub struct PermissionToolRef {
35    /// Message ID containing the tool call.
36    pub message_id: String,
37    /// Tool call ID.
38    pub call_id: String,
39}
40
41/// A permission request from the server.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct PermissionRequest {
45    /// Unique request identifier.
46    pub id: String,
47    /// Session ID.
48    #[serde(default, alias = "sessionID")]
49    pub session_id: Option<String>,
50    /// Permission type being requested.
51    pub permission: String,
52    /// Patterns being requested.
53    pub patterns: Vec<String>,
54    /// Additional metadata.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub metadata: Option<serde_json::Value>,
57    /// Patterns that can be allowed "always".
58    #[serde(default)]
59    pub always: Vec<String>,
60    /// Tool reference if this permission is for a tool invocation.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub tool: Option<PermissionToolRef>,
63}
64
65/// Reply to a permission request.
66#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(rename_all = "lowercase")]
68pub enum PermissionReply {
69    /// Allow once for this request.
70    Once,
71    /// Allow always for matching patterns.
72    Always,
73    /// Reject the permission request.
74    Reject,
75}
76
77/// Request body for replying to a permission.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct PermissionReplyRequest {
81    /// The reply to send.
82    pub reply: PermissionReply,
83    /// Optional message to include with the reply.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub message: Option<String>,
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_permission_action_serialize() {
94        assert_eq!(
95            serde_json::to_string(&PermissionAction::Allow).unwrap(),
96            r#""allow""#
97        );
98        assert_eq!(
99            serde_json::to_string(&PermissionAction::Deny).unwrap(),
100            r#""deny""#
101        );
102        assert_eq!(
103            serde_json::to_string(&PermissionAction::Ask).unwrap(),
104            r#""ask""#
105        );
106    }
107
108    #[test]
109    fn test_permission_rule_deserialize() {
110        let json = r#"{"permission":"file.read","pattern":"**/*.rs","action":"allow"}"#;
111        let rule: PermissionRule = serde_json::from_str(json).unwrap();
112        assert_eq!(rule.permission, "file.read");
113        assert_eq!(rule.pattern, "**/*.rs");
114        assert_eq!(rule.action, PermissionAction::Allow);
115    }
116
117    #[test]
118    fn test_ruleset_deserialize() {
119        let json = r#"[
120            {"permission":"file.read","pattern":"**/*.rs","action":"allow"},
121            {"permission":"bash.execute","pattern":"*","action":"ask"}
122        ]"#;
123        let ruleset: Ruleset = serde_json::from_str(json).unwrap();
124        assert_eq!(ruleset.len(), 2);
125        assert_eq!(ruleset[0].action, PermissionAction::Allow);
126        assert_eq!(ruleset[1].action, PermissionAction::Ask);
127    }
128
129    #[test]
130    fn test_permission_request_deserialize() {
131        let json = r#"{
132            "id": "req-123",
133            "sessionId": "sess-456",
134            "permission": "file.write",
135            "patterns": ["src/*.rs", "lib/*.rs"],
136            "always": ["src/*.rs"],
137            "tool": {"messageId": "msg-1", "callId": "call-1"}
138        }"#;
139        let req: PermissionRequest = serde_json::from_str(json).unwrap();
140        assert_eq!(req.id, "req-123");
141        assert_eq!(req.session_id.as_deref(), Some("sess-456"));
142        assert_eq!(req.permission, "file.write");
143        assert_eq!(req.patterns.len(), 2);
144        assert_eq!(req.always.len(), 1);
145        assert!(req.tool.is_some());
146        let tool = req.tool.unwrap();
147        assert_eq!(tool.message_id, "msg-1");
148        assert_eq!(tool.call_id, "call-1");
149    }
150
151    #[test]
152    fn test_permission_request_minimal() {
153        let json = r#"{
154            "id": "req-123",
155            "sessionId": "sess-456",
156            "permission": "file.read",
157            "patterns": ["**/*"]
158        }"#;
159        let req: PermissionRequest = serde_json::from_str(json).unwrap();
160        assert_eq!(req.id, "req-123");
161        assert!(req.always.is_empty());
162        assert!(req.tool.is_none());
163        assert!(req.metadata.is_none());
164    }
165
166    #[test]
167    fn test_permission_request_session_id_alias() {
168        let json = r#"{
169            "id": "req-123",
170            "sessionID": "sess-456",
171            "permission": "file.read",
172            "patterns": ["**/*"]
173        }"#;
174        let req: PermissionRequest = serde_json::from_str(json).unwrap();
175        assert_eq!(req.session_id.as_deref(), Some("sess-456"));
176    }
177
178    #[test]
179    fn test_permission_reply_serialize() {
180        assert_eq!(
181            serde_json::to_string(&PermissionReply::Once).unwrap(),
182            r#""once""#
183        );
184        assert_eq!(
185            serde_json::to_string(&PermissionReply::Always).unwrap(),
186            r#""always""#
187        );
188        assert_eq!(
189            serde_json::to_string(&PermissionReply::Reject).unwrap(),
190            r#""reject""#
191        );
192    }
193
194    #[test]
195    fn test_permission_reply_request_serialize() {
196        let req = PermissionReplyRequest {
197            reply: PermissionReply::Always,
198            message: Some("User approved".to_string()),
199        };
200        let json = serde_json::to_string(&req).unwrap();
201        assert!(json.contains(r#""reply":"always""#));
202        assert!(json.contains(r#""message":"User approved""#));
203    }
204
205    #[test]
206    fn test_permission_reply_request_minimal() {
207        let json = r#"{"reply":"once"}"#;
208        let req: PermissionReplyRequest = serde_json::from_str(json).unwrap();
209        assert_eq!(req.reply, PermissionReply::Once);
210        assert!(req.message.is_none());
211    }
212}