1use std::collections::HashMap;
4use std::path::{Component, PathBuf};
5use std::sync::Arc;
6
7use crate::ast::Value;
8use crate::backend::{KernelBackend, LocalBackend};
9use crate::dispatch::PipelinePosition;
10use crate::ignore_config::IgnoreConfig;
11use crate::interpreter::{ExecResult, Scope};
12use crate::nonce::NonceStore;
13use crate::output_limit::OutputLimitConfig;
14use crate::scheduler::{JobManager, PipeReader, PipeWriter, StderrStream};
15use crate::tools::ToolRegistry;
16use crate::trash::TrashBackend;
17use crate::vfs::VfsRouter;
18use tokio_util::sync::CancellationToken;
19
20use super::traits::ToolSchema;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum OutputContext {
31 #[default]
33 Interactive,
34 Piped,
36 Model,
38 Script,
40}
41
42pub struct ExecContext {
47 pub backend: Arc<dyn KernelBackend>,
52 pub scope: Scope,
54 pub cwd: PathBuf,
56 pub prev_cwd: Option<PathBuf>,
58 pub stdin: Option<String>,
60 pub stdin_data: Option<Value>,
63 pub pipe_stdin: Option<PipeReader>,
65 pub pipe_stdout: Option<PipeWriter>,
67 pub tool_schemas: Vec<ToolSchema>,
69 pub tools: Option<Arc<ToolRegistry>>,
71 pub job_manager: Option<Arc<JobManager>>,
73 pub stderr: Option<StderrStream>,
79 pub pipeline_position: PipelinePosition,
81 pub interactive: bool,
83 pub aliases: HashMap<String, String>,
85 pub ignore_config: IgnoreConfig,
87 pub output_limit: OutputLimitConfig,
89 pub allow_external_commands: bool,
94 pub nonce_store: NonceStore,
99 pub trash_backend: Option<Arc<dyn TrashBackend>>,
105 #[cfg(all(unix, feature = "native"))]
107 pub terminal_state: Option<std::sync::Arc<crate::terminal::TerminalState>>,
108 pub dispatcher: Option<Arc<dyn crate::dispatch::CommandDispatcher>>,
116 pub cancel: CancellationToken,
126}
127
128impl ExecContext {
129 pub fn new(vfs: Arc<VfsRouter>) -> Self {
134 Self {
135 backend: Arc::new(LocalBackend::new(vfs)),
136 scope: Scope::new(),
137 cwd: PathBuf::from("/"),
138 prev_cwd: None,
139 stdin: None,
140 stdin_data: None,
141 pipe_stdin: None,
142 pipe_stdout: None,
143 stderr: None,
144 tool_schemas: Vec::new(),
145 tools: None,
146 job_manager: None,
147 pipeline_position: PipelinePosition::Only,
148 interactive: false,
149 aliases: HashMap::new(),
150 ignore_config: IgnoreConfig::none(),
151 output_limit: OutputLimitConfig::none(),
152 allow_external_commands: true,
153 nonce_store: NonceStore::new(),
154 trash_backend: None,
155 #[cfg(all(unix, feature = "native"))]
156 terminal_state: None,
157 dispatcher: None,
158 cancel: CancellationToken::new(),
159 }
160 }
161
162 pub fn with_vfs_and_tools(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>) -> Self {
167 Self {
168 backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
169 scope: Scope::new(),
170 cwd: PathBuf::from("/"),
171 prev_cwd: None,
172 stdin: None,
173 stdin_data: None,
174 pipe_stdin: None,
175 pipe_stdout: None,
176 stderr: None,
177 tool_schemas: Vec::new(),
178 tools: Some(tools),
179 job_manager: None,
180 pipeline_position: PipelinePosition::Only,
181 interactive: false,
182 aliases: HashMap::new(),
183 ignore_config: IgnoreConfig::none(),
184 output_limit: OutputLimitConfig::none(),
185 allow_external_commands: true,
186 nonce_store: NonceStore::new(),
187 trash_backend: None,
188 #[cfg(all(unix, feature = "native"))]
189 terminal_state: None,
190 dispatcher: None,
191 cancel: CancellationToken::new(),
192 }
193 }
194
195 pub fn with_backend(backend: Arc<dyn KernelBackend>) -> Self {
197 Self {
198 backend,
199 scope: Scope::new(),
200 cwd: PathBuf::from("/"),
201 prev_cwd: None,
202 stdin: None,
203 stdin_data: None,
204 pipe_stdin: None,
205 pipe_stdout: None,
206 stderr: None,
207 tool_schemas: Vec::new(),
208 tools: None,
209 job_manager: None,
210 pipeline_position: PipelinePosition::Only,
211 interactive: false,
212 aliases: HashMap::new(),
213 ignore_config: IgnoreConfig::none(),
214 output_limit: OutputLimitConfig::none(),
215 allow_external_commands: true,
216 nonce_store: NonceStore::new(),
217 trash_backend: None,
218 #[cfg(all(unix, feature = "native"))]
219 terminal_state: None,
220 dispatcher: None,
221 cancel: CancellationToken::new(),
222 }
223 }
224
225 pub fn with_vfs_tools_and_scope(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>, scope: Scope) -> Self {
227 Self {
228 backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
229 scope,
230 cwd: PathBuf::from("/"),
231 prev_cwd: None,
232 stdin: None,
233 stdin_data: None,
234 pipe_stdin: None,
235 pipe_stdout: None,
236 stderr: None,
237 tool_schemas: Vec::new(),
238 tools: Some(tools),
239 job_manager: None,
240 pipeline_position: PipelinePosition::Only,
241 interactive: false,
242 aliases: HashMap::new(),
243 ignore_config: IgnoreConfig::none(),
244 output_limit: OutputLimitConfig::none(),
245 allow_external_commands: true,
246 nonce_store: NonceStore::new(),
247 trash_backend: None,
248 #[cfg(all(unix, feature = "native"))]
249 terminal_state: None,
250 dispatcher: None,
251 cancel: CancellationToken::new(),
252 }
253 }
254
255 pub fn with_scope(vfs: Arc<VfsRouter>, scope: Scope) -> Self {
260 Self {
261 backend: Arc::new(LocalBackend::new(vfs)),
262 scope,
263 cwd: PathBuf::from("/"),
264 prev_cwd: None,
265 stdin: None,
266 stdin_data: None,
267 pipe_stdin: None,
268 pipe_stdout: None,
269 stderr: None,
270 tool_schemas: Vec::new(),
271 tools: None,
272 job_manager: None,
273 pipeline_position: PipelinePosition::Only,
274 interactive: false,
275 aliases: HashMap::new(),
276 ignore_config: IgnoreConfig::none(),
277 output_limit: OutputLimitConfig::none(),
278 allow_external_commands: true,
279 nonce_store: NonceStore::new(),
280 trash_backend: None,
281 #[cfg(all(unix, feature = "native"))]
282 terminal_state: None,
283 dispatcher: None,
284 cancel: CancellationToken::new(),
285 }
286 }
287
288 pub fn with_backend_and_scope(backend: Arc<dyn KernelBackend>, scope: Scope) -> Self {
290 Self {
291 backend,
292 scope,
293 cwd: PathBuf::from("/"),
294 prev_cwd: None,
295 stdin: None,
296 stdin_data: None,
297 pipe_stdin: None,
298 pipe_stdout: None,
299 stderr: None,
300 tool_schemas: Vec::new(),
301 tools: None,
302 job_manager: None,
303 pipeline_position: PipelinePosition::Only,
304 interactive: false,
305 aliases: HashMap::new(),
306 ignore_config: IgnoreConfig::none(),
307 output_limit: OutputLimitConfig::none(),
308 allow_external_commands: true,
309 nonce_store: NonceStore::new(),
310 trash_backend: None,
311 #[cfg(all(unix, feature = "native"))]
312 terminal_state: None,
313 dispatcher: None,
314 cancel: CancellationToken::new(),
315 }
316 }
317
318 pub fn set_tool_schemas(&mut self, schemas: Vec<ToolSchema>) {
320 self.tool_schemas = schemas;
321 }
322
323 pub fn set_tools(&mut self, tools: Arc<ToolRegistry>) {
325 self.tools = Some(tools);
326 }
327
328 pub fn set_job_manager(&mut self, manager: Arc<JobManager>) {
330 self.job_manager = Some(manager);
331 }
332
333 pub fn set_trash_backend(&mut self, backend: Arc<dyn TrashBackend>) {
335 self.trash_backend = Some(backend);
336 }
337
338 pub fn set_stdin(&mut self, stdin: String) {
340 self.stdin = Some(stdin);
341 }
342
343 pub fn take_stdin(&mut self) -> Option<String> {
345 self.stdin.take()
346 }
347
348 pub fn set_stdin_with_data(&mut self, text: String, data: Option<Value>) {
353 self.stdin = Some(text);
354 self.stdin_data = data;
355 }
356
357 pub fn take_stdin_data(&mut self) -> Option<Value> {
362 self.stdin_data.take()
363 }
364
365 pub fn resolve_path(&self, path: &str) -> PathBuf {
367 let raw = if path.starts_with('/') {
368 PathBuf::from(path)
369 } else {
370 self.cwd.join(path)
371 };
372 normalize_path(&raw)
373 }
374
375 pub fn set_cwd(&mut self, path: PathBuf) {
379 self.prev_cwd = Some(self.cwd.clone());
380 self.cwd = path;
381 }
382
383 pub fn get_prev_cwd(&self) -> Option<&PathBuf> {
385 self.prev_cwd.as_ref()
386 }
387
388 pub async fn read_stdin_to_string(&mut self) -> Option<String> {
393 if let Some(mut reader) = self.pipe_stdin.take() {
394 use tokio::io::AsyncReadExt;
395 let mut buf = Vec::new();
396 reader.read_to_end(&mut buf).await.ok()?;
397 Some(String::from_utf8_lossy(&buf).into_owned())
398 } else {
399 self.stdin.take()
400 }
401 }
402
403 pub fn child_for_pipeline(&self) -> Self {
408 Self {
409 backend: self.backend.clone(),
410 scope: self.scope.clone(),
411 cwd: self.cwd.clone(),
412 prev_cwd: self.prev_cwd.clone(),
413 stdin: None,
414 stdin_data: None,
415 pipe_stdin: None,
416 pipe_stdout: None,
417 stderr: self.stderr.clone(),
418 tool_schemas: self.tool_schemas.clone(),
419 tools: self.tools.clone(),
420 job_manager: self.job_manager.clone(),
421 pipeline_position: PipelinePosition::Only,
422 interactive: self.interactive,
423 aliases: self.aliases.clone(),
424 ignore_config: self.ignore_config.clone(),
425 output_limit: self.output_limit.clone(),
426 allow_external_commands: self.allow_external_commands,
427 nonce_store: self.nonce_store.clone(),
428 trash_backend: self.trash_backend.clone(),
429 #[cfg(all(unix, feature = "native"))]
430 terminal_state: self.terminal_state.clone(),
431 dispatcher: self.dispatcher.clone(),
432 cancel: self.cancel.clone(),
433 }
434 }
435
436 pub async fn build_ignore_filter(&self, root: &std::path::Path) -> Option<crate::walker::IgnoreFilter> {
440 use crate::backend_walker_fs::BackendWalkerFs;
441 let fs = BackendWalkerFs(self.backend.as_ref());
442 self.ignore_config.build_filter(root, &fs).await
443 }
444
445 pub fn verify_nonce(&self, nonce: &str, command: &str, paths: &[&str]) -> Result<(), String> {
449 self.nonce_store.validate(nonce, command, paths)
450 }
451
452 pub fn latch_result(
463 &self,
464 command: &str,
465 paths: &[&str],
466 reason: &str,
467 confirm_hint: impl FnOnce(&str) -> String,
468 ) -> ExecResult {
469 let nonce = self.nonce_store.issue(command, paths);
470 let ttl = self.nonce_store.ttl().as_secs();
471 let authorized = if paths.is_empty() {
472 String::new()
473 } else {
474 format!("\nAuthorized: {}", paths.join(", "))
475 };
476 let hint = confirm_hint(&nonce);
477
478 let mut result = ExecResult::failure(2, format!(
479 "{command}: confirmation required ({reason}){authorized}\nTo confirm, run: {hint}\nNonce expires in {ttl} seconds."
480 ));
481 result.data = Some(Value::Json(serde_json::json!({
482 "nonce": nonce,
483 "command": command,
484 "paths": paths,
485 "hint": hint,
486 "ttl": ttl,
487 })));
488 result
489 }
490
491 pub async fn expand_glob(&self, pattern: &str) -> Result<Vec<PathBuf>, String> {
496 use crate::backend_walker_fs::BackendWalkerFs;
497 use crate::walker::{EntryTypes, FileWalker, GlobPath, WalkOptions};
498
499 let glob = GlobPath::new(pattern).map_err(|e| format!("invalid pattern: {}", e))?;
500
501 let root = if glob.is_anchored() {
502 self.resolve_path("/")
503 } else {
504 self.resolve_path(".")
505 };
506
507 let options = WalkOptions {
508 entry_types: EntryTypes::all(),
509 respect_gitignore: self.ignore_config.auto_gitignore(),
510 ..WalkOptions::default()
511 };
512
513 let fs = BackendWalkerFs(self.backend.as_ref());
514 let mut walker = FileWalker::new(&fs, &root)
515 .with_pattern(glob)
516 .with_options(options);
517
518 if let Some(filter) = self.ignore_config.build_filter(&root, &fs).await {
522 walker = walker.with_ignore(filter);
523 }
524
525 walker.collect().await.map_err(|e| e.to_string())
526 }
527
528 pub async fn expand_paths(&self, positional: &[Value]) -> Result<Vec<String>, String> {
534 let mut paths = Vec::new();
535 for arg in positional {
536 let s = match arg {
537 Value::String(s) => s.clone(),
538 Value::Int(n) => n.to_string(),
539 Value::Float(f) => f.to_string(),
540 _ => continue,
541 };
542 if crate::glob::contains_glob(&s) {
543 let expanded = self.expand_glob(&s).await?;
544 let root = self.resolve_path(".");
545 for p in expanded {
546 let rel = p.strip_prefix(&root).unwrap_or(&p);
547 paths.push(rel.to_string_lossy().to_string());
548 }
549 } else {
550 paths.push(s);
551 }
552 }
553 Ok(paths)
554 }
555}
556
557fn normalize_path(path: &std::path::Path) -> PathBuf {
559 let mut parts: Vec<Component> = Vec::new();
560 for component in path.components() {
561 match component {
562 Component::CurDir => {} Component::ParentDir => {
564 if let Some(Component::Normal(_)) = parts.last() {
566 parts.pop();
567 } else {
568 parts.push(component);
569 }
570 }
571 _ => parts.push(component),
572 }
573 }
574 if parts.is_empty() {
575 PathBuf::from("/")
576 } else {
577 parts.iter().collect()
578 }
579}