1use std::collections::HashMap;
26use std::path::PathBuf;
27use std::sync::Arc;
28use std::sync::atomic::{AtomicU64, Ordering};
29use std::time::Duration;
30
31use anyhow::{Context, Result};
32use tokio::sync::RwLock;
33
34static KERNEL_COUNTER: AtomicU64 = AtomicU64::new(1);
46
47use async_trait::async_trait;
48
49use crate::ast::{Arg, Command, Expr, FileTestOp, Stmt, StringPart, TestExpr, ToolDef, Value, BinaryOp};
50pub use kaish_types::ExecuteOptions;
51use crate::backend::{BackendError, KernelBackend};
52use kaish_glob::glob_match;
53use crate::dispatch::{CommandDispatcher, PipelinePosition};
54use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value, value_to_bool, value_to_string, ControlFlow, ExecResult, Scope};
55use crate::parser::parse;
56use crate::scheduler::{is_bool_type, schema_param_lookup, select_leaf, stderr_stream, BoundedStream, JobManager, PipelineRunner, StderrReceiver};
57#[cfg(feature = "subprocess")]
58use crate::scheduler::{drain_to_stream, DEFAULT_STREAM_MAX_SIZE};
59use crate::tools::{register_builtins, ExecContext, GlobalFlags, ToolArgs, ToolRegistry};
60#[cfg(feature = "subprocess")]
61use crate::tools::resolve_in_path;
62use crate::validator::{Severity, Validator};
63#[cfg(feature = "localfs")]
64use crate::vfs::LocalFs;
65use crate::vfs::{BuiltinFs, DevFs, JobFs, MemoryFs, VfsRouter};
66use kaish_vfs::ByteBudget;
67#[cfg(all(feature = "localfs", feature = "overlay"))]
68use kaish_vfs::OverlayFs;
69
70#[derive(Debug, Clone)]
77pub enum VfsMountMode {
78 #[cfg(feature = "localfs")]
87 Passthrough,
88
89 #[cfg(feature = "localfs")]
105 Sandboxed {
106 root: Option<PathBuf>,
109 },
110
111 NoLocal,
127}
128
129#[allow(clippy::derivable_impls)] impl Default for VfsMountMode {
131 fn default() -> Self {
132 #[cfg(feature = "localfs")]
133 { VfsMountMode::Sandboxed { root: None } }
134 #[cfg(not(feature = "localfs"))]
135 { VfsMountMode::NoLocal }
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct KernelConfig {
142 pub name: String,
144
145 pub vfs_mode: VfsMountMode,
147
148 pub cwd: PathBuf,
150
151 pub skip_validation: bool,
157
158 pub interactive: bool,
163
164 pub ignore_config: crate::ignore_config::IgnoreConfig,
166
167 pub output_limit: crate::output_limit::OutputLimitConfig,
169
170 pub allow_external_commands: bool,
180
181 pub latch_enabled: bool,
186
187 pub trash_enabled: bool,
193
194 pub nonce_store: Option<crate::nonce::NonceStore>,
200
201 pub initial_vars: HashMap<String, Value>,
209
210 pub request_timeout: Option<Duration>,
217
218 pub kill_grace: Duration,
224
225 pub vfs_budget_bytes: Option<u64>,
243
244 pub overlay: bool,
266}
267
268#[cfg(feature = "localfs")]
270fn default_sandbox_root() -> PathBuf {
271 std::env::var("HOME")
272 .map(PathBuf::from)
273 .unwrap_or_else(|_| PathBuf::from("/"))
274}
275
276impl Default for KernelConfig {
277 fn default() -> Self {
278 #[cfg(feature = "localfs")]
279 {
280 let home = default_sandbox_root();
281 Self {
282 name: "default".to_string(),
283 vfs_mode: VfsMountMode::Sandboxed { root: None },
284 cwd: home,
285 skip_validation: false,
286 interactive: false,
287 ignore_config: crate::ignore_config::IgnoreConfig::none(),
288 output_limit: crate::output_limit::OutputLimitConfig::none(),
289 allow_external_commands: cfg!(feature = "subprocess"),
290 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
291 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
292 nonce_store: None,
293 initial_vars: HashMap::new(),
294 request_timeout: None,
295 kill_grace: Duration::from_secs(2),
296 vfs_budget_bytes: None,
297 overlay: false,
298 }
299 }
300 #[cfg(not(feature = "localfs"))]
301 {
302 Self {
303 name: "default".to_string(),
304 vfs_mode: VfsMountMode::NoLocal,
305 cwd: PathBuf::from("/"),
306 skip_validation: false,
307 interactive: false,
308 ignore_config: crate::ignore_config::IgnoreConfig::none(),
309 output_limit: crate::output_limit::OutputLimitConfig::none(),
310 allow_external_commands: false,
311 latch_enabled: false,
312 trash_enabled: false,
313 nonce_store: None,
314 initial_vars: HashMap::new(),
315 request_timeout: None,
316 kill_grace: Duration::from_secs(2),
317 vfs_budget_bytes: None,
318 overlay: false,
319 }
320 }
321 }
322}
323
324impl KernelConfig {
325 #[cfg(feature = "localfs")]
327 pub fn transient() -> Self {
328 let home = default_sandbox_root();
329 Self {
330 name: "transient".to_string(),
331 vfs_mode: VfsMountMode::Sandboxed { root: None },
332 cwd: home,
333 skip_validation: false,
334 interactive: false,
335 ignore_config: crate::ignore_config::IgnoreConfig::none(),
336 output_limit: crate::output_limit::OutputLimitConfig::none(),
337 allow_external_commands: cfg!(feature = "subprocess"),
338 latch_enabled: false,
339 trash_enabled: false,
340 nonce_store: None,
341 initial_vars: HashMap::new(),
342 request_timeout: None,
343 kill_grace: Duration::from_secs(2),
344 vfs_budget_bytes: None,
345 overlay: false,
346 }
347 }
348
349 #[cfg(not(feature = "localfs"))]
351 pub fn transient() -> Self {
352 Self::isolated()
353 }
354
355 #[cfg(feature = "localfs")]
357 pub fn named(name: &str) -> Self {
358 let home = default_sandbox_root();
359 Self {
360 name: name.to_string(),
361 vfs_mode: VfsMountMode::Sandboxed { root: None },
362 cwd: home,
363 skip_validation: false,
364 interactive: false,
365 ignore_config: crate::ignore_config::IgnoreConfig::none(),
366 output_limit: crate::output_limit::OutputLimitConfig::none(),
367 allow_external_commands: cfg!(feature = "subprocess"),
368 latch_enabled: false,
369 trash_enabled: false,
370 nonce_store: None,
371 initial_vars: HashMap::new(),
372 request_timeout: None,
373 kill_grace: Duration::from_secs(2),
374 vfs_budget_bytes: None,
375 overlay: false,
376 }
377 }
378
379 #[cfg(not(feature = "localfs"))]
381 pub fn named(name: &str) -> Self {
382 Self {
383 name: name.to_string(),
384 ..Self::isolated()
385 }
386 }
387
388 #[cfg(feature = "localfs")]
393 pub fn repl() -> Self {
394 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
395 Self {
396 name: "repl".to_string(),
397 vfs_mode: VfsMountMode::Passthrough,
398 cwd,
399 skip_validation: false,
400 interactive: false,
401 ignore_config: crate::ignore_config::IgnoreConfig::none(),
402 output_limit: crate::output_limit::OutputLimitConfig::none(),
403 allow_external_commands: cfg!(feature = "subprocess"),
404 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
405 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
406 nonce_store: None,
407 initial_vars: HashMap::new(),
408 request_timeout: None,
409 kill_grace: Duration::from_secs(2),
410 vfs_budget_bytes: None,
411 overlay: false,
412 }
413 }
414
415 #[cfg(feature = "localfs")]
428 pub fn agent() -> Self {
429 let home = default_sandbox_root();
430 Self {
431 name: "agent".to_string(),
432 vfs_mode: VfsMountMode::Sandboxed { root: None },
433 cwd: home,
434 skip_validation: false,
435 interactive: false,
436 ignore_config: crate::ignore_config::IgnoreConfig::agent(),
437 output_limit: crate::output_limit::OutputLimitConfig::agent(),
438 allow_external_commands: cfg!(feature = "subprocess"),
439 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
440 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
441 nonce_store: None,
442 initial_vars: HashMap::new(),
443 request_timeout: None,
444 kill_grace: Duration::from_secs(2),
445 vfs_budget_bytes: Some(64 * 1024 * 1024),
446 overlay: false,
447 }
448 }
449
450 #[cfg(feature = "localfs")]
457 pub fn agent_with_root(root: PathBuf) -> Self {
458 Self {
459 name: "agent".to_string(),
460 vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
461 cwd: root,
462 skip_validation: false,
463 interactive: false,
464 ignore_config: crate::ignore_config::IgnoreConfig::agent(),
465 output_limit: crate::output_limit::OutputLimitConfig::agent(),
466 allow_external_commands: cfg!(feature = "subprocess"),
467 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
468 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
469 nonce_store: None,
470 initial_vars: HashMap::new(),
471 request_timeout: None,
472 kill_grace: Duration::from_secs(2),
473 vfs_budget_bytes: Some(64 * 1024 * 1024),
474 overlay: false,
475 }
476 }
477
478 pub fn isolated() -> Self {
483 Self {
484 name: "isolated".to_string(),
485 vfs_mode: VfsMountMode::NoLocal,
486 cwd: PathBuf::from("/"),
487 skip_validation: false,
488 interactive: false,
489 ignore_config: crate::ignore_config::IgnoreConfig::none(),
490 output_limit: crate::output_limit::OutputLimitConfig::none(),
491 allow_external_commands: false,
492 latch_enabled: false,
493 trash_enabled: false,
494 nonce_store: None,
495 initial_vars: HashMap::new(),
496 request_timeout: None,
497 kill_grace: Duration::from_secs(2),
498 vfs_budget_bytes: None,
499 overlay: false,
500 }
501 }
502
503 pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
505 self.vfs_mode = mode;
506 self
507 }
508
509 pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
511 self.cwd = cwd;
512 self
513 }
514
515 pub fn with_skip_validation(mut self, skip: bool) -> Self {
517 self.skip_validation = skip;
518 self
519 }
520
521 pub fn with_interactive(mut self, interactive: bool) -> Self {
523 self.interactive = interactive;
524 self
525 }
526
527 pub fn with_ignore_config(mut self, config: crate::ignore_config::IgnoreConfig) -> Self {
529 self.ignore_config = config;
530 self
531 }
532
533 pub fn with_output_limit(mut self, config: crate::output_limit::OutputLimitConfig) -> Self {
535 self.output_limit = config;
536 self
537 }
538
539 pub fn with_allow_external_commands(mut self, allow: bool) -> Self {
545 self.allow_external_commands = allow;
546 self
547 }
548
549 pub fn with_latch(mut self, enabled: bool) -> Self {
551 self.latch_enabled = enabled;
552 self
553 }
554
555 pub fn with_trash(mut self, enabled: bool) -> Self {
557 self.trash_enabled = enabled;
558 self
559 }
560
561 pub fn with_nonce_store(mut self, store: crate::nonce::NonceStore) -> Self {
566 self.nonce_store = Some(store);
567 self
568 }
569
570 pub fn with_var(mut self, name: impl Into<String>, value: Value) -> Self {
574 self.initial_vars.insert(name.into(), value);
575 self
576 }
577
578 pub fn with_initial_vars(mut self, vars: HashMap<String, Value>) -> Self {
580 self.initial_vars = vars;
581 self
582 }
583
584 pub fn with_vars(mut self, vars: HashMap<String, Value>) -> Self {
586 self.initial_vars.extend(vars);
587 self
588 }
589
590 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
595 self.request_timeout = Some(timeout);
596 self
597 }
598
599 pub fn with_kill_grace(mut self, grace: Duration) -> Self {
601 self.kill_grace = grace;
602 self
603 }
604
605 pub fn with_vfs_budget(mut self, bytes: u64) -> Self {
614 self.vfs_budget_bytes = Some(bytes);
615 self
616 }
617
618 pub fn without_vfs_budget(mut self) -> Self {
623 self.vfs_budget_bytes = None;
624 self
625 }
626
627 pub fn with_overlay(mut self, overlay: bool) -> Self {
634 self.overlay = overlay;
635 self
636 }
637}
638
639#[cfg(all(feature = "localfs", feature = "overlay"))]
646#[derive(Clone)]
647pub struct OverlayHandle {
648 pub fs: Arc<OverlayFs>,
651 pub mount_path: PathBuf,
653 pub commit_root: PathBuf,
655}
656
657pub struct Kernel {
662 name: String,
664 scope: RwLock<Scope>,
666 tools: Arc<ToolRegistry>,
668 user_tools: RwLock<HashMap<String, ToolDef>>,
670 vfs: Arc<VfsRouter>,
672 jobs: Arc<JobManager>,
674 runner: PipelineRunner,
676 exec_ctx: RwLock<ExecContext>,
678 skip_validation: bool,
680 interactive: bool,
682 allow_external_commands: bool,
684 vfs_budget: Option<Arc<kaish_vfs::ByteBudget>>,
691 #[cfg(all(feature = "localfs", feature = "overlay"))]
698 overlay_handle: Option<Arc<OverlayHandle>>,
699 request_timeout: Option<Duration>,
701 kill_grace: Duration,
703 stderr_receiver: tokio::sync::Mutex<StderrReceiver>,
708 cancel_token: std::sync::Mutex<tokio_util::sync::CancellationToken>,
714 #[cfg(all(unix, feature = "subprocess"))]
716 terminal_state: Option<Arc<crate::terminal::TerminalState>>,
717 self_weak: std::sync::OnceLock<std::sync::Weak<Self>>,
722 bg_job_id: Option<crate::scheduler::JobId>,
728 execute_lock: tokio::sync::Mutex<()>,
734}
735
736struct VfsSetupResult {
738 vfs: VfsRouter,
739 budget: Option<Arc<ByteBudget>>,
740 #[cfg(all(feature = "localfs", feature = "overlay"))]
741 overlay_handle: Option<Arc<OverlayHandle>>,
742}
743
744impl Kernel {
745 pub fn new(config: KernelConfig) -> Result<Self> {
747 let mut setup = Self::setup_vfs(&config)?;
748 let jobs = Arc::new(JobManager::new());
749
750 setup.vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
752
753 #[cfg(all(feature = "localfs", feature = "overlay"))]
754 let overlay_handle = setup.overlay_handle.take();
755
756 let kernel = Self::assemble(config, setup.vfs, jobs, false, setup.budget, |_| {}, |vfs_ref, tools| {
760 ExecContext::with_vfs_and_tools(vfs_ref.clone(), tools.clone())
761 })?;
762
763 #[cfg(all(feature = "localfs", feature = "overlay"))]
764 {
765 let mut kernel = kernel;
766 kernel.overlay_handle = overlay_handle;
767 if let Some(ref handle) = kernel.overlay_handle {
769 kernel.exec_ctx.get_mut().overlay_handle = Some(Arc::clone(handle));
770 }
771 return Ok(kernel);
772 }
773
774 #[allow(unreachable_code)]
775 Ok(kernel)
776 }
777
778 fn setup_vfs(config: &KernelConfig) -> Result<VfsSetupResult> {
792 let mut vfs = VfsRouter::new();
793
794 let budget: Option<Arc<ByteBudget>> = config
797 .vfs_budget_bytes
798 .map(|bytes| Arc::new(ByteBudget::labeled(bytes, "vfs-memory")));
799
800 fn mem(budget: &Option<Arc<ByteBudget>>) -> MemoryFs {
802 match budget {
803 Some(b) => MemoryFs::with_budget(Arc::clone(b)),
804 None => MemoryFs::new(),
805 }
806 }
807
808 #[cfg(all(feature = "localfs", feature = "overlay"))]
810 let mut overlay_handle: Option<Arc<OverlayHandle>> = None;
811
812 match &config.vfs_mode {
813 #[cfg(feature = "localfs")]
814 VfsMountMode::Passthrough => {
815 #[cfg(feature = "overlay")]
816 if config.overlay {
817 let lower = Arc::new(LocalFs::read_only(PathBuf::from("/")));
819 let overlay_fs = Arc::new(match &budget {
820 Some(b) => OverlayFs::over_with_budget(lower, Arc::clone(b)),
821 None => OverlayFs::over(lower),
822 });
823 let handle = Arc::new(OverlayHandle {
824 fs: Arc::clone(&overlay_fs),
825 mount_path: PathBuf::from("/"),
826 commit_root: PathBuf::from("/"),
827 });
828 vfs.mount_arc("/", overlay_fs as Arc<dyn kaish_vfs::Filesystem>);
829 overlay_handle = Some(handle);
830 } else {
831 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
833 }
834 #[cfg(not(feature = "overlay"))]
835 {
836 if config.overlay {
837 return Err(anyhow::anyhow!(
838 "overlay=true requires the `overlay` feature, but this build \
839 was compiled without it. Recompile with --features overlay \
840 (or the default feature set) to enable overlay mode."
841 ));
842 }
843 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
845 }
846 vfs.mount("/v", mem(&budget));
848 }
849 #[cfg(feature = "localfs")]
850 VfsMountMode::Sandboxed { root } => {
851 vfs.mount("/", mem(&budget));
857 vfs.mount("/v", mem(&budget));
858
859 vfs.mount("/dev", DevFs::new());
862
863 vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
865
866 let runtime = crate::paths::xdg_runtime_dir();
868 if runtime.exists() {
869 let runtime_str = runtime.to_string_lossy().to_string();
870 vfs.mount(&runtime_str, LocalFs::new(runtime));
871 }
872
873 let local_root = root.clone().unwrap_or_else(|| {
875 std::env::var("HOME")
876 .map(PathBuf::from)
877 .unwrap_or_else(|_| PathBuf::from("/"))
878 });
879
880 let mount_point = local_root.to_string_lossy().to_string();
881
882 #[cfg(feature = "overlay")]
883 if config.overlay {
884 let lower = Arc::new(LocalFs::read_only(local_root.clone()));
886 let overlay_fs = Arc::new(match &budget {
887 Some(b) => OverlayFs::over_with_budget(lower, Arc::clone(b)),
888 None => OverlayFs::over(lower),
889 });
890 let handle = Arc::new(OverlayHandle {
891 fs: Arc::clone(&overlay_fs),
892 mount_path: PathBuf::from(&mount_point),
893 commit_root: local_root,
894 });
895 vfs.mount_arc(&mount_point, overlay_fs as Arc<dyn kaish_vfs::Filesystem>);
896 overlay_handle = Some(handle);
897 } else {
898 vfs.mount(&mount_point, LocalFs::new(local_root));
902 }
903 #[cfg(not(feature = "overlay"))]
904 {
905 if config.overlay {
906 return Err(anyhow::anyhow!(
907 "overlay=true requires the `overlay` feature, but this build \
908 was compiled without it. Recompile with --features overlay \
909 (or the default feature set) to enable overlay mode."
910 ));
911 }
912 vfs.mount(&mount_point, LocalFs::new(local_root));
914 }
915 }
916 VfsMountMode::NoLocal => {
917 if config.overlay {
918 return Err(anyhow::anyhow!(
919 "overlay=true is incompatible with VfsMountMode::NoLocal: \
920 everything is already virtual, there is no real lower layer \
921 to wrap. Use with_overlay(false) or switch to a Passthrough \
922 or Sandboxed VFS mode."
923 ));
924 }
925 vfs.mount("/", mem(&budget));
927 vfs.mount("/tmp", mem(&budget));
928 vfs.mount("/v", mem(&budget));
929 vfs.mount("/dev", DevFs::new());
931 }
932 }
933
934 Ok(VfsSetupResult {
935 vfs,
936 budget,
937 #[cfg(all(feature = "localfs", feature = "overlay"))]
938 overlay_handle,
939 })
940 }
941
942 pub fn transient() -> Result<Self> {
944 Self::new(KernelConfig::transient())
945 }
946
947 pub fn with_backend(
981 backend: Arc<dyn KernelBackend>,
982 config: KernelConfig,
983 configure_vfs: impl FnOnce(&mut VfsRouter),
984 configure_tools: impl FnOnce(&mut ToolRegistry),
985 ) -> Result<Self> {
986 use crate::backend::VirtualOverlayBackend;
987
988 if config.overlay {
992 return Err(anyhow::anyhow!(
993 "overlay=true is incompatible with Kernel::with_backend: the embedder \
994 controls the VFS; the kernel cannot wrap it with an OverlayFs without \
995 bypassing the embedder's storage semantics. Use KernelConfig::with_overlay(false)."
996 ));
997 }
998
999 let mut vfs = VfsRouter::new();
1000 let jobs = Arc::new(JobManager::new());
1001
1002 let vfs_budget: Option<Arc<ByteBudget>> = config
1006 .vfs_budget_bytes
1007 .map(|bytes| Arc::new(ByteBudget::labeled(bytes, "vfs-memory")));
1008
1009 vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
1010 let blobs_fs = match &vfs_budget {
1011 Some(b) => MemoryFs::with_budget(Arc::clone(b)),
1012 None => MemoryFs::new(),
1013 };
1014 vfs.mount("/v/blobs", blobs_fs);
1015
1016 configure_vfs(&mut vfs);
1018
1019 Self::assemble(config, vfs, jobs, true, vfs_budget, configure_tools, |vfs_arc: &Arc<VfsRouter>, _: &Arc<ToolRegistry>| {
1024 let overlay: Arc<dyn KernelBackend> =
1025 Arc::new(VirtualOverlayBackend::new(backend, vfs_arc.clone()));
1026 ExecContext::with_backend(overlay)
1027 })
1028 }
1029
1030 fn assemble(
1036 config: KernelConfig,
1037 mut vfs: VfsRouter,
1038 jobs: Arc<JobManager>,
1039 no_host_filesystem: bool,
1040 vfs_budget: Option<Arc<ByteBudget>>,
1041 configure_tools: impl FnOnce(&mut ToolRegistry),
1042 make_ctx: impl FnOnce(&Arc<VfsRouter>, &Arc<ToolRegistry>) -> ExecContext,
1043 ) -> Result<Self> {
1044 let no_host_side_channel =
1057 no_host_filesystem || matches!(config.vfs_mode, VfsMountMode::NoLocal);
1058
1059 let KernelConfig { name, cwd, skip_validation, interactive, ignore_config, mut output_limit, allow_external_commands, latch_enabled, trash_enabled, nonce_store, initial_vars, request_timeout, kill_grace, .. } = config;
1060
1061 if no_host_side_channel {
1062 output_limit.set_spill_mode(crate::output_limit::SpillMode::Memory);
1063 jobs.set_persist_output_files(false);
1064 }
1065
1066 let mut tools = ToolRegistry::new();
1067 register_builtins(&mut tools);
1068 configure_tools(&mut tools);
1069 let tools = Arc::new(tools);
1070
1071 vfs.mount("/v/bin", BuiltinFs::new(tools.clone()));
1073
1074 let vfs = Arc::new(vfs);
1075
1076 let runner = PipelineRunner::new(tools.clone());
1077
1078 let (stderr_writer, stderr_receiver) = stderr_stream();
1079
1080 let mut exec_ctx = make_ctx(&vfs, &tools);
1081 exec_ctx.set_cwd(cwd);
1082 exec_ctx.set_job_manager(jobs.clone());
1083 exec_ctx.set_tool_schemas(tools.schemas());
1084 exec_ctx.set_tools(tools.clone());
1085 #[cfg(feature = "os-integration")]
1086 exec_ctx.set_trash_backend(Arc::new(crate::trash_system::SystemTrash));
1087 exec_ctx.stderr = Some(stderr_writer);
1088 exec_ctx.ignore_config = ignore_config;
1089 exec_ctx.output_limit = output_limit;
1090 exec_ctx.allow_external_commands = allow_external_commands;
1091 exec_ctx.vfs_budget = vfs_budget.clone();
1092 if let Some(store) = nonce_store {
1093 exec_ctx.nonce_store = store;
1094 }
1095
1096 Ok(Self {
1097 name,
1098 scope: RwLock::new({
1099 let mut scope = Scope::new();
1100 scope.set_pid(KERNEL_COUNTER.fetch_add(1, Ordering::Relaxed));
1101 for (name, value) in initial_vars {
1110 scope.set_exported(name, value);
1111 }
1112 scope.set_latch_enabled(latch_enabled);
1113 scope.set_trash_enabled(trash_enabled);
1114 scope
1115 }),
1116 tools,
1117 user_tools: RwLock::new(HashMap::new()),
1118 vfs,
1119 jobs,
1120 runner,
1121 exec_ctx: RwLock::new(exec_ctx),
1122 skip_validation,
1123 interactive,
1124 allow_external_commands,
1125 vfs_budget,
1126 request_timeout,
1127 kill_grace,
1128 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
1129 cancel_token: std::sync::Mutex::new(tokio_util::sync::CancellationToken::new()),
1130 #[cfg(all(unix, feature = "subprocess"))]
1131 terminal_state: None,
1132 self_weak: std::sync::OnceLock::new(),
1133 execute_lock: tokio::sync::Mutex::new(()),
1134 bg_job_id: None,
1135 #[cfg(all(feature = "localfs", feature = "overlay"))]
1139 overlay_handle: None,
1140 })
1141 }
1142
1143 pub fn name(&self) -> &str {
1145 &self.name
1146 }
1147
1148 pub fn into_arc(self) -> Arc<Self> {
1155 let arc = Arc::new(self);
1156 let _ = arc.self_weak.set(Arc::downgrade(&arc));
1157 arc
1158 }
1159
1160 pub async fn fork(&self) -> Arc<Self> {
1189 self.fork_inner(tokio_util::sync::CancellationToken::new(), self.bg_job_id)
1190 .await
1191 }
1192
1193 pub async fn fork_attached(&self) -> Arc<Self> {
1201 let child_token = {
1202 #[allow(clippy::expect_used)]
1203 let parent = self.cancel_token.lock().expect("cancel_token poisoned");
1204 parent.child_token()
1205 };
1206 self.fork_inner(child_token, self.bg_job_id).await
1207 }
1208
1209 pub async fn fork_for_background(
1214 &self,
1215 cancel: tokio_util::sync::CancellationToken,
1216 job_id: crate::scheduler::JobId,
1217 ) -> Arc<Self> {
1218 self.fork_inner(cancel, Some(job_id)).await
1219 }
1220
1221 async fn fork_inner(
1224 &self,
1225 cancel: tokio_util::sync::CancellationToken,
1226 bg_job_id: Option<crate::scheduler::JobId>,
1227 ) -> Arc<Self> {
1228 let scope_snapshot = self.scope.read().await.clone();
1229 let user_tools_snapshot = self.user_tools.read().await.clone();
1230
1231 let mut fork_ctx = {
1235 let parent_ctx = self.exec_ctx.read().await;
1236 parent_ctx.child_for_pipeline()
1237 };
1238 let (stderr_writer, stderr_receiver) = stderr_stream();
1239 fork_ctx.stderr = Some(stderr_writer);
1240 fork_ctx.dispatcher = None;
1243 fork_ctx.interactive = false;
1244 fork_ctx.cancel = cancel.clone();
1245 #[cfg(all(unix, feature = "subprocess"))]
1246 {
1247 fork_ctx.terminal_state = None;
1248 }
1249
1250 let fork = Self {
1251 name: format!("{}:fork", self.name),
1252 scope: RwLock::new(scope_snapshot),
1253 tools: Arc::clone(&self.tools),
1254 user_tools: RwLock::new(user_tools_snapshot),
1255 vfs: Arc::clone(&self.vfs),
1256 jobs: Arc::clone(&self.jobs),
1257 runner: self.runner.clone(),
1258 exec_ctx: RwLock::new(fork_ctx),
1259 skip_validation: self.skip_validation,
1260 interactive: false,
1262 allow_external_commands: self.allow_external_commands,
1263 vfs_budget: self.vfs_budget.clone(),
1267 request_timeout: self.request_timeout,
1268 kill_grace: self.kill_grace,
1269 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
1270 cancel_token: std::sync::Mutex::new(cancel),
1271 #[cfg(all(unix, feature = "subprocess"))]
1272 terminal_state: None,
1273 self_weak: std::sync::OnceLock::new(),
1274 execute_lock: tokio::sync::Mutex::new(()),
1275 bg_job_id,
1276 #[cfg(all(feature = "localfs", feature = "overlay"))]
1280 overlay_handle: self.overlay_handle.clone(),
1281 };
1282
1283 fork.into_arc()
1284 }
1285
1286 pub fn dispatcher(&self) -> Option<Arc<dyn CommandDispatcher>> {
1291 self.self_weak
1292 .get()
1293 .and_then(|weak| weak.upgrade())
1294 .map(|arc| arc as Arc<dyn CommandDispatcher>)
1295 }
1296
1297 #[cfg(all(unix, feature = "subprocess"))]
1302 pub fn init_terminal(&mut self) {
1303 if !self.interactive {
1304 return;
1305 }
1306 match crate::terminal::TerminalState::init() {
1307 Ok(state) => {
1308 let state = Arc::new(state);
1309 self.terminal_state = Some(state.clone());
1310 self.exec_ctx.get_mut().terminal_state = Some(state);
1312 tracing::debug!("terminal job control initialized");
1313 }
1314 Err(e) => {
1315 tracing::warn!("failed to initialize terminal job control: {}", e);
1316 }
1317 }
1318 }
1319
1320 pub fn set_trash_backend(&mut self, backend: Option<Arc<dyn crate::trash::TrashBackend>>) {
1328 self.exec_ctx.get_mut().trash_backend = backend;
1329 }
1330
1331 pub fn cancel(&self) {
1337 #[allow(clippy::expect_used)]
1338 let token = self.cancel_token.lock().expect("cancel_token poisoned");
1339 token.cancel();
1340 }
1341
1342 pub fn is_cancelled(&self) -> bool {
1344 #[allow(clippy::expect_used)]
1345 let token = self.cancel_token.lock().expect("cancel_token poisoned");
1346 token.is_cancelled()
1347 }
1348
1349 fn reset_cancel(&self) -> tokio_util::sync::CancellationToken {
1351 #[allow(clippy::expect_used)]
1352 let mut token = self.cancel_token.lock().expect("cancel_token poisoned");
1353 if token.is_cancelled() {
1354 *token = tokio_util::sync::CancellationToken::new();
1355 }
1356 token.clone()
1357 }
1358
1359 async fn acquire_execute_lock(&self) -> tokio::sync::MutexGuard<'_, ()> {
1365 match self.execute_lock.try_lock() {
1366 Ok(guard) => guard,
1367 Err(_) => {
1368 tracing::warn!(
1369 target: "kaish::kernel::concurrency",
1370 kernel = %self.name,
1371 "execute() contended — serializing concurrent caller; \
1372 use Kernel::fork() for parallelism instead of sharing"
1373 );
1374 self.execute_lock.lock().await
1375 }
1376 }
1377 }
1378
1379 pub async fn execute(&self, input: &str) -> Result<ExecResult> {
1384 self.run_inner(input, ExecuteOptions::default(), None, None).await
1385 }
1386
1387 pub async fn execute_with_options(
1407 &self,
1408 input: &str,
1409 opts: ExecuteOptions,
1410 ) -> Result<ExecResult> {
1411 self.run_inner(input, opts, None, None).await
1412 }
1413
1414 pub async fn execute_with_options_streaming(
1418 &self,
1419 input: &str,
1420 opts: ExecuteOptions,
1421 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1422 ) -> Result<ExecResult> {
1423 self.run_inner(input, opts, None, Some(on_output)).await
1424 }
1425
1426 pub async fn execute_with_pipe_stdin(
1438 &self,
1439 input: &str,
1440 opts: ExecuteOptions,
1441 pipe_stdin: crate::scheduler::PipeReader,
1442 ) -> Result<ExecResult> {
1443 self.run_inner(input, opts, Some(pipe_stdin), None).await
1444 }
1445
1446 pub async fn execute_with_pipe_stdin_streaming(
1450 &self,
1451 input: &str,
1452 opts: ExecuteOptions,
1453 pipe_stdin: crate::scheduler::PipeReader,
1454 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1455 ) -> Result<ExecResult> {
1456 self.run_inner(input, opts, Some(pipe_stdin), Some(on_output)).await
1457 }
1458
1459 #[deprecated(note = "use Kernel::execute_with_options with ExecuteOptions::with_vars")]
1465 pub async fn execute_with_vars(
1466 &self,
1467 input: &str,
1468 vars: HashMap<String, Value>,
1469 ) -> Result<ExecResult> {
1470 self.run_inner(input, ExecuteOptions::new().with_vars(vars), None, None).await
1471 }
1472
1473 #[deprecated(note = "use Kernel::execute_with_options_streaming")]
1478 pub async fn execute_streaming(
1479 &self,
1480 input: &str,
1481 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1482 ) -> Result<ExecResult> {
1483 self.run_inner(input, ExecuteOptions::default(), None, Some(on_output)).await
1484 }
1485
1486 async fn run_inner(
1497 &self,
1498 input: &str,
1499 opts: ExecuteOptions,
1500 pipe_stdin: Option<crate::scheduler::PipeReader>,
1501 on_output: Option<&mut (dyn FnMut(&ExecResult) + Send)>,
1502 ) -> Result<ExecResult> {
1503 use opentelemetry::context::FutureExt;
1504
1505 let embedder_baggage = opts.baggage.clone();
1508
1509 let result = match crate::telemetry::extract_parent(&opts) {
1510 Some(parent) => self
1511 .execute_with_options_inner(input, opts, pipe_stdin, on_output)
1512 .with_context(parent)
1513 .await,
1514 None => self.execute_with_options_inner(input, opts, pipe_stdin, on_output).await,
1515 };
1516
1517 result.map(|mut r| {
1518 crate::telemetry::merge_egress_baggage(&mut r, embedder_baggage);
1519 r
1520 })
1521 }
1522
1523 #[tracing::instrument(level = "info", skip(self, opts, pipe_stdin, on_output), fields(input_len = input.len()))]
1527 async fn execute_with_options_inner(
1528 &self,
1529 input: &str,
1530 opts: ExecuteOptions,
1531 pipe_stdin: Option<crate::scheduler::PipeReader>,
1532 on_output: Option<&mut (dyn FnMut(&ExecResult) + Send)>,
1533 ) -> Result<ExecResult> {
1534 let _guard = self.acquire_execute_lock().await;
1535
1536 let internal = self.reset_cancel();
1544 let (effective_cancel, watcher_handle): (
1549 tokio_util::sync::CancellationToken,
1550 Option<tokio::task::JoinHandle<()>>,
1551 ) = if let Some(ext) = opts.cancel_token {
1552 let combined = tokio_util::sync::CancellationToken::new();
1553 let combined_writer = combined.clone();
1554 let i = internal.clone();
1555 let handle = tokio::spawn(async move {
1556 tokio::select! {
1557 _ = i.cancelled() => combined_writer.cancel(),
1558 _ = ext.cancelled() => combined_writer.cancel(),
1559 }
1560 });
1561 (combined, Some(handle))
1562 } else {
1563 (internal, None)
1564 };
1565
1566 let timeout = opts.timeout.or(self.request_timeout);
1568
1569 if timeout == Some(Duration::ZERO) {
1571 if let Some(h) = watcher_handle {
1572 h.abort();
1573 }
1574 return Ok(ExecResult::failure(124, "timeout: timed out after 0s".to_string()));
1575 }
1576
1577 struct VarsFrameGuard<'a> {
1581 kernel: &'a Kernel,
1582 newly_exported: Vec<String>,
1583 }
1584 impl Drop for VarsFrameGuard<'_> {
1585 fn drop(&mut self) {
1586 let Ok(mut scope) = self.kernel.scope.try_write() else {
1595 tracing::error!(
1596 "vars frame guard: scope lock unexpectedly busy; \
1597 skipping pop_frame to avoid runtime deadlock — \
1598 transient vars may leak"
1599 );
1600 return;
1601 };
1602 scope.pop_frame();
1603 for name in self.newly_exported.drain(..) {
1604 scope.unexport(&name);
1605 }
1606 }
1607 }
1608
1609 struct CwdGuard<'a> {
1613 kernel: &'a Kernel,
1614 saved: PathBuf,
1615 }
1616 impl Drop for CwdGuard<'_> {
1617 fn drop(&mut self) {
1618 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1619 tracing::error!(
1620 "cwd guard: exec_ctx lock unexpectedly busy; \
1621 skipping cwd restore — kernel cwd may be wrong for next call"
1622 );
1623 return;
1624 };
1625 ec.cwd = std::mem::take(&mut self.saved);
1626 }
1627 }
1628 let _cwd_guard: Option<CwdGuard<'_>> = if let Some(new_cwd) = opts.cwd {
1629 let mut ec = self.exec_ctx.write().await;
1630 let saved = std::mem::replace(&mut ec.cwd, new_cwd);
1631 drop(ec);
1632 Some(CwdGuard { kernel: self, saved })
1633 } else {
1634 None
1635 };
1636
1637 struct StdinGuard<'a> {
1643 kernel: &'a Kernel,
1644 saved: Option<String>,
1645 }
1646 impl Drop for StdinGuard<'_> {
1647 fn drop(&mut self) {
1648 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1649 tracing::error!(
1650 "stdin guard: exec_ctx lock unexpectedly busy; \
1651 skipping stdin restore — stale stdin may leak to next call"
1652 );
1653 return;
1654 };
1655 ec.stdin = self.saved.take();
1656 }
1657 }
1658 let _stdin_guard: Option<StdinGuard<'_>> = if let Some(stdin) = opts.stdin {
1659 let mut ec = self.exec_ctx.write().await;
1660 let saved = ec.stdin.replace(stdin);
1661 drop(ec);
1662 Some(StdinGuard { kernel: self, saved })
1663 } else {
1664 None
1665 };
1666
1667 struct PipeStdinGuard<'a> {
1673 kernel: &'a Kernel,
1674 saved: Option<crate::scheduler::PipeReader>,
1675 }
1676 impl Drop for PipeStdinGuard<'_> {
1677 fn drop(&mut self) {
1678 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1679 tracing::error!(
1680 "pipe stdin guard: exec_ctx lock unexpectedly busy; \
1681 skipping restore — stale pipe stdin may leak to next call"
1682 );
1683 return;
1684 };
1685 ec.pipe_stdin = self.saved.take();
1686 }
1687 }
1688 let _pipe_stdin_guard: Option<PipeStdinGuard<'_>> = if let Some(reader) = pipe_stdin {
1689 let mut ec = self.exec_ctx.write().await;
1690 let saved = ec.pipe_stdin.replace(reader);
1691 drop(ec);
1692 Some(PipeStdinGuard { kernel: self, saved })
1693 } else {
1694 None
1695 };
1696
1697 let _vars_guard: Option<VarsFrameGuard<'_>> = if !opts.vars.is_empty() {
1698 let mut scope = self.scope.write().await;
1699 scope.push_frame();
1700 let mut newly = Vec::with_capacity(opts.vars.len());
1701 for (name, value) in opts.vars {
1702 if !scope.is_exported(&name) {
1703 newly.push(name.clone());
1704 }
1705 scope.set_exported(name, value);
1706 }
1707 drop(scope);
1708 Some(VarsFrameGuard { kernel: self, newly_exported: newly })
1709 } else {
1710 None
1711 };
1712
1713 {
1720 #[allow(clippy::expect_used)]
1721 let mut cur = self.cancel_token.lock().expect("cancel_token poisoned");
1722 *cur = effective_cancel.clone();
1723 }
1724
1725 let watchdog = timeout.map(|d| Arc::new(crate::watchdog::Watchdog::new(d)));
1730 {
1731 let mut ec = self.exec_ctx.write().await;
1732 ec.watchdog = watchdog.clone();
1733 }
1734
1735 let mut noop_cb: Box<dyn FnMut(&ExecResult) + Send> = Box::new(|_| {});
1739 let cb_ref: &mut (dyn FnMut(&ExecResult) + Send) = match on_output {
1740 Some(cb) => cb,
1741 None => &mut *noop_cb,
1742 };
1743
1744 let result = if let Some(d) = timeout {
1745 #[allow(clippy::expect_used)]
1746 let watchdog = watchdog.clone().expect("watchdog constructed when timeout is set");
1747 let elapsed = Arc::new(std::sync::atomic::AtomicBool::new(false));
1748 let timer = tokio::spawn(watchdog.run(elapsed.clone(), effective_cancel.clone()));
1749 let r = self.execute_streaming_inner(input, cb_ref).await;
1750 timer.abort();
1751 match r {
1752 Ok(mut res) => {
1753 if elapsed.load(std::sync::atomic::Ordering::SeqCst) {
1754 res.code = 124;
1755 if res.err.is_empty() {
1756 res.err = format!("timeout: timed out after {:?}", d);
1757 }
1758 }
1759 Ok(res)
1760 }
1761 Err(e) => Err(e),
1762 }
1763 } else {
1764 self.execute_streaming_inner(input, cb_ref).await
1765 };
1766
1767 {
1772 #[allow(clippy::expect_used)]
1773 let mut cur = self.cancel_token.lock().expect("cancel_token poisoned");
1774 *cur = tokio_util::sync::CancellationToken::new();
1775 }
1776
1777 {
1781 let mut ec = self.exec_ctx.write().await;
1782 ec.watchdog = None;
1783 }
1784
1785 if let Some(h) = watcher_handle {
1788 h.abort();
1789 }
1790
1791 result
1794 }
1795
1796 async fn execute_streaming_inner(
1802 &self,
1803 input: &str,
1804 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1805 ) -> Result<ExecResult> {
1806 let program = parse(input).map_err(|errors| {
1807 let msg = errors
1808 .iter()
1809 .map(|e| e.format(input))
1810 .collect::<Vec<_>>()
1811 .join("\n");
1812 anyhow::anyhow!("parse error:\n{}", msg)
1813 })?;
1814
1815 {
1817 let scope = self.scope.read().await;
1818 if scope.show_ast() {
1819 let output = format!("{:#?}\n", program);
1820 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(output)));
1821 }
1822 }
1823
1824 if !self.skip_validation {
1826 let user_tools = self.user_tools.read().await;
1827 let validator = Validator::new(&self.tools, &user_tools);
1828 let issues = validator.validate(&program);
1829
1830 let errors: Vec<_> = issues
1832 .iter()
1833 .filter(|i| i.severity == Severity::Error)
1834 .collect();
1835
1836 if !errors.is_empty() {
1837 let error_msg = errors
1838 .iter()
1839 .map(|e| e.format(input))
1840 .collect::<Vec<_>>()
1841 .join("\n");
1842 return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
1843 }
1844
1845 for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
1847 tracing::trace!("validation: {}", warning.format(input));
1848 }
1849 }
1850
1851 let mut result = ExecResult::success("");
1852
1853 let cancel = self.reset_cancel();
1855
1856 for stmt in program.statements {
1857 if matches!(stmt, Stmt::Empty) {
1858 continue;
1859 }
1860
1861 if cancel.is_cancelled() {
1863 result.code = 130;
1864 return Ok(result);
1865 }
1866
1867 let flow = self.execute_stmt_flow(&stmt).await?;
1868
1869 let drained_stderr = {
1873 let mut receiver = self.stderr_receiver.lock().await;
1874 receiver.drain_lossy()
1875 };
1876
1877 match flow {
1878 ControlFlow::Normal(mut r) => {
1879 if !drained_stderr.is_empty() {
1880 if !r.err.is_empty() && !r.err.ends_with('\n') {
1881 r.err.push('\n');
1882 }
1883 let combined = format!("{}{}", drained_stderr, r.err);
1885 r.err = combined;
1886 }
1887 on_output(&r);
1888 let last_output = r.output().cloned();
1892 accumulate_result(&mut result, &r);
1893 result.set_output(last_output);
1894 }
1895 ControlFlow::Exit { code } => {
1896 if !drained_stderr.is_empty() {
1897 result.err.push_str(&drained_stderr);
1898 }
1899 result.code = code;
1900 return Ok(result);
1901 }
1902 ControlFlow::Return { mut value } => {
1903 if !drained_stderr.is_empty() {
1904 value.err = format!("{}{}", drained_stderr, value.err);
1905 }
1906 on_output(&value);
1907 result = value;
1908 }
1909 ControlFlow::Break { result: mut r, .. } | ControlFlow::Continue { result: mut r, .. } => {
1910 if !drained_stderr.is_empty() {
1911 r.err = format!("{}{}", drained_stderr, r.err);
1912 }
1913 on_output(&r);
1914 result = r;
1915 }
1916 }
1917 }
1918
1919 Ok(result)
1920 }
1921
1922 fn execute_stmt_flow<'a>(
1924 &'a self,
1925 stmt: &'a Stmt,
1926 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + Send + 'a>> {
1927 use tracing::Instrument;
1928 let span = tracing::debug_span!("execute_stmt_flow", stmt_type = %stmt.kind_name());
1929 Box::pin(async move {
1930 match stmt {
1931 Stmt::Assignment(assign) => {
1932 let value = self.eval_expr_async(&assign.value).await
1934 .context("failed to evaluate assignment")?;
1935 let mut scope = self.scope.write().await;
1936 if assign.local {
1937 scope.set(&assign.name, value.clone());
1939 } else {
1940 scope.set_global(&assign.name, value.clone());
1942 }
1943 drop(scope);
1944
1945 Ok(ControlFlow::ok(ExecResult::success("")))
1947 }
1948 Stmt::Command(cmd) => {
1949 let pipeline = crate::ast::Pipeline {
1952 commands: vec![cmd.clone()],
1953 background: false,
1954 };
1955 let result = self.execute_pipeline(&pipeline).await?;
1956 self.update_last_result(&result).await;
1957
1958 if !result.ok() {
1960 let scope = self.scope.read().await;
1961 if scope.error_exit_enabled() {
1962 return Ok(ControlFlow::exit_code(result.code));
1963 }
1964 }
1965
1966 Ok(ControlFlow::ok(result))
1967 }
1968 Stmt::Pipeline(pipeline) => {
1969 let result = self.execute_pipeline(pipeline).await?;
1970 self.update_last_result(&result).await;
1971
1972 if !result.ok() {
1974 let scope = self.scope.read().await;
1975 if scope.error_exit_enabled() {
1976 return Ok(ControlFlow::exit_code(result.code));
1977 }
1978 }
1979
1980 Ok(ControlFlow::ok(result))
1981 }
1982 Stmt::If(if_stmt) => {
1983 let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
1985
1986 let branch = if is_truthy(&cond_value) {
1987 &if_stmt.then_branch
1988 } else {
1989 if_stmt.else_branch.as_deref().unwrap_or(&[])
1990 };
1991
1992 let mut result = ExecResult::success("");
1993 for stmt in branch {
1994 let flow = self.execute_stmt_flow(stmt).await?;
1995 match flow {
1996 ControlFlow::Normal(r) => {
1997 accumulate_result(&mut result, &r);
1998 self.drain_stderr_into(&mut result).await;
1999 }
2000 other => {
2001 self.drain_stderr_into(&mut result).await;
2002 return Ok(other);
2003 }
2004 }
2005 }
2006 Ok(ControlFlow::ok(result))
2007 }
2008 Stmt::For(for_loop) => {
2009 let mut items: Vec<Value> = Vec::new();
2012 for item_expr in &for_loop.items {
2013 if let Expr::GlobPattern(pattern) = item_expr {
2015 let glob_enabled = {
2016 let scope = self.scope.read().await;
2017 scope.glob_enabled()
2018 };
2019 if glob_enabled {
2020 let (paths, cwd) = {
2021 let ctx = self.exec_ctx.read().await;
2022 let paths = ctx.expand_glob(pattern).await
2023 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
2024 let cwd = ctx.resolve_path(".");
2025 (paths, cwd)
2026 };
2027 if paths.is_empty() {
2028 return Err(anyhow::anyhow!("no matches: {}", pattern));
2029 }
2030 for path in paths {
2031 let display = if !pattern.starts_with('/') {
2032 path.strip_prefix(&cwd)
2033 .unwrap_or(&path)
2034 .to_string_lossy().into_owned()
2035 } else {
2036 path.to_string_lossy().into_owned()
2037 };
2038 items.push(Value::String(display));
2039 }
2040 continue;
2041 }
2042 }
2043 let from_command_subst = matches!(item_expr, Expr::CommandSubst(_));
2049 let item = self.eval_expr_async(item_expr).await?;
2050 match item {
2051 Value::Json(serde_json::Value::Array(arr)) => {
2054 for elem in arr {
2055 items.push(json_to_value(elem));
2056 }
2057 }
2058 Value::String(s) if from_command_subst => {
2066 let trimmed = s.trim_end_matches(['\n', '\r']);
2067 if trimmed.is_empty() {
2068 continue;
2069 }
2070 if trimmed.contains('\n') {
2071 for line in trimmed.split('\n') {
2072 let line = line.trim_end_matches('\r');
2073 items.push(Value::String(line.to_string()));
2074 }
2075 } else {
2076 items.push(Value::String(trimmed.to_string()));
2077 }
2078 }
2079 Value::Bytes(_) => {
2082 anyhow::bail!(
2083 "for: cannot iterate over binary data — decode it \
2084 (base64/xxd) first"
2085 );
2086 }
2087 other => items.push(other),
2089 }
2090 }
2091
2092 let mut result = ExecResult::success("");
2093 {
2094 let mut scope = self.scope.write().await;
2095 scope.push_frame();
2096 }
2097
2098 'outer: for item in items {
2099 if self.is_cancelled() {
2101 let mut scope = self.scope.write().await;
2102 scope.pop_frame();
2103 result.code = 130;
2104 return Ok(ControlFlow::ok(result));
2105 }
2106 {
2107 let mut scope = self.scope.write().await;
2108 scope.set(&for_loop.variable, item);
2109 }
2110 for stmt in &for_loop.body {
2111 let mut flow = match self.execute_stmt_flow(stmt).await {
2112 Ok(f) => f,
2113 Err(e) => {
2114 let mut scope = self.scope.write().await;
2115 scope.pop_frame();
2116 return Err(e);
2117 }
2118 };
2119 self.drain_stderr_into(&mut result).await;
2120 match &mut flow {
2121 ControlFlow::Normal(r) => {
2122 accumulate_result(&mut result, r);
2123 if !r.ok() {
2124 let scope = self.scope.read().await;
2125 if scope.error_exit_enabled() {
2126 drop(scope);
2127 let mut scope = self.scope.write().await;
2128 scope.pop_frame();
2129 return Ok(ControlFlow::exit_code(r.code));
2130 }
2131 }
2132 }
2133 ControlFlow::Break { .. } => {
2134 if flow.decrement_level() {
2135 accumulate_flow_output(&mut result, &flow);
2136 break 'outer;
2137 }
2138 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2139 let mut scope = self.scope.write().await;
2140 scope.pop_frame();
2141 return Ok(flow);
2142 }
2143 ControlFlow::Continue { .. } => {
2144 if flow.decrement_level() {
2145 accumulate_flow_output(&mut result, &flow);
2146 continue 'outer;
2147 }
2148 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2149 let mut scope = self.scope.write().await;
2150 scope.pop_frame();
2151 return Ok(flow);
2152 }
2153 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
2154 let mut scope = self.scope.write().await;
2155 scope.pop_frame();
2156 return Ok(flow);
2157 }
2158 }
2159 }
2160 }
2161
2162 {
2163 let mut scope = self.scope.write().await;
2164 scope.pop_frame();
2165 }
2166 Ok(ControlFlow::ok(result))
2167 }
2168 Stmt::While(while_loop) => {
2169 let mut result = ExecResult::success("");
2170
2171 'outer: loop {
2172 if self.is_cancelled() {
2175 result.code = 130;
2176 return Ok(ControlFlow::ok(result));
2177 }
2178
2179 let cond_value = self.eval_expr_async(&while_loop.condition).await?;
2180
2181 if !is_truthy(&cond_value) {
2182 break;
2183 }
2184
2185 for stmt in &while_loop.body {
2187 let mut flow = self.execute_stmt_flow(stmt).await?;
2188 self.drain_stderr_into(&mut result).await;
2189 match &mut flow {
2190 ControlFlow::Normal(r) => {
2191 accumulate_result(&mut result, r);
2192 if !r.ok() {
2193 let scope = self.scope.read().await;
2194 if scope.error_exit_enabled() {
2195 return Ok(ControlFlow::exit_code(r.code));
2196 }
2197 }
2198 }
2199 ControlFlow::Break { .. } => {
2200 if flow.decrement_level() {
2201 accumulate_flow_output(&mut result, &flow);
2202 break 'outer;
2203 }
2204 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2205 return Ok(flow);
2206 }
2207 ControlFlow::Continue { .. } => {
2208 if flow.decrement_level() {
2209 accumulate_flow_output(&mut result, &flow);
2210 continue 'outer;
2211 }
2212 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2213 return Ok(flow);
2214 }
2215 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
2216 return Ok(flow);
2217 }
2218 }
2219 }
2220 }
2221
2222 Ok(ControlFlow::ok(result))
2223 }
2224 Stmt::Case(case_stmt) => {
2225 let match_value = {
2227 let value = self.eval_expr_async(&case_stmt.expr).await?;
2228 value_to_string(&value)
2229 };
2230
2231 for branch in &case_stmt.branches {
2233 let matched = branch.patterns.iter().any(|pattern| {
2234 glob_match(pattern, &match_value)
2235 });
2236
2237 if matched {
2238 let mut result = ExecResult::success("");
2240 for stmt in &branch.body {
2241 let flow = self.execute_stmt_flow(stmt).await?;
2242 match flow {
2243 ControlFlow::Normal(r) => {
2244 accumulate_result(&mut result, &r);
2245 self.drain_stderr_into(&mut result).await;
2246 }
2247 other => {
2248 self.drain_stderr_into(&mut result).await;
2249 return Ok(other);
2250 }
2251 }
2252 }
2253 return Ok(ControlFlow::ok(result));
2254 }
2255 }
2256
2257 Ok(ControlFlow::ok(ExecResult::success("")))
2259 }
2260 Stmt::Break(levels) => {
2261 Ok(ControlFlow::break_n(levels.unwrap_or(1)))
2262 }
2263 Stmt::Continue(levels) => {
2264 Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
2265 }
2266 Stmt::Return(expr) => {
2267 let result = if let Some(e) = expr {
2270 let val = self.eval_expr_async(e).await?;
2271 let code = crate::interpreter::value_to_exit_code(&val)
2272 .map_err(|e| anyhow::anyhow!("return: {}", e))?;
2273 ExecResult::from_parts(code, String::new(), String::new(), None)
2274 } else {
2275 ExecResult::success("")
2276 };
2277 Ok(ControlFlow::return_value(result))
2278 }
2279 Stmt::Exit(expr) => {
2280 let code = if let Some(e) = expr {
2281 let val = self.eval_expr_async(e).await?;
2282 crate::interpreter::value_to_exit_code(&val)
2283 .map_err(|e| anyhow::anyhow!("exit: {}", e))?
2284 } else {
2285 0
2286 };
2287 Ok(ControlFlow::exit_code(code))
2288 }
2289 Stmt::ToolDef(tool_def) => {
2290 let mut user_tools = self.user_tools.write().await;
2291 user_tools.insert(tool_def.name.clone(), tool_def.clone());
2292 Ok(ControlFlow::ok(ExecResult::success("")))
2293 }
2294 Stmt::AndChain { left, right } => {
2295 {
2298 let mut scope = self.scope.write().await;
2299 scope.suppress_errexit();
2300 }
2301 let left_flow = match self.execute_stmt_flow(left).await {
2302 Ok(f) => f,
2303 Err(e) => {
2304 let mut scope = self.scope.write().await;
2305 scope.unsuppress_errexit();
2306 return Err(e);
2307 }
2308 };
2309 {
2310 let mut scope = self.scope.write().await;
2311 scope.unsuppress_errexit();
2312 }
2313 match left_flow {
2314 ControlFlow::Normal(mut left_result) => {
2315 self.drain_stderr_into(&mut left_result).await;
2316 self.update_last_result(&left_result).await;
2317 if left_result.ok() {
2318 let right_flow = self.execute_stmt_flow(right).await?;
2319 match right_flow {
2320 ControlFlow::Normal(mut right_result) => {
2321 self.drain_stderr_into(&mut right_result).await;
2322 self.update_last_result(&right_result).await;
2323 let mut combined = left_result;
2324 accumulate_result(&mut combined, &right_result);
2325 Ok(ControlFlow::ok(combined))
2326 }
2327 other => Ok(other),
2328 }
2329 } else {
2330 Ok(ControlFlow::ok(left_result))
2331 }
2332 }
2333 _ => Ok(left_flow),
2334 }
2335 }
2336 Stmt::OrChain { left, right } => {
2337 {
2340 let mut scope = self.scope.write().await;
2341 scope.suppress_errexit();
2342 }
2343 let left_flow = match self.execute_stmt_flow(left).await {
2344 Ok(f) => f,
2345 Err(e) => {
2346 let mut scope = self.scope.write().await;
2347 scope.unsuppress_errexit();
2348 return Err(e);
2349 }
2350 };
2351 {
2352 let mut scope = self.scope.write().await;
2353 scope.unsuppress_errexit();
2354 }
2355 match left_flow {
2356 ControlFlow::Normal(mut left_result) => {
2357 self.drain_stderr_into(&mut left_result).await;
2358 self.update_last_result(&left_result).await;
2359 if !left_result.ok() {
2360 let right_flow = self.execute_stmt_flow(right).await?;
2361 match right_flow {
2362 ControlFlow::Normal(mut right_result) => {
2363 self.drain_stderr_into(&mut right_result).await;
2364 self.update_last_result(&right_result).await;
2365 let mut combined = left_result;
2366 accumulate_result(&mut combined, &right_result);
2367 Ok(ControlFlow::ok(combined))
2368 }
2369 other => Ok(other),
2370 }
2371 } else {
2372 Ok(ControlFlow::ok(left_result))
2373 }
2374 }
2375 _ => Ok(left_flow), }
2377 }
2378 Stmt::Test(test_expr) => {
2379 let is_true = self.eval_test_async(test_expr).await?;
2380 if is_true {
2381 Ok(ControlFlow::ok(ExecResult::success("")))
2382 } else {
2383 Ok(ControlFlow::ok(ExecResult::failure(1, "")))
2384 }
2385 }
2386 Stmt::EnvScoped { assignments, body } => {
2387 {
2394 let mut scope = self.scope.write().await;
2395 scope.push_frame();
2396 }
2397 let mut prior_export: Vec<(String, bool)> =
2398 Vec::with_capacity(assignments.len());
2399 let mut setup_err: Option<anyhow::Error> = None;
2400 for assign in assignments {
2401 match self.eval_expr_async(&assign.value).await {
2402 Ok(value) => {
2403 let mut scope = self.scope.write().await;
2404 prior_export
2405 .push((assign.name.clone(), scope.is_exported(&assign.name)));
2406 scope.set_exported(&assign.name, value);
2407 }
2408 Err(e) => {
2409 setup_err = Some(e);
2410 break;
2411 }
2412 }
2413 }
2414
2415 let flow = if setup_err.is_none() {
2416 self.execute_stmt_flow(body).await
2417 } else {
2418 Ok(ControlFlow::ok(ExecResult::success("")))
2419 };
2420
2421 {
2424 let mut scope = self.scope.write().await;
2425 scope.pop_frame();
2426 for (name, was_exported) in &prior_export {
2427 if !*was_exported {
2428 scope.unexport(name);
2429 }
2430 }
2431 }
2432
2433 match setup_err {
2434 Some(e) => Err(e),
2435 None => flow,
2436 }
2437 }
2438 Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
2439 }
2440 }.instrument(span))
2441 }
2442
2443 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(background = pipeline.background, command_count = pipeline.commands.len()))]
2445 async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
2446 if pipeline.commands.is_empty() {
2447 return Ok(ExecResult::success(""));
2448 }
2449
2450 if pipeline.background {
2452 return self.execute_background(pipeline).await;
2453 }
2454
2455 let (mut ctx, has_pipe_stdin) = {
2463 let ec = self.exec_ctx.read().await;
2464 let scope = self.scope.read().await;
2465 let has_pipe_stdin = ec.pipe_stdin.is_some();
2469 (ExecContext {
2470 backend: ec.backend.clone(),
2471 scope: scope.clone(),
2472 cwd: ec.cwd.clone(),
2473 prev_cwd: ec.prev_cwd.clone(),
2474 stdin: ec.stdin.clone(),
2479 stdin_data: ec.stdin_data.clone(),
2480 stdin_data_rx: None,
2481 pipe_stdin: None,
2482 pipe_stdout: None,
2483 stderr: ec.stderr.clone(),
2484 tool_schemas: ec.tool_schemas.clone(),
2485 tools: ec.tools.clone(),
2486 job_manager: ec.job_manager.clone(),
2487 pipeline_position: PipelinePosition::Only,
2488 interactive: self.interactive,
2489 aliases: ec.aliases.clone(),
2490 ignore_config: ec.ignore_config.clone(),
2491 output_limit: ec.output_limit.clone(),
2492 allow_external_commands: self.allow_external_commands,
2493 nonce_store: ec.nonce_store.clone(),
2494 trash_backend: ec.trash_backend.clone(),
2495 #[cfg(all(unix, feature = "subprocess"))]
2496 terminal_state: ec.terminal_state.clone(),
2497 dispatcher: self.dispatcher(),
2498 cancel: {
2499 #[allow(clippy::expect_used)]
2500 let token = self.cancel_token.lock().expect("cancel_token poisoned");
2501 token.clone()
2502 },
2503 output_format: None,
2504 vfs_budget: self.vfs_budget.clone(),
2505 watchdog: ec.watchdog.clone(),
2506 #[cfg(all(feature = "localfs", feature = "overlay"))]
2507 overlay_handle: self.overlay_handle.clone(),
2508 }, has_pipe_stdin)
2509 }; if ctx.stdin.is_some() || ctx.stdin_data.is_some() || has_pipe_stdin {
2517 let mut ec = self.exec_ctx.write().await;
2518 ctx.pipe_stdin = ec.pipe_stdin.take();
2519 ec.stdin = None;
2520 ec.stdin_data = None;
2521 }
2522
2523 let mut result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
2524
2525 if ctx.output_limit.is_enabled() {
2527 let _ = crate::output_limit::spill_if_needed(&mut result, &ctx.output_limit).await;
2528 }
2529
2530 if result.did_spill {
2533 result.original_code = Some(result.code);
2534 result.code = 3;
2535 }
2536
2537 {
2539 let mut ec = self.exec_ctx.write().await;
2540 ec.cwd = ctx.cwd.clone();
2541 ec.prev_cwd = ctx.prev_cwd.clone();
2542 ec.aliases = ctx.aliases.clone();
2543 ec.ignore_config = ctx.ignore_config.clone();
2544 ec.output_limit = ctx.output_limit.clone();
2545 }
2546 {
2547 let mut scope = self.scope.write().await;
2548 *scope = ctx.scope.clone();
2549 }
2550
2551 Ok(result)
2552 }
2553
2554 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
2562 async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
2563 use tokio::sync::oneshot;
2564
2565 let command_str = self.format_pipeline(pipeline);
2567
2568 let stdout = Arc::new(BoundedStream::default_size());
2570 let stderr = Arc::new(BoundedStream::default_size());
2571
2572 let (tx, rx) = oneshot::channel();
2574
2575 let job_id = self.jobs.register_with_streams(
2577 command_str.clone(),
2578 rx,
2579 stdout.clone(),
2580 stderr.clone(),
2581 ).await;
2582
2583 let cancel = tokio_util::sync::CancellationToken::new();
2594 self.jobs.set_cancel_token(job_id, cancel.clone()).await;
2595 let fork = self.fork_for_background(cancel, job_id).await;
2596 let runner = self.runner.clone();
2597 let commands = pipeline.commands.clone();
2598
2599 let mut bg_ctx = {
2603 let ec = fork.exec_ctx.read().await;
2604 ec.child_for_pipeline()
2605 };
2606 bg_ctx.scope = fork.scope.read().await.clone();
2607 bg_ctx.dispatcher = fork.dispatcher();
2611
2612 tokio::spawn(crate::telemetry::bind_current_context(async move {
2615 let result = runner.run(&commands, &mut bg_ctx, fork.as_ref()).await;
2618
2619 let text = result.text_out();
2621 if !text.is_empty() {
2622 stdout.write(text.as_bytes()).await;
2623 }
2624 if !result.err.is_empty() {
2625 stderr.write(result.err.as_bytes()).await;
2626 }
2627
2628 stdout.close().await;
2630 stderr.close().await;
2631
2632 let _ = tx.send(result);
2634 }));
2635
2636 Ok(ExecResult::success(format!("[{}]", job_id)))
2637 }
2638
2639 fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
2641 pipeline.commands
2642 .iter()
2643 .map(|cmd| {
2644 let mut parts = vec![cmd.name.clone()];
2645 for arg in &cmd.args {
2646 match arg {
2647 Arg::Positional(expr) => {
2648 parts.push(self.format_expr(expr));
2649 }
2650 Arg::Named { key, value } => {
2651 parts.push(format!("--{}={}", key, self.format_expr(value)));
2652 }
2653 Arg::WordAssign { key, value } => {
2654 parts.push(format!("{}={}", key, self.format_expr(value)));
2655 }
2656 Arg::ShortFlag(name) => {
2657 parts.push(format!("-{}", name));
2658 }
2659 Arg::LongFlag(name) => {
2660 parts.push(format!("--{}", name));
2661 }
2662 Arg::DoubleDash => {
2663 parts.push("--".to_string());
2664 }
2665 }
2666 }
2667 parts.join(" ")
2668 })
2669 .collect::<Vec<_>>()
2670 .join(" | ")
2671 }
2672
2673 fn format_expr(&self, expr: &Expr) -> String {
2675 match expr {
2676 Expr::Literal(Value::String(s)) => {
2677 if s.contains(' ') || s.contains('"') {
2678 format!("'{}'", s.replace('\'', "\\'"))
2679 } else {
2680 s.clone()
2681 }
2682 }
2683 Expr::Literal(Value::Int(i)) => i.to_string(),
2684 Expr::Literal(Value::Float(f)) => f.to_string(),
2685 Expr::Literal(Value::Bool(b)) => b.to_string(),
2686 Expr::Literal(Value::Null) => "null".to_string(),
2687 Expr::VarRef(path) => {
2688 let name = path.segments.iter()
2689 .map(|seg| match seg {
2690 crate::ast::VarSegment::Field(f) => f.clone(),
2691 })
2692 .collect::<Vec<_>>()
2693 .join(".");
2694 format!("${{{}}}", name)
2695 }
2696 Expr::Interpolated(_) => "\"...\"".to_string(),
2697 Expr::HereDocBody { .. } => "<<heredoc".to_string(),
2698 _ => "...".to_string(),
2699 }
2700 }
2701
2702 async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
2704 self.execute_command_depth(name, args, 0).await
2705 }
2706
2707 #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
2708 async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
2709 match name {
2711 "true" => return Ok(ExecResult::success("")),
2712 "false" => return Ok(ExecResult::failure(1, "")),
2713 "source" | "." => return self.execute_source(args).await,
2714 _ => {}
2715 }
2716
2717 if alias_depth < 10 {
2719 let alias_value = {
2720 let ctx = self.exec_ctx.read().await;
2721 ctx.aliases.get(name).cloned()
2722 };
2723 if let Some(alias_val) = alias_value {
2724 let parts: Vec<&str> = alias_val.split_whitespace().collect();
2726 if let Some((alias_cmd, alias_args)) = parts.split_first() {
2727 let mut new_args: Vec<Arg> = alias_args
2728 .iter()
2729 .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
2730 .collect();
2731 new_args.extend_from_slice(args);
2732 return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
2733 }
2734 }
2735 }
2736
2737 if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
2739 return match self.tools.get(builtin_name) {
2740 Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
2741 None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
2742 };
2743 }
2744
2745 {
2747 let user_tools = self.user_tools.read().await;
2748 if let Some(tool_def) = user_tools.get(name) {
2749 let tool_def = tool_def.clone();
2750 drop(user_tools);
2751 return self.execute_user_tool(tool_def, args).await;
2752 }
2753 }
2754
2755 let tool = match self.tools.get(name) {
2757 Some(t) => t,
2758 None => {
2759 if let Some(result) = self.try_execute_script(name, args).await? {
2761 return Ok(result);
2762 }
2763 if let Some(result) = self.try_execute_external(name, args).await? {
2765 return Ok(result);
2766 }
2767
2768 let backend = self.exec_ctx.read().await.backend.clone();
2773 let tool_schema = backend.get_tool(name).await.ok().flatten().map(|t| {
2774 let mut s = t.schema;
2775 if s.subcommands.is_empty() {
2781 s.map_positionals = true;
2782 }
2783 s
2784 });
2785 let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
2786 let mut ctx = self.exec_ctx.write().await;
2787 {
2788 let scope = self.scope.read().await;
2789 ctx.scope = scope.clone();
2790 }
2791 let backend = ctx.backend.clone();
2792 match backend.call_tool(name, tool_args, &mut *ctx).await {
2793 Ok(tool_result) => {
2794 let mut scope = self.scope.write().await;
2795 *scope = ctx.scope.clone();
2796 let mut exec = ExecResult::from_output(
2797 tool_result.code as i64, tool_result.stdout, tool_result.stderr,
2798 );
2799 exec.set_output(tool_result.output);
2800 return Ok(exec);
2801 }
2802 Err(BackendError::ToolNotFound(_)) => {
2803 }
2805 Err(e) => {
2806 tracing::debug!("backend error for {name}: {e}");
2809 }
2810 }
2811
2812 return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
2813 }
2814 };
2815
2816 let schema = tool.schema();
2818 let tool_args = self.build_args_async(args, Some(&schema)).await?;
2819
2820 let schema_claims = |flag: &str| -> bool {
2822 let bare = flag.trim_start_matches('-');
2823 schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
2824 };
2825 let wants_help =
2826 (tool_args.flags.contains("help") && !schema_claims("help"))
2827 || (tool_args.flags.contains("h") && !schema_claims("-h"));
2828 if wants_help {
2829 let help_topic = crate::help::HelpTopic::Tool(name.to_string());
2830 let ctx = self.exec_ctx.read().await;
2831 let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
2832 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
2833 }
2834
2835 let mut ctx = {
2841 let ec = self.exec_ctx.write().await;
2842 let scope = self.scope.read().await;
2843 ExecContext {
2844 backend: ec.backend.clone(),
2845 scope: scope.clone(),
2846 cwd: ec.cwd.clone(),
2847 prev_cwd: ec.prev_cwd.clone(),
2848 stdin: ec.stdin.clone(),
2849 stdin_data: ec.stdin_data.clone(),
2850 stdin_data_rx: None,
2851 pipe_stdin: None, pipe_stdout: None,
2853 stderr: ec.stderr.clone(),
2854 tool_schemas: ec.tool_schemas.clone(),
2855 tools: ec.tools.clone(),
2856 job_manager: ec.job_manager.clone(),
2857 pipeline_position: ec.pipeline_position,
2858 interactive: self.interactive,
2859 aliases: ec.aliases.clone(),
2860 ignore_config: ec.ignore_config.clone(),
2861 output_limit: ec.output_limit.clone(),
2862 allow_external_commands: self.allow_external_commands,
2863 nonce_store: ec.nonce_store.clone(),
2864 trash_backend: ec.trash_backend.clone(),
2865 #[cfg(all(unix, feature = "subprocess"))]
2866 terminal_state: ec.terminal_state.clone(),
2867 dispatcher: self.dispatcher(),
2868 cancel: ec.cancel.clone(),
2874 output_format: None,
2875 vfs_budget: self.vfs_budget.clone(),
2876 watchdog: ec.watchdog.clone(),
2877 #[cfg(all(feature = "localfs", feature = "overlay"))]
2878 overlay_handle: self.overlay_handle.clone(),
2879 }
2880 }; {
2886 let mut ec = self.exec_ctx.write().await;
2887 ctx.stdin = ec.stdin.take();
2888 ctx.stdin_data = ec.stdin_data.take();
2889 ctx.stdin_data_rx = ec.stdin_data_rx.take();
2890 ctx.pipe_stdin = ec.pipe_stdin.take();
2891 ctx.pipe_stdout = ec.pipe_stdout.take();
2892 }
2893
2894 GlobalFlags::apply_from_args(&tool_args, &mut ctx);
2899
2900 let result = tool.execute(tool_args, &mut ctx).await;
2901
2902 {
2909 let mut scope = self.scope.write().await;
2910 *scope = ctx.scope.clone();
2911 }
2912 {
2913 let mut ec = self.exec_ctx.write().await;
2914 ec.cwd = ctx.cwd;
2915 ec.prev_cwd = ctx.prev_cwd;
2916 ec.aliases = ctx.aliases;
2917 ec.output_limit = ctx.output_limit.clone();
2922 ec.pipe_stdin = ctx.pipe_stdin.take();
2923 ec.pipe_stdout = ctx.pipe_stdout.take();
2924 }
2925
2926 let result = finalize_output(result, ctx.output_format, schema.owns_output);
2931
2932 Ok(result)
2933 }
2934
2935 async fn scope_home(&self) -> Option<String> {
2940 match self.scope.read().await.get("HOME") {
2941 Some(Value::String(s)) => Some(s.clone()),
2942 _ => None,
2943 }
2944 }
2945
2946 #[allow(clippy::too_many_arguments)]
2967 async fn consume_flag_positionals(
2968 &self,
2969 args: &[Arg],
2970 flag_name: &str,
2971 canonical: &str,
2972 consumes: usize,
2973 repeatable: bool,
2974 positional_indices: &[usize],
2975 consumed: &mut std::collections::HashSet<usize>,
2976 current_idx: usize,
2977 tool_args: &mut ToolArgs,
2978 ) -> Result<()> {
2979 let home = self.scope_home().await;
2980 let mut collected: Vec<Value> = Vec::with_capacity(consumes.max(1));
2981 for _ in 0..consumes.max(1) {
2982 let allow_word_assign = consumes <= 1;
2988 let next_pos = positional_indices
2989 .iter()
2990 .find(|idx| {
2991 **idx > current_idx
2992 && !consumed.contains(idx)
2993 && (allow_word_assign || matches!(args[**idx], Arg::Positional(_)))
2994 })
2995 .copied();
2996 match next_pos {
2997 Some(pos_idx) => match &args[pos_idx] {
2998 Arg::Positional(expr) => {
2999 let value = self.eval_expr_async(expr).await?;
3000 let value = apply_tilde_expansion(value, home.as_deref());
3001 collected.push(value);
3002 consumed.insert(pos_idx);
3003 }
3004 Arg::WordAssign { key, value } => {
3007 let val = self.eval_expr_async(value).await?;
3008 let val = apply_tilde_expansion(val, home.as_deref());
3009 let val_str = crate::interpreter::value_to_string(&val);
3010 collected.push(Value::String(format!("{key}={val_str}")));
3011 consumed.insert(pos_idx);
3012 }
3013 _ => {}
3014 },
3015 None => {
3016 if consumes <= 1 && collected.is_empty() {
3017 tool_args.flags.insert(flag_name.to_string());
3021 return Ok(());
3022 }
3023 anyhow::bail!(
3024 "--{flag_name} requires {consumes} argument{}, got {}",
3025 if consumes == 1 { "" } else { "s" },
3026 collected.len()
3027 );
3028 }
3029 }
3030 }
3031
3032 if consumes <= 1 {
3033 if let Some(v) = collected.pop() {
3034 if repeatable {
3035 push_repeatable_value(tool_args, flag_name, canonical, v)?;
3036 } else {
3037 tool_args.named.insert(canonical.to_string(), v);
3038 }
3039 }
3040 return Ok(());
3041 }
3042
3043 let occ: Vec<serde_json::Value> = collected
3045 .into_iter()
3046 .map(|v| crate::interpreter::value_to_json(&v))
3047 .collect();
3048 let entry = tool_args
3049 .named
3050 .entry(canonical.to_string())
3051 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
3052 if let Value::Json(serde_json::Value::Array(outer)) = entry {
3053 outer.push(serde_json::Value::Array(occ));
3054 } else {
3055 anyhow::bail!(
3056 "--{flag_name}: named[{canonical}] already holds a non-array value"
3057 );
3058 }
3059 Ok(())
3060 }
3061
3062 async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
3066 let mut tool_args = ToolArgs::new();
3067 let home = self.scope_home().await;
3068 let leaf = match schema {
3074 Some(s) => Some(select_leaf(s, args)?),
3075 None => None,
3076 };
3077 let mut param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
3084 if let Some(l) = leaf {
3085 param_lookup.extend(schema_param_lookup(l));
3086 }
3087 let accepts_word_assign = schema
3090 .map(|s| crate::tools::accepts_word_assign(s.name.as_str()))
3091 .unwrap_or(false);
3092
3093 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
3095 let mut past_double_dash = false;
3096
3097 let positional_indices: Vec<usize> = args.iter().enumerate()
3105 .filter_map(|(i, a)| {
3106 let consumable = matches!(a, Arg::Positional(_))
3107 || (!accepts_word_assign && matches!(a, Arg::WordAssign { .. }));
3108 consumable.then_some(i)
3109 })
3110 .collect();
3111
3112 let mut i = 0;
3113 while i < args.len() {
3114 match &args[i] {
3115 Arg::DoubleDash => {
3116 past_double_dash = true;
3117 }
3118 Arg::Positional(expr) => {
3119 if !consumed.contains(&i) {
3120 if let Expr::GlobPattern(pattern) = expr {
3122 let glob_enabled = {
3123 let scope = self.scope.read().await;
3124 scope.glob_enabled()
3125 };
3126 if glob_enabled {
3127 let (paths, cwd) = {
3128 let ctx = self.exec_ctx.read().await;
3129 let paths = ctx.expand_glob(pattern).await
3130 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3131 let cwd = ctx.resolve_path(".");
3132 (paths, cwd)
3133 };
3134 if paths.is_empty() {
3135 return Err(anyhow::anyhow!("no matches: {}", pattern));
3136 }
3137 for path in paths {
3138 let display = if !pattern.starts_with('/') {
3139 path.strip_prefix(&cwd)
3140 .unwrap_or(&path)
3141 .to_string_lossy().into_owned()
3142 } else {
3143 path.to_string_lossy().into_owned()
3144 };
3145 tool_args.positional.push(Value::String(display));
3146 }
3147 i += 1;
3148 continue;
3149 }
3150 }
3151 let value = self.eval_expr_async(expr).await?;
3152 let value = apply_tilde_expansion(value, home.as_deref());
3153 tool_args.positional.push(value);
3154 }
3155 }
3156 Arg::Named { key, value } => {
3157 let val = self.eval_expr_async(value).await?;
3158 let val = apply_tilde_expansion(val, home.as_deref());
3159 if let Some(&(canonical, _, _, true)) = param_lookup.get(key.as_str()) {
3165 push_repeatable_value(&mut tool_args, key, canonical, val)?;
3166 } else {
3167 tool_args.named.insert(key.clone(), val);
3168 }
3169 }
3170 Arg::WordAssign { key, value } => {
3171 if consumed.contains(&i) {
3174 i += 1;
3175 continue;
3176 }
3177 let val = self.eval_expr_async(value).await?;
3178 let val = apply_tilde_expansion(val, home.as_deref());
3179 if accepts_word_assign {
3180 tool_args.named.insert(key.clone(), val);
3181 } else {
3182 let val_str = crate::interpreter::value_to_string(&val);
3185 tool_args.positional.push(Value::String(format!("{key}={val_str}")));
3186 }
3187 }
3188 Arg::ShortFlag(name) => {
3189 if past_double_dash {
3190 tool_args.positional.push(Value::String(format!("-{name}")));
3191 } else if name.len() == 1 {
3192 let flag_name = name.as_str();
3193 let lookup = param_lookup.get(flag_name);
3194 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3195
3196 if is_bool {
3197 tool_args.flags.insert(flag_name.to_string());
3198 } else {
3199 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(flag_name);
3201 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3202 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3203 self.consume_flag_positionals(
3204 args,
3205 name,
3206 canonical,
3207 consumes,
3208 repeatable,
3209 &positional_indices,
3210 &mut consumed,
3211 i,
3212 &mut tool_args,
3213 )
3214 .await?;
3215 }
3216 } else if let Some(&(canonical, typ, consumes, repeatable)) = param_lookup.get(name.as_str()) {
3217 if is_bool_type(typ) {
3219 tool_args.flags.insert(canonical.to_string());
3220 } else {
3221 self.consume_flag_positionals(
3222 args,
3223 name,
3224 canonical,
3225 consumes,
3226 repeatable,
3227 &positional_indices,
3228 &mut consumed,
3229 i,
3230 &mut tool_args,
3231 )
3232 .await?;
3233 }
3234 } else if let Some(&(canonical, _, consumes, repeatable)) = param_lookup
3235 .get(&name[..1])
3236 .filter(|(_, typ, ..)| !is_bool_type(typ))
3237 {
3238 bind_glued_short_value(
3245 &mut tool_args,
3246 &name[..1],
3247 canonical,
3248 consumes,
3249 repeatable,
3250 name[1..].to_string(),
3251 )?;
3252 } else {
3253 let bytes = name.as_bytes();
3267 let mut p = 0;
3268 while p < bytes.len() {
3269 let key = &name[p..p + 1];
3270 match param_lookup.get(key) {
3271 Some(&(canonical, typ, consumes, repeatable))
3272 if !is_bool_type(typ) =>
3273 {
3274 let glued = name[p + 1..].to_string();
3275 if glued.is_empty() {
3276 self.consume_flag_positionals(
3280 args,
3281 key,
3282 canonical,
3283 consumes,
3284 repeatable,
3285 &positional_indices,
3286 &mut consumed,
3287 i,
3288 &mut tool_args,
3289 )
3290 .await?;
3291 } else {
3292 bind_glued_short_value(
3293 &mut tool_args,
3294 key,
3295 canonical,
3296 consumes,
3297 repeatable,
3298 glued,
3299 )?;
3300 }
3301 break;
3302 }
3303 _ => {
3304 tool_args.flags.insert(key.to_string());
3305 p += 1;
3306 }
3307 }
3308 }
3309 }
3310 }
3311 Arg::LongFlag(name) => {
3312 if past_double_dash {
3313 tool_args.positional.push(Value::String(format!("--{name}")));
3314 } else {
3315 let lookup = param_lookup.get(name.as_str());
3316 let ambiguous_value = (lookup.is_none()
3325 && leaf.is_some_and(|s| s.map_positionals)
3326 && !consumed.contains(&(i + 1)))
3327 .then(|| match args.get(i + 1) {
3328 Some(Arg::Positional(Expr::Literal(Value::String(s)))) => {
3331 Some(s.clone())
3332 }
3333 Some(Arg::Positional(_)) => Some("VALUE".to_string()),
3334 _ => None,
3335 })
3336 .flatten();
3337 if let Some(val) = ambiguous_value {
3338 let tool = leaf.map(|s| s.name.as_str()).unwrap_or("command");
3339 anyhow::bail!(
3340 "{tool}: --{name} is not a declared flag, so the \
3341 space-separated value would be silently dropped. \
3342 Use --{name}={val}, or have {tool} declare --{name} \
3343 in its schema."
3344 );
3345 }
3346 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3347
3348 if is_bool {
3349 tool_args.flags.insert(name.clone());
3350 } else {
3351 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(name.as_str());
3352 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3353 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3354 self.consume_flag_positionals(
3355 args,
3356 name,
3357 canonical,
3358 consumes,
3359 repeatable,
3360 &positional_indices,
3361 &mut consumed,
3362 i,
3363 &mut tool_args,
3364 )
3365 .await?;
3366 }
3367 }
3368 }
3369 }
3370 i += 1;
3371 }
3372
3373 if let Some(schema) = leaf.filter(|s| s.map_positionals) {
3380 let pre_dash_count = if past_double_dash {
3381 let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
3382 positional_indices.iter()
3383 .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
3384 .count()
3385 } else {
3386 tool_args.positional.len()
3387 };
3388
3389 let mut remaining = Vec::new();
3390 let mut positional_iter = tool_args.positional.drain(..).enumerate();
3391
3392 for param in &schema.params {
3393 if tool_args.named.contains_key(¶m.name) || tool_args.flags.contains(¶m.name) {
3394 continue;
3395 }
3396 if is_bool_type(¶m.param_type) {
3397 continue;
3398 }
3399 loop {
3400 match positional_iter.next() {
3401 Some((idx, val)) if idx < pre_dash_count => {
3402 tool_args.named.insert(param.name.clone(), val);
3403 break;
3404 }
3405 Some((_, val)) => {
3406 remaining.push(val);
3407 }
3408 None => break,
3409 }
3410 }
3411 }
3412
3413 remaining.extend(positional_iter.map(|(_, v)| v));
3414 tool_args.positional = remaining;
3415 }
3416
3417 Ok(tool_args)
3418 }
3419
3420 #[cfg(feature = "subprocess")]
3430 async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
3431 let mut argv = Vec::new();
3432 let home = self.scope_home().await;
3433 for arg in args {
3434 match arg {
3435 Arg::Positional(expr) => {
3436 if let Expr::GlobPattern(pattern) = expr {
3438 let glob_enabled = {
3439 let scope = self.scope.read().await;
3440 scope.glob_enabled()
3441 };
3442 if glob_enabled {
3443 let (paths, cwd) = {
3444 let ctx = self.exec_ctx.read().await;
3445 let paths = ctx.expand_glob(pattern).await
3446 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3447 let cwd = ctx.resolve_path(".");
3448 (paths, cwd)
3449 };
3450 if paths.is_empty() {
3451 return Err(anyhow::anyhow!("no matches: {}", pattern));
3452 }
3453 for path in paths {
3454 let display = if !pattern.starts_with('/') {
3455 path.strip_prefix(&cwd)
3456 .unwrap_or(&path)
3457 .to_string_lossy().into_owned()
3458 } else {
3459 path.to_string_lossy().into_owned()
3460 };
3461 argv.push(display);
3462 }
3463 continue;
3464 }
3465 }
3466 let value = self.eval_expr_async(expr).await?;
3467 let value = apply_tilde_expansion(value, home.as_deref());
3468 argv.push(value_to_string(&value));
3469 }
3470 Arg::Named { key, value } => {
3471 let val = self.eval_expr_async(value).await?;
3472 let val = apply_tilde_expansion(val, home.as_deref());
3473 argv.push(format!("--{}={}", key, value_to_string(&val)));
3474 }
3475 Arg::WordAssign { key, value } => {
3476 let val = self.eval_expr_async(value).await?;
3477 let val = apply_tilde_expansion(val, home.as_deref());
3478 argv.push(format!("{}={}", key, value_to_string(&val)));
3479 }
3480 Arg::ShortFlag(name) => {
3481 argv.push(format!("-{}", name));
3483 }
3484 Arg::LongFlag(name) => {
3485 argv.push(format!("--{}", name));
3487 }
3488 Arg::DoubleDash => {
3489 argv.push("--".to_string());
3491 }
3492 }
3493 }
3494 Ok(argv)
3495 }
3496
3497 fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
3502 Box::pin(async move {
3503 match expr {
3504 Expr::Literal(value) => Ok(value.clone()),
3505 Expr::VarRef(path) => {
3506 let scope = self.scope.read().await;
3507 scope.resolve_path(path)
3508 .ok_or_else(|| anyhow::anyhow!("undefined variable"))
3509 }
3510 Expr::Interpolated(parts) => {
3511 let mut result = String::new();
3512 for part in parts {
3513 result.push_str(&self.eval_string_part_async(part).await?);
3514 }
3515 Ok(Value::String(result))
3516 }
3517 Expr::HereDocBody { parts, strip_tabs } => {
3518 let mut result = String::new();
3519 for sp in parts {
3520 result.push_str(&self.eval_string_part_async(&sp.part).await?);
3521 }
3522 if *strip_tabs {
3523 Ok(Value::String(crate::interpreter::strip_leading_tabs(&result)))
3524 } else {
3525 Ok(Value::String(result))
3526 }
3527 }
3528 Expr::BinaryOp { left, op, right } => match op {
3529 BinaryOp::And => {
3530 let left_val = self.eval_expr_async(left).await?;
3531 if !is_truthy(&left_val) {
3532 return Ok(left_val);
3533 }
3534 self.eval_expr_async(right).await
3535 }
3536 BinaryOp::Or => {
3537 let left_val = self.eval_expr_async(left).await?;
3538 if is_truthy(&left_val) {
3539 return Ok(left_val);
3540 }
3541 self.eval_expr_async(right).await
3542 }
3543 },
3544 Expr::CommandSubst(stmts) => {
3545 let saved_scope = { self.scope.read().await.clone() };
3548 let saved_cwd = {
3549 let ec = self.exec_ctx.read().await;
3550 (ec.cwd.clone(), ec.prev_cwd.clone())
3551 };
3552
3553 let run_result = self.execute_block_capturing(stmts).await;
3555
3556 {
3558 let mut scope = self.scope.write().await;
3559 *scope = saved_scope;
3560 if let Ok(ref r) = run_result {
3561 scope.set_last_result(r.clone());
3562 }
3563 }
3564 {
3565 let mut ec = self.exec_ctx.write().await;
3566 ec.cwd = saved_cwd.0;
3567 ec.prev_cwd = saved_cwd.1;
3568 }
3569
3570 let result = run_result?;
3572
3573 if let Some(bytes) = result.out_bytes() {
3576 Ok(Value::Bytes(bytes.to_vec()))
3577 } else if let Some(data) = &result.data {
3579 Ok(data.clone())
3580 } else if let Some(output) = result.output() {
3581 if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
3583 let items: Vec<serde_json::Value> = output.root.iter()
3584 .map(|n| serde_json::Value::String(n.display_name().to_string()))
3585 .collect();
3586 Ok(Value::Json(serde_json::Value::Array(items)))
3587 } else {
3588 Ok(Value::String(
3595 result.text_out().trim_end_matches('\n').to_string(),
3596 ))
3597 }
3598 } else {
3599 Ok(Value::String(
3601 result.text_out().trim_end_matches('\n').to_string(),
3602 ))
3603 }
3604 }
3605 Expr::Test(test_expr) => {
3606 Ok(Value::Bool(self.eval_test_async(test_expr).await?))
3607 }
3608 Expr::Positional(n) => {
3609 let scope = self.scope.read().await;
3610 match scope.get_positional(*n) {
3611 Some(s) => Ok(Value::String(s.to_string())),
3612 None => Ok(Value::String(String::new())),
3613 }
3614 }
3615 Expr::AllArgs => {
3616 let scope = self.scope.read().await;
3617 Ok(Value::String(scope.all_args().join(" ")))
3618 }
3619 Expr::ArgCount => {
3620 let scope = self.scope.read().await;
3621 Ok(Value::Int(scope.arg_count() as i64))
3622 }
3623 Expr::VarLength(name) => {
3624 let scope = self.scope.read().await;
3625 match scope.get(name) {
3626 Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
3627 None => Ok(Value::Int(0)),
3628 }
3629 }
3630 Expr::VarWithDefault { name, default } => {
3631 let scope = self.scope.read().await;
3632 let use_default = match scope.get(name) {
3633 Some(value) => value_to_string(value).is_empty(),
3634 None => true,
3635 };
3636 drop(scope); if use_default {
3638 self.eval_string_parts_async(default).await.map(Value::String)
3640 } else {
3641 let scope = self.scope.read().await;
3642 scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
3643 }
3644 }
3645 Expr::Arithmetic(expr_str) => {
3646 let scope = self.scope.read().await;
3647 crate::arithmetic::eval_arithmetic(expr_str, &scope)
3648 .map(Value::Int)
3649 .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
3650 }
3651 Expr::Command(cmd) => {
3652 let result = self.execute_command(&cmd.name, &cmd.args).await?;
3654 Ok(Value::Bool(result.code == 0))
3655 }
3656 Expr::LastExitCode => {
3657 let scope = self.scope.read().await;
3658 Ok(Value::Int(scope.last_result().code))
3659 }
3660 Expr::CurrentPid => {
3661 let scope = self.scope.read().await;
3662 Ok(Value::Int(scope.pid() as i64))
3663 }
3664 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
3665 }
3666 })
3667 }
3668
3669 fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
3671 Box::pin(async move {
3672 let mut result = String::new();
3673 for part in parts {
3674 result.push_str(&self.eval_string_part_async(part).await?);
3675 }
3676 Ok(result)
3677 })
3678 }
3679
3680 fn eval_test_async<'a>(&'a self, test_expr: &'a TestExpr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
3684 Box::pin(async move {
3685 match test_expr {
3686 TestExpr::FileTest { op, path } => {
3687 let path_value = self.eval_expr_async(path).await?;
3688 let path_str = value_to_string(&path_value);
3689 let backend = self.exec_ctx.read().await.backend.clone();
3690 let entry = backend.stat(std::path::Path::new(&path_str)).await.ok();
3691 Ok(match op {
3692 FileTestOp::Exists => entry.is_some(),
3693 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
3694 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
3695 FileTestOp::Readable => entry.is_some(),
3696 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
3697 e.permissions.is_none_or(|p| p & 0o222 != 0)
3698 }),
3699 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
3700 e.permissions.is_some_and(|p| p & 0o111 != 0)
3701 }),
3702 })
3703 }
3704 TestExpr::StringTest { op, value } => {
3705 let val = self.eval_expr_async(value).await?;
3706 let s = value_to_string(&val);
3707 Ok(match op {
3708 crate::ast::StringTestOp::IsEmpty => s.is_empty(),
3709 crate::ast::StringTestOp::IsNonEmpty => !s.is_empty(),
3710 })
3711 }
3712 TestExpr::Comparison { left, op, right } => {
3713 let left_val = self.eval_expr_async(left).await?;
3715 let right_val = self.eval_expr_async(right).await?;
3716 let resolved = TestExpr::Comparison {
3717 left: Box::new(Expr::Literal(left_val)),
3718 op: *op,
3719 right: Box::new(Expr::Literal(right_val)),
3720 };
3721 let expr = Expr::Test(Box::new(resolved));
3722 let mut scope = self.scope.write().await;
3723 let value = eval_expr(&expr, &mut scope)
3724 .map_err(|e| anyhow::anyhow!("{}", e))?;
3725 Ok(value_to_bool(&value))
3726 }
3727 TestExpr::And { left, right } => {
3728 if !self.eval_test_async(left).await? {
3729 Ok(false)
3730 } else {
3731 self.eval_test_async(right).await
3732 }
3733 }
3734 TestExpr::Or { left, right } => {
3735 if self.eval_test_async(left).await? {
3736 Ok(true)
3737 } else {
3738 self.eval_test_async(right).await
3739 }
3740 }
3741 TestExpr::Not { expr } => {
3742 Ok(!self.eval_test_async(expr).await?)
3743 }
3744 }
3745 })
3746 }
3747
3748 fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
3749 Box::pin(async move {
3750 match part {
3751 StringPart::Literal(s) => Ok(s.clone()),
3752 StringPart::Var(path) => {
3753 let scope = self.scope.read().await;
3754 match scope.resolve_path(path) {
3755 Some(value) => Ok(value_to_string(&value)),
3756 None => Ok(String::new()), }
3758 }
3759 StringPart::VarWithDefault { name, default } => {
3760 let scope = self.scope.read().await;
3761 let use_default = match scope.get(name) {
3762 Some(value) => value_to_string(value).is_empty(),
3763 None => true,
3764 };
3765 drop(scope); if use_default {
3767 self.eval_string_parts_async(default).await
3769 } else {
3770 let scope = self.scope.read().await;
3771 Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
3772 }
3773 }
3774 StringPart::VarLength(name) => {
3775 let scope = self.scope.read().await;
3776 match scope.get(name) {
3777 Some(value) => Ok(value_to_string(value).len().to_string()),
3778 None => Ok("0".to_string()),
3779 }
3780 }
3781 StringPart::Positional(n) => {
3782 let scope = self.scope.read().await;
3783 match scope.get_positional(*n) {
3784 Some(s) => Ok(s.to_string()),
3785 None => Ok(String::new()),
3786 }
3787 }
3788 StringPart::AllArgs => {
3789 let scope = self.scope.read().await;
3790 Ok(scope.all_args().join(" "))
3791 }
3792 StringPart::ArgCount => {
3793 let scope = self.scope.read().await;
3794 Ok(scope.arg_count().to_string())
3795 }
3796 StringPart::Arithmetic(expr) => {
3797 let scope = self.scope.read().await;
3798 match crate::arithmetic::eval_arithmetic(expr, &scope) {
3799 Ok(value) => Ok(value.to_string()),
3800 Err(_) => Ok(String::new()),
3801 }
3802 }
3803 StringPart::CommandSubst(stmts) => {
3804 let saved_scope = { self.scope.read().await.clone() };
3807 let saved_cwd = {
3808 let ec = self.exec_ctx.read().await;
3809 (ec.cwd.clone(), ec.prev_cwd.clone())
3810 };
3811
3812 let run_result = self.execute_block_capturing(stmts).await;
3814
3815 {
3817 let mut scope = self.scope.write().await;
3818 *scope = saved_scope;
3819 if let Ok(ref r) = run_result {
3820 scope.set_last_result(r.clone());
3821 }
3822 }
3823 {
3824 let mut ec = self.exec_ctx.write().await;
3825 ec.cwd = saved_cwd.0;
3826 ec.prev_cwd = saved_cwd.1;
3827 }
3828
3829 let result = run_result?;
3831
3832 match result.try_text_out() {
3835 Ok(s) => Ok(s.trim_end_matches('\n').to_string()),
3836 Err(e) => anyhow::bail!(
3837 "command substitution in a string produced binary data ({e}) — \
3838 pipe through base64/xxd"
3839 ),
3840 }
3841 }
3842 StringPart::LastExitCode => {
3843 let scope = self.scope.read().await;
3844 Ok(scope.last_result().code.to_string())
3845 }
3846 StringPart::CurrentPid => {
3847 let scope = self.scope.read().await;
3848 Ok(scope.pid().to_string())
3849 }
3850 }
3851 })
3852 }
3853
3854 async fn update_last_result(&self, result: &ExecResult) {
3856 let mut scope = self.scope.write().await;
3857 scope.set_last_result(result.clone());
3858 }
3859
3860 async fn drain_stderr_into(&self, result: &mut ExecResult) {
3866 let drained = {
3867 let mut receiver = self.stderr_receiver.lock().await;
3868 receiver.drain_lossy()
3869 };
3870 if !drained.is_empty() {
3871 if !result.err.is_empty() && !result.err.ends_with('\n') {
3872 result.err.push('\n');
3873 }
3874 result.err.push_str(&drained);
3875 }
3876 }
3877
3878 async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
3884 let tool_args = self.build_args_async(args, None).await?;
3886
3887 {
3889 let mut scope = self.scope.write().await;
3890 scope.push_frame();
3891 }
3892
3893 let saved_positional = {
3895 let mut scope = self.scope.write().await;
3896 let saved = scope.save_positional();
3897
3898 let positional_args: Vec<String> = tool_args.positional
3900 .iter()
3901 .map(value_to_string)
3902 .collect();
3903 scope.set_positional(&def.name, positional_args);
3904
3905 saved
3906 };
3907
3908 let mut accumulated_out: Vec<u8> = Vec::new();
3913 let mut accumulated_err = String::new();
3914 let mut last_code = 0i64;
3915 let mut last_data: Option<Value> = None;
3916
3917 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
3918 match r.out_bytes() {
3919 Some(b) => buf.extend_from_slice(b),
3920 None => buf.extend_from_slice(r.text_out().as_bytes()),
3921 }
3922 }
3923
3924 let mut exec_error: Option<anyhow::Error> = None;
3926 let mut exit_code: Option<i64> = None;
3927
3928 for stmt in &def.body {
3929 match self.execute_stmt_flow(stmt).await {
3930 Ok(flow) => {
3931 let drained = {
3933 let mut receiver = self.stderr_receiver.lock().await;
3934 receiver.drain_lossy()
3935 };
3936 if !drained.is_empty() {
3937 accumulated_err.push_str(&drained);
3938 }
3939
3940 match flow {
3941 ControlFlow::Normal(r) => {
3942 push_out(&mut accumulated_out, &r);
3943 accumulated_err.push_str(&r.err);
3944 last_code = r.code;
3945 last_data = r.data;
3946 }
3947 ControlFlow::Return { value } => {
3948 push_out(&mut accumulated_out, &value);
3949 accumulated_err.push_str(&value.err);
3950 last_code = value.code;
3951 last_data = value.data;
3952 break;
3953 }
3954 ControlFlow::Exit { code } => {
3955 exit_code = Some(code);
3956 break;
3957 }
3958 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
3959 push_out(&mut accumulated_out, &r);
3960 accumulated_err.push_str(&r.err);
3961 last_code = r.code;
3962 last_data = r.data;
3963 }
3964 }
3965 }
3966 Err(e) => {
3967 exec_error = Some(e);
3968 break;
3969 }
3970 }
3971 }
3972
3973 {
3975 let mut scope = self.scope.write().await;
3976 scope.pop_frame();
3977 scope.set_positional(saved_positional.0, saved_positional.1);
3978 }
3979
3980 if let Some(e) = exec_error {
3982 return Err(e);
3983 }
3984 let code = exit_code.unwrap_or(last_code);
3985 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(code);
3986 result.err = accumulated_err;
3987 result.data = last_data;
3988 Ok(result)
3989 }
3990
3991 async fn execute_block_capturing(&self, stmts: &[Stmt]) -> Result<ExecResult> {
3999 let mut accumulated_out: Vec<u8> = Vec::new();
4003 let mut accumulated_err = String::new();
4004 let mut last_code = 0i64;
4005 let mut last_data: Option<Value> = None;
4006
4007 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
4009 match r.out_bytes() {
4010 Some(b) => buf.extend_from_slice(b),
4011 None => buf.extend_from_slice(r.text_out().as_bytes()),
4012 }
4013 }
4014
4015 for stmt in stmts {
4016 let flow = self.execute_stmt_flow(stmt).await?;
4017
4018 let drained = {
4021 let mut receiver = self.stderr_receiver.lock().await;
4022 receiver.drain_lossy()
4023 };
4024 if !drained.is_empty() {
4025 accumulated_err.push_str(&drained);
4026 }
4027
4028 match flow {
4029 ControlFlow::Normal(r)
4030 | ControlFlow::Break { result: r, .. }
4031 | ControlFlow::Continue { result: r, .. } => {
4032 push_out(&mut accumulated_out, &r);
4033 accumulated_err.push_str(&r.err);
4034 last_code = r.code;
4035 last_data = r.data;
4036 }
4037 ControlFlow::Return { value } => {
4038 push_out(&mut accumulated_out, &value);
4039 accumulated_err.push_str(&value.err);
4040 last_code = value.code;
4041 last_data = value.data;
4042 break;
4043 }
4044 ControlFlow::Exit { code } => {
4045 last_code = code;
4046 break;
4047 }
4048 }
4049 }
4050
4051 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(last_code);
4052 result.err = accumulated_err;
4053 result.data = last_data;
4054 Ok(result)
4055 }
4056
4057 async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
4062 let tool_args = self.build_args_async(args, None).await?;
4064 let path = match tool_args.positional.first() {
4065 Some(Value::String(s)) => s.clone(),
4066 Some(v) => value_to_string(v),
4067 None => {
4068 return Ok(ExecResult::failure(1, "source: missing filename"));
4069 }
4070 };
4071
4072 let full_path = {
4074 let ctx = self.exec_ctx.read().await;
4075 if path.starts_with('/') {
4076 std::path::PathBuf::from(&path)
4077 } else {
4078 ctx.cwd.join(&path)
4079 }
4080 };
4081
4082 let content = {
4084 let ctx = self.exec_ctx.read().await;
4085 match ctx.backend.read(&full_path, None).await {
4086 Ok(bytes) => {
4087 String::from_utf8(bytes).map_err(|e| {
4088 anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
4089 })?
4090 }
4091 Err(e) => {
4092 return Ok(ExecResult::failure(
4093 1,
4094 format!("source: {}: {}", path, e),
4095 ));
4096 }
4097 }
4098 };
4099
4100 let program = match crate::parser::parse(&content) {
4102 Ok(p) => p,
4103 Err(errors) => {
4104 let msg = errors
4105 .iter()
4106 .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
4107 .collect::<Vec<_>>()
4108 .join("\n");
4109 return Ok(ExecResult::failure(1, format!("source: {}", msg)));
4110 }
4111 };
4112
4113 let mut result = ExecResult::success("");
4115 for stmt in program.statements {
4116 if matches!(stmt, crate::ast::Stmt::Empty) {
4117 continue;
4118 }
4119
4120 match self.execute_stmt_flow(&stmt).await {
4121 Ok(flow) => {
4122 self.drain_stderr_into(&mut result).await;
4123 match flow {
4124 ControlFlow::Normal(r) => {
4125 result = r.clone();
4126 self.update_last_result(&r).await;
4127 }
4128 ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
4129 return Err(anyhow::anyhow!(
4130 "source: {}: unexpected break/continue outside loop",
4131 path
4132 ));
4133 }
4134 ControlFlow::Return { value } => {
4135 return Ok(value);
4136 }
4137 ControlFlow::Exit { code } => {
4138 result.code = code;
4139 return Ok(result);
4140 }
4141 }
4142 }
4143 Err(e) => {
4144 return Err(e.context(format!("source: {}", path)));
4145 }
4146 }
4147 }
4148
4149 Ok(result)
4150 }
4151
4152 async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4157 let path_value = {
4159 let scope = self.scope.read().await;
4160 scope
4161 .get("PATH")
4162 .map(value_to_string)
4163 .unwrap_or_else(|| "/bin".to_string())
4164 };
4165
4166 for dir in path_value.split(':') {
4168 if dir.is_empty() {
4169 continue;
4170 }
4171
4172 let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
4174
4175 let exists = {
4177 let ctx = self.exec_ctx.read().await;
4178 ctx.backend.exists(&script_path).await
4179 };
4180
4181 if !exists {
4182 continue;
4183 }
4184
4185 let content = {
4187 let ctx = self.exec_ctx.read().await;
4188 match ctx.backend.read(&script_path, None).await {
4189 Ok(bytes) => match String::from_utf8(bytes) {
4190 Ok(s) => s,
4191 Err(e) => {
4192 return Ok(Some(ExecResult::failure(
4193 1,
4194 format!("{}: invalid UTF-8: {}", script_path.display(), e),
4195 )));
4196 }
4197 },
4198 Err(e) => {
4199 return Ok(Some(ExecResult::failure(
4200 1,
4201 format!("{}: {}", script_path.display(), e),
4202 )));
4203 }
4204 }
4205 };
4206
4207 let program = match crate::parser::parse(&content) {
4209 Ok(p) => p,
4210 Err(errors) => {
4211 let msg = errors
4212 .iter()
4213 .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
4214 .collect::<Vec<_>>()
4215 .join("\n");
4216 return Ok(Some(ExecResult::failure(1, msg)));
4217 }
4218 };
4219
4220 let tool_args = self.build_args_async(args, None).await?;
4222
4223 let mut isolated_scope = Scope::new();
4225
4226 let positional_args: Vec<String> = tool_args.positional
4228 .iter()
4229 .map(value_to_string)
4230 .collect();
4231 isolated_scope.set_positional(name, positional_args);
4232
4233 let original_scope = {
4235 let mut scope = self.scope.write().await;
4236 std::mem::replace(&mut *scope, isolated_scope)
4237 };
4238
4239 let mut result = ExecResult::success("");
4241 let mut exec_error: Option<anyhow::Error> = None;
4242 let mut exit_code: Option<i64> = None;
4243
4244 for stmt in program.statements {
4245 if matches!(stmt, crate::ast::Stmt::Empty) {
4246 continue;
4247 }
4248
4249 match self.execute_stmt_flow(&stmt).await {
4250 Ok(flow) => {
4251 match flow {
4252 ControlFlow::Normal(r) => result = r,
4253 ControlFlow::Return { value } => {
4254 result = value;
4255 break;
4256 }
4257 ControlFlow::Exit { code } => {
4258 exit_code = Some(code);
4259 break;
4260 }
4261 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
4262 result = r;
4263 }
4264 }
4265 }
4266 Err(e) => {
4267 exec_error = Some(e);
4268 break;
4269 }
4270 }
4271 }
4272
4273 {
4275 let mut scope = self.scope.write().await;
4276 *scope = original_scope;
4277 }
4278
4279 if let Some(e) = exec_error {
4281 return Err(e.context(format!("script: {}", script_path.display())));
4282 }
4283 if let Some(code) = exit_code {
4284 result.code = code;
4285 return Ok(Some(result));
4286 }
4287
4288 return Ok(Some(result));
4289 }
4290
4291 Ok(None)
4293 }
4294
4295 #[cfg(not(feature = "subprocess"))]
4309 async fn try_execute_external(&self, _name: &str, _args: &[Arg]) -> Result<Option<ExecResult>> {
4310 Ok(None)
4311 }
4312
4313 #[cfg(feature = "subprocess")]
4315 #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
4316 async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4317 let cancel = {
4323 let ec = self.exec_ctx.read().await;
4324 ec.cancel.clone()
4325 };
4326 let kill_grace = self.kill_grace;
4327 if !self.allow_external_commands {
4328 return Ok(None);
4329 }
4330
4331 let real_cwd = {
4336 let ctx = self.exec_ctx.read().await;
4337 match ctx.backend.resolve_real_path(&ctx.cwd) {
4338 Some(p) => p,
4339 None => return Ok(None),
4340 }
4341 };
4342
4343 let executable = if name.contains('/') {
4344 let resolved = if std::path::Path::new(name).is_absolute() {
4346 std::path::PathBuf::from(name)
4347 } else {
4348 real_cwd.join(name)
4349 };
4350 if !resolved.exists() {
4351 return Ok(Some(ExecResult::failure(
4352 127,
4353 format!("{}: No such file or directory", name),
4354 )));
4355 }
4356 if !resolved.is_file() {
4357 return Ok(Some(ExecResult::failure(
4358 126,
4359 format!("{}: Is a directory", name),
4360 )));
4361 }
4362 #[cfg(unix)]
4363 {
4364 use std::os::unix::fs::PermissionsExt;
4365 let mode = std::fs::metadata(&resolved)
4366 .map(|m| m.permissions().mode())
4367 .unwrap_or(0);
4368 if mode & 0o111 == 0 {
4369 return Ok(Some(ExecResult::failure(
4370 126,
4371 format!("{}: Permission denied", name),
4372 )));
4373 }
4374 }
4375 resolved.to_string_lossy().into_owned()
4376 } else {
4377 let path_var = {
4381 let scope = self.scope.read().await;
4382 scope.get("PATH").map(value_to_string).unwrap_or_default()
4383 };
4384
4385 match resolve_in_path(name, &path_var) {
4387 Some(path) => path,
4388 None => return Ok(None), }
4390 };
4391
4392 tracing::debug!(executable = %executable, "resolved external command");
4393
4394 let argv = self.build_args_flat(args).await?;
4396
4397 let (pipe_stdin, stdin_string) = {
4406 let mut ctx = self.exec_ctx.write().await;
4407 (ctx.pipe_stdin.take(), ctx.take_stdin())
4408 };
4409 let has_stdin = pipe_stdin.is_some() || stdin_string.is_some();
4410
4411 use tokio::process::Command;
4413
4414 let mut cmd = Command::new(&executable);
4415 cmd.args(&argv);
4416 cmd.current_dir(&real_cwd);
4417
4418 cmd.env_clear();
4422 {
4423 let scope = self.scope.read().await;
4424 for (var_name, value) in scope.exported_vars() {
4425 cmd.env(var_name, value_to_string(&value));
4426 }
4427 }
4428
4429 cmd.stdin(if has_stdin {
4431 std::process::Stdio::piped()
4432 } else if self.interactive {
4433 std::process::Stdio::inherit()
4434 } else {
4435 std::process::Stdio::null()
4436 });
4437
4438 let pipeline_position = {
4442 let ctx = self.exec_ctx.read().await;
4443 ctx.pipeline_position
4444 };
4445 let inherit_output = self.interactive
4446 && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
4447
4448 if inherit_output {
4449 cmd.stdout(std::process::Stdio::inherit());
4450 cmd.stderr(std::process::Stdio::inherit());
4451 } else {
4452 cmd.stdout(std::process::Stdio::piped());
4453 cmd.stderr(std::process::Stdio::piped());
4454 }
4455
4456 #[cfg(unix)]
4462 {
4463 let restore_jc_signals = self.terminal_state.is_some() && inherit_output;
4464 #[allow(unsafe_code)]
4466 unsafe {
4467 cmd.pre_exec(move || {
4468 nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
4470 .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
4471 if restore_jc_signals {
4472 use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
4473 let mut sa: nix::libc::sigaction = std::mem::zeroed();
4474 sa.sa_sigaction = SIG_DFL;
4475 if sigaction(SIGTSTP, &sa, std::ptr::null_mut()) != 0 {
4476 return Err(std::io::Error::last_os_error());
4477 }
4478 if sigaction(SIGTTOU, &sa, std::ptr::null_mut()) != 0 {
4479 return Err(std::io::Error::last_os_error());
4480 }
4481 if sigaction(SIGTTIN, &sa, std::ptr::null_mut()) != 0 {
4482 return Err(std::io::Error::last_os_error());
4483 }
4484 if sigaction(SIGINT, &sa, std::ptr::null_mut()) != 0 {
4485 return Err(std::io::Error::last_os_error());
4486 }
4487 }
4488 Ok(())
4489 });
4490 }
4491 }
4492
4493 let in_jc_inherit_path = inherit_output && self.terminal_state.is_some();
4500 if !in_jc_inherit_path {
4501 cmd.kill_on_drop(true);
4502 }
4503
4504 let mut child = match cmd.spawn() {
4509 Ok(child) => child,
4510 Err(e) => {
4511 return Ok(Some(ExecResult::failure(
4512 127,
4513 format!("{}: {}", name, e),
4514 )));
4515 }
4516 };
4517 let kill_target = crate::pidfd::KillTarget::from_child(&child);
4518
4519 if let Some(job_id) = self.bg_job_id
4524 && let Some(pid) = child.id()
4525 {
4526 self.jobs.add_pgid(job_id, pid).await;
4527 }
4528
4529 let stdin_task: Option<tokio::task::JoinHandle<()>> = if let Some(mut pipe_in) = pipe_stdin {
4536 child.stdin.take().map(|mut child_stdin| {
4537 tokio::spawn(async move {
4538 use tokio::io::{AsyncReadExt, AsyncWriteExt};
4539 let mut buf = [0u8; 8192];
4540 loop {
4541 match pipe_in.read(&mut buf).await {
4542 Ok(0) => break, Ok(n) => {
4544 if child_stdin.write_all(&buf[..n]).await.is_err() {
4545 break; }
4547 }
4548 Err(_) => break,
4549 }
4550 }
4551 })
4553 })
4554 } else if let Some(data) = stdin_string {
4555 child.stdin.take().map(|mut child_stdin| {
4563 tokio::spawn(async move {
4564 use tokio::io::AsyncWriteExt;
4565 let _ = child_stdin.write_all(data.as_bytes()).await;
4566 })
4567 })
4568 } else {
4569 None
4570 };
4571
4572 struct AbortStdinCopyOnDrop(Option<tokio::task::JoinHandle<()>>);
4580 impl Drop for AbortStdinCopyOnDrop {
4581 fn drop(&mut self) {
4582 if let Some(t) = self.0.take() {
4583 t.abort();
4584 }
4585 }
4586 }
4587 let _stdin_copy_guard = AbortStdinCopyOnDrop(stdin_task);
4588
4589 if inherit_output {
4590 #[cfg(unix)]
4592 if let Some(ref term) = self.terminal_state {
4593 let child_id = child.id().unwrap_or(0);
4594 let pid = nix::unistd::Pid::from_raw(child_id as i32);
4595 let pgid = pid; if let Err(e) = term.give_terminal_to(pgid) {
4599 tracing::warn!("failed to give terminal to child: {}", e);
4600 }
4601
4602 let term_clone = term.clone();
4603 let cmd_name = name.to_string();
4604 let cmd_display = format!("{} {}", name, argv.join(" "));
4605 let jobs = self.jobs.clone();
4606
4607 let wait_complete = std::sync::Arc::new(
4621 std::sync::atomic::AtomicBool::new(false)
4622 );
4623 let cancel_watcher = {
4624 let cancel = cancel.clone();
4625 let wc = wait_complete.clone();
4626 let target = kill_target.as_ref().map(|t| {
4634 crate::pidfd::KillTarget::from_pid(t.pid())
4646 });
4647 tokio::spawn(async move {
4648 cancel.cancelled().await;
4649 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
4650 use nix::sys::signal::Signal;
4651 if let Some(t) = &target {
4652 t.signal(Signal::SIGTERM);
4653 t.signal_pg(Signal::SIGTERM);
4654 } else {
4655 let _ = nix::sys::signal::kill(pid, Signal::SIGTERM);
4656 let _ = nix::sys::signal::killpg(pid, Signal::SIGTERM);
4657 }
4658 if kill_grace > Duration::ZERO {
4659 tokio::time::sleep(kill_grace).await;
4660 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
4661 }
4662 if let Some(t) = &target {
4663 t.signal(Signal::SIGKILL);
4664 t.signal_pg(Signal::SIGKILL);
4665 } else {
4666 let _ = nix::sys::signal::kill(pid, Signal::SIGKILL);
4667 let _ = nix::sys::signal::killpg(pid, Signal::SIGKILL);
4668 }
4669 })
4670 };
4671 struct AbortOnDrop(tokio::task::JoinHandle<()>);
4672 impl Drop for AbortOnDrop {
4673 fn drop(&mut self) {
4674 self.0.abort();
4675 }
4676 }
4677 let _watcher_guard = AbortOnDrop(cancel_watcher);
4678
4679 let wait_complete_setter = wait_complete.clone();
4680 let code = tokio::task::block_in_place(move || {
4681 let result = term_clone.wait_for_foreground(pid);
4682 wait_complete_setter.store(true, std::sync::atomic::Ordering::SeqCst);
4684
4685 if let Err(e) = term_clone.reclaim_terminal() {
4687 tracing::warn!("failed to reclaim terminal: {}", e);
4688 }
4689
4690 match result {
4691 crate::terminal::WaitResult::Exited(code) => code as i64,
4692 crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
4693 crate::terminal::WaitResult::Stopped(_sig) => {
4694 let rt = tokio::runtime::Handle::current();
4696 let job_id = rt.block_on(jobs.register_stopped(
4697 cmd_display,
4698 child_id,
4699 child_id, ));
4701 eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
4702 148 }
4704 }
4705 });
4706
4707 return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
4708 }
4709
4710 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
4712 Ok(s) => s,
4713 Err(e) => {
4714 return Ok(Some(ExecResult::failure(
4715 1,
4716 format!("{}: failed to wait: {}", name, e),
4717 )));
4718 }
4719 };
4720
4721 let code = status.code().unwrap_or_else(|| {
4722 #[cfg(unix)]
4723 {
4724 use std::os::unix::process::ExitStatusExt;
4725 128 + status.signal().unwrap_or(0)
4726 }
4727 #[cfg(not(unix))]
4728 {
4729 -1
4730 }
4731 }) as i64;
4732
4733 Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
4735 } else {
4736 let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
4738 let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
4739
4740 let stdout_pipe = child.stdout.take();
4741 let stderr_pipe = child.stderr.take();
4742
4743 let stdout_clone = stdout_stream.clone();
4744 let stderr_clone = stderr_stream.clone();
4745
4746 let stdout_task = stdout_pipe.map(|pipe| {
4747 tokio::spawn(async move {
4748 drain_to_stream(pipe, stdout_clone).await;
4749 })
4750 });
4751
4752 let stderr_task = stderr_pipe.map(|pipe| {
4753 tokio::spawn(async move {
4754 drain_to_stream(pipe, stderr_clone).await;
4755 })
4756 });
4757
4758 let cancelled_before_wait = cancel.is_cancelled();
4759 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
4760 Ok(s) => s,
4761 Err(e) => {
4762 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
4764 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
4765 return Ok(Some(ExecResult::failure(
4766 1,
4767 format!("{}: failed to wait: {}", name, e),
4768 )));
4769 }
4770 };
4771
4772 if cancelled_before_wait || cancel.is_cancelled() {
4776 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
4777 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
4778 } else {
4779 if let Some(task) = stdout_task {
4780 let _ = task.await;
4782 }
4783 if let Some(task) = stderr_task {
4784 let _ = task.await;
4785 }
4786 }
4787
4788 let code = status.code().unwrap_or_else(|| {
4789 #[cfg(unix)]
4790 {
4791 use std::os::unix::process::ExitStatusExt;
4792 128 + status.signal().unwrap_or(0)
4793 }
4794 #[cfg(not(unix))]
4795 {
4796 -1
4797 }
4798 }) as i64;
4799
4800 let stdout = stdout_stream.read().await;
4804 let stderr = stderr_stream.read_string().await;
4805 let mut result = ExecResult::success_text_or_bytes(stdout).with_code(code);
4806 result.err = stderr;
4807 Ok(Some(result))
4808 }
4809 }
4810
4811 pub async fn get_var(&self, name: &str) -> Option<Value> {
4815 let scope = self.scope.read().await;
4816 scope.get(name).cloned()
4817 }
4818
4819 #[cfg(test)]
4821 pub async fn error_exit_enabled(&self) -> bool {
4822 let scope = self.scope.read().await;
4823 scope.error_exit_enabled()
4824 }
4825
4826 pub async fn set_var(&self, name: &str, value: Value) {
4828 let mut scope = self.scope.write().await;
4829 scope.set(name.to_string(), value);
4830 }
4831
4832 pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
4834 let mut scope = self.scope.write().await;
4835 scope.set_positional(script_name, args);
4836 }
4837
4838 pub async fn list_vars(&self) -> Vec<(String, Value)> {
4840 let scope = self.scope.read().await;
4841 scope.all()
4842 }
4843
4844 pub async fn exported_vars(&self) -> Vec<(String, Value)> {
4847 let scope = self.scope.read().await;
4848 scope.exported_vars()
4849 }
4850
4851 pub async fn cwd(&self) -> PathBuf {
4855 self.exec_ctx.read().await.cwd.clone()
4856 }
4857
4858 pub async fn set_cwd(&self, path: PathBuf) {
4860 let mut ctx = self.exec_ctx.write().await;
4861 ctx.set_cwd(path);
4862 }
4863
4864 pub async fn try_set_cwd(&self, path: PathBuf) -> bool {
4870 let backend = self.exec_ctx.read().await.backend.clone();
4873 let is_dir = matches!(backend.stat(&path).await, Ok(entry) if entry.is_dir());
4874 if is_dir {
4875 self.exec_ctx.write().await.set_cwd(path);
4876 }
4877 is_dir
4878 }
4879
4880 pub async fn last_result(&self) -> ExecResult {
4884 let scope = self.scope.read().await;
4885 scope.last_result().clone()
4886 }
4887
4888 pub async fn has_function(&self, name: &str) -> bool {
4892 self.user_tools.read().await.contains_key(name)
4893 }
4894
4895 pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
4897 self.tools.schemas()
4898 }
4899
4900 pub fn jobs(&self) -> Arc<JobManager> {
4904 self.jobs.clone()
4905 }
4906
4907 pub fn vfs(&self) -> Arc<VfsRouter> {
4911 self.vfs.clone()
4912 }
4913
4914 pub async fn reset(&self) -> Result<()> {
4921 {
4922 let mut scope = self.scope.write().await;
4923 *scope = Scope::new();
4924 }
4925 {
4926 let mut ctx = self.exec_ctx.write().await;
4927 ctx.cwd = PathBuf::from("/");
4928 }
4929 Ok(())
4930 }
4931
4932 pub async fn shutdown(self) -> Result<()> {
4934 self.jobs.wait_all().await;
4936 Ok(())
4937 }
4938
4939 async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
4950 if let Some(d) = self.dispatcher() {
4955 ctx.dispatcher = Some(d);
4956 }
4957
4958 {
4960 let mut scope = self.scope.write().await;
4961 *scope = ctx.scope.clone();
4962 }
4963 {
4964 let mut ec = self.exec_ctx.write().await;
4965 ec.cwd = ctx.cwd.clone();
4966 ec.prev_cwd = ctx.prev_cwd.clone();
4967 ec.stdin = ctx.stdin.take();
4968 ec.stdin_data = ctx.stdin_data.take();
4969 ec.stdin_data_rx = ctx.stdin_data_rx.take();
4974 ec.pipe_stdin = ctx.pipe_stdin.take();
4980 ec.pipe_stdout = ctx.pipe_stdout.take();
4981 if let Some(stderr) = ctx.stderr.clone() {
4982 ec.stderr = Some(stderr);
4983 }
4984 ec.aliases = ctx.aliases.clone();
4985 ec.ignore_config = ctx.ignore_config.clone();
4986 ec.output_limit = ctx.output_limit.clone();
4987 ec.pipeline_position = ctx.pipeline_position;
4988 ec.cancel = ctx.cancel.clone();
4993 ec.watchdog = ctx.watchdog.clone();
4997 }
4998
4999 let result = self.execute_command(&cmd.name, &cmd.args).await?;
5001
5002 {
5004 let scope = self.scope.read().await;
5005 ctx.scope = scope.clone();
5006 }
5007 {
5008 let mut ec = self.exec_ctx.write().await;
5009 ctx.cwd = ec.cwd.clone();
5010 ctx.prev_cwd = ec.prev_cwd.clone();
5011 ctx.aliases = ec.aliases.clone();
5012 ctx.ignore_config = ec.ignore_config.clone();
5013 ctx.output_limit = ec.output_limit.clone();
5014 ctx.pipe_stdin = ec.pipe_stdin.take();
5019 ctx.pipe_stdout = ec.pipe_stdout.take();
5020 }
5021
5022 Ok(result)
5023 }
5024}
5025
5026#[async_trait]
5027impl CommandDispatcher for Kernel {
5028 async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
5034 self.dispatch_command(cmd, ctx).await
5035 }
5036
5037 async fn eval_expr(&self, expr: &Expr, _ctx: &ExecContext) -> Result<Value> {
5044 self.eval_expr_async(expr).await
5045 }
5046
5047 async fn fork(&self) -> Arc<dyn CommandDispatcher> {
5053 let fork: Arc<Kernel> = Kernel::fork(self).await;
5054 fork
5055 }
5056
5057 async fn fork_attached(&self) -> Arc<dyn CommandDispatcher> {
5059 let fork: Arc<Kernel> = Kernel::fork_attached(self).await;
5060 fork
5061 }
5062}
5063
5064fn finalize_output(
5072 result: ExecResult,
5073 format: Option<crate::interpreter::OutputFormat>,
5074 owns_output: bool,
5075) -> ExecResult {
5076 match format {
5077 Some(_) if owns_output => result,
5078 Some(format) => apply_output_format(result, format),
5079 None => result,
5080 }
5081}
5082
5083fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
5092 accumulated.materialize();
5096 match new.out_bytes() {
5097 Some(new_bytes) => {
5101 let mut combined: Vec<u8> = match accumulated.out_bytes() {
5102 Some(b) => b.to_vec(),
5103 None => accumulated.text_out().into_owned().into_bytes(),
5104 };
5105 combined.extend_from_slice(new_bytes);
5106 accumulated.set_out_bytes(combined);
5107 }
5108 None => accumulated.push_out(&new.text_out()),
5109 }
5110 accumulated.err.push_str(&new.err);
5111 accumulated.code = new.code;
5112 accumulated.data = new.data.clone();
5113 accumulated.did_spill = new.did_spill;
5114 accumulated.original_code = new.original_code;
5115 accumulated.content_type = new.content_type.clone();
5116 accumulated.baggage.clone_from(&new.baggage);
5117}
5118
5119fn fold_loop_output_into_flow(loop_output: ExecResult, flow: &mut ControlFlow) {
5125 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5126 let mut merged = loop_output;
5127 accumulate_result(&mut merged, result);
5128 *result = merged;
5129 }
5130}
5131
5132fn accumulate_flow_output(accumulated: &mut ExecResult, flow: &ControlFlow) {
5136 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5137 accumulate_result(accumulated, result);
5138 }
5139}
5140
5141fn is_truthy(value: &Value) -> bool {
5143 match value {
5144 Value::Null => false,
5145 Value::Bool(b) => *b,
5146 Value::Int(i) => *i != 0,
5147 Value::Float(f) => *f != 0.0,
5148 Value::String(s) => !s.is_empty(),
5149 Value::Json(json) => match json {
5150 serde_json::Value::Null => false,
5151 serde_json::Value::Array(arr) => !arr.is_empty(),
5152 serde_json::Value::Object(obj) => !obj.is_empty(),
5153 serde_json::Value::Bool(b) => *b,
5154 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
5155 serde_json::Value::String(s) => !s.is_empty(),
5156 },
5157 Value::Bytes(b) => !b.is_empty(), }
5159}
5160
5161fn apply_tilde_expansion(value: Value, home: Option<&str>) -> Value {
5167 match value {
5168 Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s, home)),
5169 _ => value,
5170 }
5171}
5172
5173pub(crate) fn push_repeatable_value(
5182 tool_args: &mut ToolArgs,
5183 flag_name: &str,
5184 canonical: &str,
5185 v: Value,
5186) -> anyhow::Result<()> {
5187 let occ = crate::interpreter::value_to_json(&v);
5188 let entry = tool_args
5189 .named
5190 .entry(canonical.to_string())
5191 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
5192 if let Value::Json(serde_json::Value::Array(items)) = entry {
5193 items.push(occ);
5194 Ok(())
5195 } else {
5196 anyhow::bail!("--{flag_name}: named[{canonical}] already holds a non-array value")
5197 }
5198}
5199
5200pub(crate) fn bind_glued_short_value(
5207 tool_args: &mut ToolArgs,
5208 flag_name: &str,
5209 canonical: &str,
5210 consumes: usize,
5211 repeatable: bool,
5212 value: String,
5213) -> anyhow::Result<()> {
5214 if consumes > 1 {
5215 anyhow::bail!(
5216 "-{flag_name} takes {consumes} arguments; use the separated form, not a glued value"
5217 );
5218 }
5219 if repeatable {
5220 push_repeatable_value(tool_args, flag_name, canonical, Value::String(value))
5221 } else {
5222 tool_args
5223 .named
5224 .insert(canonical.to_string(), Value::String(value));
5225 Ok(())
5226 }
5227}
5228
5229#[cfg(all(unix, feature = "subprocess"))]
5235pub(crate) async fn wait_or_kill(
5236 child: &mut tokio::process::Child,
5237 target: Option<&crate::pidfd::KillTarget>,
5238 cancel: &tokio_util::sync::CancellationToken,
5239 grace: Duration,
5240) -> std::io::Result<std::process::ExitStatus> {
5241 tokio::select! {
5242 biased;
5243 status = child.wait() => status,
5244 _ = cancel.cancelled() => kill_with_grace(child, target, grace).await,
5245 }
5246}
5247
5248#[cfg(all(not(unix), feature = "subprocess"))]
5249pub(crate) async fn wait_or_kill(
5250 child: &mut tokio::process::Child,
5251 _target: Option<&()>,
5252 cancel: &tokio_util::sync::CancellationToken,
5253 _grace: Duration,
5254) -> std::io::Result<std::process::ExitStatus> {
5255 tokio::select! {
5256 biased;
5257 status = child.wait() => status,
5258 _ = cancel.cancelled() => {
5259 let _ = child.start_kill();
5260 child.wait().await
5261 }
5262 }
5263}
5264
5265#[cfg(all(unix, feature = "subprocess"))]
5271pub(crate) async fn kill_with_grace(
5272 child: &mut tokio::process::Child,
5273 target: Option<&crate::pidfd::KillTarget>,
5274 grace: Duration,
5275) -> std::io::Result<std::process::ExitStatus> {
5276 use nix::sys::signal::Signal;
5277
5278 if let Some(t) = target {
5279 t.signal(Signal::SIGTERM);
5280 t.signal_pg(Signal::SIGTERM);
5281 if grace > Duration::ZERO
5282 && let Ok(status) = tokio::time::timeout(grace, child.wait()).await
5283 {
5284 return status;
5285 }
5286 t.signal(Signal::SIGKILL);
5287 t.signal_pg(Signal::SIGKILL);
5288 }
5289 child.wait().await
5290}
5291
5292#[cfg(all(test, feature = "subprocess"))]
5293#[allow(clippy::expect_used)]
5294mod tests {
5295 use super::*;
5296
5297 #[tokio::test]
5298 async fn test_kernel_transient() {
5299 let kernel = Kernel::transient().expect("failed to create kernel");
5300 assert_eq!(kernel.name(), "transient");
5301 }
5302
5303 #[tokio::test]
5304 async fn test_kernel_execute_echo() {
5305 let kernel = Kernel::transient().expect("failed to create kernel");
5306 let result = kernel.execute("echo hello").await.expect("execution failed");
5307 assert!(result.ok());
5308 assert_eq!(result.text_out().trim(), "hello");
5309 }
5310
5311 #[tokio::test]
5312 async fn test_multiple_statements_accumulate_output() {
5313 let kernel = Kernel::transient().expect("failed to create kernel");
5314 let result = kernel
5315 .execute("echo one\necho two\necho three")
5316 .await
5317 .expect("execution failed");
5318 assert!(result.ok());
5319 assert!(result.text_out().contains("one"), "missing 'one': {}", result.text_out());
5321 assert!(result.text_out().contains("two"), "missing 'two': {}", result.text_out());
5322 assert!(result.text_out().contains("three"), "missing 'three': {}", result.text_out());
5323 }
5324
5325 #[tokio::test]
5326 async fn test_and_chain_accumulates_output() {
5327 let kernel = Kernel::transient().expect("failed to create kernel");
5328 let result = kernel
5329 .execute("echo first && echo second")
5330 .await
5331 .expect("execution failed");
5332 assert!(result.ok());
5333 assert!(result.text_out().contains("first"), "missing 'first': {}", result.text_out());
5334 assert!(result.text_out().contains("second"), "missing 'second': {}", result.text_out());
5335 }
5336
5337 #[tokio::test]
5338 async fn test_for_loop_accumulates_output() {
5339 let kernel = Kernel::transient().expect("failed to create kernel");
5340 let result = kernel
5341 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
5342 .await
5343 .expect("execution failed");
5344 assert!(result.ok());
5345 assert!(result.text_out().contains("item: a"), "missing 'item: a': {}", result.text_out());
5346 assert!(result.text_out().contains("item: b"), "missing 'item: b': {}", result.text_out());
5347 assert!(result.text_out().contains("item: c"), "missing 'item: c': {}", result.text_out());
5348 }
5349
5350 #[tokio::test]
5351 async fn test_while_loop_accumulates_output() {
5352 let kernel = Kernel::transient().expect("failed to create kernel");
5353 let result = kernel
5354 .execute(r#"
5355 N=3
5356 while [[ ${N} -gt 0 ]]; do
5357 echo "N=${N}"
5358 N=$((N - 1))
5359 done
5360 "#)
5361 .await
5362 .expect("execution failed");
5363 assert!(result.ok());
5364 assert!(result.text_out().contains("N=3"), "missing 'N=3': {}", result.text_out());
5365 assert!(result.text_out().contains("N=2"), "missing 'N=2': {}", result.text_out());
5366 assert!(result.text_out().contains("N=1"), "missing 'N=1': {}", result.text_out());
5367 }
5368
5369 #[tokio::test]
5370 async fn test_kernel_set_var() {
5371 let kernel = Kernel::transient().expect("failed to create kernel");
5372
5373 kernel.execute("X=42").await.expect("set failed");
5374
5375 let value = kernel.get_var("X").await;
5376 assert_eq!(value, Some(Value::Int(42)));
5377 }
5378
5379 #[tokio::test]
5380 async fn test_kernel_var_expansion() {
5381 let kernel = Kernel::transient().expect("failed to create kernel");
5382
5383 kernel.execute("NAME=\"world\"").await.expect("set failed");
5384 let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
5385
5386 assert!(result.ok());
5387 assert_eq!(result.text_out().trim(), "hello world");
5388 }
5389
5390 #[tokio::test]
5391 async fn test_kernel_last_result() {
5392 let kernel = Kernel::transient().expect("failed to create kernel");
5393
5394 kernel.execute("echo test").await.expect("echo failed");
5395
5396 let last = kernel.last_result().await;
5397 assert!(last.ok());
5398 assert_eq!(last.text_out().trim(), "test");
5399 }
5400
5401 #[tokio::test]
5402 async fn test_kernel_tool_not_found() {
5403 let kernel = Kernel::transient().expect("failed to create kernel");
5404
5405 let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
5406 assert!(!result.ok());
5407 assert_eq!(result.code, 127);
5408 assert!(result.err.contains("command not found"));
5409 }
5410
5411 #[tokio::test]
5412 async fn test_external_command_true() {
5413 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
5415
5416 let result = kernel.execute("true").await.expect("execution failed");
5418 assert!(result.ok(), "true should succeed: {:?}", result);
5420 }
5421
5422 #[tokio::test]
5423 async fn test_external_command_basic() {
5424 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
5426
5427 let path_var = std::env::var("PATH").unwrap_or_default();
5432 eprintln!("System PATH: {}", path_var);
5433
5434 kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
5436
5437 let result = kernel.execute("uname").await.expect("execution failed");
5440 eprintln!("uname result: {:?}", result);
5441 assert!(result.ok() || result.code == 127, "uname: {:?}", result);
5443 }
5444
5445 #[tokio::test]
5446 async fn test_kernel_reset() {
5447 let kernel = Kernel::transient().expect("failed to create kernel");
5448
5449 kernel.execute("X=1").await.expect("set failed");
5450 assert!(kernel.get_var("X").await.is_some());
5451
5452 kernel.reset().await.expect("reset failed");
5453 assert!(kernel.get_var("X").await.is_none());
5454 }
5455
5456 #[tokio::test]
5457 async fn test_kernel_cwd() {
5458 let kernel = Kernel::transient().expect("failed to create kernel");
5459
5460 let cwd = kernel.cwd().await;
5462 let home = std::env::var("HOME")
5463 .map(PathBuf::from)
5464 .unwrap_or_else(|_| PathBuf::from("/"));
5465 assert_eq!(cwd, home);
5466
5467 kernel.set_cwd(PathBuf::from("/tmp")).await;
5468 assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
5469 }
5470
5471 #[tokio::test]
5472 async fn test_kernel_list_vars() {
5473 let kernel = Kernel::transient().expect("failed to create kernel");
5474
5475 kernel.execute("A=1").await.ok();
5476 kernel.execute("B=2").await.ok();
5477
5478 let vars = kernel.list_vars().await;
5479 assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
5480 assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
5481 }
5482
5483 #[tokio::test]
5484 async fn test_is_truthy() {
5485 assert!(!is_truthy(&Value::Null));
5486 assert!(!is_truthy(&Value::Bool(false)));
5487 assert!(is_truthy(&Value::Bool(true)));
5488 assert!(!is_truthy(&Value::Int(0)));
5489 assert!(is_truthy(&Value::Int(1)));
5490 assert!(!is_truthy(&Value::String("".into())));
5491 assert!(is_truthy(&Value::String("x".into())));
5492 }
5493
5494 #[tokio::test]
5495 async fn test_jq_in_pipeline() {
5496 let kernel = Kernel::transient().expect("failed to create kernel");
5497 let result = kernel
5499 .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
5500 .await
5501 .expect("execution failed");
5502 assert!(result.ok(), "jq pipeline failed: {}", result.err);
5503 assert_eq!(result.text_out().trim(), "Alice");
5504 }
5505
5506 #[tokio::test]
5507 async fn test_user_defined_tool() {
5508 let kernel = Kernel::transient().expect("failed to create kernel");
5509
5510 kernel
5512 .execute(r#"greet() { echo "Hello, $1!" }"#)
5513 .await
5514 .expect("function definition failed");
5515
5516 let result = kernel
5518 .execute(r#"greet "World""#)
5519 .await
5520 .expect("function call failed");
5521
5522 assert!(result.ok(), "greet failed: {}", result.err);
5523 assert_eq!(result.text_out().trim(), "Hello, World!");
5524 }
5525
5526 #[tokio::test]
5527 async fn test_user_tool_positional_args() {
5528 let kernel = Kernel::transient().expect("failed to create kernel");
5529
5530 kernel
5532 .execute(r#"greet() { echo "Hi $1" }"#)
5533 .await
5534 .expect("function definition failed");
5535
5536 let result = kernel
5538 .execute(r#"greet "Amy""#)
5539 .await
5540 .expect("function call failed");
5541
5542 assert!(result.ok(), "greet failed: {}", result.err);
5543 assert_eq!(result.text_out().trim(), "Hi Amy");
5544 }
5545
5546 #[tokio::test]
5547 async fn test_function_shared_scope() {
5548 let kernel = Kernel::transient().expect("failed to create kernel");
5549
5550 kernel
5552 .execute(r#"SECRET="hidden""#)
5553 .await
5554 .expect("set failed");
5555
5556 kernel
5558 .execute(r#"access_parent() {
5559 echo "${SECRET}"
5560 SECRET="modified"
5561 }"#)
5562 .await
5563 .expect("function definition failed");
5564
5565 let result = kernel.execute("access_parent").await.expect("function call failed");
5567
5568 assert!(
5570 result.text_out().contains("hidden"),
5571 "Function should access parent scope, got: {}",
5572 result.text_out()
5573 );
5574
5575 let secret = kernel.get_var("SECRET").await;
5577 assert_eq!(
5578 secret,
5579 Some(Value::String("modified".into())),
5580 "Function should modify parent scope"
5581 );
5582 }
5583
5584 #[tokio::test]
5585 #[ignore = "exec replaces the test binary via CommandExt::exec, hangs libtest; cannot be run under cargo test"]
5586 async fn test_exec_builtin() {
5587 let kernel = Kernel::transient().expect("failed to create kernel");
5588 let result = kernel
5590 .execute(r#"exec command="/bin/echo" argv="hello world""#)
5591 .await
5592 .expect("exec failed");
5593
5594 assert!(result.ok(), "exec failed: {}", result.err);
5595 assert_eq!(result.text_out().trim(), "hello world");
5596 }
5597
5598 #[tokio::test]
5599 async fn test_while_false_never_runs() {
5600 let kernel = Kernel::transient().expect("failed to create kernel");
5601
5602 let result = kernel
5604 .execute(r#"
5605 while false; do
5606 echo "should not run"
5607 done
5608 "#)
5609 .await
5610 .expect("while false failed");
5611
5612 assert!(result.ok());
5613 assert!(result.text_out().is_empty(), "while false should not execute body: {}", result.text_out());
5614 }
5615
5616 #[tokio::test]
5617 async fn test_while_string_comparison() {
5618 let kernel = Kernel::transient().expect("failed to create kernel");
5619
5620 kernel.execute(r#"FLAG="go""#).await.expect("set failed");
5622
5623 let result = kernel
5626 .execute(r#"
5627 while [[ ${FLAG} == "go" ]]; do
5628 FLAG="stop"
5629 echo "running"
5630 done
5631 "#)
5632 .await
5633 .expect("while with string cmp failed");
5634
5635 assert!(result.ok());
5636 assert!(result.text_out().contains("running"), "should have run once: {}", result.text_out());
5637
5638 let flag = kernel.get_var("FLAG").await;
5640 assert_eq!(flag, Some(Value::String("stop".into())));
5641 }
5642
5643 #[tokio::test]
5644 async fn test_while_numeric_comparison() {
5645 let kernel = Kernel::transient().expect("failed to create kernel");
5646
5647 kernel.execute("N=5").await.expect("set failed");
5649
5650 let result = kernel
5652 .execute(r#"
5653 while [[ ${N} -gt 3 ]]; do
5654 N=3
5655 echo "N was greater"
5656 done
5657 "#)
5658 .await
5659 .expect("while with > failed");
5660
5661 assert!(result.ok());
5662 assert!(result.text_out().contains("N was greater"), "should have run once: {}", result.text_out());
5663 }
5664
5665 #[tokio::test]
5666 async fn test_break_in_while_loop() {
5667 let kernel = Kernel::transient().expect("failed to create kernel");
5668
5669 let result = kernel
5670 .execute(r#"
5671 I=0
5672 while true; do
5673 I=1
5674 echo "before break"
5675 break
5676 echo "after break"
5677 done
5678 "#)
5679 .await
5680 .expect("while with break failed");
5681
5682 assert!(result.ok());
5683 assert!(result.text_out().contains("before break"), "should see before break: {}", result.text_out());
5684 assert!(!result.text_out().contains("after break"), "should not see after break: {}", result.text_out());
5685
5686 let i = kernel.get_var("I").await;
5688 assert_eq!(i, Some(Value::Int(1)));
5689 }
5690
5691 #[tokio::test]
5692 async fn test_continue_in_while_loop() {
5693 let kernel = Kernel::transient().expect("failed to create kernel");
5694
5695 let result = kernel
5700 .execute(r#"
5701 STATE="start"
5702 AFTER_CONTINUE="no"
5703 while [[ ${STATE} != "done" ]]; do
5704 if [[ ${STATE} == "start" ]]; then
5705 STATE="middle"
5706 continue
5707 AFTER_CONTINUE="yes"
5708 fi
5709 if [[ ${STATE} == "middle" ]]; then
5710 STATE="done"
5711 fi
5712 done
5713 "#)
5714 .await
5715 .expect("while with continue failed");
5716
5717 assert!(result.ok());
5718
5719 let state = kernel.get_var("STATE").await;
5721 assert_eq!(state, Some(Value::String("done".into())));
5722
5723 let after = kernel.get_var("AFTER_CONTINUE").await;
5725 assert_eq!(after, Some(Value::String("no".into())));
5726 }
5727
5728 #[tokio::test]
5729 async fn test_break_with_level() {
5730 let kernel = Kernel::transient().expect("failed to create kernel");
5731
5732 let result = kernel
5737 .execute(r#"
5738 OUTER=0
5739 while true; do
5740 OUTER=1
5741 for X in "1 2"; do
5742 break 2
5743 done
5744 OUTER=2
5745 done
5746 "#)
5747 .await
5748 .expect("nested break failed");
5749
5750 assert!(result.ok());
5751
5752 let outer = kernel.get_var("OUTER").await;
5754 assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
5755 }
5756
5757 #[tokio::test]
5758 async fn test_return_from_tool() {
5759 let kernel = Kernel::transient().expect("failed to create kernel");
5760
5761 kernel
5763 .execute(r#"early_return() {
5764 if [[ $1 == 1 ]]; then
5765 return 42
5766 fi
5767 echo "not returned"
5768 }"#)
5769 .await
5770 .expect("function definition failed");
5771
5772 let result = kernel
5775 .execute("early_return 1")
5776 .await
5777 .expect("function call failed");
5778
5779 assert_eq!(result.code, 42);
5781 assert!(result.text_out().is_empty());
5783 }
5784
5785 #[tokio::test]
5786 async fn test_return_without_value() {
5787 let kernel = Kernel::transient().expect("failed to create kernel");
5788
5789 kernel
5791 .execute(r#"early_exit() {
5792 if [[ $1 == "stop" ]]; then
5793 return
5794 fi
5795 echo "continued"
5796 }"#)
5797 .await
5798 .expect("function definition failed");
5799
5800 let result = kernel
5802 .execute(r#"early_exit "stop""#)
5803 .await
5804 .expect("function call failed");
5805
5806 assert!(result.ok());
5807 assert!(result.text_out().is_empty() || result.text_out().trim().is_empty());
5808 }
5809
5810 #[tokio::test]
5811 async fn test_exit_stops_execution() {
5812 let kernel = Kernel::transient().expect("failed to create kernel");
5813
5814 kernel
5816 .execute(r#"
5817 BEFORE="yes"
5818 exit 0
5819 AFTER="yes"
5820 "#)
5821 .await
5822 .expect("execution failed");
5823
5824 let before = kernel.get_var("BEFORE").await;
5826 assert_eq!(before, Some(Value::String("yes".into())));
5827
5828 let after = kernel.get_var("AFTER").await;
5829 assert!(after.is_none(), "AFTER should not be set after exit");
5830 }
5831
5832 #[tokio::test]
5833 async fn test_exit_with_code() {
5834 let kernel = Kernel::transient().expect("failed to create kernel");
5835
5836 let result = kernel
5838 .execute("exit 42")
5839 .await
5840 .expect("exit failed");
5841
5842 assert_eq!(result.code, 42);
5843 assert!(result.text_out().is_empty(), "exit should not produce stdout");
5844 }
5845
5846 #[tokio::test]
5847 async fn test_set_e_stops_on_failure() {
5848 let kernel = Kernel::transient().expect("failed to create kernel");
5849
5850 kernel.execute("set -e").await.expect("set -e failed");
5852
5853 kernel
5855 .execute(r#"
5856 STEP1="done"
5857 false
5858 STEP2="done"
5859 "#)
5860 .await
5861 .expect("execution failed");
5862
5863 let step1 = kernel.get_var("STEP1").await;
5865 assert_eq!(step1, Some(Value::String("done".into())));
5866
5867 let step2 = kernel.get_var("STEP2").await;
5868 assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
5869 }
5870
5871 #[tokio::test]
5872 async fn test_set_plus_e_disables_error_exit() {
5873 let kernel = Kernel::transient().expect("failed to create kernel");
5874
5875 kernel.execute("set -e").await.expect("set -e failed");
5877 kernel.execute("set +e").await.expect("set +e failed");
5878
5879 kernel
5881 .execute(r#"
5882 STEP1="done"
5883 false
5884 STEP2="done"
5885 "#)
5886 .await
5887 .expect("execution failed");
5888
5889 let step1 = kernel.get_var("STEP1").await;
5891 assert_eq!(step1, Some(Value::String("done".into())));
5892
5893 let step2 = kernel.get_var("STEP2").await;
5894 assert_eq!(step2, Some(Value::String("done".into())));
5895 }
5896
5897 #[tokio::test]
5898 async fn test_set_ignores_unknown_options() {
5899 let kernel = Kernel::transient().expect("failed to create kernel");
5900
5901 let result = kernel
5903 .execute("set -e -u -o pipefail")
5904 .await
5905 .expect("set with unknown options failed");
5906
5907 assert!(result.ok(), "set should succeed with unknown options");
5908
5909 kernel
5911 .execute(r#"
5912 BEFORE="yes"
5913 false
5914 AFTER="yes"
5915 "#)
5916 .await
5917 .ok();
5918
5919 let after = kernel.get_var("AFTER").await;
5920 assert!(after.is_none(), "-e should be enabled despite unknown options");
5921 }
5922
5923 #[tokio::test]
5924 async fn test_set_no_args_shows_settings() {
5925 let kernel = Kernel::transient().expect("failed to create kernel");
5926
5927 kernel.execute("set -e").await.expect("set -e failed");
5929
5930 let result = kernel.execute("set").await.expect("set failed");
5932
5933 assert!(result.ok());
5934 assert!(result.text_out().contains("set -e"), "should show -e is enabled: {}", result.text_out());
5935 }
5936
5937 #[tokio::test]
5938 async fn test_set_e_in_pipeline() {
5939 let kernel = Kernel::transient().expect("failed to create kernel");
5940
5941 kernel.execute("set -e").await.expect("set -e failed");
5942
5943 kernel
5945 .execute(r#"
5946 BEFORE="yes"
5947 false | cat
5948 AFTER="yes"
5949 "#)
5950 .await
5951 .ok();
5952
5953 let before = kernel.get_var("BEFORE").await;
5954 assert_eq!(before, Some(Value::String("yes".into())));
5955
5956 }
5961
5962 #[tokio::test]
5963 async fn test_set_e_with_and_chain() {
5964 let kernel = Kernel::transient().expect("failed to create kernel");
5965
5966 kernel.execute("set -e").await.expect("set -e failed");
5967
5968 kernel
5971 .execute(r#"
5972 RESULT="initial"
5973 false && RESULT="chained"
5974 RESULT="continued"
5975 "#)
5976 .await
5977 .ok();
5978
5979 let result = kernel.get_var("RESULT").await;
5982 assert!(result.is_some(), "RESULT should be set");
5985 }
5986
5987 #[tokio::test]
5988 async fn test_set_e_exits_in_for_loop() {
5989 let kernel = Kernel::transient().expect("failed to create kernel");
5990
5991 kernel.execute("set -e").await.expect("set -e failed");
5992
5993 kernel
5994 .execute(r#"
5995 REACHED="no"
5996 for x in 1 2 3; do
5997 false
5998 REACHED="yes"
5999 done
6000 "#)
6001 .await
6002 .ok();
6003
6004 let reached = kernel.get_var("REACHED").await;
6006 assert_eq!(reached, Some(Value::String("no".into())),
6007 "set -e should exit on failure in for loop body");
6008 }
6009
6010 #[tokio::test]
6011 async fn test_for_loop_continues_without_set_e() {
6012 let kernel = Kernel::transient().expect("failed to create kernel");
6013
6014 kernel
6016 .execute(r#"
6017 COUNT=0
6018 for x in 1 2 3; do
6019 false
6020 COUNT=$((COUNT + 1))
6021 done
6022 "#)
6023 .await
6024 .ok();
6025
6026 let count = kernel.get_var("COUNT").await;
6027 let count_val = match &count {
6029 Some(Value::Int(n)) => *n,
6030 Some(Value::String(s)) => s.parse().unwrap_or(-1),
6031 _ => -1,
6032 };
6033 assert_eq!(count_val, 3,
6034 "without set -e, loop should complete all iterations (got {:?})", count);
6035 }
6036
6037 #[tokio::test]
6042 async fn test_source_sets_variables() {
6043 let kernel = Kernel::transient().expect("failed to create kernel");
6044
6045 kernel
6047 .execute(r#"write "/test.kai" 'FOO="bar"'"#)
6048 .await
6049 .expect("write failed");
6050
6051 let result = kernel
6053 .execute(r#"source "/test.kai""#)
6054 .await
6055 .expect("source failed");
6056
6057 assert!(result.ok(), "source should succeed");
6058
6059 let foo = kernel.get_var("FOO").await;
6061 assert_eq!(foo, Some(Value::String("bar".into())));
6062 }
6063
6064 #[tokio::test]
6065 async fn test_source_with_dot_alias() {
6066 let kernel = Kernel::transient().expect("failed to create kernel");
6067
6068 kernel
6070 .execute(r#"write "/vars.kai" 'X=42'"#)
6071 .await
6072 .expect("write failed");
6073
6074 let result = kernel
6076 .execute(r#". "/vars.kai""#)
6077 .await
6078 .expect(". failed");
6079
6080 assert!(result.ok(), ". should succeed");
6081
6082 let x = kernel.get_var("X").await;
6084 assert_eq!(x, Some(Value::Int(42)));
6085 }
6086
6087 #[tokio::test]
6088 async fn test_source_not_found() {
6089 let kernel = Kernel::transient().expect("failed to create kernel");
6090
6091 let result = kernel
6093 .execute(r#"source "/nonexistent.kai""#)
6094 .await
6095 .expect("source should not fail with error");
6096
6097 assert!(!result.ok(), "source of non-existent file should fail");
6098 assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
6099 }
6100
6101 #[tokio::test]
6102 async fn test_source_missing_filename() {
6103 let kernel = Kernel::transient().expect("failed to create kernel");
6104
6105 let result = kernel
6107 .execute("source")
6108 .await
6109 .expect("source should not fail with error");
6110
6111 assert!(!result.ok(), "source without filename should fail");
6112 assert!(result.err.contains("missing filename"), "error should mention missing filename");
6113 }
6114
6115 #[tokio::test]
6116 async fn test_source_executes_multiple_statements() {
6117 let kernel = Kernel::transient().expect("failed to create kernel");
6118
6119 kernel
6121 .execute(r#"write "/multi.kai" 'A=1
6122B=2
6123C=3'"#)
6124 .await
6125 .expect("write failed");
6126
6127 kernel
6129 .execute(r#"source "/multi.kai""#)
6130 .await
6131 .expect("source failed");
6132
6133 assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
6135 assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
6136 assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
6137 }
6138
6139 #[tokio::test]
6140 async fn test_source_can_define_functions() {
6141 let kernel = Kernel::transient().expect("failed to create kernel");
6142
6143 kernel
6145 .execute(r#"write "/functions.kai" 'greet() {
6146 echo "Hello, $1!"
6147}'"#)
6148 .await
6149 .expect("write failed");
6150
6151 kernel
6153 .execute(r#"source "/functions.kai""#)
6154 .await
6155 .expect("source failed");
6156
6157 let result = kernel
6159 .execute(r#"greet "World""#)
6160 .await
6161 .expect("greet failed");
6162
6163 assert!(result.ok());
6164 assert!(result.text_out().contains("Hello, World!"));
6165 }
6166
6167 #[tokio::test]
6168 async fn test_source_inherits_error_exit() {
6169 let kernel = Kernel::transient().expect("failed to create kernel");
6170
6171 kernel.execute("set -e").await.expect("set -e failed");
6173
6174 kernel
6176 .execute(r#"write "/fail.kai" 'BEFORE="yes"
6177false
6178AFTER="yes"'"#)
6179 .await
6180 .expect("write failed");
6181
6182 kernel
6184 .execute(r#"source "/fail.kai""#)
6185 .await
6186 .ok();
6187
6188 let before = kernel.get_var("BEFORE").await;
6190 assert_eq!(before, Some(Value::String("yes".into())));
6191
6192 }
6195
6196 #[tokio::test]
6201 async fn test_set_e_and_chain_left_fails() {
6202 let kernel = Kernel::transient().expect("failed to create kernel");
6204 kernel.execute("set -e").await.expect("set -e failed");
6205
6206 kernel
6207 .execute("false && echo hi; REACHED=1")
6208 .await
6209 .expect("execution failed");
6210
6211 let reached = kernel.get_var("REACHED").await;
6212 assert_eq!(
6213 reached,
6214 Some(Value::Int(1)),
6215 "set -e should not trigger on left side of &&"
6216 );
6217 }
6218
6219 #[tokio::test]
6220 async fn test_set_e_and_chain_right_fails() {
6221 let kernel = Kernel::transient().expect("failed to create kernel");
6223 kernel.execute("set -e").await.expect("set -e failed");
6224
6225 kernel
6226 .execute("true && false; REACHED=1")
6227 .await
6228 .expect("execution failed");
6229
6230 let reached = kernel.get_var("REACHED").await;
6231 assert!(
6232 reached.is_none(),
6233 "set -e should trigger when right side of && fails"
6234 );
6235 }
6236
6237 #[tokio::test]
6238 async fn test_set_e_or_chain_recovers() {
6239 let kernel = Kernel::transient().expect("failed to create kernel");
6241 kernel.execute("set -e").await.expect("set -e failed");
6242
6243 kernel
6244 .execute("false || echo recovered; REACHED=1")
6245 .await
6246 .expect("execution failed");
6247
6248 let reached = kernel.get_var("REACHED").await;
6249 assert_eq!(
6250 reached,
6251 Some(Value::Int(1)),
6252 "set -e should not trigger when || recovers the failure"
6253 );
6254 }
6255
6256 #[tokio::test]
6257 async fn test_set_e_or_chain_both_fail() {
6258 let kernel = Kernel::transient().expect("failed to create kernel");
6260 kernel.execute("set -e").await.expect("set -e failed");
6261
6262 kernel
6263 .execute("false || false; REACHED=1")
6264 .await
6265 .expect("execution failed");
6266
6267 let reached = kernel.get_var("REACHED").await;
6268 assert!(
6269 reached.is_none(),
6270 "set -e should trigger when || chain ultimately fails"
6271 );
6272 }
6273
6274 fn schedule_cancel(kernel: &Arc<Kernel>, delay: std::time::Duration) {
6281 let k = Arc::clone(kernel);
6282 std::thread::spawn(move || {
6283 std::thread::sleep(delay);
6284 k.cancel();
6285 });
6286 }
6287
6288 #[tokio::test]
6289 async fn test_cancel_interrupts_for_loop() {
6290 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6291
6292 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
6294
6295 let result = kernel
6296 .execute("for i in $(seq 1 100000); do X=$i; done")
6297 .await
6298 .expect("execute failed");
6299
6300 assert_eq!(result.code, 130, "cancelled execution should exit with code 130");
6301
6302 let x = kernel.get_var("X").await;
6304 if let Some(Value::Int(n)) = x {
6305 assert!(n < 100000, "loop should have been interrupted before finishing, got X={n}");
6306 }
6307 }
6308
6309 #[tokio::test]
6310 async fn test_cancel_interrupts_while_loop() {
6311 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6312 kernel.execute("COUNT=0").await.expect("init failed");
6313
6314 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
6315
6316 let result = kernel
6317 .execute("while true; do COUNT=$((COUNT + 1)); done")
6318 .await
6319 .expect("execute failed");
6320
6321 assert_eq!(result.code, 130);
6322
6323 let count = kernel.get_var("COUNT").await;
6324 if let Some(Value::Int(n)) = count {
6325 assert!(n > 0, "loop should have run at least once");
6326 }
6327 }
6328
6329 #[tokio::test]
6330 async fn test_reset_after_cancel() {
6331 let kernel = Kernel::transient().expect("failed to create kernel");
6333 kernel.cancel(); let result = kernel.execute("echo hello").await.expect("execute failed");
6336 assert!(result.ok(), "execute after cancel should succeed");
6337 assert_eq!(result.text_out().trim(), "hello");
6338 }
6339
6340 #[tokio::test]
6341 async fn test_cancel_interrupts_statement_sequence() {
6342 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6343
6344 schedule_cancel(&kernel, std::time::Duration::from_millis(50));
6346
6347 let result = kernel
6348 .execute("STEP=1; sleep 5; STEP=2; sleep 5; STEP=3")
6349 .await
6350 .expect("execute failed");
6351
6352 assert_eq!(result.code, 130);
6353
6354 let step = kernel.get_var("STEP").await;
6356 assert_eq!(step, Some(Value::Int(1)), "cancel should stop before STEP=2");
6357 }
6358
6359 #[tokio::test]
6364 async fn test_case_simple_match() {
6365 let kernel = Kernel::transient().expect("failed to create kernel");
6366
6367 let result = kernel
6368 .execute(r#"
6369 case "hello" in
6370 hello) echo "matched hello" ;;
6371 world) echo "matched world" ;;
6372 esac
6373 "#)
6374 .await
6375 .expect("case failed");
6376
6377 assert!(result.ok());
6378 assert_eq!(result.text_out().trim(), "matched hello");
6379 }
6380
6381 #[tokio::test]
6382 async fn test_case_wildcard_match() {
6383 let kernel = Kernel::transient().expect("failed to create kernel");
6384
6385 let result = kernel
6386 .execute(r#"
6387 case "main.rs" in
6388 *.py) echo "Python" ;;
6389 *.rs) echo "Rust" ;;
6390 *) echo "Unknown" ;;
6391 esac
6392 "#)
6393 .await
6394 .expect("case failed");
6395
6396 assert!(result.ok());
6397 assert_eq!(result.text_out().trim(), "Rust");
6398 }
6399
6400 #[tokio::test]
6401 async fn test_case_default_match() {
6402 let kernel = Kernel::transient().expect("failed to create kernel");
6403
6404 let result = kernel
6405 .execute(r#"
6406 case "unknown.xyz" in
6407 *.py) echo "Python" ;;
6408 *.rs) echo "Rust" ;;
6409 *) echo "Default" ;;
6410 esac
6411 "#)
6412 .await
6413 .expect("case failed");
6414
6415 assert!(result.ok());
6416 assert_eq!(result.text_out().trim(), "Default");
6417 }
6418
6419 #[tokio::test]
6420 async fn test_case_no_match() {
6421 let kernel = Kernel::transient().expect("failed to create kernel");
6422
6423 let result = kernel
6425 .execute(r#"
6426 case "nope" in
6427 "yes") echo "yes" ;;
6428 "no") echo "no" ;;
6429 esac
6430 "#)
6431 .await
6432 .expect("case failed");
6433
6434 assert!(result.ok());
6435 assert!(result.text_out().is_empty(), "no match should produce empty output");
6436 }
6437
6438 #[tokio::test]
6439 async fn test_case_with_variable() {
6440 let kernel = Kernel::transient().expect("failed to create kernel");
6441
6442 kernel.execute(r#"LANG="rust""#).await.expect("set failed");
6443
6444 let result = kernel
6445 .execute(r#"
6446 case ${LANG} in
6447 python) echo "snake" ;;
6448 rust) echo "crab" ;;
6449 go) echo "gopher" ;;
6450 esac
6451 "#)
6452 .await
6453 .expect("case failed");
6454
6455 assert!(result.ok());
6456 assert_eq!(result.text_out().trim(), "crab");
6457 }
6458
6459 #[tokio::test]
6460 async fn test_case_multiple_patterns() {
6461 let kernel = Kernel::transient().expect("failed to create kernel");
6462
6463 let result = kernel
6464 .execute(r#"
6465 case "yes" in
6466 "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
6467 "n"|"no"|"N"|"NO") echo "negative" ;;
6468 esac
6469 "#)
6470 .await
6471 .expect("case failed");
6472
6473 assert!(result.ok());
6474 assert_eq!(result.text_out().trim(), "affirmative");
6475 }
6476
6477 #[tokio::test]
6478 async fn test_case_glob_question_mark() {
6479 let kernel = Kernel::transient().expect("failed to create kernel");
6480
6481 let result = kernel
6482 .execute(r#"
6483 case "test1" in
6484 test?) echo "matched test?" ;;
6485 *) echo "default" ;;
6486 esac
6487 "#)
6488 .await
6489 .expect("case failed");
6490
6491 assert!(result.ok());
6492 assert_eq!(result.text_out().trim(), "matched test?");
6493 }
6494
6495 #[tokio::test]
6496 async fn test_case_char_class() {
6497 let kernel = Kernel::transient().expect("failed to create kernel");
6498
6499 let result = kernel
6500 .execute(r#"
6501 case "Yes" in
6502 [Yy]*) echo "yes-like" ;;
6503 [Nn]*) echo "no-like" ;;
6504 esac
6505 "#)
6506 .await
6507 .expect("case failed");
6508
6509 assert!(result.ok());
6510 assert_eq!(result.text_out().trim(), "yes-like");
6511 }
6512
6513 #[tokio::test]
6518 async fn test_cat_from_pipeline() {
6519 let kernel = Kernel::transient().expect("failed to create kernel");
6520
6521 let result = kernel
6522 .execute(r#"echo "piped text" | cat"#)
6523 .await
6524 .expect("cat pipeline failed");
6525
6526 assert!(result.ok(), "cat failed: {}", result.err);
6527 assert_eq!(result.text_out().trim(), "piped text");
6528 }
6529
6530 #[tokio::test]
6531 async fn test_cat_from_pipeline_multiline() {
6532 let kernel = Kernel::transient().expect("failed to create kernel");
6533
6534 let result = kernel
6535 .execute(r#"echo "line1\nline2" | cat -n"#)
6536 .await
6537 .expect("cat pipeline failed");
6538
6539 assert!(result.ok(), "cat failed: {}", result.err);
6540 assert!(result.text_out().contains("1\t"), "output: {}", result.text_out());
6541 }
6542
6543 #[tokio::test]
6548 async fn test_heredoc_basic() {
6549 let kernel = Kernel::transient().expect("failed to create kernel");
6550
6551 let result = kernel
6552 .execute("cat <<EOF\nhello\nEOF")
6553 .await
6554 .expect("heredoc failed");
6555
6556 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
6557 assert_eq!(result.text_out().trim(), "hello");
6558 }
6559
6560 #[tokio::test]
6561 async fn test_arithmetic_in_string() {
6562 let kernel = Kernel::transient().expect("failed to create kernel");
6563
6564 let result = kernel
6565 .execute(r#"echo "result: $((1 + 2))""#)
6566 .await
6567 .expect("arithmetic in string failed");
6568
6569 assert!(result.ok(), "echo failed: {}", result.err);
6570 assert_eq!(result.text_out().trim(), "result: 3");
6571 }
6572
6573 #[tokio::test]
6574 async fn test_heredoc_multiline() {
6575 let kernel = Kernel::transient().expect("failed to create kernel");
6576
6577 let result = kernel
6578 .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
6579 .await
6580 .expect("heredoc failed");
6581
6582 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
6583 assert!(result.text_out().contains("line1"), "output: {}", result.text_out());
6584 assert!(result.text_out().contains("line2"), "output: {}", result.text_out());
6585 assert!(result.text_out().contains("line3"), "output: {}", result.text_out());
6586 }
6587
6588 #[tokio::test]
6589 async fn test_heredoc_variable_expansion() {
6590 let kernel = Kernel::transient().expect("failed to create kernel");
6592
6593 kernel.execute("GREETING=hello").await.expect("set var");
6594
6595 let result = kernel
6596 .execute("cat <<EOF\n$GREETING world\nEOF")
6597 .await
6598 .expect("heredoc expansion failed");
6599
6600 assert!(result.ok(), "heredoc expansion failed: {}", result.err);
6601 assert_eq!(result.text_out().trim(), "hello world");
6602 }
6603
6604 #[tokio::test]
6605 async fn test_heredoc_quoted_no_expansion() {
6606 let kernel = Kernel::transient().expect("failed to create kernel");
6608
6609 kernel.execute("GREETING=hello").await.expect("set var");
6610
6611 let result = kernel
6612 .execute("cat <<'EOF'\n$GREETING world\nEOF")
6613 .await
6614 .expect("quoted heredoc failed");
6615
6616 assert!(result.ok(), "quoted heredoc failed: {}", result.err);
6617 assert_eq!(result.text_out().trim(), "$GREETING world");
6618 }
6619
6620 #[tokio::test]
6621 async fn test_heredoc_default_value_expansion() {
6622 let kernel = Kernel::transient().expect("failed to create kernel");
6624
6625 let result = kernel
6626 .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
6627 .await
6628 .expect("heredoc default expansion failed");
6629
6630 assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
6631 assert_eq!(result.text_out().trim(), "fallback");
6632 }
6633
6634 #[tokio::test]
6639 async fn test_read_from_pipeline() {
6640 let kernel = Kernel::transient().expect("failed to create kernel");
6641
6642 let result = kernel
6644 .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
6645 .await
6646 .expect("read pipeline failed");
6647
6648 assert!(result.ok(), "read failed: {}", result.err);
6649 assert!(result.text_out().contains("Hello, Alice"), "output: {}", result.text_out());
6650 }
6651
6652 #[tokio::test]
6653 async fn test_read_multiple_vars_from_pipeline() {
6654 let kernel = Kernel::transient().expect("failed to create kernel");
6655
6656 let result = kernel
6657 .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
6658 .await
6659 .expect("read pipeline failed");
6660
6661 assert!(result.ok(), "read failed: {}", result.err);
6662 assert!(result.text_out().contains("John is 42"), "output: {}", result.text_out());
6663 }
6664
6665 #[tokio::test]
6670 async fn test_posix_function_with_positional_params() {
6671 let kernel = Kernel::transient().expect("failed to create kernel");
6672
6673 kernel
6675 .execute(r#"greet() { echo "Hello, $1!" }"#)
6676 .await
6677 .expect("function definition failed");
6678
6679 let result = kernel
6681 .execute(r#"greet "Amy""#)
6682 .await
6683 .expect("function call failed");
6684
6685 assert!(result.ok(), "greet failed: {}", result.err);
6686 assert_eq!(result.text_out().trim(), "Hello, Amy!");
6687 }
6688
6689 #[tokio::test]
6690 async fn test_posix_function_multiple_args() {
6691 let kernel = Kernel::transient().expect("failed to create kernel");
6692
6693 kernel
6695 .execute(r#"add_greeting() { echo "$1 $2!" }"#)
6696 .await
6697 .expect("function definition failed");
6698
6699 let result = kernel
6701 .execute(r#"add_greeting "Hello" "World""#)
6702 .await
6703 .expect("function call failed");
6704
6705 assert!(result.ok(), "function failed: {}", result.err);
6706 assert_eq!(result.text_out().trim(), "Hello World!");
6707 }
6708
6709 #[tokio::test]
6710 async fn test_bash_function_with_positional_params() {
6711 let kernel = Kernel::transient().expect("failed to create kernel");
6712
6713 kernel
6715 .execute(r#"function greet { echo "Hi $1" }"#)
6716 .await
6717 .expect("function definition failed");
6718
6719 let result = kernel
6721 .execute(r#"greet "Bob""#)
6722 .await
6723 .expect("function call failed");
6724
6725 assert!(result.ok(), "greet failed: {}", result.err);
6726 assert_eq!(result.text_out().trim(), "Hi Bob");
6727 }
6728
6729 #[tokio::test]
6730 async fn test_shell_function_with_all_args() {
6731 let kernel = Kernel::transient().expect("failed to create kernel");
6732
6733 kernel
6735 .execute(r#"echo_all() { echo "args: $@" }"#)
6736 .await
6737 .expect("function definition failed");
6738
6739 let result = kernel
6741 .execute(r#"echo_all "a" "b" "c""#)
6742 .await
6743 .expect("function call failed");
6744
6745 assert!(result.ok(), "function failed: {}", result.err);
6746 assert_eq!(result.text_out().trim(), "args: a b c");
6747 }
6748
6749 #[tokio::test]
6750 async fn test_shell_function_with_arg_count() {
6751 let kernel = Kernel::transient().expect("failed to create kernel");
6752
6753 kernel
6755 .execute(r#"count_args() { echo "count: $#" }"#)
6756 .await
6757 .expect("function definition failed");
6758
6759 let result = kernel
6761 .execute(r#"count_args "x" "y" "z""#)
6762 .await
6763 .expect("function call failed");
6764
6765 assert!(result.ok(), "function failed: {}", result.err);
6766 assert_eq!(result.text_out().trim(), "count: 3");
6767 }
6768
6769 #[tokio::test]
6770 async fn test_shell_function_shared_scope() {
6771 let kernel = Kernel::transient().expect("failed to create kernel");
6772
6773 kernel
6775 .execute(r#"PARENT_VAR="visible""#)
6776 .await
6777 .expect("set failed");
6778
6779 kernel
6781 .execute(r#"modify_parent() {
6782 echo "saw: ${PARENT_VAR}"
6783 PARENT_VAR="changed by function"
6784 }"#)
6785 .await
6786 .expect("function definition failed");
6787
6788 let result = kernel.execute("modify_parent").await.expect("function failed");
6790
6791 assert!(
6792 result.text_out().contains("visible"),
6793 "Shell function should access parent scope, got: {}",
6794 result.text_out()
6795 );
6796
6797 let var = kernel.get_var("PARENT_VAR").await;
6799 assert_eq!(
6800 var,
6801 Some(Value::String("changed by function".into())),
6802 "Shell function should modify parent scope"
6803 );
6804 }
6805
6806 #[tokio::test]
6811 async fn test_script_execution_from_path() {
6812 let kernel = Kernel::transient().expect("failed to create kernel");
6813
6814 kernel.execute(r#"mkdir "/bin""#).await.ok();
6816 kernel
6817 .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
6818 .await
6819 .expect("write script failed");
6820
6821 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
6823
6824 let result = kernel
6826 .execute("hello")
6827 .await
6828 .expect("script execution failed");
6829
6830 assert!(result.ok(), "script failed: {}", result.err);
6831 assert_eq!(result.text_out().trim(), "Hello from script!");
6832 }
6833
6834 #[tokio::test]
6835 async fn test_script_with_args() {
6836 let kernel = Kernel::transient().expect("failed to create kernel");
6837
6838 kernel.execute(r#"mkdir "/bin""#).await.ok();
6840 kernel
6841 .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
6842 .await
6843 .expect("write script failed");
6844
6845 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
6847
6848 let result = kernel
6850 .execute(r#"greet "World""#)
6851 .await
6852 .expect("script execution failed");
6853
6854 assert!(result.ok(), "script failed: {}", result.err);
6855 assert_eq!(result.text_out().trim(), "Hello, World!");
6856 }
6857
6858 #[tokio::test]
6859 async fn test_script_not_found() {
6860 let kernel = Kernel::transient().expect("failed to create kernel");
6861
6862 kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
6864
6865 let result = kernel
6867 .execute("noscript")
6868 .await
6869 .expect("execution failed");
6870
6871 assert!(!result.ok(), "should fail with command not found");
6872 assert_eq!(result.code, 127);
6873 assert!(result.err.contains("command not found"));
6874 }
6875
6876 #[tokio::test]
6877 async fn test_script_path_search_order() {
6878 let kernel = Kernel::transient().expect("failed to create kernel");
6879
6880 kernel.execute(r#"mkdir "/first""#).await.ok();
6883 kernel.execute(r#"mkdir "/second""#).await.ok();
6884 kernel
6885 .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
6886 .await
6887 .expect("write failed");
6888 kernel
6889 .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
6890 .await
6891 .expect("write failed");
6892
6893 kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
6895
6896 let result = kernel
6898 .execute("myscript")
6899 .await
6900 .expect("script execution failed");
6901
6902 assert!(result.ok(), "script failed: {}", result.err);
6903 assert_eq!(result.text_out().trim(), "from first");
6904 }
6905
6906 #[tokio::test]
6911 async fn test_last_exit_code_success() {
6912 let kernel = Kernel::transient().expect("failed to create kernel");
6913
6914 let result = kernel.execute("true; echo $?").await.expect("execution failed");
6916 assert!(result.text_out().contains("0"), "expected 0, got: {}", result.text_out());
6917 }
6918
6919 #[tokio::test]
6920 async fn test_last_exit_code_failure() {
6921 let kernel = Kernel::transient().expect("failed to create kernel");
6922
6923 let result = kernel.execute("false; echo $?").await.expect("execution failed");
6925 assert!(result.text_out().contains("1"), "expected 1, got: {}", result.text_out());
6926 }
6927
6928 #[tokio::test]
6929 async fn test_current_pid() {
6930 let kernel = Kernel::transient().expect("failed to create kernel");
6931
6932 let result = kernel.execute("echo $$").await.expect("execution failed");
6933 let pid: u32 = result.text_out().trim().parse().expect("PID should be a number");
6935 assert!(pid > 0, "PID should be positive");
6936 }
6937
6938 #[tokio::test]
6939 async fn test_unset_variable_expands_to_empty() {
6940 let kernel = Kernel::transient().expect("failed to create kernel");
6941
6942 let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
6944 assert_eq!(result.text_out().trim(), "prefix::suffix");
6945 }
6946
6947 #[tokio::test]
6948 async fn test_eq_ne_operators() {
6949 let kernel = Kernel::transient().expect("failed to create kernel");
6950
6951 let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
6953 assert_eq!(result.text_out().trim(), "eq works");
6954
6955 let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
6957 assert_eq!(result.text_out().trim(), "ne works");
6958
6959 let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
6961 assert_eq!(result.text_out().trim(), "correct");
6962 }
6963
6964 #[tokio::test]
6965 async fn test_escaped_dollar_in_string() {
6966 let kernel = Kernel::transient().expect("failed to create kernel");
6967
6968 let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
6970 assert_eq!(result.text_out().trim(), "$100");
6971 }
6972
6973 #[tokio::test]
6974 async fn test_special_vars_in_interpolation() {
6975 let kernel = Kernel::transient().expect("failed to create kernel");
6976
6977 let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
6979 assert_eq!(result.text_out().trim(), "exit: 0");
6980
6981 let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
6983 assert!(result.text_out().starts_with("pid: "), "unexpected output: {}", result.text_out());
6984 let text = result.text_out();
6985 let pid_part = text.trim().strip_prefix("pid: ").unwrap();
6986 let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
6987 }
6988
6989 #[tokio::test]
6994 async fn test_command_subst_assignment() {
6995 let kernel = Kernel::transient().expect("failed to create kernel");
6996
6997 let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
6999 assert_eq!(result.text_out().trim(), "hello");
7000 }
7001
7002 #[tokio::test]
7003 async fn test_command_subst_with_args() {
7004 let kernel = Kernel::transient().expect("failed to create kernel");
7005
7006 let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
7008 assert_eq!(result.text_out().trim(), "a b c");
7009 }
7010
7011 #[tokio::test]
7012 async fn test_command_subst_nested_vars() {
7013 let kernel = Kernel::transient().expect("failed to create kernel");
7014
7015 let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
7017 assert_eq!(result.text_out().trim(), "hello world");
7018 }
7019
7020 #[tokio::test]
7021 async fn test_background_job_basic() {
7022 use std::time::Duration;
7023
7024 let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
7025
7026 let result = kernel.execute("echo hello &").await.expect("execution failed");
7028 assert!(result.ok(), "background command should succeed: {}", result.err);
7029 assert!(result.text_out().contains("[1]"), "should return job ID: {}", result.text_out());
7030
7031 tokio::time::sleep(Duration::from_millis(100)).await;
7033
7034 let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
7036 assert!(status.ok(), "status should succeed: {}", status.err);
7037 assert!(
7038 status.text_out().contains("done:") || status.text_out().contains("running"),
7039 "should have valid status: {}",
7040 status.text_out()
7041 );
7042
7043 let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
7045 assert!(stdout.ok());
7046 assert!(stdout.text_out().contains("hello"));
7047 }
7048
7049 #[tokio::test]
7050 async fn test_heredoc_piped_to_command() {
7051 let kernel = Kernel::transient().expect("kernel");
7053 let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
7054 assert!(result.ok(), "heredoc | cat failed: {}", result.err);
7055 assert_eq!(result.text_out().trim(), "hello world");
7056 }
7057
7058 fn transient_with_tempdir() -> (Kernel, tempfile::TempDir, String) {
7066 let kernel = Kernel::transient().expect("kernel");
7067 let tmp = tempfile::tempdir().expect("tempdir");
7068 let dir = tmp.path().display().to_string();
7069 (kernel, tmp, dir)
7070 }
7071
7072 #[tokio::test]
7073 async fn test_for_loop_glob_iterates() {
7074 let (kernel, _tmp, dir) = transient_with_tempdir();
7076 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7077 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
7078 let result = kernel.execute(&format!(r#"
7079 N=0
7080 for F in $(glob "{dir}/*.txt"); do
7081 N=$((N + 1))
7082 done
7083 echo $N
7084 "#)).await.unwrap();
7085 assert!(result.ok(), "for glob failed: {}", result.err);
7086 assert_eq!(result.text_out().trim(), "2", "Should iterate 2 files, got: {}", result.text_out());
7087 }
7088
7089 #[tokio::test]
7090 async fn test_bare_glob_expansion_echo() {
7091 let (kernel, _tmp, dir) = transient_with_tempdir();
7092 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7093 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
7094 kernel.execute(&format!("echo c > {dir}/c.rs")).await.unwrap();
7095 kernel.execute(&format!("cd {dir}")).await.unwrap();
7096 let result = kernel.execute("echo *.txt").await.unwrap();
7097 assert!(result.ok(), "echo *.txt failed: {}", result.err);
7098 let out = result.text_out();
7099 let out = out.trim();
7100 assert!(out.contains("a.txt"), "missing a.txt in: {}", out);
7102 assert!(out.contains("b.txt"), "missing b.txt in: {}", out);
7103 assert!(!out.contains("c.rs"), "should not contain c.rs in: {}", out);
7104 }
7105
7106 #[tokio::test]
7107 async fn test_bare_glob_no_matches_errors() {
7108 let (kernel, _tmp, dir) = transient_with_tempdir();
7109 kernel.execute(&format!("cd {dir}")).await.unwrap();
7110 let result = kernel.execute("echo *.nonexistent").await;
7111 match &result {
7112 Ok(exec) => {
7113 assert!(!exec.ok(), "expected failure, got success: out={}, err={}", exec.text_out(), exec.err);
7115 assert!(exec.err.contains("no matches"), "error should say no matches: {}", exec.err);
7116 }
7117 Err(e) => {
7118 assert!(e.to_string().contains("no matches"), "error should say no matches: {}", e);
7119 }
7120 }
7121 }
7122
7123 #[tokio::test]
7124 async fn test_bare_glob_disabled_with_set() {
7125 let (kernel, _tmp, dir) = transient_with_tempdir();
7126 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7127 kernel.execute(&format!("cd {dir}")).await.unwrap();
7128 kernel.execute("set +o glob").await.unwrap();
7130 let result = kernel.execute("echo *.txt").await.unwrap();
7131 assert!(result.ok(), "echo should succeed: {}", result.err);
7133 assert_eq!(result.text_out().trim(), "*.txt", "should be literal: {}", result.text_out());
7134 }
7135
7136 #[tokio::test]
7137 async fn test_bare_glob_quoted_not_expanded() {
7138 let (kernel, _tmp, dir) = transient_with_tempdir();
7139 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7140 kernel.execute(&format!("cd {dir}")).await.unwrap();
7141 let result = kernel.execute("echo \"*.txt\"").await.unwrap();
7143 assert!(result.ok(), "echo should succeed: {}", result.err);
7144 assert_eq!(result.text_out().trim(), "*.txt", "quoted should be literal: {}", result.text_out());
7145 }
7146
7147 #[tokio::test]
7148 async fn test_bare_glob_for_loop() {
7149 let (kernel, _tmp, dir) = transient_with_tempdir();
7150 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7151 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
7152 kernel.execute(&format!("cd {dir}")).await.unwrap();
7153 let result = kernel.execute(r#"
7154 N=0
7155 for f in *.txt; do
7156 N=$((N + 1))
7157 done
7158 echo $N
7159 "#).await.unwrap();
7160 assert!(result.ok(), "for loop failed: {}", result.err);
7161 assert_eq!(result.text_out().trim(), "2", "should iterate 2 files: {}", result.text_out());
7162 }
7163
7164 #[tokio::test]
7165 async fn test_glob_in_assignment_is_literal() {
7166 let kernel = Kernel::transient().expect("kernel");
7167 let result = kernel.execute("X=*.txt; echo $X").await.unwrap();
7168 assert!(result.ok());
7169 assert_eq!(result.text_out().trim(), "*.txt", "glob in assignment should be literal");
7170 }
7171
7172 #[tokio::test]
7173 async fn test_glob_in_test_expr_is_literal() {
7174 let kernel = Kernel::transient().expect("kernel");
7175 let result = kernel.execute(r#"
7176 if [[ *.txt == "*.txt" ]]; then
7177 echo "match"
7178 else
7179 echo "no"
7180 fi
7181 "#).await.unwrap();
7182 assert!(result.ok());
7183 assert_eq!(result.text_out().trim(), "match", "glob in test expr should be literal");
7184 }
7185
7186 #[tokio::test]
7187 async fn test_command_subst_echo_not_iterable() {
7188 let kernel = Kernel::transient().expect("kernel");
7190 let result = kernel.execute(r#"
7191 N=0
7192 for X in $(echo "a b c"); do N=$((N + 1)); done
7193 echo $N
7194 "#).await.unwrap();
7195 assert!(result.ok());
7196 assert_eq!(result.text_out().trim(), "1", "echo should be one item: {}", result.text_out());
7197 }
7198
7199 #[test]
7202 fn test_accumulate_preserves_own_newlines() {
7203 let mut acc = ExecResult::success("line1\n");
7206 let new = ExecResult::success("line2\n");
7207 accumulate_result(&mut acc, &new);
7208 assert_eq!(&*acc.text_out(), "line1\nline2\n");
7209 assert!(!acc.text_out().contains("\n\n"), "should not have double newlines: {:?}", acc.text_out());
7210 }
7211
7212 #[test]
7213 fn test_accumulate_inserts_no_separator() {
7214 let mut acc = ExecResult::success("line1");
7217 let new = ExecResult::success("line2");
7218 accumulate_result(&mut acc, &new);
7219 assert_eq!(&*acc.text_out(), "line1line2");
7220 }
7221
7222 #[test]
7223 fn test_accumulate_empty_into_nonempty() {
7224 let mut acc = ExecResult::success("");
7225 let new = ExecResult::success("hello\n");
7226 accumulate_result(&mut acc, &new);
7227 assert_eq!(&*acc.text_out(), "hello\n");
7228 }
7229
7230 #[test]
7231 fn test_accumulate_nonempty_into_empty() {
7232 let mut acc = ExecResult::success("hello\n");
7233 let new = ExecResult::success("");
7234 accumulate_result(&mut acc, &new);
7235 assert_eq!(&*acc.text_out(), "hello\n");
7236 }
7237
7238 #[test]
7239 fn test_accumulate_stderr_no_double_newlines() {
7240 let mut acc = ExecResult::failure(1, "err1\n");
7241 let new = ExecResult::failure(1, "err2\n");
7242 accumulate_result(&mut acc, &new);
7243 assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
7244 }
7245
7246 #[tokio::test]
7247 async fn test_multiple_echo_no_blank_lines() {
7248 let kernel = Kernel::transient().expect("kernel");
7249 let result = kernel
7250 .execute("echo one\necho two\necho three")
7251 .await
7252 .expect("execution failed");
7253 assert!(result.ok());
7254 assert_eq!(&*result.text_out(), "one\ntwo\nthree\n");
7255 }
7256
7257 #[tokio::test]
7258 async fn test_for_loop_no_blank_lines() {
7259 let kernel = Kernel::transient().expect("kernel");
7260 let result = kernel
7261 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
7262 .await
7263 .expect("execution failed");
7264 assert!(result.ok());
7265 assert_eq!(&*result.text_out(), "item: a\nitem: b\nitem: c\n");
7266 }
7267
7268 #[tokio::test]
7269 async fn test_for_command_subst_no_blank_lines() {
7270 let kernel = Kernel::transient().expect("kernel");
7271 let result = kernel
7272 .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
7273 .await
7274 .expect("execution failed");
7275 assert!(result.ok());
7276 assert_eq!(&*result.text_out(), "n=1\nn=2\nn=3\n");
7277 }
7278
7279 fn multi_consume_schema() -> crate::tools::ToolSchema {
7287 use crate::tools::{ParamSchema, ToolSchema};
7288 ToolSchema::new("test", "multi-consume smoke")
7289 .param(
7290 ParamSchema::optional("pair", "array", Value::Null, "name+value pair")
7291 .consumes(2),
7292 )
7293 }
7294
7295 fn pos(s: &str) -> Arg {
7296 Arg::Positional(Expr::Literal(Value::String(s.to_string())))
7297 }
7298
7299 #[tokio::test]
7300 async fn build_args_multi_consume_single_occurrence() {
7301 let kernel = Kernel::transient().expect("kernel");
7302 let schema = multi_consume_schema();
7303 let args = vec![
7305 Arg::LongFlag("pair".into()),
7306 pos("NAME"),
7307 pos("VALUE"),
7308 pos("filter"),
7309 ];
7310 let built = kernel
7311 .build_args_async(&args, Some(&schema))
7312 .await
7313 .expect("build_args should succeed");
7314
7315 let pair = built.named.get("pair").expect("named[pair] missing");
7318 match pair {
7319 Value::Json(serde_json::Value::Array(occurrences)) => {
7320 assert_eq!(occurrences.len(), 1, "expected one occurrence");
7321 match &occurrences[0] {
7322 serde_json::Value::Array(values) => {
7323 assert_eq!(values.len(), 2, "pair must have 2 values");
7324 assert_eq!(values[0], serde_json::Value::String("NAME".into()));
7325 assert_eq!(values[1], serde_json::Value::String("VALUE".into()));
7326 }
7327 other => panic!("expected inner array, got {other:?}"),
7328 }
7329 }
7330 other => panic!("expected Json(Array(...)) for named[pair], got {other:?}"),
7331 }
7332
7333 assert_eq!(built.positional.len(), 1);
7335 assert_eq!(built.positional[0], Value::String("filter".into()));
7336 }
7337 #[tokio::test]
7338 async fn build_args_multi_consume_two_occurrences_accumulate() {
7339 let kernel = Kernel::transient().expect("kernel");
7340 let schema = multi_consume_schema();
7341 let args = vec![
7343 Arg::LongFlag("pair".into()),
7344 pos("A"),
7345 pos("1"),
7346 Arg::LongFlag("pair".into()),
7347 pos("B"),
7348 pos("2"),
7349 pos("filter"),
7350 ];
7351 let built = kernel
7352 .build_args_async(&args, Some(&schema))
7353 .await
7354 .expect("build_args should succeed");
7355
7356 let pair = built.named.get("pair").expect("named[pair] missing");
7357 match pair {
7358 Value::Json(serde_json::Value::Array(occurrences)) => {
7359 assert_eq!(occurrences.len(), 2, "expected two occurrences");
7360 match &occurrences[0] {
7362 serde_json::Value::Array(values) => {
7363 assert_eq!(values[0], serde_json::Value::String("A".into()));
7364 assert_eq!(values[1], serde_json::Value::String("1".into()));
7365 }
7366 other => panic!("expected inner array, got {other:?}"),
7367 }
7368 match &occurrences[1] {
7369 serde_json::Value::Array(values) => {
7370 assert_eq!(values[0], serde_json::Value::String("B".into()));
7371 assert_eq!(values[1], serde_json::Value::String("2".into()));
7372 }
7373 other => panic!("expected inner array, got {other:?}"),
7374 }
7375 }
7376 other => panic!("expected Json(Array(...)), got {other:?}"),
7377 }
7378 }
7379
7380 use crate::tools::{ParamSchema, ToolSchema};
7388
7389 fn kj_like_schema() -> ToolSchema {
7392 ToolSchema::new("kj", "incomplete backend schema")
7393 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
7394 .with_positional_mapping()
7395 }
7396
7397 #[tokio::test]
7398 async fn build_args_undeclared_space_flag_errors_under_map_positionals() {
7399 let kernel = Kernel::transient().expect("kernel");
7400 let schema = kj_like_schema();
7401 let args = vec![
7403 pos("context"),
7404 pos("create"),
7405 pos("exp"),
7406 Arg::LongFlag("type".into()),
7407 pos("explorer"),
7408 ];
7409 let err = kernel
7410 .build_args_async(&args, Some(&schema))
7411 .await
7412 .expect_err("undeclared --type with a space value must fail loud");
7413 let msg = err.to_string();
7414 assert!(msg.contains("--type"), "message should name the flag: {msg}");
7415 assert!(msg.contains("--type=explorer"), "message should suggest the = form: {msg}");
7416 assert!(msg.contains("kj"), "message should name the tool: {msg}");
7417 }
7418
7419 #[tokio::test]
7420 async fn build_args_declared_space_flag_still_binds() {
7421 let kernel = Kernel::transient().expect("kernel");
7422 let schema = ToolSchema::new("kj", "complete schema")
7424 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
7425 .param(ParamSchema::optional("type", "string", Value::Null, "role type"))
7426 .with_positional_mapping();
7427 let args = vec![
7428 pos("exp"),
7429 Arg::LongFlag("type".into()),
7430 pos("explorer"),
7431 ];
7432 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7433 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7434 }
7435
7436 #[tokio::test]
7437 async fn build_args_equals_form_binds_for_undeclared_flag() {
7438 let kernel = Kernel::transient().expect("kernel");
7439 let schema = kj_like_schema();
7440 let args = vec![
7442 pos("exp"),
7443 Arg::Named { key: "type".into(), value: Expr::Literal(Value::String("explorer".into())) },
7444 ];
7445 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7446 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7447 }
7448
7449 #[tokio::test]
7450 async fn build_args_undeclared_bool_flag_at_end_is_ok() {
7451 let kernel = Kernel::transient().expect("kernel");
7452 let schema = kj_like_schema();
7453 let args = vec![pos("exp"), Arg::LongFlag("force".into())];
7455 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7456 assert!(built.flags.contains("force"));
7457 }
7458
7459 #[tokio::test]
7460 async fn build_args_undeclared_flag_before_another_flag_is_ok() {
7461 let kernel = Kernel::transient().expect("kernel");
7462 let schema = kj_like_schema();
7463 let args = vec![
7465 Arg::LongFlag("verbose".into()),
7466 Arg::Named { key: "name".into(), value: Expr::Literal(Value::String("x".into())) },
7467 ];
7468 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7469 assert!(built.flags.contains("verbose"));
7470 }
7471
7472 #[tokio::test]
7473 async fn build_args_undeclared_space_flag_ok_for_builtin_schema() {
7474 let kernel = Kernel::transient().expect("kernel");
7475 let schema = ToolSchema::new("frobnicate", "builtin-style")
7478 .param(ParamSchema::optional("name", "string", Value::Null, "name"));
7479 let args = vec![Arg::LongFlag("frob".into()), pos("value")];
7480 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7481 assert!(built.flags.contains("frob"));
7482 }
7483
7484 fn kj_tree_schema() -> ToolSchema {
7494 ToolSchema::new("kj", "subcommand tool").subcommand(
7495 ToolSchema::new("context", "context ops")
7496 .with_command_aliases(["ctx"])
7497 .subcommand(
7498 ToolSchema::new("create", "create context")
7499 .param(ParamSchema::new("type", "string").with_aliases(["t"]))
7500 .param(ParamSchema::new("force", "bool")),
7501 ),
7502 )
7503 }
7504
7505 #[tokio::test]
7506 async fn build_args_binds_deep_leaf_value_flag_space_form() {
7507 let kernel = Kernel::transient().expect("kernel");
7508 let schema = kj_tree_schema();
7509 let args = vec![
7511 pos("context"),
7512 pos("create"),
7513 Arg::LongFlag("type".into()),
7514 pos("explorer"),
7515 ];
7516 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7517 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7519 let positionals: Vec<&str> = built
7521 .positional
7522 .iter()
7523 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
7524 .collect();
7525 assert_eq!(positionals, vec!["context", "create"]);
7526 }
7527
7528 #[tokio::test]
7529 async fn build_args_leaf_bool_flag_does_not_swallow_positional() {
7530 let kernel = Kernel::transient().expect("kernel");
7531 let schema = kj_tree_schema();
7532 let args = vec![
7535 pos("context"),
7536 pos("create"),
7537 Arg::LongFlag("force".into()),
7538 pos("somearg"),
7539 ];
7540 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7541 assert!(built.flags.contains("force"), "force should be a bare flag");
7542 let positionals: Vec<&str> = built
7543 .positional
7544 .iter()
7545 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
7546 .collect();
7547 assert_eq!(positionals, vec!["context", "create", "somearg"]);
7548 }
7549
7550 #[tokio::test]
7551 async fn build_args_alias_routed_leaf_binds_value_flag() {
7552 let kernel = Kernel::transient().expect("kernel");
7553 let schema = kj_tree_schema();
7554 let args = vec![
7556 pos("ctx"),
7557 pos("create"),
7558 Arg::ShortFlag("t".into()),
7559 pos("explorer"),
7560 ];
7561 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7562 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7563 }
7564
7565 #[tokio::test]
7566 async fn build_args_computed_subcommand_selector_fails_loud() {
7567 let kernel = Kernel::transient().expect("kernel");
7568 let schema = kj_tree_schema();
7569 let args = vec![Arg::Positional(Expr::CommandSubst(vec![Stmt::Command(
7571 crate::ast::Command { name: "echo".into(), args: vec![], redirects: vec![] },
7572 )]))];
7573 let err = kernel
7574 .build_args_async(&args, Some(&schema))
7575 .await
7576 .expect_err("computed subcommand selector must error");
7577 assert!(
7578 err.to_string().contains("subcommand name is required"),
7579 "got: {err}"
7580 );
7581 }
7582
7583 #[test]
7586 fn finalize_output_renders_when_kernel_owns_it() {
7587 use crate::interpreter::{OutputData, OutputFormat};
7588 let r = ExecResult::with_output(OutputData::text("RAW"));
7589 let out = finalize_output(r, Some(OutputFormat::Json), false);
7590 assert_ne!(out.text_out(), "RAW", "kernel should reformat to JSON");
7592 }
7593
7594 #[test]
7595 fn finalize_output_skips_when_tool_owns_output() {
7596 use crate::interpreter::{OutputData, OutputFormat};
7597 let r = ExecResult::with_output(OutputData::text("RAW"));
7598 let out = finalize_output(r, Some(OutputFormat::Json), true);
7599 assert_eq!(out.text_out(), "RAW", "owned output must be left as-is");
7601 }
7602
7603 #[test]
7604 fn finalize_output_no_format_is_noop() {
7605 use crate::interpreter::OutputData;
7606 let r = ExecResult::with_output(OutputData::text("RAW"));
7607 let out = finalize_output(r, None, false);
7608 assert_eq!(out.text_out(), "RAW");
7609 }
7610
7611 #[tokio::test]
7614 async fn test_initial_vars_set_and_exported() {
7615 let config = KernelConfig::transient()
7616 .with_var("INIT_FOO", Value::String("bar".into()));
7617 let kernel = Kernel::new(config).expect("failed to create kernel");
7618
7619 assert_eq!(
7620 kernel.get_var("INIT_FOO").await,
7621 Some(Value::String("bar".into()))
7622 );
7623 assert!(
7624 kernel.scope.read().await.is_exported("INIT_FOO"),
7625 "initial_vars entries must be marked exported"
7626 );
7627 }
7628
7629 #[tokio::test]
7630 async fn test_execute_with_vars_overlay_visible() {
7631 let kernel = Kernel::transient().expect("failed to create kernel");
7632 let mut overlay = HashMap::new();
7633 overlay.insert("OVERLAY_X".to_string(), Value::String("yes".into()));
7634
7635 let result = kernel
7636 .execute_with_options(r#"echo "${OVERLAY_X}""#, ExecuteOptions::new().with_vars(overlay))
7637 .await
7638 .expect("execute failed");
7639
7640 assert!(result.ok());
7641 assert_eq!(result.text_out().trim(), "yes");
7642 }
7643
7644 #[tokio::test]
7645 async fn test_execute_with_vars_overlay_cleanup() {
7646 let kernel = Kernel::transient().expect("failed to create kernel");
7647 let mut overlay = HashMap::new();
7648 overlay.insert("EPHEMERAL".to_string(), Value::String("transient".into()));
7649
7650 kernel
7651 .execute_with_options("echo ignored", ExecuteOptions::new().with_vars(overlay))
7652 .await
7653 .expect("execute failed");
7654
7655 assert_eq!(kernel.get_var("EPHEMERAL").await, None);
7656 assert!(
7657 !kernel.scope.read().await.is_exported("EPHEMERAL"),
7658 "overlay-only export must be cleared on return"
7659 );
7660 }
7661
7662 #[tokio::test]
7663 async fn test_execute_with_vars_does_not_clobber_existing_export() {
7664 let kernel = Kernel::transient().expect("failed to create kernel");
7665 kernel
7666 .execute("export OUTER=outer")
7667 .await
7668 .expect("export failed");
7669
7670 let mut overlay = HashMap::new();
7671 overlay.insert("OUTER".to_string(), Value::String("inner".into()));
7672 let result = kernel
7673 .execute_with_options(r#"echo "${OUTER}""#, ExecuteOptions::new().with_vars(overlay))
7674 .await
7675 .expect("execute failed");
7676 assert_eq!(result.text_out().trim(), "inner");
7677
7678 assert_eq!(
7679 kernel.get_var("OUTER").await,
7680 Some(Value::String("outer".into())),
7681 "outer value must reappear after pop"
7682 );
7683 assert!(
7684 kernel.scope.read().await.is_exported("OUTER"),
7685 "outer export must survive overlay"
7686 );
7687 }
7688
7689 #[tokio::test]
7690 async fn test_execute_with_vars_inner_assignment_is_local() {
7691 let kernel = Kernel::transient().expect("failed to create kernel");
7692 let mut overlay = HashMap::new();
7693 overlay.insert("LOCAL_FOO".to_string(), Value::String("from-overlay".into()));
7694
7695 let result = kernel
7700 .execute_with_options(
7701 r#"LOCAL_FOO="reassigned"; echo "${LOCAL_FOO}""#,
7702 ExecuteOptions::new().with_vars(overlay),
7703 )
7704 .await
7705 .expect("execute failed");
7706 assert!(result.ok());
7707
7708 assert_eq!(kernel.get_var("LOCAL_FOO").await, None);
7711 }
7712
7713 #[tokio::test]
7714 async fn test_external_command_sees_exported_var() {
7715 let kernel = Kernel::transient().expect("failed to create kernel");
7716 let path = std::env::var("PATH").unwrap_or_default();
7720 let result = kernel
7721 .execute(&format!(
7722 "PATH=\"{path}\"; export EXT_FOO=bar; printenv EXT_FOO"
7723 ))
7724 .await
7725 .expect("execute failed");
7726
7727 assert!(result.ok(), "printenv should succeed: stderr={}", result.err);
7728 assert_eq!(result.text_out().trim(), "bar");
7729 }
7730
7731 #[tokio::test]
7732 async fn test_external_command_does_not_see_unexported_var() {
7733 let kernel = Kernel::transient().expect("failed to create kernel");
7734
7735 let result = kernel
7738 .execute("EXT_BAR=hidden; printenv EXT_BAR")
7739 .await
7740 .expect("execute failed");
7741
7742 assert!(!result.ok(), "printenv should fail when var is unexported");
7743 assert!(
7744 result.text_out().trim().is_empty(),
7745 "no stdout when var is missing, got: {}",
7746 result.text_out()
7747 );
7748 }
7749
7750 #[tokio::test]
7751 async fn test_external_command_does_not_see_os_env() {
7752 assert!(
7758 std::env::var_os("PATH").is_some(),
7759 "test precondition: cargo should set PATH"
7760 );
7761
7762 let kernel = Kernel::transient().expect("failed to create kernel");
7763 let result = kernel
7764 .execute("printenv PATH")
7765 .await
7766 .expect("execute failed");
7767
7768 assert!(
7769 !result.ok(),
7770 "printenv PATH must fail in hermetic kernel, got stdout={:?}",
7771 result.text_out()
7772 );
7773 assert!(
7774 result.text_out().trim().is_empty(),
7775 "no PATH in subprocess env, got stdout={:?}",
7776 result.text_out()
7777 );
7778 }
7779
7780 #[tokio::test]
7781 async fn test_execute_with_vars_overlay_reaches_subprocess() {
7782 let kernel = Kernel::transient().expect("failed to create kernel");
7783 let mut overlay = HashMap::new();
7784 overlay.insert("SUB_FOO".to_string(), Value::String("subproc".into()));
7785 overlay.insert(
7787 "PATH".to_string(),
7788 Value::String(std::env::var("PATH").unwrap_or_default()),
7789 );
7790
7791 let result = kernel
7792 .execute_with_options("printenv SUB_FOO", ExecuteOptions::new().with_vars(overlay))
7793 .await
7794 .expect("execute failed");
7795
7796 assert!(
7797 result.ok(),
7798 "printenv should succeed: code={} stdout={:?} stderr={:?}",
7799 result.code,
7800 result.text_out(),
7801 result.err
7802 );
7803 assert_eq!(result.text_out().trim(), "subproc");
7804 }
7805}