agentsight_protocol/
lib.rs1use serde::{Deserialize, Serialize};
6
7pub const PROTOCOL_VERSION: u32 = 1;
8pub const PRODUCT: &str = "agentsight";
9const SESSION_PREFIX: &str = "/api/v1/sessions/";
10
11#[derive(Debug, Serialize, Deserialize)]
12pub struct SessionMessageRequest {
13 pub message: String,
14}
15
16impl SessionMessageRequest {
17 pub fn validate(&self) -> Result<&str, &'static str> {
18 let message = self.message.trim();
19 (!message.is_empty() && message.len() <= 65_536)
20 .then_some(message)
21 .ok_or("message must contain 1-65536 bytes")
22 }
23}
24
25#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
26pub fn info_path() -> String {
27 "/api/v1/info".into()
28}
29
30#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
31pub fn snapshot_path(limit: usize) -> String {
32 format!("/api/v1/snapshot?audit_limit={limit}")
33}
34
35#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
36pub fn overview_path() -> String {
37 "/api/v1/overview".into()
38}
39
40#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
41pub fn session_path(id: &str) -> String {
42 format!("{SESSION_PREFIX}{id}")
43}
44
45#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
46pub fn session_messages_path(id: &str) -> String {
47 format!("{SESSION_PREFIX}{id}/messages")
48}
49
50#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
51pub fn session_message_body(message: &str) -> String {
52 serde_json::to_string(&SessionMessageRequest {
53 message: message.into(),
54 })
55 .expect("serializing a string request cannot fail")
56}
57
58pub fn session_detail_id(path: &str) -> Option<&str> {
59 let id = path.strip_prefix(SESSION_PREFIX)?;
60 (!id.is_empty() && !id.contains('/')).then_some(id)
61}
62
63pub fn session_message_id(path: &str) -> Option<&str> {
64 let id = path
65 .strip_prefix(SESSION_PREFIX)?
66 .strip_suffix("/messages")?;
67 (!id.is_empty() && !id.contains('/')).then_some(id)
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn public_paths_preserve_the_node_contract() {
76 assert_eq!(info_path(), "/api/v1/info");
77 assert_eq!(snapshot_path(500), "/api/v1/snapshot?audit_limit=500");
78 assert_eq!(overview_path(), "/api/v1/overview");
79 assert_eq!(session_path("s-1"), "/api/v1/sessions/s-1");
80 assert_eq!(
81 session_messages_path("s-1"),
82 "/api/v1/sessions/s-1/messages"
83 );
84 assert_eq!(session_detail_id("/api/v1/sessions/s-1"), Some("s-1"));
85 assert_eq!(
86 session_message_id("/api/v1/sessions/s-1/messages"),
87 Some("s-1")
88 );
89 assert_eq!(session_detail_id("/api/v1/sessions/s-1/messages"), None);
90 }
91
92 #[test]
93 fn message_validation_preserves_the_published_api() {
94 let request = SessionMessageRequest {
95 message: " hello ".into(),
96 };
97 assert_eq!(request.validate(), Ok("hello"));
98
99 for message in ["".to_string(), " \n\t ".to_string(), "x".repeat(65_537)] {
100 assert_eq!(
101 SessionMessageRequest { message }.validate(),
102 Err("message must contain 1-65536 bytes")
103 );
104 }
105 assert!(
106 SessionMessageRequest {
107 message: "x".repeat(65_536)
108 }
109 .validate()
110 .is_ok()
111 );
112 }
113}