1use serde::{Deserialize, Serialize};
2
3pub const ERR_CAPABILITY_DENIED: u16 = 1001;
7
8pub const ERR_RATE_LIMITED: u16 = 1002;
12
13pub const ERR_BROKER_FAILED: u16 = 1003;
20
21pub const ERR_UCAN_INVALID: u16 = 1010;
28
29pub const ERR_UCAN_EXPIRED: u16 = 1011;
33
34pub const ERR_DELEGATION_TOO_DEEP: u16 = 1012;
38
39pub const ERR_AUDIENCE_MISMATCH: u16 = 1013;
44
45pub const ERR_CURSOR_EXPIRED: u16 = 1020;
52
53pub const ERR_CURSOR_INVALID: u16 = 1021;
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
65#[serde(tag = "type")]
66pub enum Request {
67 #[serde(rename = "ping")]
68 Ping,
69
70 #[serde(rename = "hello")]
82 Hello {
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 client_id: Option<String>,
85 #[serde(default)]
86 requested_capabilities: Vec<String>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 ucan_tokens: Vec<String>,
89 },
90
91 #[serde(rename = "tool_list")]
92 ToolList,
93
94 #[serde(rename = "tool_schema")]
95 ToolSchema { tool_id: String },
96
97 #[serde(rename = "run_tool")]
98 RunTool {
99 tool_id: String,
100 args: serde_json::Value,
101 dry_run: bool,
102 },
103
104 #[serde(rename = "run_tool_continue")]
109 RunToolContinue { tool_id: String, cursor: String },
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
115#[serde(tag = "type")]
116pub enum Response {
117 #[serde(rename = "pong")]
118 Pong,
119
120 #[serde(rename = "hello_ack")]
121 HelloAck {
122 #[serde(default)]
123 granted_capabilities: Vec<String>,
124 #[serde(default)]
125 server_version: String,
126 #[serde(default)]
127 supported_tiers: Vec<String>,
128 },
129
130 #[serde(rename = "tool_list")]
131 ToolListResponse { tools: serde_json::Value },
132
133 #[serde(rename = "tool_schema")]
134 ToolSchemaResponse { schema: serde_json::Value },
135
136 #[serde(rename = "tool_result")]
137 ToolResultResponse {
138 tool_id: String,
139 result: serde_json::Value,
140 success: bool,
141 dry_run: bool,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
148 next_cursor: Option<String>,
149 },
150
151 #[serde(rename = "error")]
152 Error {
153 message: String,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 code: Option<u16>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 retryable: Option<bool>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 details: Option<serde_json::Value>,
160 },
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn ping_serializes_with_type_tag() {
169 let j = serde_json::to_string(&Request::Ping).unwrap();
170 assert_eq!(j, r#"{"type":"ping"}"#);
171 }
172
173 #[test]
174 fn run_tool_roundtrip() {
175 let r = Request::RunTool {
176 tool_id: "anos:fs.read".into(),
177 args: serde_json::json!({"path": "/tmp/x"}),
178 dry_run: false,
179 };
180 let j = serde_json::to_string(&r).unwrap();
181 let back: Request = serde_json::from_str(&j).unwrap();
182 match back {
183 Request::RunTool {
184 tool_id, dry_run, ..
185 } => {
186 assert_eq!(tool_id, "anos:fs.read");
187 assert!(!dry_run);
188 }
189 _ => panic!("wrong variant"),
190 }
191 }
192
193 #[test]
194 fn tool_list_response_carries_array() {
195 let r = Response::ToolListResponse {
196 tools: serde_json::json!([{"id": "a"}, {"id": "b"}]),
197 };
198 let j = serde_json::to_string(&r).unwrap();
199 assert!(j.contains("\"type\":\"tool_list\""));
200 let back: Response = serde_json::from_str(&j).unwrap();
201 match back {
202 Response::ToolListResponse { tools } => {
203 assert_eq!(tools.as_array().unwrap().len(), 2);
204 }
205 _ => panic!("wrong variant"),
206 }
207 }
208
209 #[test]
210 fn error_deserializes_with_optional_fields_missing() {
211 let j = r#"{"type":"error","message":"boom"}"#;
212 let back: Response = serde_json::from_str(j).unwrap();
213 match back {
214 Response::Error {
215 message,
216 code,
217 retryable,
218 details,
219 } => {
220 assert_eq!(message, "boom");
221 assert!(code.is_none());
222 assert!(retryable.is_none());
223 assert!(details.is_none());
224 }
225 _ => panic!("wrong variant"),
226 }
227 }
228
229 #[test]
232 fn run_tool_continue_round_trips() {
233 let r = Request::RunToolContinue {
234 tool_id: "celia:fhir.list_observations".into(),
235 cursor: "abc123".into(),
236 };
237 let j = serde_json::to_string(&r).unwrap();
238 assert!(j.contains(r#""type":"run_tool_continue""#));
239 let back: Request = serde_json::from_str(&j).unwrap();
240 match back {
241 Request::RunToolContinue { tool_id, cursor } => {
242 assert_eq!(tool_id, "celia:fhir.list_observations");
243 assert_eq!(cursor, "abc123");
244 }
245 _ => panic!("wrong variant: {j}"),
246 }
247 }
248
249 #[test]
250 fn tool_result_response_without_next_cursor_omits_field_on_wire() {
251 let r = Response::ToolResultResponse {
252 tool_id: "x".into(),
253 result: serde_json::json!({}),
254 success: true,
255 dry_run: false,
256 next_cursor: None,
257 };
258 let j = serde_json::to_string(&r).unwrap();
259 assert!(
260 !j.contains("next_cursor"),
261 "next_cursor: None must be omitted on the wire (back-compat), got: {j}"
262 );
263 }
264
265 #[test]
266 fn tool_result_response_with_next_cursor_includes_field_on_wire() {
267 let r = Response::ToolResultResponse {
268 tool_id: "x".into(),
269 result: serde_json::json!({}),
270 success: true,
271 dry_run: false,
272 next_cursor: Some("abc".into()),
273 };
274 let j = serde_json::to_string(&r).unwrap();
275 assert!(
276 j.contains(r#""next_cursor":"abc""#),
277 "next_cursor: Some(_) must serialize, got: {j}"
278 );
279 }
280
281 #[test]
282 fn tool_result_response_back_compat_default_when_field_missing() {
283 let j =
286 r#"{"type":"tool_result","tool_id":"x","result":{},"success":true,"dry_run":false}"#;
287 let back: Response = serde_json::from_str(j).unwrap();
288 match back {
289 Response::ToolResultResponse { next_cursor, .. } => {
290 assert!(next_cursor.is_none(), "missing field must default to None");
291 }
292 _ => panic!("wrong variant"),
293 }
294 }
295
296 #[test]
297 fn err_cursor_codes_distinct_from_existing_families() {
298 assert_eq!(ERR_CURSOR_EXPIRED, 1020);
300 assert_eq!(ERR_CURSOR_INVALID, 1021);
301 assert_ne!(ERR_CURSOR_EXPIRED, ERR_CURSOR_INVALID);
302 assert_ne!(ERR_CURSOR_EXPIRED, ERR_AUDIENCE_MISMATCH);
303 assert_ne!(ERR_CURSOR_INVALID, ERR_RATE_LIMITED);
304 }
305}