sqlite_graphrag/spawn/
mod.rs1pub mod claude_adapter;
8pub mod codex_adapter;
9pub mod compat_matrix;
10pub mod env_whitelist;
11pub mod error_propagator;
12pub mod executor_version;
13pub mod opencode_adapter;
14pub mod preflight;
15
16use crate::errors::AppError;
17use async_trait::async_trait;
18use executor_version::ExecutorVersion;
19use std::collections::BTreeMap;
20use std::process::Stdio;
21
22#[derive(Debug, Clone)]
24pub struct ParsedOutput {
25 pub items: Vec<serde_json::Value>,
26 pub raw_stdout: String,
27 pub raw_stderr: String,
28 pub exit_code: i32,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ExecutorCapabilities {
34 pub supports_mcp_map: bool,
35 pub supports_ask_for_approval_flag: bool,
36 pub supports_strict_schema: bool,
37 pub default_flags: Vec<String>,
38 pub removed_flags: Vec<String>,
39}
40
41impl ExecutorCapabilities {
42 pub fn empty() -> Self {
43 Self {
44 supports_mcp_map: false,
45 supports_ask_for_approval_flag: false,
46 supports_strict_schema: false,
47 default_flags: Vec::new(),
48 removed_flags: Vec::new(),
49 }
50 }
51}
52
53#[async_trait]
55pub trait VersionAdapter: Send + Sync {
56 fn name(&self) -> &'static str;
58
59 async fn detect(&self) -> Result<ExecutorVersion, AppError>;
61
62 fn capabilities_for(&self, version: &ExecutorVersion) -> ExecutorCapabilities;
64
65 fn build_args(
67 &self,
68 prompt: &str,
69 caps: &ExecutorCapabilities,
70 compat_mode: CompatMode,
71 ) -> Vec<String>;
72
73 fn parse_output(&self, raw_stdout: &str, raw_stderr: &str, exit_code: i32) -> ParsedOutput;
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum CompatMode {
80 Strict,
82 Lenient,
84 Auto,
86}
87
88impl CompatMode {
89 pub fn parse(s: &str) -> Self {
90 match s.to_ascii_lowercase().as_str() {
91 "strict" => Self::Strict,
92 "lenient" => Self::Lenient,
93 _ => Self::Auto,
94 }
95 }
96}
97
98#[derive(Debug, Default)]
101pub struct VersionCache {
102 inner: std::sync::Mutex<BTreeMap<String, ExecutorVersion>>,
103}
104
105impl VersionCache {
106 pub fn new() -> Self {
107 Self::default()
108 }
109
110 pub fn get(&self, name: &str) -> Option<ExecutorVersion> {
111 self.inner.lock().ok().and_then(|m| m.get(name).cloned())
112 }
113
114 pub fn put(&self, name: &str, version: ExecutorVersion) {
115 if let Ok(mut m) = self.inner.lock() {
116 m.insert(name.to_string(), version);
117 }
118 }
119
120 pub fn clear(&self) {
121 if let Ok(mut m) = self.inner.lock() {
122 m.clear();
123 }
124 }
125}
126
127static VERSION_CACHE: std::sync::OnceLock<VersionCache> = std::sync::OnceLock::new();
128
129pub fn global_version_cache() -> &'static VersionCache {
130 VERSION_CACHE.get_or_init(VersionCache::new)
131}
132
133pub fn base_command(binary: &str) -> std::process::Command {
135 let mut cmd = std::process::Command::new(binary);
136 cmd.stdin(Stdio::null())
137 .stdout(Stdio::piped())
138 .stderr(Stdio::piped());
139 cmd
140}
141
142pub fn spawn_isolation_dir() -> Result<std::path::PathBuf, AppError> {
145 let dir = std::env::temp_dir().join(format!("sqlite-graphrag-spawn-{}", std::process::id()));
146 std::fs::create_dir_all(&dir).map_err(|e| {
147 AppError::Io(std::io::Error::new(
148 e.kind(),
149 format!(
150 "failed to create spawn isolation dir {}: {e}",
151 dir.display()
152 ),
153 ))
154 })?;
155 Ok(dir)
156}
157
158pub fn apply_cwd_isolation(
162 cmd: &mut std::process::Command,
163) -> Result<std::path::PathBuf, AppError> {
164 let dir = spawn_isolation_dir()?;
165 cmd.current_dir(&dir);
166 cmd.env("CLAUDE_CONFIG_DIR", &dir);
167 Ok(dir)
168}
169
170pub fn apply_cwd_isolation_tokio(
172 cmd: &mut tokio::process::Command,
173) -> Result<std::path::PathBuf, AppError> {
174 let dir = spawn_isolation_dir()?;
175 cmd.current_dir(&dir);
176 cmd.env("CLAUDE_CONFIG_DIR", &dir);
177 Ok(dir)
178}
179
180#[cfg(test)]
181mod isolation_tests {
182 use super::*;
183
184 #[test]
185 fn test_spawn_isolation_dir_creates_in_temp() {
186 let dir = spawn_isolation_dir().unwrap();
187 assert!(dir.exists());
188 assert!(dir.starts_with(std::env::temp_dir()));
189 let mut check = dir.as_path();
190 while let Some(parent) = check.parent() {
191 assert!(!parent.join(".mcp.json").exists() || parent == std::path::Path::new("/"));
192 check = parent;
193 if parent == std::path::Path::new("/") {
194 break;
195 }
196 }
197 }
198
199 #[test]
200 fn test_apply_cwd_isolation_modifies_command() {
201 let mut cmd = std::process::Command::new("false");
202 let dir = apply_cwd_isolation(&mut cmd).unwrap();
203 assert!(dir.exists());
204 let debug = format!("{:?}", cmd);
205 assert!(debug.contains("sqlite-graphrag-spawn-"));
206 }
207}