opencode_ctl/tools/
permissions.rs1use 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}