Skip to main content

opencode_ctl/tools/
permissions.rs

1//! `permission_reply`, `form_reply` and `saved_permissions`.
2
3use serde_json::{json, Map, Value};
4
5use crate::clock::Clock;
6use crate::engine::Engine;
7use crate::error::{ErrorCode, ToolError, ToolResult};
8use crate::transport::Transport;
9
10use super::{FormReplyArgs, PermissionReplyArgs, SavedPermissionsArgs};
11
12fn non_empty(v: Option<&Value>) -> bool {
13    match v {
14        Some(Value::Array(a)) => !a.is_empty(),
15        Some(Value::Object(o)) => !o.is_empty(),
16        _ => false,
17    }
18}
19
20impl<T: Transport, C: Clock> Engine<T, C> {
21    pub async fn permission_reply(&self, a: PermissionReplyArgs) -> ToolResult {
22        let id = self.resolve_session(a.session_id.as_deref())?;
23        self.guard_created(&id)?;
24        let decision = a.decision.as_str();
25        if !matches!(decision, "once" | "always" | "reject") {
26            return Err(ToolError::new(
27                ErrorCode::InvalidArgument,
28                format!("decision '{decision}' is not once, always or reject"),
29            ));
30        }
31        if decision == "always" {
32            if self.settings.permissions_locked {
33                return Err(ToolError::new(
34                    ErrorCode::PermissionsLocked,
35                    "permissions are locked: a saved (always) rule is refused",
36                )
37                .hint("reply once, or ask the owner to unlock permissions"));
38            }
39            if a.confirm != Some(true) {
40                return Err(ToolError::new(
41                    ErrorCode::ConfirmRequired,
42                    "decision always saves a standing rule and needs confirm: true",
43                ));
44            }
45        }
46        let (list, _, _) = self
47            .call_json(
48                "session.permission.list",
49                vec![("sessionID".to_string(), id.clone())],
50                None,
51            )
52            .await?;
53        let request = list
54            .as_array()
55            .and_then(|l| {
56                l.iter()
57                    .find(|p| p.get("id").and_then(Value::as_str) == Some(a.request_id.as_str()))
58            })
59            .ok_or_else(|| {
60                ToolError::new(
61                    ErrorCode::Conflict,
62                    format!("permission request {} is no longer pending", a.request_id),
63                )
64            })?;
65        let saved = decision == "always" && non_empty(request.get("save"));
66        let mut body = Map::new();
67        body.insert("decision".into(), json!(decision));
68        if let Some(m) = a.message {
69            body.insert("message".into(), json!(m));
70        }
71        self.call_json(
72            "session.permission.reply",
73            vec![
74                ("sessionID".to_string(), id),
75                ("requestID".to_string(), a.request_id),
76            ],
77            Some(Value::Object(body)),
78        )
79        .await?;
80        Ok(json!({"replied": true, "saved": saved}))
81    }
82
83    pub async fn form_reply(&self, a: FormReplyArgs) -> ToolResult {
84        let id = self.resolve_session(a.session_id.as_deref())?;
85        self.guard_created(&id)?;
86        self.call_json(
87            "session.form.reply",
88            vec![
89                ("sessionID".to_string(), id),
90                ("formID".to_string(), a.form_id),
91            ],
92            Some(json!({"answer": a.answer})),
93        )
94        .await?;
95        Ok(json!({"replied": true}))
96    }
97
98    pub async fn saved_permissions(&self, a: SavedPermissionsArgs) -> ToolResult {
99        if a.remove_id.is_some() && a.confirm != Some(true) {
100            return Err(ToolError::new(
101                ErrorCode::ConfirmRequired,
102                "removing a saved permission rule needs confirm: true",
103            ));
104        }
105        let mut params = Vec::new();
106        if let Some(p) = a.project_id {
107            params.push(("projectID".to_string(), p));
108        }
109        let (data, _, _) = self
110            .call_json("permission.saved.list", params, None)
111            .await?;
112        let mut rules = data.as_array().cloned().unwrap_or_default();
113        if let Some(rid) = a.remove_id {
114            self.call_json(
115                "permission.saved.remove",
116                vec![("id".to_string(), rid.clone())],
117                None,
118            )
119            .await?;
120            rules.retain(|r| r.get("id").and_then(Value::as_str) != Some(rid.as_str()));
121        }
122        Ok(json!({"rules": rules}))
123    }
124}