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 = "explanation.get")]
39 ExplanationGet {
40 request_id: String,
42 token: String,
44 #[serde(default)]
46 scope: vtcode_memory::explanation::ExplanationScope,
47 #[serde(default)]
49 offset: usize,
50 },
51 #[serde(rename = "explanation.evidence")]
53 ExplanationEvidence {
54 request_id: String,
56 token: String,
58 reference: vtcode_memory::explanation::EvidenceRef,
60 #[serde(default)]
62 offset: usize,
63 },
64 #[serde(rename = "explanation.navigate")]
66 ExplanationNavigate {
67 request_id: String,
69 token: String,
71 reference: vtcode_memory::explanation::EvidenceRef,
73 },
74 #[serde(rename = "pair")]
76 Pair {
77 request_id: String,
79 #[serde(default)]
81 code: String,
82 #[serde(default)]
84 resume_token: Option<String>,
85 #[serde(default)]
87 origin: Option<String>,
88 #[serde(default)]
90 after_sequence: Option<u64>,
91 },
92 #[serde(rename = "status")]
94 Status {
95 request_id: String,
97 token: String,
99 },
100 #[serde(rename = "workspace.list_files", alias = "list_files")]
102 ListFiles {
103 request_id: String,
105 token: String,
107 },
108 #[serde(rename = "workspace.read_file", alias = "read_file")]
110 ReadFile {
111 request_id: String,
113 token: String,
115 path: String,
117 },
118 #[serde(rename = "patch.propose", alias = "propose_changes")]
120 ProposeChanges {
121 request_id: String,
123 token: String,
125 changes: Vec<FileChange>,
127 },
128 #[serde(rename = "patch.apply", alias = "apply_proposal")]
130 ApplyProposal {
131 request_id: String,
133 token: String,
135 proposal_id: String,
137 },
138 #[serde(rename = "checks.run", alias = "run_checks")]
140 RunChecks {
141 request_id: String,
143 token: String,
145 command: String,
147 },
148 #[serde(rename = "patch.revert", alias = "revert_last_change")]
150 RevertLastChange {
151 request_id: String,
153 token: String,
155 change_id: String,
157 },
158 #[serde(rename = "turn.request", alias = "request_turn")]
160 RequestTurn {
161 request_id: String,
163 token: String,
165 #[serde(default)]
167 proposal_id: Option<String>,
168 prompt: String,
170 },
171 #[serde(rename = "cancel")]
173 Cancel {
174 request_id: String,
176 token: String,
178 target_id: String,
180 },
181}
182
183impl BridgeRequest {
184 pub fn request_id(&self) -> &str {
186 match self {
187 Self::ExplanationGet { request_id, .. }
188 | Self::ExplanationEvidence { request_id, .. }
189 | Self::ExplanationNavigate { request_id, .. } => request_id,
190 Self::Pair { request_id, .. }
191 | Self::Status { request_id, .. }
192 | Self::ListFiles { request_id, .. }
193 | Self::ReadFile { request_id, .. }
194 | Self::ProposeChanges { request_id, .. }
195 | Self::ApplyProposal { request_id, .. }
196 | Self::RunChecks { request_id, .. }
197 | Self::RevertLastChange { request_id, .. }
198 | Self::RequestTurn { request_id, .. }
199 | Self::Cancel { request_id, .. } => request_id,
200 }
201 }
202
203 pub fn token(&self) -> Option<&str> {
205 match self {
206 Self::ExplanationGet { token, .. }
207 | Self::ExplanationEvidence { token, .. }
208 | Self::ExplanationNavigate { token, .. } => Some(token),
209 Self::Pair { .. } => None,
210 Self::Status { token, .. }
211 | Self::ListFiles { token, .. }
212 | Self::ReadFile { token, .. }
213 | Self::ProposeChanges { token, .. }
214 | Self::ApplyProposal { token, .. }
215 | Self::RunChecks { token, .. }
216 | Self::RevertLastChange { token, .. }
217 | Self::RequestTurn { token, .. }
218 | Self::Cancel { token, .. } => Some(token),
219 }
220 }
221}
222
223#[derive(Debug, Clone, Serialize)]
225pub struct BridgeResponse {
226 #[serde(rename = "type")]
228 pub kind: &'static str,
229 pub request_id: String,
231 pub ok: bool,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub payload: Option<Value>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub error: Option<BridgeErrorPayload>,
239}
240
241#[derive(Debug, Clone, Serialize)]
243pub struct BridgeErrorPayload {
244 pub code: &'static str,
246 pub message: String,
248}
249
250impl BridgeResponse {
251 pub fn success(request_id: impl Into<String>, payload: impl Serialize) -> Self {
253 Self {
254 kind: "response",
255 request_id: request_id.into(),
256 ok: true,
257 payload: serde_json::to_value(payload).ok(),
258 error: None,
259 }
260 }
261
262 pub fn failure(request_id: impl Into<String>, code: &'static str, message: impl Into<String>) -> Self {
264 Self {
265 kind: "response",
266 request_id: request_id.into(),
267 ok: false,
268 payload: None,
269 error: Some(BridgeErrorPayload { code, message: message.into() }),
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn request_ids_are_bounded_and_invalid_ids_are_not_echoed() {
280 assert!(!is_valid_request_id(""));
281 assert!(is_valid_request_id("browser-1"));
282 assert!(is_valid_request_id(&"x".repeat(MAX_REQUEST_ID_BYTES)));
283 assert!(!is_valid_request_id(&"x".repeat(MAX_REQUEST_ID_BYTES + 1)));
284 assert_eq!(response_request_id(""), "unknown");
285 assert_eq!(response_request_id(&"x".repeat(MAX_REQUEST_ID_BYTES + 1)), "unknown");
286 }
287
288 #[test]
289 fn turn_request_can_reference_a_staged_proposal() {
290 let request: BridgeRequest = serde_json::from_value(serde_json::json!({
291 "type": "turn.request",
292 "request_id": "browser-1",
293 "token": "session-token",
294 "proposal_id": "proposal-1",
295 "prompt": "Implement the staged change"
296 }))
297 .expect("turn request should deserialize");
298
299 assert!(matches!(
300 request,
301 BridgeRequest::RequestTurn { proposal_id: Some(proposal_id), prompt, .. }
302 if proposal_id == "proposal-1" && prompt == "Implement the staged change"
303 ));
304 }
305
306 #[test]
307 fn bridge_settings_are_non_secret() {
308 let settings = BridgeSettings {
309 host: "127.0.0.1".to_string(),
310 port: 4321,
311 pairing_ttl_secs: 300,
312 max_frame_bytes: 1_048_576,
313 max_in_flight_requests: 8,
314 remote_enabled: false,
315 };
316 let serialized = serde_json::to_string(&settings).expect("settings should serialize");
317 assert!(serialized.contains("pairing_ttl_secs"));
318 assert!(!serialized.contains("token"));
319 assert!(!serialized.contains("code"));
320 }
321}
322
323#[derive(Debug, Clone, Serialize)]
325pub struct PairPayload {
326 pub token: String,
328 pub protocol_version: &'static str,
330 pub expires_in_secs: u64,
332}
333
334#[derive(Debug, Clone, Serialize)]
341pub struct BridgeSettings {
342 pub host: String,
344 pub port: u16,
346 pub pairing_ttl_secs: u64,
348 pub max_frame_bytes: usize,
350 pub max_in_flight_requests: usize,
352 pub remote_enabled: bool,
354}
355
356#[derive(Debug, Clone, Serialize)]
358pub struct StatusPayload {
359 pub protocol_version: &'static str,
361 pub connected: bool,
363 pub runtime: crate::runtime::RuntimeStatus,
365 pub authenticated_origin: String,
367 pub settings: BridgeSettings,
369 pub latest_sequence: u64,
371}
372
373#[derive(Debug, Clone, Serialize)]
375pub struct BridgeEventMessage {
376 #[serde(rename = "type")]
378 pub kind: &'static str,
379 pub sequence: u64,
381 pub event: VersionedThreadEvent,
383}