1use serde_json::{json, Map, Value};
4
5use crate::clock::Clock;
6use crate::compact::depth_cap;
7use crate::engine::Engine;
8use crate::error::{ErrorCode, ToolError, ToolResult};
9use crate::host::Host;
10use crate::openapi::{
11 gate, validate_params, validate_session_id, GateInput, OperationTable, TableSource,
12 SNAPSHOT_VERSION,
13};
14use crate::transport::{Params, Transport};
15
16use super::{OpencodeApiArgs, OpencodeOperationsArgs};
17
18const DEPTH: usize = 6;
19
20fn source_label(source: TableSource) -> String {
21 match source {
22 TableSource::Live => "live".to_string(),
23 TableSource::Snapshot => format!("snapshot(v{SNAPSHOT_VERSION})"),
24 }
25}
26
27fn params_of(raw: Option<Value>) -> Result<Params, ToolError> {
29 let bad = |m: String| ToolError::new(ErrorCode::InvalidArgument, m);
30 match raw {
31 None | Some(Value::Null) => Ok(Vec::new()),
32 Some(Value::Object(map)) => map
33 .into_iter()
34 .map(|(k, v)| match v {
35 Value::String(s) => Ok((k, s)),
36 _ => Err(bad(format!("param {k} must be a string"))),
37 })
38 .collect(),
39 Some(_) => Err(bad("params must be an object of strings".to_string())),
40 }
41}
42
43impl<T: Transport, C: Clock> Engine<T, C> {
44 async fn hatch_warning(
46 &self,
47 source: TableSource,
48 table_warning: Option<String>,
49 ) -> Option<String> {
50 if source == TableSource::Snapshot {
51 if let Some(v) = self.observed_server_version().await {
52 if v != SNAPSHOT_VERSION {
53 return Some(format!(
54 "snapshot v{SNAPSHOT_VERSION} differs from server {v}"
55 ));
56 }
57 }
58 }
59 table_warning
60 }
61
62 pub async fn opencode_operations(&self, a: OpencodeOperationsArgs) -> ToolResult {
63 let (mut table, mut source, mut warning) = self.operation_table().await;
64 if source == TableSource::Snapshot {
65 if let Ok((info, _, _)) = self.call_json("server.info", Vec::new(), None).await {
67 self.invalidate_if_changed(&info).await;
68 (table, source, warning) = self.operation_table().await;
69 }
70 }
71 let warning = self.hatch_warning(source, warning).await;
72 let needle = a.filter.as_deref().map(str::to_lowercase);
73 let operations: Vec<Value> = table
74 .iter()
75 .filter(|o| match &needle {
76 Some(n) => o.id.to_lowercase().contains(n) || o.summary.to_lowercase().contains(n),
77 None => true,
78 })
79 .map(|o| {
80 json!({
81 "id": o.id,
82 "method": o.method.as_str(),
83 "path": o.path,
84 "summary": o.summary,
85 "mutating": o.mutating(),
86 })
87 })
88 .collect();
89 let mut out = Map::new();
90 out.insert("source".into(), json!(source_label(source)));
91 if let Some(w) = warning {
92 out.insert("warning".into(), json!(w));
93 }
94 out.insert("operations".into(), Value::Array(operations));
95 Ok(Value::Object(out))
96 }
97
98 pub async fn opencode_api(&self, a: OpencodeApiArgs) -> ToolResult {
99 let params = params_of(a.params)?;
100 let id = a.operation.as_str();
101 let snapshot = OperationTable::snapshot();
102
103 let (mut table, mut source, mut warning) = self.operation_table().await;
104 let known_mutating = table
105 .get(id)
106 .or_else(|| snapshot.get(id))
107 .is_some_and(|o| o.mutating());
108 if known_mutating || source == TableSource::Snapshot {
109 match self.call_json("server.info", Vec::new(), None).await {
112 Ok((info, _, _)) => {
113 self.invalidate_if_changed(&info).await;
114 (table, source, warning) = self.operation_table().await;
115 }
116 Err(e) if known_mutating => return Err(e),
117 Err(_) => {}
118 }
119 }
120 let warning = self.hatch_warning(source, warning).await;
121
122 let server_version = self.observed_server_version().await;
123 let live = (source == TableSource::Live).then_some(&table);
124 let (op, source) = gate(GateInput {
125 live,
126 snapshot: &snapshot,
127 id,
128 allow_mutation: a.allow_mutation.unwrap_or(false),
129 locked: self.settings.permissions_locked,
130 server_version: server_version.as_deref(),
131 })?;
132 validate_params(&op, ¶ms)?;
133 for (name, value) in ¶ms {
134 if name == "sessionID" {
135 validate_session_id(value)?;
136 }
137 }
138 if self.host == Host::Opencode && op.mutating() {
139 for (name, value) in ¶ms {
140 if name == "sessionID" {
141 self.guard_created(value)?;
142 }
143 }
144 }
145
146 let (data, cursor, meta) = self.call_json(id, params, a.body).await?;
147 let data = if op.success_status == 204 {
148 json!({ "ok": true })
149 } else {
150 data
151 };
152 let meta = match (meta, cursor) {
153 (Some(Value::Object(mut m)), Some(c)) => {
154 m.insert("cursor".into(), c);
155 Some(Value::Object(m))
156 }
157 (None, Some(c)) => Some(json!({ "cursor": c })),
158 (m, _) => m,
159 };
160
161 let mut out = Map::new();
162 out.insert("source".into(), json!(source_label(source)));
163 if let Some(w) = warning {
164 out.insert("warning".into(), json!(w));
165 }
166 if let Some(m) = meta {
167 out.insert("meta".into(), depth_cap(m, DEPTH));
168 }
169 out.insert("data".into(), depth_cap(data, DEPTH));
170 Ok(Value::Object(out))
171 }
172}