1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6pub const PROTOCOL_ID: &str = "pi-workflows.client.v1";
7
8const OPERATIONS: &[&str] = &[
9 "run.start",
10 "run.pause",
11 "run.resume",
12 "run.cancel",
13 "run.status",
14 "run.list",
15 "checkpoint.answer",
16 "decision.answer",
17 "interaction.submit",
18 "interaction.update",
19 "notification.claim",
20 "notification.deliver",
21 "turn.claim",
22 "turn.resolve",
23 "controller.list",
24 "controller.get",
25 "controller.apply",
26 "controller.reconcile",
27 "controller.delete",
28 "host.status",
29 "host.stop",
30 "view.runs.watch",
31 "view.runs.page",
32 "view.run.get",
33 "view.run.watch",
34 "view.run.unwatch",
35 "view.page",
36 "view.content",
37 "view.session.watch",
38 "activity.report",
39 "state.status",
40 "state.verify",
41 "state.backup",
42 "state.prune",
43];
44
45const OUTCOMES: &[&str] = &[
46 "accepted",
47 "adopted",
48 "rejected",
49 "conflict",
50 "notFound",
51 "claimLost",
52 "unavailable",
53];
54
55const EVENTS: &[&str] = &[
56 "runs",
57 "run_snapshot",
58 "run_patch",
59 "run_page",
60 "session_snapshot",
61 "unavailable",
62];
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct ClientRequest {
67 pub schema: String,
68 #[serde(rename = "type")]
69 pub message_type: String,
70 #[serde(rename = "requestId")]
71 pub request_id: String,
72 #[serde(rename = "clientId")]
73 pub client_id: String,
74 pub operation: String,
75 #[serde(rename = "idempotencyKey")]
76 pub idempotency_key: String,
77 #[serde(rename = "runId", skip_serializing_if = "Option::is_none")]
78 pub run_id: Option<String>,
79 #[serde(rename = "expectedRevision", skip_serializing_if = "Option::is_none")]
80 pub expected_revision: Option<u64>,
81 pub payload: Value,
82}
83
84#[derive(Debug, Clone, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct ServerHello {
87 pub schema: String,
88 #[serde(rename = "type")]
89 pub message_type: String,
90 #[serde(rename = "connectionId")]
91 pub connection_id: String,
92 #[serde(rename = "packageVersion")]
93 pub package_version: String,
94}
95
96#[derive(Debug, Clone, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct ServerResponse {
99 pub schema: String,
100 #[serde(rename = "type")]
101 pub message_type: String,
102 #[serde(rename = "requestId")]
103 pub request_id: String,
104 pub outcome: String,
105 pub revision: Option<u64>,
106 pub receipt: Option<Value>,
107 pub error: Option<String>,
108}
109
110#[derive(Debug, Clone, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct ServerEvent {
113 pub schema: String,
114 #[serde(rename = "type")]
115 pub message_type: String,
116 #[serde(rename = "subscriptionId")]
117 pub subscription_id: String,
118 pub event: String,
119 pub revision: Option<u64>,
120 #[serde(rename = "runId")]
121 pub run_id: Option<String>,
122 pub payload: Value,
123}
124
125#[derive(Debug, Clone)]
126pub enum ServerMessage {
127 Hello(ServerHello),
128 Response(ServerResponse),
129 Event(ServerEvent),
130}
131
132pub fn parse_client_request(text: &str) -> Result<ClientRequest, String> {
133 let value = parse_canonical_value(text)?;
134 let request: ClientRequest =
135 serde_json::from_value(value).map_err(|error| error.to_string())?;
136 if request.schema != PROTOCOL_ID
137 || request.message_type != "request"
138 || !valid_id(&request.request_id)
139 || !valid_id(&request.client_id)
140 || !valid_id(&request.idempotency_key)
141 || request
142 .run_id
143 .as_deref()
144 .is_some_and(|value| !valid_id(value))
145 || !OPERATIONS.contains(&request.operation.as_str())
146 {
147 return Err("invalid client request".to_string());
148 }
149 Ok(request)
150}
151
152pub fn parse_server_message(text: &str) -> Result<ServerMessage, String> {
153 let value = parse_canonical_value(text)?;
154 let message_type = value
155 .get("type")
156 .and_then(Value::as_str)
157 .ok_or_else(|| "client message has no type".to_string())?;
158 let message = match message_type {
159 "hello" => {
160 ServerMessage::Hello(serde_json::from_value(value).map_err(|error| error.to_string())?)
161 }
162 "response" => ServerMessage::Response(
163 serde_json::from_value(value).map_err(|error| error.to_string())?,
164 ),
165 "event" => {
166 ServerMessage::Event(serde_json::from_value(value).map_err(|error| error.to_string())?)
167 }
168 _ => return Err("invalid client message type".to_string()),
169 };
170 match &message {
171 ServerMessage::Hello(value)
172 if value.schema != PROTOCOL_ID
173 || value.message_type != "hello"
174 || !valid_id(&value.connection_id)
175 || value.package_version.is_empty() =>
176 {
177 Err("invalid client hello".to_string())
178 }
179 ServerMessage::Response(value)
180 if value.schema != PROTOCOL_ID
181 || value.message_type != "response"
182 || !valid_id(&value.request_id)
183 || !OUTCOMES.contains(&value.outcome.as_str())
184 || value.error.as_deref().is_some_and(str::is_empty) =>
185 {
186 Err("invalid client response".to_string())
187 }
188 ServerMessage::Event(value)
189 if value.schema != PROTOCOL_ID
190 || value.message_type != "event"
191 || !valid_id(&value.subscription_id)
192 || value
193 .run_id
194 .as_deref()
195 .is_some_and(|value| !valid_id(value))
196 || !EVENTS.contains(&value.event.as_str()) =>
197 {
198 Err("invalid client event".to_string())
199 }
200 _ => Ok(message),
201 }
202}
203
204fn parse_canonical_value(text: &str) -> Result<Value, String> {
205 if text.len() > 1024 * 1024 {
206 return Err("client message exceeds 1 MiB".to_string());
207 }
208 let value: Value = serde_json::from_str(text).map_err(|error| error.to_string())?;
209 if canonical_json(&value)? != text {
210 return Err("client message is not canonical JSON".to_string());
211 }
212 Ok(value)
213}
214
215fn valid_id(value: &str) -> bool {
216 !value.is_empty() && value.len() <= 256
217}
218
219pub fn encode_request(request: &ClientRequest) -> Result<String, String> {
220 let value = serde_json::to_value(request).map_err(|error| error.to_string())?;
221 canonical_json(&value)
222}
223
224pub fn canonical_json(value: &Value) -> Result<String, String> {
225 let mut output = String::new();
226 write_canonical_json(value, &mut output)?;
227 Ok(output)
228}
229
230fn write_canonical_json(value: &Value, output: &mut String) -> Result<(), String> {
231 match value {
232 Value::Null => output.push_str("null"),
233 Value::Bool(value) => output.push_str(if *value { "true" } else { "false" }),
234 Value::Number(value) => output.push_str(&canonical_number(value)?),
235 Value::String(value) => output.push_str(
236 &serde_json::to_string(value)
237 .map_err(|error| format!("invalid JSON string: {error}"))?,
238 ),
239 Value::Array(values) => {
240 output.push('[');
241 for (index, value) in values.iter().enumerate() {
242 if index > 0 {
243 output.push(',');
244 }
245 write_canonical_json(value, output)?;
246 }
247 output.push(']');
248 }
249 Value::Object(object) => {
250 output.push('{');
251 let mut keys = object.keys().collect::<Vec<_>>();
252 keys.sort_by(|left, right| left.encode_utf16().cmp(right.encode_utf16()));
253 for (index, key) in keys.into_iter().enumerate() {
254 if index > 0 {
255 output.push(',');
256 }
257 output.push_str(
258 &serde_json::to_string(key)
259 .map_err(|error| format!("invalid JSON key: {error}"))?,
260 );
261 output.push(':');
262 write_canonical_json(&object[key], output)?;
263 }
264 output.push('}');
265 }
266 }
267 Ok(())
268}
269
270fn canonical_number(value: &serde_json::Number) -> Result<String, String> {
271 if let Some(value) = value.as_i64() {
272 return Ok(value.to_string());
273 }
274 if let Some(value) = value.as_u64() {
275 return Ok(value.to_string());
276 }
277 let value = value
278 .as_f64()
279 .filter(|value| value.is_finite())
280 .ok_or_else(|| "JSON number is not finite".to_string())?;
281 let mut buffer = ryu_js::Buffer::new();
282 Ok(buffer.format_finite(value).to_string())
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
286#[serde(rename_all = "snake_case")]
287pub enum PageKind {
288 Steps,
289 Trace,
290 TraceAtStep,
291 SessionEntries,
292 SessionEvents,
293 Settings,
294 FollowUps,
295 Updates,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct TargetPatch {
300 #[serde(rename = "targetType")]
301 pub target_type: String,
302 #[serde(rename = "targetKey")]
303 pub target_key: String,
304 pub patch: Vec<PatchOp>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(tag = "op", rename_all = "snake_case")]
310pub enum PatchOp {
311 Add { path: String, value: Value },
312 Replace { path: String, value: Value },
313 Remove { path: String },
314 Append { path: String, value: Vec<Value> },
315}
316
317pub fn apply_patch(target: &mut Value, patch: &[PatchOp]) -> Result<(), String> {
320 for op in patch {
321 match op {
322 PatchOp::Add { path, value } => {
323 set_path(target, path, value.clone(), false)?;
324 }
325 PatchOp::Replace { path, value } => {
326 set_path(target, path, value.clone(), true)?;
327 }
328 PatchOp::Remove { path } => {
329 remove_path(target, path)?;
330 }
331 PatchOp::Append { path, value } => {
332 let array = resolve_path(target, path)?
333 .as_array_mut()
334 .ok_or_else(|| format!("append target {path} is not an array"))?;
335 array.extend(value.iter().cloned());
336 }
337 }
338 }
339 Ok(())
340}
341
342fn unescape_token(token: &str) -> String {
343 token.replace("~1", "/").replace("~0", "~")
344}
345
346fn resolve_path<'a>(target: &'a mut Value, path: &str) -> Result<&'a mut Value, String> {
347 let mut current = target;
348 for token in path.split('/').skip(1) {
349 let token = unescape_token(token);
350 current = match current {
351 Value::Object(object) => object
352 .get_mut(&token)
353 .ok_or_else(|| format!("missing key {token} in {path}"))?,
354 Value::Array(array) => {
355 let index: usize = token
356 .parse()
357 .map_err(|_| format!("bad array index {token} in {path}"))?;
358 array
359 .get_mut(index)
360 .ok_or_else(|| format!("index {index} out of bounds in {path}"))?
361 }
362 _ => return Err(format!("cannot traverse into scalar at {token} in {path}")),
363 };
364 }
365 Ok(current)
366}
367
368fn set_path(target: &mut Value, path: &str, value: Value, replace: bool) -> Result<(), String> {
369 if path.is_empty() {
370 *target = value;
371 return Ok(());
372 }
373 let Some((parent_path, key)) = path.rsplit_once('/') else {
374 return Err(format!("bad path {path}"));
375 };
376 let parent = resolve_path(target, parent_path)?;
377 let key = unescape_token(key);
378 match parent {
379 Value::Object(object) => {
380 if replace && !object.contains_key(&key) {
381 return Err(format!("missing key {key} in {path}"));
382 }
383 object.insert(key, value);
384 Ok(())
385 }
386 Value::Array(array) => {
387 if !replace && key == "-" {
388 array.push(value);
389 return Ok(());
390 }
391 let index: usize = key
392 .parse()
393 .map_err(|_| format!("bad array index {key} in {path}"))?;
394 if replace {
395 let member = array
396 .get_mut(index)
397 .ok_or_else(|| format!("index {index} out of bounds in {path}"))?;
398 *member = value;
399 return Ok(());
400 }
401 if index > array.len() {
402 return Err(format!("index {index} out of bounds in {path}"));
403 }
404 array.insert(index, value);
405 Ok(())
406 }
407 _ => Err(format!("cannot set {key} on scalar in {path}")),
408 }
409}
410
411fn remove_path(target: &mut Value, path: &str) -> Result<(), String> {
412 let Some((parent_path, key)) = path.rsplit_once('/') else {
413 return Err(format!("bad path {path}"));
414 };
415 let parent = resolve_path(target, parent_path)?;
416 let key = unescape_token(key);
417 match parent {
418 Value::Object(object) => {
419 object
420 .remove(&key)
421 .ok_or_else(|| format!("missing key {key} in {path}"))?;
422 Ok(())
423 }
424 Value::Array(array) => {
425 let index: usize = key
426 .parse()
427 .map_err(|_| format!("bad array index {key} in {path}"))?;
428 if index >= array.len() {
429 return Err(format!("index {index} out of bounds in {path}"));
430 }
431 array.remove(index);
432 Ok(())
433 }
434 _ => Err(format!("cannot remove {key} from scalar in {path}")),
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use serde_json::json;
442
443 #[test]
444 fn canonical_json_matches_ecmascript_numbers_and_utf16_key_order() {
445 let numbers: Value =
446 serde_json::from_str("[1e20,1e21,1e-6,1e-7,-0,333333333.33333329]").unwrap();
447 assert_eq!(
448 canonical_json(&numbers).unwrap(),
449 "[100000000000000000000,1e+21,0.000001,1e-7,0,333333333.3333333]"
450 );
451 let keys: Value = serde_json::from_str("{\"\":1,\"𐀀\":2}").unwrap();
452 assert_eq!(canonical_json(&keys).unwrap(), "{\"𐀀\":2,\"\":1}");
453 }
454
455 #[test]
456 fn applies_replace_append_and_remove() {
457 let mut view = json!({ "state": { "status": "running" }, "events": [1] });
458 apply_patch(
459 &mut view,
460 &[
461 PatchOp::Replace {
462 path: "/state/status".into(),
463 value: json!("completed"),
464 },
465 PatchOp::Append {
466 path: "/events".into(),
467 value: vec![json!(2), json!(3)],
468 },
469 PatchOp::Add {
470 path: "/session".into(),
471 value: json!({ "binding": null, "entries": [] }),
472 },
473 ],
474 )
475 .unwrap();
476 assert_eq!(
477 view,
478 json!({
479 "state": { "status": "completed" },
480 "events": [1, 2, 3],
481 "session": { "binding": null, "entries": [] }
482 })
483 );
484 apply_patch(
485 &mut view,
486 &[PatchOp::Remove {
487 path: "/session".into(),
488 }],
489 )
490 .unwrap();
491 assert!(view.get("session").is_none());
492 }
493
494 #[test]
495 fn array_add_inserts_but_replace_overwrites() {
496 let mut view = json!({ "events": [1, 2] });
497 apply_patch(
498 &mut view,
499 &[
500 PatchOp::Add {
501 path: "/events/1".into(),
502 value: json!(3),
503 },
504 PatchOp::Replace {
505 path: "/events/0".into(),
506 value: json!(4),
507 },
508 ],
509 )
510 .unwrap();
511 assert_eq!(view, json!({ "events": [4, 3, 2] }));
512 }
513
514 #[test]
515 fn escapes_json_pointer_tokens() {
516 let mut view = json!({ "a/b": { "c~d": 1 } });
517 apply_patch(
518 &mut view,
519 &[PatchOp::Replace {
520 path: "/a~1b/c~0d".into(),
521 value: json!(2),
522 }],
523 )
524 .unwrap();
525 assert_eq!(view, json!({ "a/b": { "c~d": 2 } }));
526 }
527
528 #[test]
529 fn gap_or_missing_path_errors() {
530 let mut view = json!({ "events": [] });
531 assert!(apply_patch(
532 &mut view,
533 &[PatchOp::Replace {
534 path: "/missing/deep".into(),
535 value: json!(1),
536 }],
537 )
538 .is_err());
539 }
540}