vtcode_core/exec/
sdk_ipc.rs1use crate::utils::file_utils::{
48 parse_json_with_context, read_file_with_context, write_file_with_context,
49};
50use anyhow::{Context, Result};
51use std::path::PathBuf;
52use std::sync::Arc;
53use std::time::Duration;
54use tokio::fs;
55use tokio::time::sleep;
56use uuid::Uuid;
57
58use crate::tools::request_response::{ToolCallRequest, ToolCallResponse};
59
60const REQUEST_POLL_INTERVAL: Duration = Duration::from_millis(50);
61
62pub type ToolRequest = ToolCallRequest;
64
65pub type ToolResponse = ToolCallResponse;
67
68pub struct ToolIpcHandler {
70 ipc_dir: PathBuf,
71 pii_tokenizer: Option<Arc<crate::exec::PiiTokenizer>>,
72}
73
74impl ToolIpcHandler {
75 pub fn new(ipc_dir: PathBuf) -> Self {
77 Self {
78 ipc_dir,
79 pii_tokenizer: None,
80 }
81 }
82
83 pub fn with_pii_protection(ipc_dir: PathBuf) -> Result<Self> {
85 Ok(Self {
86 ipc_dir,
87 pii_tokenizer: Some(Arc::new(crate::exec::PiiTokenizer::new()?)),
88 })
89 }
90
91 pub fn enable_pii_protection(&mut self) -> Result<()> {
93 self.pii_tokenizer = Some(Arc::new(crate::exec::PiiTokenizer::new()?));
94 Ok(())
95 }
96
97 pub async fn read_request(&self) -> Result<Option<ToolRequest>> {
99 let request_file = self.ipc_dir.join("request.json");
100
101 if !request_file.exists() {
102 return Ok(None);
103 }
104
105 let content = read_file_with_context(&request_file, "request file").await?;
106 let request: ToolRequest = parse_json_with_context(&content, "request JSON")?;
107
108 let _ = fs::remove_file(&request_file).await;
110
111 Ok(Some(request))
112 }
113
114 pub fn process_request_for_pii(&self, request: &mut ToolRequest) -> Result<()> {
116 if let Some(tokenizer) = &self.pii_tokenizer {
117 let args_str =
118 serde_json::to_string(&request.args).context("failed to serialize request args")?;
119 let (tokenized, _) = tokenizer
120 .tokenize_string(&args_str)
121 .context("PII tokenization failed")?;
122 request.args = parse_json_with_context(&tokenized, "tokenized args")?;
123 }
124 Ok(())
125 }
126
127 pub fn process_response_for_pii(&self, response: &mut ToolResponse) -> Result<()> {
129 if let Some(tokenizer) = &self.pii_tokenizer
130 && let Some(result) = &response.result
131 {
132 let result_str =
133 serde_json::to_string(result).context("failed to serialize response result")?;
134 let detokenized = tokenizer
135 .detokenize_string(&result_str)
136 .context("PII de-tokenization failed")?;
137 response.result = Some(parse_json_with_context(
138 &detokenized,
139 "de-tokenized result",
140 )?);
141 }
142 Ok(())
143 }
144
145 pub async fn write_response(&self, mut response: ToolResponse) -> Result<()> {
147 self.process_response_for_pii(&mut response)?;
149
150 let response_file = self.ipc_dir.join("response.json");
151
152 let json = serde_json::to_string(&response).context("failed to serialize response")?;
153
154 write_file_with_context(&response_file, &json, "response file").await?;
155
156 Ok(())
157 }
158
159 pub async fn wait_for_request(&self, timeout: Duration) -> Result<Option<ToolRequest>> {
161 let start = std::time::Instant::now();
162
163 loop {
164 if let Some(request) = self.read_request().await? {
165 return Ok(Some(request));
166 }
167
168 let Some(remaining_timeout) = timeout.checked_sub(start.elapsed()) else {
169 return Ok(None);
170 };
171
172 sleep(remaining_timeout.min(REQUEST_POLL_INTERVAL)).await;
173 }
174 }
175
176 pub fn new_request_id() -> String {
178 Uuid::new_v4().to_string()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use serde_json::json;
186 use tempfile::tempdir;
187 use tokio::time::Instant;
188
189 #[test]
190 fn serialize_tool_request() {
191 let request = ToolRequest {
192 id: "test-id".into(),
193 tool_name: "read_file".into(),
194 args: json!({"path": "/test"}),
195 metadata: None,
196 };
197
198 let json = serde_json::to_string(&request).expect("ToolRequest should serialize");
199 assert!(json.contains("test-id"));
200 assert!(json.contains("read_file"));
201 }
202
203 #[test]
204 fn serialize_success_response() {
205 let response = ToolResponse {
206 id: "test-id".into(),
207 success: true,
208 result: Some(json!({"data": "test"})),
209 error: None,
210 duration_ms: None,
211 cache_hit: None,
212 };
213
214 let json = serde_json::to_string(&response).expect("ToolResponse should serialize");
215 assert!(json.contains("test-id"));
216 assert!(json.contains("true"));
217 assert!(!json.contains("error"));
218 }
219
220 #[test]
221 fn serialize_error_response() {
222 let response = ToolResponse {
223 id: "test-id".into(),
224 success: false,
225 result: None,
226 error: Some("File not found".into()),
227 duration_ms: None,
228 cache_hit: None,
229 };
230
231 let json = serde_json::to_string(&response).expect("ToolResponse should serialize");
232 assert!(json.contains("test-id"));
233 assert!(json.contains("false"));
234 assert!(json.contains("File not found"));
235 }
236
237 #[tokio::test]
238 async fn wait_for_request_reads_delayed_request() {
239 let temp_dir = tempdir().expect("temp dir should create");
240 let handler = ToolIpcHandler::new(temp_dir.path().to_path_buf());
241 let request = ToolRequest {
242 id: "test-id".into(),
243 tool_name: "read_file".into(),
244 args: json!({"path": "/tmp/test"}),
245 metadata: None,
246 };
247 let request_json =
248 serde_json::to_string(&request).expect("request should serialize to JSON");
249 let request_path = temp_dir.path().join("request.json");
250
251 tokio::spawn(async move {
252 sleep(Duration::from_millis(10)).await;
253 fs::write(request_path, request_json)
254 .await
255 .expect("request file should write");
256 });
257
258 let received = handler
259 .wait_for_request(Duration::from_millis(200))
260 .await
261 .expect("request wait should succeed");
262
263 assert_eq!(received.expect("request should arrive").id, "test-id");
264 }
265
266 #[tokio::test]
267 async fn wait_for_request_respects_short_timeout() {
268 let temp_dir = tempdir().expect("temp dir should create");
269 let handler = ToolIpcHandler::new(temp_dir.path().to_path_buf());
270 let start = Instant::now();
271
272 let received = handler
273 .wait_for_request(Duration::from_millis(5))
274 .await
275 .expect("request wait should succeed");
276
277 assert!(received.is_none());
278 assert!(start.elapsed() < Duration::from_millis(40));
279 }
280}