1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use vtcode_exec_events::VersionedThreadEvent;
4
5pub const PROTOCOL_VERSION: &str = "1";
7
8pub(crate) const MAX_REQUEST_ID_BYTES: usize = 256;
9
10pub(crate) fn is_valid_request_id(request_id: &str) -> bool {
11 !request_id.is_empty() && request_id.len() <= MAX_REQUEST_ID_BYTES
12}
13
14pub(crate) fn response_request_id(request_id: &str) -> &str {
15 if is_valid_request_id(request_id) {
16 request_id
17 } else {
18 "unknown"
19 }
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24pub struct FileChange {
25 pub path: String,
27 pub base_digest: String,
29 pub content: String,
31}
32
33#[derive(Debug, Clone, Deserialize)]
35#[serde(tag = "type")]
36pub enum BridgeRequest {
37 #[serde(rename = "pair")]
39 Pair {
40 request_id: String,
42 #[serde(default)]
44 code: String,
45 #[serde(default)]
47 resume_token: Option<String>,
48 #[serde(default)]
50 origin: Option<String>,
51 #[serde(default)]
53 after_sequence: Option<u64>,
54 },
55 #[serde(rename = "status")]
57 Status {
58 request_id: String,
60 token: String,
62 },
63 #[serde(rename = "workspace.list_files", alias = "list_files")]
65 ListFiles {
66 request_id: String,
68 token: String,
70 },
71 #[serde(rename = "workspace.read_file", alias = "read_file")]
73 ReadFile {
74 request_id: String,
76 token: String,
78 path: String,
80 },
81 #[serde(rename = "patch.propose", alias = "propose_changes")]
83 ProposeChanges {
84 request_id: String,
86 token: String,
88 changes: Vec<FileChange>,
90 },
91 #[serde(rename = "patch.apply", alias = "apply_proposal")]
93 ApplyProposal {
94 request_id: String,
96 token: String,
98 proposal_id: String,
100 },
101 #[serde(rename = "checks.run", alias = "run_checks")]
103 RunChecks {
104 request_id: String,
106 token: String,
108 command: String,
110 },
111 #[serde(rename = "patch.revert", alias = "revert_last_change")]
113 RevertLastChange {
114 request_id: String,
116 token: String,
118 change_id: String,
120 },
121 #[serde(rename = "turn.request", alias = "request_turn")]
123 RequestTurn {
124 request_id: String,
126 token: String,
128 #[serde(default)]
130 proposal_id: Option<String>,
131 prompt: String,
133 },
134 #[serde(rename = "cancel")]
136 Cancel {
137 request_id: String,
139 token: String,
141 target_id: String,
143 },
144}
145
146impl BridgeRequest {
147 pub fn request_id(&self) -> &str {
149 match self {
150 Self::Pair { request_id, .. }
151 | Self::Status { request_id, .. }
152 | Self::ListFiles { request_id, .. }
153 | Self::ReadFile { request_id, .. }
154 | Self::ProposeChanges { request_id, .. }
155 | Self::ApplyProposal { request_id, .. }
156 | Self::RunChecks { request_id, .. }
157 | Self::RevertLastChange { request_id, .. }
158 | Self::RequestTurn { request_id, .. }
159 | Self::Cancel { request_id, .. } => request_id,
160 }
161 }
162
163 pub fn token(&self) -> Option<&str> {
165 match self {
166 Self::Pair { .. } => None,
167 Self::Status { token, .. }
168 | Self::ListFiles { token, .. }
169 | Self::ReadFile { token, .. }
170 | Self::ProposeChanges { token, .. }
171 | Self::ApplyProposal { token, .. }
172 | Self::RunChecks { token, .. }
173 | Self::RevertLastChange { token, .. }
174 | Self::RequestTurn { token, .. }
175 | Self::Cancel { token, .. } => Some(token),
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize)]
182pub struct BridgeResponse {
183 #[serde(rename = "type")]
185 pub kind: &'static str,
186 pub request_id: String,
188 pub ok: bool,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub payload: Option<Value>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub error: Option<BridgeErrorPayload>,
196}
197
198#[derive(Debug, Clone, Serialize)]
200pub struct BridgeErrorPayload {
201 pub code: &'static str,
203 pub message: String,
205}
206
207impl BridgeResponse {
208 pub fn success(request_id: impl Into<String>, payload: impl Serialize) -> Self {
210 Self {
211 kind: "response",
212 request_id: request_id.into(),
213 ok: true,
214 payload: serde_json::to_value(payload).ok(),
215 error: None,
216 }
217 }
218
219 pub fn failure(request_id: impl Into<String>, code: &'static str, message: impl Into<String>) -> Self {
221 Self {
222 kind: "response",
223 request_id: request_id.into(),
224 ok: false,
225 payload: None,
226 error: Some(BridgeErrorPayload { code, message: message.into() }),
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn request_ids_are_bounded_and_invalid_ids_are_not_echoed() {
237 assert!(!is_valid_request_id(""));
238 assert!(is_valid_request_id("browser-1"));
239 assert!(is_valid_request_id(&"x".repeat(MAX_REQUEST_ID_BYTES)));
240 assert!(!is_valid_request_id(&"x".repeat(MAX_REQUEST_ID_BYTES + 1)));
241 assert_eq!(response_request_id(""), "unknown");
242 assert_eq!(response_request_id(&"x".repeat(MAX_REQUEST_ID_BYTES + 1)), "unknown");
243 }
244
245 #[test]
246 fn turn_request_can_reference_a_staged_proposal() {
247 let request: BridgeRequest = serde_json::from_value(serde_json::json!({
248 "type": "turn.request",
249 "request_id": "browser-1",
250 "token": "session-token",
251 "proposal_id": "proposal-1",
252 "prompt": "Implement the staged change"
253 }))
254 .expect("turn request should deserialize");
255
256 assert!(matches!(
257 request,
258 BridgeRequest::RequestTurn { proposal_id: Some(proposal_id), prompt, .. }
259 if proposal_id == "proposal-1" && prompt == "Implement the staged change"
260 ));
261 }
262
263 #[test]
264 fn bridge_settings_are_non_secret() {
265 let settings = BridgeSettings {
266 host: "127.0.0.1".to_string(),
267 port: 4321,
268 pairing_ttl_secs: 300,
269 max_frame_bytes: 1_048_576,
270 max_in_flight_requests: 8,
271 remote_enabled: false,
272 };
273 let serialized = serde_json::to_string(&settings).expect("settings should serialize");
274 assert!(serialized.contains("pairing_ttl_secs"));
275 assert!(!serialized.contains("token"));
276 assert!(!serialized.contains("code"));
277 }
278}
279
280#[derive(Debug, Clone, Serialize)]
282pub struct PairPayload {
283 pub token: String,
285 pub protocol_version: &'static str,
287 pub expires_in_secs: u64,
289}
290
291#[derive(Debug, Clone, Serialize)]
298pub struct BridgeSettings {
299 pub host: String,
301 pub port: u16,
303 pub pairing_ttl_secs: u64,
305 pub max_frame_bytes: usize,
307 pub max_in_flight_requests: usize,
309 pub remote_enabled: bool,
311}
312
313#[derive(Debug, Clone, Serialize)]
315pub struct StatusPayload {
316 pub protocol_version: &'static str,
318 pub connected: bool,
320 pub runtime: crate::runtime::RuntimeStatus,
322 pub authenticated_origin: String,
324 pub settings: BridgeSettings,
326 pub latest_sequence: u64,
328}
329
330#[derive(Debug, Clone, Serialize)]
332pub struct BridgeEventMessage {
333 #[serde(rename = "type")]
335 pub kind: &'static str,
336 pub sequence: u64,
338 pub event: VersionedThreadEvent,
340}