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 pipe_stdin: None,
2481 pipe_stdout: None,
2482 stderr: ec.stderr.clone(),
2483 tool_schemas: ec.tool_schemas.clone(),
2484 tools: ec.tools.clone(),
2485 job_manager: ec.job_manager.clone(),
2486 pipeline_position: PipelinePosition::Only,
2487 interactive: self.interactive,
2488 aliases: ec.aliases.clone(),
2489 ignore_config: ec.ignore_config.clone(),
2490 output_limit: ec.output_limit.clone(),
2491 allow_external_commands: self.allow_external_commands,
2492 nonce_store: ec.nonce_store.clone(),
2493 trash_backend: ec.trash_backend.clone(),
2494 #[cfg(all(unix, feature = "subprocess"))]
2495 terminal_state: ec.terminal_state.clone(),
2496 dispatcher: self.dispatcher(),
2497 cancel: {
2498 #[allow(clippy::expect_used)]
2499 let token = self.cancel_token.lock().expect("cancel_token poisoned");
2500 token.clone()
2501 },
2502 output_format: None,
2503 vfs_budget: self.vfs_budget.clone(),
2504 watchdog: ec.watchdog.clone(),
2505 #[cfg(all(feature = "localfs", feature = "overlay"))]
2506 overlay_handle: self.overlay_handle.clone(),
2507 }, has_pipe_stdin)
2508 }; if ctx.stdin.is_some() || ctx.stdin_data.is_some() || has_pipe_stdin {
2516 let mut ec = self.exec_ctx.write().await;
2517 ctx.pipe_stdin = ec.pipe_stdin.take();
2518 ec.stdin = None;
2519 ec.stdin_data = None;
2520 }
2521
2522 let mut result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
2523
2524 if ctx.output_limit.is_enabled() {
2526 let _ = crate::output_limit::spill_if_needed(&mut result, &ctx.output_limit).await;
2527 }
2528
2529 if result.did_spill {
2532 result.original_code = Some(result.code);
2533 result.code = 3;
2534 }
2535
2536 {
2538 let mut ec = self.exec_ctx.write().await;
2539 ec.cwd = ctx.cwd.clone();
2540 ec.prev_cwd = ctx.prev_cwd.clone();
2541 ec.aliases = ctx.aliases.clone();
2542 ec.ignore_config = ctx.ignore_config.clone();
2543 ec.output_limit = ctx.output_limit.clone();
2544 }
2545 {
2546 let mut scope = self.scope.write().await;
2547 *scope = ctx.scope.clone();
2548 }
2549
2550 Ok(result)
2551 }
2552
2553 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
2561 async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
2562 use tokio::sync::oneshot;
2563
2564 let command_str = self.format_pipeline(pipeline);
2566
2567 let stdout = Arc::new(BoundedStream::default_size());
2569 let stderr = Arc::new(BoundedStream::default_size());
2570
2571 let (tx, rx) = oneshot::channel();
2573
2574 let job_id = self.jobs.register_with_streams(
2576 command_str.clone(),
2577 rx,
2578 stdout.clone(),
2579 stderr.clone(),
2580 ).await;
2581
2582 let cancel = tokio_util::sync::CancellationToken::new();
2593 self.jobs.set_cancel_token(job_id, cancel.clone()).await;
2594 let fork = self.fork_for_background(cancel, job_id).await;
2595 let runner = self.runner.clone();
2596 let commands = pipeline.commands.clone();
2597
2598 let mut bg_ctx = {
2602 let ec = fork.exec_ctx.read().await;
2603 ec.child_for_pipeline()
2604 };
2605 bg_ctx.scope = fork.scope.read().await.clone();
2606 bg_ctx.dispatcher = fork.dispatcher();
2610
2611 tokio::spawn(crate::telemetry::bind_current_context(async move {
2614 let result = runner.run(&commands, &mut bg_ctx, fork.as_ref()).await;
2617
2618 let text = result.text_out();
2620 if !text.is_empty() {
2621 stdout.write(text.as_bytes()).await;
2622 }
2623 if !result.err.is_empty() {
2624 stderr.write(result.err.as_bytes()).await;
2625 }
2626
2627 stdout.close().await;
2629 stderr.close().await;
2630
2631 let _ = tx.send(result);
2633 }));
2634
2635 Ok(ExecResult::success(format!("[{}]", job_id)))
2636 }
2637
2638 fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
2640 pipeline.commands
2641 .iter()
2642 .map(|cmd| {
2643 let mut parts = vec![cmd.name.clone()];
2644 for arg in &cmd.args {
2645 match arg {
2646 Arg::Positional(expr) => {
2647 parts.push(self.format_expr(expr));
2648 }
2649 Arg::Named { key, value } => {
2650 parts.push(format!("--{}={}", key, self.format_expr(value)));
2651 }
2652 Arg::WordAssign { key, value } => {
2653 parts.push(format!("{}={}", key, self.format_expr(value)));
2654 }
2655 Arg::ShortFlag(name) => {
2656 parts.push(format!("-{}", name));
2657 }
2658 Arg::LongFlag(name) => {
2659 parts.push(format!("--{}", name));
2660 }
2661 Arg::DoubleDash => {
2662 parts.push("--".to_string());
2663 }
2664 }
2665 }
2666 parts.join(" ")
2667 })
2668 .collect::<Vec<_>>()
2669 .join(" | ")
2670 }
2671
2672 fn format_expr(&self, expr: &Expr) -> String {
2674 match expr {
2675 Expr::Literal(Value::String(s)) => {
2676 if s.contains(' ') || s.contains('"') {
2677 format!("'{}'", s.replace('\'', "\\'"))
2678 } else {
2679 s.clone()
2680 }
2681 }
2682 Expr::Literal(Value::Int(i)) => i.to_string(),
2683 Expr::Literal(Value::Float(f)) => f.to_string(),
2684 Expr::Literal(Value::Bool(b)) => b.to_string(),
2685 Expr::Literal(Value::Null) => "null".to_string(),
2686 Expr::VarRef(path) => {
2687 let name = path.segments.iter()
2688 .map(|seg| match seg {
2689 crate::ast::VarSegment::Field(f) => f.clone(),
2690 })
2691 .collect::<Vec<_>>()
2692 .join(".");
2693 format!("${{{}}}", name)
2694 }
2695 Expr::Interpolated(_) => "\"...\"".to_string(),
2696 Expr::HereDocBody { .. } => "<<heredoc".to_string(),
2697 _ => "...".to_string(),
2698 }
2699 }
2700
2701 async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
2703 self.execute_command_depth(name, args, 0).await
2704 }
2705
2706 #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
2707 async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
2708 match name {
2710 "true" => return Ok(ExecResult::success("")),
2711 "false" => return Ok(ExecResult::failure(1, "")),
2712 "source" | "." => return self.execute_source(args).await,
2713 _ => {}
2714 }
2715
2716 if alias_depth < 10 {
2718 let alias_value = {
2719 let ctx = self.exec_ctx.read().await;
2720 ctx.aliases.get(name).cloned()
2721 };
2722 if let Some(alias_val) = alias_value {
2723 let parts: Vec<&str> = alias_val.split_whitespace().collect();
2725 if let Some((alias_cmd, alias_args)) = parts.split_first() {
2726 let mut new_args: Vec<Arg> = alias_args
2727 .iter()
2728 .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
2729 .collect();
2730 new_args.extend_from_slice(args);
2731 return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
2732 }
2733 }
2734 }
2735
2736 if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
2738 return match self.tools.get(builtin_name) {
2739 Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
2740 None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
2741 };
2742 }
2743
2744 {
2746 let user_tools = self.user_tools.read().await;
2747 if let Some(tool_def) = user_tools.get(name) {
2748 let tool_def = tool_def.clone();
2749 drop(user_tools);
2750 return self.execute_user_tool(tool_def, args).await;
2751 }
2752 }
2753
2754 let tool = match self.tools.get(name) {
2756 Some(t) => t,
2757 None => {
2758 if let Some(result) = self.try_execute_script(name, args).await? {
2760 return Ok(result);
2761 }
2762 if let Some(result) = self.try_execute_external(name, args).await? {
2764 return Ok(result);
2765 }
2766
2767 let backend = self.exec_ctx.read().await.backend.clone();
2772 let tool_schema = backend.get_tool(name).await.ok().flatten().map(|t| {
2773 let mut s = t.schema;
2774 if s.subcommands.is_empty() {
2780 s.map_positionals = true;
2781 }
2782 s
2783 });
2784 let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
2785 let mut ctx = self.exec_ctx.write().await;
2786 {
2787 let scope = self.scope.read().await;
2788 ctx.scope = scope.clone();
2789 }
2790 let backend = ctx.backend.clone();
2791 match backend.call_tool(name, tool_args, &mut *ctx).await {
2792 Ok(tool_result) => {
2793 let mut scope = self.scope.write().await;
2794 *scope = ctx.scope.clone();
2795 let mut exec = ExecResult::from_output(
2796 tool_result.code as i64, tool_result.stdout, tool_result.stderr,
2797 );
2798 exec.set_output(tool_result.output);
2799 return Ok(exec);
2800 }
2801 Err(BackendError::ToolNotFound(_)) => {
2802 }
2804 Err(e) => {
2805 tracing::debug!("backend error for {name}: {e}");
2808 }
2809 }
2810
2811 return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
2812 }
2813 };
2814
2815 let schema = tool.schema();
2817 let tool_args = self.build_args_async(args, Some(&schema)).await?;
2818
2819 let schema_claims = |flag: &str| -> bool {
2821 let bare = flag.trim_start_matches('-');
2822 schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
2823 };
2824 let wants_help =
2825 (tool_args.flags.contains("help") && !schema_claims("help"))
2826 || (tool_args.flags.contains("h") && !schema_claims("-h"));
2827 if wants_help {
2828 let help_topic = crate::help::HelpTopic::Tool(name.to_string());
2829 let ctx = self.exec_ctx.read().await;
2830 let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
2831 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
2832 }
2833
2834 let mut ctx = {
2840 let ec = self.exec_ctx.write().await;
2841 let scope = self.scope.read().await;
2842 ExecContext {
2843 backend: ec.backend.clone(),
2844 scope: scope.clone(),
2845 cwd: ec.cwd.clone(),
2846 prev_cwd: ec.prev_cwd.clone(),
2847 stdin: ec.stdin.clone(),
2848 stdin_data: ec.stdin_data.clone(),
2849 pipe_stdin: None, pipe_stdout: None,
2851 stderr: ec.stderr.clone(),
2852 tool_schemas: ec.tool_schemas.clone(),
2853 tools: ec.tools.clone(),
2854 job_manager: ec.job_manager.clone(),
2855 pipeline_position: ec.pipeline_position,
2856 interactive: self.interactive,
2857 aliases: ec.aliases.clone(),
2858 ignore_config: ec.ignore_config.clone(),
2859 output_limit: ec.output_limit.clone(),
2860 allow_external_commands: self.allow_external_commands,
2861 nonce_store: ec.nonce_store.clone(),
2862 trash_backend: ec.trash_backend.clone(),
2863 #[cfg(all(unix, feature = "subprocess"))]
2864 terminal_state: ec.terminal_state.clone(),
2865 dispatcher: self.dispatcher(),
2866 cancel: ec.cancel.clone(),
2872 output_format: None,
2873 vfs_budget: self.vfs_budget.clone(),
2874 watchdog: ec.watchdog.clone(),
2875 #[cfg(all(feature = "localfs", feature = "overlay"))]
2876 overlay_handle: self.overlay_handle.clone(),
2877 }
2878 }; {
2884 let mut ec = self.exec_ctx.write().await;
2885 ctx.stdin = ec.stdin.take();
2886 ctx.stdin_data = ec.stdin_data.take();
2887 ctx.pipe_stdin = ec.pipe_stdin.take();
2888 ctx.pipe_stdout = ec.pipe_stdout.take();
2889 }
2890
2891 GlobalFlags::apply_from_args(&tool_args, &mut ctx);
2896
2897 let result = tool.execute(tool_args, &mut ctx).await;
2898
2899 {
2906 let mut scope = self.scope.write().await;
2907 *scope = ctx.scope.clone();
2908 }
2909 {
2910 let mut ec = self.exec_ctx.write().await;
2911 ec.cwd = ctx.cwd;
2912 ec.prev_cwd = ctx.prev_cwd;
2913 ec.aliases = ctx.aliases;
2914 ec.output_limit = ctx.output_limit.clone();
2919 ec.pipe_stdin = ctx.pipe_stdin.take();
2920 ec.pipe_stdout = ctx.pipe_stdout.take();
2921 }
2922
2923 let result = finalize_output(result, ctx.output_format, schema.owns_output);
2928
2929 Ok(result)
2930 }
2931
2932 async fn scope_home(&self) -> Option<String> {
2937 match self.scope.read().await.get("HOME") {
2938 Some(Value::String(s)) => Some(s.clone()),
2939 _ => None,
2940 }
2941 }
2942
2943 #[allow(clippy::too_many_arguments)]
2964 async fn consume_flag_positionals(
2965 &self,
2966 args: &[Arg],
2967 flag_name: &str,
2968 canonical: &str,
2969 consumes: usize,
2970 repeatable: bool,
2971 positional_indices: &[usize],
2972 consumed: &mut std::collections::HashSet<usize>,
2973 current_idx: usize,
2974 tool_args: &mut ToolArgs,
2975 ) -> Result<()> {
2976 let home = self.scope_home().await;
2977 let mut collected: Vec<Value> = Vec::with_capacity(consumes.max(1));
2978 for _ in 0..consumes.max(1) {
2979 let allow_word_assign = consumes <= 1;
2985 let next_pos = positional_indices
2986 .iter()
2987 .find(|idx| {
2988 **idx > current_idx
2989 && !consumed.contains(idx)
2990 && (allow_word_assign || matches!(args[**idx], Arg::Positional(_)))
2991 })
2992 .copied();
2993 match next_pos {
2994 Some(pos_idx) => match &args[pos_idx] {
2995 Arg::Positional(expr) => {
2996 let value = self.eval_expr_async(expr).await?;
2997 let value = apply_tilde_expansion(value, home.as_deref());
2998 collected.push(value);
2999 consumed.insert(pos_idx);
3000 }
3001 Arg::WordAssign { key, value } => {
3004 let val = self.eval_expr_async(value).await?;
3005 let val = apply_tilde_expansion(val, home.as_deref());
3006 let val_str = crate::interpreter::value_to_string(&val);
3007 collected.push(Value::String(format!("{key}={val_str}")));
3008 consumed.insert(pos_idx);
3009 }
3010 _ => {}
3011 },
3012 None => {
3013 if consumes <= 1 && collected.is_empty() {
3014 tool_args.flags.insert(flag_name.to_string());
3018 return Ok(());
3019 }
3020 anyhow::bail!(
3021 "--{flag_name} requires {consumes} argument{}, got {}",
3022 if consumes == 1 { "" } else { "s" },
3023 collected.len()
3024 );
3025 }
3026 }
3027 }
3028
3029 if consumes <= 1 {
3030 if let Some(v) = collected.pop() {
3031 if repeatable {
3032 push_repeatable_value(tool_args, flag_name, canonical, v)?;
3033 } else {
3034 tool_args.named.insert(canonical.to_string(), v);
3035 }
3036 }
3037 return Ok(());
3038 }
3039
3040 let occ: Vec<serde_json::Value> = collected
3042 .into_iter()
3043 .map(|v| crate::interpreter::value_to_json(&v))
3044 .collect();
3045 let entry = tool_args
3046 .named
3047 .entry(canonical.to_string())
3048 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
3049 if let Value::Json(serde_json::Value::Array(outer)) = entry {
3050 outer.push(serde_json::Value::Array(occ));
3051 } else {
3052 anyhow::bail!(
3053 "--{flag_name}: named[{canonical}] already holds a non-array value"
3054 );
3055 }
3056 Ok(())
3057 }
3058
3059 async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
3063 let mut tool_args = ToolArgs::new();
3064 let home = self.scope_home().await;
3065 let leaf = match schema {
3071 Some(s) => Some(select_leaf(s, args)?),
3072 None => None,
3073 };
3074 let mut param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
3081 if let Some(l) = leaf {
3082 param_lookup.extend(schema_param_lookup(l));
3083 }
3084 let accepts_word_assign = schema
3087 .map(|s| crate::tools::accepts_word_assign(s.name.as_str()))
3088 .unwrap_or(false);
3089
3090 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
3092 let mut past_double_dash = false;
3093
3094 let positional_indices: Vec<usize> = args.iter().enumerate()
3102 .filter_map(|(i, a)| {
3103 let consumable = matches!(a, Arg::Positional(_))
3104 || (!accepts_word_assign && matches!(a, Arg::WordAssign { .. }));
3105 consumable.then_some(i)
3106 })
3107 .collect();
3108
3109 let mut i = 0;
3110 while i < args.len() {
3111 match &args[i] {
3112 Arg::DoubleDash => {
3113 past_double_dash = true;
3114 }
3115 Arg::Positional(expr) => {
3116 if !consumed.contains(&i) {
3117 if let Expr::GlobPattern(pattern) = expr {
3119 let glob_enabled = {
3120 let scope = self.scope.read().await;
3121 scope.glob_enabled()
3122 };
3123 if glob_enabled {
3124 let (paths, cwd) = {
3125 let ctx = self.exec_ctx.read().await;
3126 let paths = ctx.expand_glob(pattern).await
3127 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3128 let cwd = ctx.resolve_path(".");
3129 (paths, cwd)
3130 };
3131 if paths.is_empty() {
3132 return Err(anyhow::anyhow!("no matches: {}", pattern));
3133 }
3134 for path in paths {
3135 let display = if !pattern.starts_with('/') {
3136 path.strip_prefix(&cwd)
3137 .unwrap_or(&path)
3138 .to_string_lossy().into_owned()
3139 } else {
3140 path.to_string_lossy().into_owned()
3141 };
3142 tool_args.positional.push(Value::String(display));
3143 }
3144 i += 1;
3145 continue;
3146 }
3147 }
3148 let value = self.eval_expr_async(expr).await?;
3149 let value = apply_tilde_expansion(value, home.as_deref());
3150 tool_args.positional.push(value);
3151 }
3152 }
3153 Arg::Named { key, value } => {
3154 let val = self.eval_expr_async(value).await?;
3155 let val = apply_tilde_expansion(val, home.as_deref());
3156 if let Some(&(canonical, _, _, true)) = param_lookup.get(key.as_str()) {
3162 push_repeatable_value(&mut tool_args, key, canonical, val)?;
3163 } else {
3164 tool_args.named.insert(key.clone(), val);
3165 }
3166 }
3167 Arg::WordAssign { key, value } => {
3168 if consumed.contains(&i) {
3171 i += 1;
3172 continue;
3173 }
3174 let val = self.eval_expr_async(value).await?;
3175 let val = apply_tilde_expansion(val, home.as_deref());
3176 if accepts_word_assign {
3177 tool_args.named.insert(key.clone(), val);
3178 } else {
3179 let val_str = crate::interpreter::value_to_string(&val);
3182 tool_args.positional.push(Value::String(format!("{key}={val_str}")));
3183 }
3184 }
3185 Arg::ShortFlag(name) => {
3186 if past_double_dash {
3187 tool_args.positional.push(Value::String(format!("-{name}")));
3188 } else if name.len() == 1 {
3189 let flag_name = name.as_str();
3190 let lookup = param_lookup.get(flag_name);
3191 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3192
3193 if is_bool {
3194 tool_args.flags.insert(flag_name.to_string());
3195 } else {
3196 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(flag_name);
3198 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3199 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3200 self.consume_flag_positionals(
3201 args,
3202 name,
3203 canonical,
3204 consumes,
3205 repeatable,
3206 &positional_indices,
3207 &mut consumed,
3208 i,
3209 &mut tool_args,
3210 )
3211 .await?;
3212 }
3213 } else if let Some(&(canonical, typ, consumes, repeatable)) = param_lookup.get(name.as_str()) {
3214 if is_bool_type(typ) {
3216 tool_args.flags.insert(canonical.to_string());
3217 } else {
3218 self.consume_flag_positionals(
3219 args,
3220 name,
3221 canonical,
3222 consumes,
3223 repeatable,
3224 &positional_indices,
3225 &mut consumed,
3226 i,
3227 &mut tool_args,
3228 )
3229 .await?;
3230 }
3231 } else if let Some(&(canonical, _, _, _)) = param_lookup
3232 .get(&name[..1])
3233 .filter(|(_, typ, ..)| !is_bool_type(typ))
3234 {
3235 tool_args
3243 .named
3244 .insert(canonical.to_string(), Value::String(name[1..].to_string()));
3245 } else {
3246 for c in name.chars() {
3248 tool_args.flags.insert(c.to_string());
3249 }
3250 }
3251 }
3252 Arg::LongFlag(name) => {
3253 if past_double_dash {
3254 tool_args.positional.push(Value::String(format!("--{name}")));
3255 } else {
3256 let lookup = param_lookup.get(name.as_str());
3257 let ambiguous_value = (lookup.is_none()
3266 && leaf.is_some_and(|s| s.map_positionals)
3267 && !consumed.contains(&(i + 1)))
3268 .then(|| match args.get(i + 1) {
3269 Some(Arg::Positional(Expr::Literal(Value::String(s)))) => {
3272 Some(s.clone())
3273 }
3274 Some(Arg::Positional(_)) => Some("VALUE".to_string()),
3275 _ => None,
3276 })
3277 .flatten();
3278 if let Some(val) = ambiguous_value {
3279 let tool = leaf.map(|s| s.name.as_str()).unwrap_or("command");
3280 anyhow::bail!(
3281 "{tool}: --{name} is not a declared flag, so the \
3282 space-separated value would be silently dropped. \
3283 Use --{name}={val}, or have {tool} declare --{name} \
3284 in its schema."
3285 );
3286 }
3287 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3288
3289 if is_bool {
3290 tool_args.flags.insert(name.clone());
3291 } else {
3292 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(name.as_str());
3293 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3294 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3295 self.consume_flag_positionals(
3296 args,
3297 name,
3298 canonical,
3299 consumes,
3300 repeatable,
3301 &positional_indices,
3302 &mut consumed,
3303 i,
3304 &mut tool_args,
3305 )
3306 .await?;
3307 }
3308 }
3309 }
3310 }
3311 i += 1;
3312 }
3313
3314 if let Some(schema) = leaf.filter(|s| s.map_positionals) {
3321 let pre_dash_count = if past_double_dash {
3322 let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
3323 positional_indices.iter()
3324 .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
3325 .count()
3326 } else {
3327 tool_args.positional.len()
3328 };
3329
3330 let mut remaining = Vec::new();
3331 let mut positional_iter = tool_args.positional.drain(..).enumerate();
3332
3333 for param in &schema.params {
3334 if tool_args.named.contains_key(¶m.name) || tool_args.flags.contains(¶m.name) {
3335 continue;
3336 }
3337 if is_bool_type(¶m.param_type) {
3338 continue;
3339 }
3340 loop {
3341 match positional_iter.next() {
3342 Some((idx, val)) if idx < pre_dash_count => {
3343 tool_args.named.insert(param.name.clone(), val);
3344 break;
3345 }
3346 Some((_, val)) => {
3347 remaining.push(val);
3348 }
3349 None => break,
3350 }
3351 }
3352 }
3353
3354 remaining.extend(positional_iter.map(|(_, v)| v));
3355 tool_args.positional = remaining;
3356 }
3357
3358 Ok(tool_args)
3359 }
3360
3361 #[cfg(feature = "subprocess")]
3371 async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
3372 let mut argv = Vec::new();
3373 let home = self.scope_home().await;
3374 for arg in args {
3375 match arg {
3376 Arg::Positional(expr) => {
3377 if let Expr::GlobPattern(pattern) = expr {
3379 let glob_enabled = {
3380 let scope = self.scope.read().await;
3381 scope.glob_enabled()
3382 };
3383 if glob_enabled {
3384 let (paths, cwd) = {
3385 let ctx = self.exec_ctx.read().await;
3386 let paths = ctx.expand_glob(pattern).await
3387 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3388 let cwd = ctx.resolve_path(".");
3389 (paths, cwd)
3390 };
3391 if paths.is_empty() {
3392 return Err(anyhow::anyhow!("no matches: {}", pattern));
3393 }
3394 for path in paths {
3395 let display = if !pattern.starts_with('/') {
3396 path.strip_prefix(&cwd)
3397 .unwrap_or(&path)
3398 .to_string_lossy().into_owned()
3399 } else {
3400 path.to_string_lossy().into_owned()
3401 };
3402 argv.push(display);
3403 }
3404 continue;
3405 }
3406 }
3407 let value = self.eval_expr_async(expr).await?;
3408 let value = apply_tilde_expansion(value, home.as_deref());
3409 argv.push(value_to_string(&value));
3410 }
3411 Arg::Named { key, value } => {
3412 let val = self.eval_expr_async(value).await?;
3413 let val = apply_tilde_expansion(val, home.as_deref());
3414 argv.push(format!("--{}={}", key, value_to_string(&val)));
3415 }
3416 Arg::WordAssign { key, value } => {
3417 let val = self.eval_expr_async(value).await?;
3418 let val = apply_tilde_expansion(val, home.as_deref());
3419 argv.push(format!("{}={}", key, value_to_string(&val)));
3420 }
3421 Arg::ShortFlag(name) => {
3422 argv.push(format!("-{}", name));
3424 }
3425 Arg::LongFlag(name) => {
3426 argv.push(format!("--{}", name));
3428 }
3429 Arg::DoubleDash => {
3430 argv.push("--".to_string());
3432 }
3433 }
3434 }
3435 Ok(argv)
3436 }
3437
3438 fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
3443 Box::pin(async move {
3444 match expr {
3445 Expr::Literal(value) => Ok(value.clone()),
3446 Expr::VarRef(path) => {
3447 let scope = self.scope.read().await;
3448 scope.resolve_path(path)
3449 .ok_or_else(|| anyhow::anyhow!("undefined variable"))
3450 }
3451 Expr::Interpolated(parts) => {
3452 let mut result = String::new();
3453 for part in parts {
3454 result.push_str(&self.eval_string_part_async(part).await?);
3455 }
3456 Ok(Value::String(result))
3457 }
3458 Expr::HereDocBody { parts, strip_tabs } => {
3459 let mut result = String::new();
3460 for sp in parts {
3461 result.push_str(&self.eval_string_part_async(&sp.part).await?);
3462 }
3463 if *strip_tabs {
3464 Ok(Value::String(crate::interpreter::strip_leading_tabs(&result)))
3465 } else {
3466 Ok(Value::String(result))
3467 }
3468 }
3469 Expr::BinaryOp { left, op, right } => match op {
3470 BinaryOp::And => {
3471 let left_val = self.eval_expr_async(left).await?;
3472 if !is_truthy(&left_val) {
3473 return Ok(left_val);
3474 }
3475 self.eval_expr_async(right).await
3476 }
3477 BinaryOp::Or => {
3478 let left_val = self.eval_expr_async(left).await?;
3479 if is_truthy(&left_val) {
3480 return Ok(left_val);
3481 }
3482 self.eval_expr_async(right).await
3483 }
3484 },
3485 Expr::CommandSubst(stmts) => {
3486 let saved_scope = { self.scope.read().await.clone() };
3489 let saved_cwd = {
3490 let ec = self.exec_ctx.read().await;
3491 (ec.cwd.clone(), ec.prev_cwd.clone())
3492 };
3493
3494 let run_result = self.execute_block_capturing(stmts).await;
3496
3497 {
3499 let mut scope = self.scope.write().await;
3500 *scope = saved_scope;
3501 if let Ok(ref r) = run_result {
3502 scope.set_last_result(r.clone());
3503 }
3504 }
3505 {
3506 let mut ec = self.exec_ctx.write().await;
3507 ec.cwd = saved_cwd.0;
3508 ec.prev_cwd = saved_cwd.1;
3509 }
3510
3511 let result = run_result?;
3513
3514 if let Some(bytes) = result.out_bytes() {
3517 Ok(Value::Bytes(bytes.to_vec()))
3518 } else if let Some(data) = &result.data {
3520 Ok(data.clone())
3521 } else if let Some(output) = result.output() {
3522 if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
3524 let items: Vec<serde_json::Value> = output.root.iter()
3525 .map(|n| serde_json::Value::String(n.display_name().to_string()))
3526 .collect();
3527 Ok(Value::Json(serde_json::Value::Array(items)))
3528 } else {
3529 Ok(Value::String(result.text_out().trim_end().to_string()))
3530 }
3531 } else {
3532 Ok(Value::String(result.text_out().trim_end().to_string()))
3534 }
3535 }
3536 Expr::Test(test_expr) => {
3537 Ok(Value::Bool(self.eval_test_async(test_expr).await?))
3538 }
3539 Expr::Positional(n) => {
3540 let scope = self.scope.read().await;
3541 match scope.get_positional(*n) {
3542 Some(s) => Ok(Value::String(s.to_string())),
3543 None => Ok(Value::String(String::new())),
3544 }
3545 }
3546 Expr::AllArgs => {
3547 let scope = self.scope.read().await;
3548 Ok(Value::String(scope.all_args().join(" ")))
3549 }
3550 Expr::ArgCount => {
3551 let scope = self.scope.read().await;
3552 Ok(Value::Int(scope.arg_count() as i64))
3553 }
3554 Expr::VarLength(name) => {
3555 let scope = self.scope.read().await;
3556 match scope.get(name) {
3557 Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
3558 None => Ok(Value::Int(0)),
3559 }
3560 }
3561 Expr::VarWithDefault { name, default } => {
3562 let scope = self.scope.read().await;
3563 let use_default = match scope.get(name) {
3564 Some(value) => value_to_string(value).is_empty(),
3565 None => true,
3566 };
3567 drop(scope); if use_default {
3569 self.eval_string_parts_async(default).await.map(Value::String)
3571 } else {
3572 let scope = self.scope.read().await;
3573 scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
3574 }
3575 }
3576 Expr::Arithmetic(expr_str) => {
3577 let scope = self.scope.read().await;
3578 crate::arithmetic::eval_arithmetic(expr_str, &scope)
3579 .map(Value::Int)
3580 .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
3581 }
3582 Expr::Command(cmd) => {
3583 let result = self.execute_command(&cmd.name, &cmd.args).await?;
3585 Ok(Value::Bool(result.code == 0))
3586 }
3587 Expr::LastExitCode => {
3588 let scope = self.scope.read().await;
3589 Ok(Value::Int(scope.last_result().code))
3590 }
3591 Expr::CurrentPid => {
3592 let scope = self.scope.read().await;
3593 Ok(Value::Int(scope.pid() as i64))
3594 }
3595 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
3596 }
3597 })
3598 }
3599
3600 fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
3602 Box::pin(async move {
3603 let mut result = String::new();
3604 for part in parts {
3605 result.push_str(&self.eval_string_part_async(part).await?);
3606 }
3607 Ok(result)
3608 })
3609 }
3610
3611 fn eval_test_async<'a>(&'a self, test_expr: &'a TestExpr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
3615 Box::pin(async move {
3616 match test_expr {
3617 TestExpr::FileTest { op, path } => {
3618 let path_value = self.eval_expr_async(path).await?;
3619 let path_str = value_to_string(&path_value);
3620 let backend = self.exec_ctx.read().await.backend.clone();
3621 let entry = backend.stat(std::path::Path::new(&path_str)).await.ok();
3622 Ok(match op {
3623 FileTestOp::Exists => entry.is_some(),
3624 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
3625 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
3626 FileTestOp::Readable => entry.is_some(),
3627 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
3628 e.permissions.is_none_or(|p| p & 0o222 != 0)
3629 }),
3630 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
3631 e.permissions.is_some_and(|p| p & 0o111 != 0)
3632 }),
3633 })
3634 }
3635 TestExpr::StringTest { op, value } => {
3636 let val = self.eval_expr_async(value).await?;
3637 let s = value_to_string(&val);
3638 Ok(match op {
3639 crate::ast::StringTestOp::IsEmpty => s.is_empty(),
3640 crate::ast::StringTestOp::IsNonEmpty => !s.is_empty(),
3641 })
3642 }
3643 TestExpr::Comparison { left, op, right } => {
3644 let left_val = self.eval_expr_async(left).await?;
3646 let right_val = self.eval_expr_async(right).await?;
3647 let resolved = TestExpr::Comparison {
3648 left: Box::new(Expr::Literal(left_val)),
3649 op: *op,
3650 right: Box::new(Expr::Literal(right_val)),
3651 };
3652 let expr = Expr::Test(Box::new(resolved));
3653 let mut scope = self.scope.write().await;
3654 let value = eval_expr(&expr, &mut scope)
3655 .map_err(|e| anyhow::anyhow!("{}", e))?;
3656 Ok(value_to_bool(&value))
3657 }
3658 TestExpr::And { left, right } => {
3659 if !self.eval_test_async(left).await? {
3660 Ok(false)
3661 } else {
3662 self.eval_test_async(right).await
3663 }
3664 }
3665 TestExpr::Or { left, right } => {
3666 if self.eval_test_async(left).await? {
3667 Ok(true)
3668 } else {
3669 self.eval_test_async(right).await
3670 }
3671 }
3672 TestExpr::Not { expr } => {
3673 Ok(!self.eval_test_async(expr).await?)
3674 }
3675 }
3676 })
3677 }
3678
3679 fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
3680 Box::pin(async move {
3681 match part {
3682 StringPart::Literal(s) => Ok(s.clone()),
3683 StringPart::Var(path) => {
3684 let scope = self.scope.read().await;
3685 match scope.resolve_path(path) {
3686 Some(value) => Ok(value_to_string(&value)),
3687 None => Ok(String::new()), }
3689 }
3690 StringPart::VarWithDefault { name, default } => {
3691 let scope = self.scope.read().await;
3692 let use_default = match scope.get(name) {
3693 Some(value) => value_to_string(value).is_empty(),
3694 None => true,
3695 };
3696 drop(scope); if use_default {
3698 self.eval_string_parts_async(default).await
3700 } else {
3701 let scope = self.scope.read().await;
3702 Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
3703 }
3704 }
3705 StringPart::VarLength(name) => {
3706 let scope = self.scope.read().await;
3707 match scope.get(name) {
3708 Some(value) => Ok(value_to_string(value).len().to_string()),
3709 None => Ok("0".to_string()),
3710 }
3711 }
3712 StringPart::Positional(n) => {
3713 let scope = self.scope.read().await;
3714 match scope.get_positional(*n) {
3715 Some(s) => Ok(s.to_string()),
3716 None => Ok(String::new()),
3717 }
3718 }
3719 StringPart::AllArgs => {
3720 let scope = self.scope.read().await;
3721 Ok(scope.all_args().join(" "))
3722 }
3723 StringPart::ArgCount => {
3724 let scope = self.scope.read().await;
3725 Ok(scope.arg_count().to_string())
3726 }
3727 StringPart::Arithmetic(expr) => {
3728 let scope = self.scope.read().await;
3729 match crate::arithmetic::eval_arithmetic(expr, &scope) {
3730 Ok(value) => Ok(value.to_string()),
3731 Err(_) => Ok(String::new()),
3732 }
3733 }
3734 StringPart::CommandSubst(stmts) => {
3735 let saved_scope = { self.scope.read().await.clone() };
3738 let saved_cwd = {
3739 let ec = self.exec_ctx.read().await;
3740 (ec.cwd.clone(), ec.prev_cwd.clone())
3741 };
3742
3743 let run_result = self.execute_block_capturing(stmts).await;
3745
3746 {
3748 let mut scope = self.scope.write().await;
3749 *scope = saved_scope;
3750 if let Ok(ref r) = run_result {
3751 scope.set_last_result(r.clone());
3752 }
3753 }
3754 {
3755 let mut ec = self.exec_ctx.write().await;
3756 ec.cwd = saved_cwd.0;
3757 ec.prev_cwd = saved_cwd.1;
3758 }
3759
3760 let result = run_result?;
3762
3763 match result.try_text_out() {
3766 Ok(s) => Ok(s.trim_end_matches('\n').to_string()),
3767 Err(e) => anyhow::bail!(
3768 "command substitution in a string produced binary data ({e}) — \
3769 pipe through base64/xxd"
3770 ),
3771 }
3772 }
3773 StringPart::LastExitCode => {
3774 let scope = self.scope.read().await;
3775 Ok(scope.last_result().code.to_string())
3776 }
3777 StringPart::CurrentPid => {
3778 let scope = self.scope.read().await;
3779 Ok(scope.pid().to_string())
3780 }
3781 }
3782 })
3783 }
3784
3785 async fn update_last_result(&self, result: &ExecResult) {
3787 let mut scope = self.scope.write().await;
3788 scope.set_last_result(result.clone());
3789 }
3790
3791 async fn drain_stderr_into(&self, result: &mut ExecResult) {
3797 let drained = {
3798 let mut receiver = self.stderr_receiver.lock().await;
3799 receiver.drain_lossy()
3800 };
3801 if !drained.is_empty() {
3802 if !result.err.is_empty() && !result.err.ends_with('\n') {
3803 result.err.push('\n');
3804 }
3805 result.err.push_str(&drained);
3806 }
3807 }
3808
3809 async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
3815 let tool_args = self.build_args_async(args, None).await?;
3817
3818 {
3820 let mut scope = self.scope.write().await;
3821 scope.push_frame();
3822 }
3823
3824 let saved_positional = {
3826 let mut scope = self.scope.write().await;
3827 let saved = scope.save_positional();
3828
3829 let positional_args: Vec<String> = tool_args.positional
3831 .iter()
3832 .map(value_to_string)
3833 .collect();
3834 scope.set_positional(&def.name, positional_args);
3835
3836 saved
3837 };
3838
3839 let mut accumulated_out: Vec<u8> = Vec::new();
3844 let mut accumulated_err = String::new();
3845 let mut last_code = 0i64;
3846 let mut last_data: Option<Value> = None;
3847
3848 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
3849 match r.out_bytes() {
3850 Some(b) => buf.extend_from_slice(b),
3851 None => buf.extend_from_slice(r.text_out().as_bytes()),
3852 }
3853 }
3854
3855 let mut exec_error: Option<anyhow::Error> = None;
3857 let mut exit_code: Option<i64> = None;
3858
3859 for stmt in &def.body {
3860 match self.execute_stmt_flow(stmt).await {
3861 Ok(flow) => {
3862 let drained = {
3864 let mut receiver = self.stderr_receiver.lock().await;
3865 receiver.drain_lossy()
3866 };
3867 if !drained.is_empty() {
3868 accumulated_err.push_str(&drained);
3869 }
3870
3871 match flow {
3872 ControlFlow::Normal(r) => {
3873 push_out(&mut accumulated_out, &r);
3874 accumulated_err.push_str(&r.err);
3875 last_code = r.code;
3876 last_data = r.data;
3877 }
3878 ControlFlow::Return { value } => {
3879 push_out(&mut accumulated_out, &value);
3880 accumulated_err.push_str(&value.err);
3881 last_code = value.code;
3882 last_data = value.data;
3883 break;
3884 }
3885 ControlFlow::Exit { code } => {
3886 exit_code = Some(code);
3887 break;
3888 }
3889 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
3890 push_out(&mut accumulated_out, &r);
3891 accumulated_err.push_str(&r.err);
3892 last_code = r.code;
3893 last_data = r.data;
3894 }
3895 }
3896 }
3897 Err(e) => {
3898 exec_error = Some(e);
3899 break;
3900 }
3901 }
3902 }
3903
3904 {
3906 let mut scope = self.scope.write().await;
3907 scope.pop_frame();
3908 scope.set_positional(saved_positional.0, saved_positional.1);
3909 }
3910
3911 if let Some(e) = exec_error {
3913 return Err(e);
3914 }
3915 let code = exit_code.unwrap_or(last_code);
3916 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(code);
3917 result.err = accumulated_err;
3918 result.data = last_data;
3919 Ok(result)
3920 }
3921
3922 async fn execute_block_capturing(&self, stmts: &[Stmt]) -> Result<ExecResult> {
3930 let mut accumulated_out: Vec<u8> = Vec::new();
3934 let mut accumulated_err = String::new();
3935 let mut last_code = 0i64;
3936 let mut last_data: Option<Value> = None;
3937
3938 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
3940 match r.out_bytes() {
3941 Some(b) => buf.extend_from_slice(b),
3942 None => buf.extend_from_slice(r.text_out().as_bytes()),
3943 }
3944 }
3945
3946 for stmt in stmts {
3947 let flow = self.execute_stmt_flow(stmt).await?;
3948
3949 let drained = {
3952 let mut receiver = self.stderr_receiver.lock().await;
3953 receiver.drain_lossy()
3954 };
3955 if !drained.is_empty() {
3956 accumulated_err.push_str(&drained);
3957 }
3958
3959 match flow {
3960 ControlFlow::Normal(r)
3961 | ControlFlow::Break { result: r, .. }
3962 | ControlFlow::Continue { result: r, .. } => {
3963 push_out(&mut accumulated_out, &r);
3964 accumulated_err.push_str(&r.err);
3965 last_code = r.code;
3966 last_data = r.data;
3967 }
3968 ControlFlow::Return { value } => {
3969 push_out(&mut accumulated_out, &value);
3970 accumulated_err.push_str(&value.err);
3971 last_code = value.code;
3972 last_data = value.data;
3973 break;
3974 }
3975 ControlFlow::Exit { code } => {
3976 last_code = code;
3977 break;
3978 }
3979 }
3980 }
3981
3982 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(last_code);
3983 result.err = accumulated_err;
3984 result.data = last_data;
3985 Ok(result)
3986 }
3987
3988 async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
3993 let tool_args = self.build_args_async(args, None).await?;
3995 let path = match tool_args.positional.first() {
3996 Some(Value::String(s)) => s.clone(),
3997 Some(v) => value_to_string(v),
3998 None => {
3999 return Ok(ExecResult::failure(1, "source: missing filename"));
4000 }
4001 };
4002
4003 let full_path = {
4005 let ctx = self.exec_ctx.read().await;
4006 if path.starts_with('/') {
4007 std::path::PathBuf::from(&path)
4008 } else {
4009 ctx.cwd.join(&path)
4010 }
4011 };
4012
4013 let content = {
4015 let ctx = self.exec_ctx.read().await;
4016 match ctx.backend.read(&full_path, None).await {
4017 Ok(bytes) => {
4018 String::from_utf8(bytes).map_err(|e| {
4019 anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
4020 })?
4021 }
4022 Err(e) => {
4023 return Ok(ExecResult::failure(
4024 1,
4025 format!("source: {}: {}", path, e),
4026 ));
4027 }
4028 }
4029 };
4030
4031 let program = match crate::parser::parse(&content) {
4033 Ok(p) => p,
4034 Err(errors) => {
4035 let msg = errors
4036 .iter()
4037 .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
4038 .collect::<Vec<_>>()
4039 .join("\n");
4040 return Ok(ExecResult::failure(1, format!("source: {}", msg)));
4041 }
4042 };
4043
4044 let mut result = ExecResult::success("");
4046 for stmt in program.statements {
4047 if matches!(stmt, crate::ast::Stmt::Empty) {
4048 continue;
4049 }
4050
4051 match self.execute_stmt_flow(&stmt).await {
4052 Ok(flow) => {
4053 self.drain_stderr_into(&mut result).await;
4054 match flow {
4055 ControlFlow::Normal(r) => {
4056 result = r.clone();
4057 self.update_last_result(&r).await;
4058 }
4059 ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
4060 return Err(anyhow::anyhow!(
4061 "source: {}: unexpected break/continue outside loop",
4062 path
4063 ));
4064 }
4065 ControlFlow::Return { value } => {
4066 return Ok(value);
4067 }
4068 ControlFlow::Exit { code } => {
4069 result.code = code;
4070 return Ok(result);
4071 }
4072 }
4073 }
4074 Err(e) => {
4075 return Err(e.context(format!("source: {}", path)));
4076 }
4077 }
4078 }
4079
4080 Ok(result)
4081 }
4082
4083 async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4088 let path_value = {
4090 let scope = self.scope.read().await;
4091 scope
4092 .get("PATH")
4093 .map(value_to_string)
4094 .unwrap_or_else(|| "/bin".to_string())
4095 };
4096
4097 for dir in path_value.split(':') {
4099 if dir.is_empty() {
4100 continue;
4101 }
4102
4103 let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
4105
4106 let exists = {
4108 let ctx = self.exec_ctx.read().await;
4109 ctx.backend.exists(&script_path).await
4110 };
4111
4112 if !exists {
4113 continue;
4114 }
4115
4116 let content = {
4118 let ctx = self.exec_ctx.read().await;
4119 match ctx.backend.read(&script_path, None).await {
4120 Ok(bytes) => match String::from_utf8(bytes) {
4121 Ok(s) => s,
4122 Err(e) => {
4123 return Ok(Some(ExecResult::failure(
4124 1,
4125 format!("{}: invalid UTF-8: {}", script_path.display(), e),
4126 )));
4127 }
4128 },
4129 Err(e) => {
4130 return Ok(Some(ExecResult::failure(
4131 1,
4132 format!("{}: {}", script_path.display(), e),
4133 )));
4134 }
4135 }
4136 };
4137
4138 let program = match crate::parser::parse(&content) {
4140 Ok(p) => p,
4141 Err(errors) => {
4142 let msg = errors
4143 .iter()
4144 .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
4145 .collect::<Vec<_>>()
4146 .join("\n");
4147 return Ok(Some(ExecResult::failure(1, msg)));
4148 }
4149 };
4150
4151 let tool_args = self.build_args_async(args, None).await?;
4153
4154 let mut isolated_scope = Scope::new();
4156
4157 let positional_args: Vec<String> = tool_args.positional
4159 .iter()
4160 .map(value_to_string)
4161 .collect();
4162 isolated_scope.set_positional(name, positional_args);
4163
4164 let original_scope = {
4166 let mut scope = self.scope.write().await;
4167 std::mem::replace(&mut *scope, isolated_scope)
4168 };
4169
4170 let mut result = ExecResult::success("");
4172 let mut exec_error: Option<anyhow::Error> = None;
4173 let mut exit_code: Option<i64> = None;
4174
4175 for stmt in program.statements {
4176 if matches!(stmt, crate::ast::Stmt::Empty) {
4177 continue;
4178 }
4179
4180 match self.execute_stmt_flow(&stmt).await {
4181 Ok(flow) => {
4182 match flow {
4183 ControlFlow::Normal(r) => result = r,
4184 ControlFlow::Return { value } => {
4185 result = value;
4186 break;
4187 }
4188 ControlFlow::Exit { code } => {
4189 exit_code = Some(code);
4190 break;
4191 }
4192 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
4193 result = r;
4194 }
4195 }
4196 }
4197 Err(e) => {
4198 exec_error = Some(e);
4199 break;
4200 }
4201 }
4202 }
4203
4204 {
4206 let mut scope = self.scope.write().await;
4207 *scope = original_scope;
4208 }
4209
4210 if let Some(e) = exec_error {
4212 return Err(e.context(format!("script: {}", script_path.display())));
4213 }
4214 if let Some(code) = exit_code {
4215 result.code = code;
4216 return Ok(Some(result));
4217 }
4218
4219 return Ok(Some(result));
4220 }
4221
4222 Ok(None)
4224 }
4225
4226 #[cfg(not(feature = "subprocess"))]
4240 async fn try_execute_external(&self, _name: &str, _args: &[Arg]) -> Result<Option<ExecResult>> {
4241 Ok(None)
4242 }
4243
4244 #[cfg(feature = "subprocess")]
4246 #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
4247 async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4248 let cancel = {
4254 let ec = self.exec_ctx.read().await;
4255 ec.cancel.clone()
4256 };
4257 let kill_grace = self.kill_grace;
4258 if !self.allow_external_commands {
4259 return Ok(None);
4260 }
4261
4262 let real_cwd = {
4267 let ctx = self.exec_ctx.read().await;
4268 match ctx.backend.resolve_real_path(&ctx.cwd) {
4269 Some(p) => p,
4270 None => return Ok(None),
4271 }
4272 };
4273
4274 let executable = if name.contains('/') {
4275 let resolved = if std::path::Path::new(name).is_absolute() {
4277 std::path::PathBuf::from(name)
4278 } else {
4279 real_cwd.join(name)
4280 };
4281 if !resolved.exists() {
4282 return Ok(Some(ExecResult::failure(
4283 127,
4284 format!("{}: No such file or directory", name),
4285 )));
4286 }
4287 if !resolved.is_file() {
4288 return Ok(Some(ExecResult::failure(
4289 126,
4290 format!("{}: Is a directory", name),
4291 )));
4292 }
4293 #[cfg(unix)]
4294 {
4295 use std::os::unix::fs::PermissionsExt;
4296 let mode = std::fs::metadata(&resolved)
4297 .map(|m| m.permissions().mode())
4298 .unwrap_or(0);
4299 if mode & 0o111 == 0 {
4300 return Ok(Some(ExecResult::failure(
4301 126,
4302 format!("{}: Permission denied", name),
4303 )));
4304 }
4305 }
4306 resolved.to_string_lossy().into_owned()
4307 } else {
4308 let path_var = {
4310 let scope = self.scope.read().await;
4311 scope
4312 .get("PATH")
4313 .map(value_to_string)
4314 .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default())
4315 };
4316
4317 match resolve_in_path(name, &path_var) {
4319 Some(path) => path,
4320 None => return Ok(None), }
4322 };
4323
4324 tracing::debug!(executable = %executable, "resolved external command");
4325
4326 let argv = self.build_args_flat(args).await?;
4328
4329 let (pipe_stdin, stdin_string) = {
4338 let mut ctx = self.exec_ctx.write().await;
4339 (ctx.pipe_stdin.take(), ctx.take_stdin())
4340 };
4341 let has_stdin = pipe_stdin.is_some() || stdin_string.is_some();
4342
4343 use tokio::process::Command;
4345
4346 let mut cmd = Command::new(&executable);
4347 cmd.args(&argv);
4348 cmd.current_dir(&real_cwd);
4349
4350 cmd.env_clear();
4354 {
4355 let scope = self.scope.read().await;
4356 for (var_name, value) in scope.exported_vars() {
4357 cmd.env(var_name, value_to_string(&value));
4358 }
4359 }
4360
4361 cmd.stdin(if has_stdin {
4363 std::process::Stdio::piped()
4364 } else if self.interactive {
4365 std::process::Stdio::inherit()
4366 } else {
4367 std::process::Stdio::null()
4368 });
4369
4370 let pipeline_position = {
4374 let ctx = self.exec_ctx.read().await;
4375 ctx.pipeline_position
4376 };
4377 let inherit_output = self.interactive
4378 && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
4379
4380 if inherit_output {
4381 cmd.stdout(std::process::Stdio::inherit());
4382 cmd.stderr(std::process::Stdio::inherit());
4383 } else {
4384 cmd.stdout(std::process::Stdio::piped());
4385 cmd.stderr(std::process::Stdio::piped());
4386 }
4387
4388 #[cfg(unix)]
4394 {
4395 let restore_jc_signals = self.terminal_state.is_some() && inherit_output;
4396 #[allow(unsafe_code)]
4398 unsafe {
4399 cmd.pre_exec(move || {
4400 nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
4402 .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
4403 if restore_jc_signals {
4404 use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
4405 let mut sa: nix::libc::sigaction = std::mem::zeroed();
4406 sa.sa_sigaction = SIG_DFL;
4407 if sigaction(SIGTSTP, &sa, std::ptr::null_mut()) != 0 {
4408 return Err(std::io::Error::last_os_error());
4409 }
4410 if sigaction(SIGTTOU, &sa, std::ptr::null_mut()) != 0 {
4411 return Err(std::io::Error::last_os_error());
4412 }
4413 if sigaction(SIGTTIN, &sa, std::ptr::null_mut()) != 0 {
4414 return Err(std::io::Error::last_os_error());
4415 }
4416 if sigaction(SIGINT, &sa, std::ptr::null_mut()) != 0 {
4417 return Err(std::io::Error::last_os_error());
4418 }
4419 }
4420 Ok(())
4421 });
4422 }
4423 }
4424
4425 let in_jc_inherit_path = inherit_output && self.terminal_state.is_some();
4432 if !in_jc_inherit_path {
4433 cmd.kill_on_drop(true);
4434 }
4435
4436 let mut child = match cmd.spawn() {
4441 Ok(child) => child,
4442 Err(e) => {
4443 return Ok(Some(ExecResult::failure(
4444 127,
4445 format!("{}: {}", name, e),
4446 )));
4447 }
4448 };
4449 let kill_target = crate::pidfd::KillTarget::from_child(&child);
4450
4451 if let Some(job_id) = self.bg_job_id
4456 && let Some(pid) = child.id()
4457 {
4458 self.jobs.add_pgid(job_id, pid).await;
4459 }
4460
4461 let stdin_task: Option<tokio::task::JoinHandle<()>> = if let Some(mut pipe_in) = pipe_stdin {
4468 child.stdin.take().map(|mut child_stdin| {
4469 tokio::spawn(async move {
4470 use tokio::io::{AsyncReadExt, AsyncWriteExt};
4471 let mut buf = [0u8; 8192];
4472 loop {
4473 match pipe_in.read(&mut buf).await {
4474 Ok(0) => break, Ok(n) => {
4476 if child_stdin.write_all(&buf[..n]).await.is_err() {
4477 break; }
4479 }
4480 Err(_) => break,
4481 }
4482 }
4483 })
4485 })
4486 } else if let Some(data) = stdin_string
4487 && let Some(mut stdin) = child.stdin.take()
4488 {
4489 use tokio::io::AsyncWriteExt;
4490 if let Err(e) = stdin.write_all(data.as_bytes()).await {
4491 return Ok(Some(ExecResult::failure(
4492 1,
4493 format!("{}: failed to write stdin: {}", name, e),
4494 )));
4495 }
4496 None
4498 } else {
4499 None
4500 };
4501
4502 struct AbortStdinCopyOnDrop(Option<tokio::task::JoinHandle<()>>);
4510 impl Drop for AbortStdinCopyOnDrop {
4511 fn drop(&mut self) {
4512 if let Some(t) = self.0.take() {
4513 t.abort();
4514 }
4515 }
4516 }
4517 let _stdin_copy_guard = AbortStdinCopyOnDrop(stdin_task);
4518
4519 if inherit_output {
4520 #[cfg(unix)]
4522 if let Some(ref term) = self.terminal_state {
4523 let child_id = child.id().unwrap_or(0);
4524 let pid = nix::unistd::Pid::from_raw(child_id as i32);
4525 let pgid = pid; if let Err(e) = term.give_terminal_to(pgid) {
4529 tracing::warn!("failed to give terminal to child: {}", e);
4530 }
4531
4532 let term_clone = term.clone();
4533 let cmd_name = name.to_string();
4534 let cmd_display = format!("{} {}", name, argv.join(" "));
4535 let jobs = self.jobs.clone();
4536
4537 let wait_complete = std::sync::Arc::new(
4551 std::sync::atomic::AtomicBool::new(false)
4552 );
4553 let cancel_watcher = {
4554 let cancel = cancel.clone();
4555 let wc = wait_complete.clone();
4556 let target = kill_target.as_ref().map(|t| {
4564 crate::pidfd::KillTarget::from_pid(t.pid())
4576 });
4577 tokio::spawn(async move {
4578 cancel.cancelled().await;
4579 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
4580 use nix::sys::signal::Signal;
4581 if let Some(t) = &target {
4582 t.signal(Signal::SIGTERM);
4583 t.signal_pg(Signal::SIGTERM);
4584 } else {
4585 let _ = nix::sys::signal::kill(pid, Signal::SIGTERM);
4586 let _ = nix::sys::signal::killpg(pid, Signal::SIGTERM);
4587 }
4588 if kill_grace > Duration::ZERO {
4589 tokio::time::sleep(kill_grace).await;
4590 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
4591 }
4592 if let Some(t) = &target {
4593 t.signal(Signal::SIGKILL);
4594 t.signal_pg(Signal::SIGKILL);
4595 } else {
4596 let _ = nix::sys::signal::kill(pid, Signal::SIGKILL);
4597 let _ = nix::sys::signal::killpg(pid, Signal::SIGKILL);
4598 }
4599 })
4600 };
4601 struct AbortOnDrop(tokio::task::JoinHandle<()>);
4602 impl Drop for AbortOnDrop {
4603 fn drop(&mut self) {
4604 self.0.abort();
4605 }
4606 }
4607 let _watcher_guard = AbortOnDrop(cancel_watcher);
4608
4609 let wait_complete_setter = wait_complete.clone();
4610 let code = tokio::task::block_in_place(move || {
4611 let result = term_clone.wait_for_foreground(pid);
4612 wait_complete_setter.store(true, std::sync::atomic::Ordering::SeqCst);
4614
4615 if let Err(e) = term_clone.reclaim_terminal() {
4617 tracing::warn!("failed to reclaim terminal: {}", e);
4618 }
4619
4620 match result {
4621 crate::terminal::WaitResult::Exited(code) => code as i64,
4622 crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
4623 crate::terminal::WaitResult::Stopped(_sig) => {
4624 let rt = tokio::runtime::Handle::current();
4626 let job_id = rt.block_on(jobs.register_stopped(
4627 cmd_display,
4628 child_id,
4629 child_id, ));
4631 eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
4632 148 }
4634 }
4635 });
4636
4637 return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
4638 }
4639
4640 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
4642 Ok(s) => s,
4643 Err(e) => {
4644 return Ok(Some(ExecResult::failure(
4645 1,
4646 format!("{}: failed to wait: {}", name, e),
4647 )));
4648 }
4649 };
4650
4651 let code = status.code().unwrap_or_else(|| {
4652 #[cfg(unix)]
4653 {
4654 use std::os::unix::process::ExitStatusExt;
4655 128 + status.signal().unwrap_or(0)
4656 }
4657 #[cfg(not(unix))]
4658 {
4659 -1
4660 }
4661 }) as i64;
4662
4663 Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
4665 } else {
4666 let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
4668 let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
4669
4670 let stdout_pipe = child.stdout.take();
4671 let stderr_pipe = child.stderr.take();
4672
4673 let stdout_clone = stdout_stream.clone();
4674 let stderr_clone = stderr_stream.clone();
4675
4676 let stdout_task = stdout_pipe.map(|pipe| {
4677 tokio::spawn(async move {
4678 drain_to_stream(pipe, stdout_clone).await;
4679 })
4680 });
4681
4682 let stderr_task = stderr_pipe.map(|pipe| {
4683 tokio::spawn(async move {
4684 drain_to_stream(pipe, stderr_clone).await;
4685 })
4686 });
4687
4688 let cancelled_before_wait = cancel.is_cancelled();
4689 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
4690 Ok(s) => s,
4691 Err(e) => {
4692 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
4694 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
4695 return Ok(Some(ExecResult::failure(
4696 1,
4697 format!("{}: failed to wait: {}", name, e),
4698 )));
4699 }
4700 };
4701
4702 if cancelled_before_wait || cancel.is_cancelled() {
4706 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
4707 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
4708 } else {
4709 if let Some(task) = stdout_task {
4710 let _ = task.await;
4712 }
4713 if let Some(task) = stderr_task {
4714 let _ = task.await;
4715 }
4716 }
4717
4718 let code = status.code().unwrap_or_else(|| {
4719 #[cfg(unix)]
4720 {
4721 use std::os::unix::process::ExitStatusExt;
4722 128 + status.signal().unwrap_or(0)
4723 }
4724 #[cfg(not(unix))]
4725 {
4726 -1
4727 }
4728 }) as i64;
4729
4730 let stdout = stdout_stream.read().await;
4734 let stderr = stderr_stream.read_string().await;
4735 let mut result = ExecResult::success_text_or_bytes(stdout).with_code(code);
4736 result.err = stderr;
4737 Ok(Some(result))
4738 }
4739 }
4740
4741 pub async fn get_var(&self, name: &str) -> Option<Value> {
4745 let scope = self.scope.read().await;
4746 scope.get(name).cloned()
4747 }
4748
4749 #[cfg(test)]
4751 pub async fn error_exit_enabled(&self) -> bool {
4752 let scope = self.scope.read().await;
4753 scope.error_exit_enabled()
4754 }
4755
4756 pub async fn set_var(&self, name: &str, value: Value) {
4758 let mut scope = self.scope.write().await;
4759 scope.set(name.to_string(), value);
4760 }
4761
4762 pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
4764 let mut scope = self.scope.write().await;
4765 scope.set_positional(script_name, args);
4766 }
4767
4768 pub async fn list_vars(&self) -> Vec<(String, Value)> {
4770 let scope = self.scope.read().await;
4771 scope.all()
4772 }
4773
4774 pub async fn exported_vars(&self) -> Vec<(String, Value)> {
4777 let scope = self.scope.read().await;
4778 scope.exported_vars()
4779 }
4780
4781 pub async fn cwd(&self) -> PathBuf {
4785 self.exec_ctx.read().await.cwd.clone()
4786 }
4787
4788 pub async fn set_cwd(&self, path: PathBuf) {
4790 let mut ctx = self.exec_ctx.write().await;
4791 ctx.set_cwd(path);
4792 }
4793
4794 pub async fn try_set_cwd(&self, path: PathBuf) -> bool {
4800 let backend = self.exec_ctx.read().await.backend.clone();
4803 let is_dir = matches!(backend.stat(&path).await, Ok(entry) if entry.is_dir());
4804 if is_dir {
4805 self.exec_ctx.write().await.set_cwd(path);
4806 }
4807 is_dir
4808 }
4809
4810 pub async fn last_result(&self) -> ExecResult {
4814 let scope = self.scope.read().await;
4815 scope.last_result().clone()
4816 }
4817
4818 pub async fn has_function(&self, name: &str) -> bool {
4822 self.user_tools.read().await.contains_key(name)
4823 }
4824
4825 pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
4827 self.tools.schemas()
4828 }
4829
4830 pub fn jobs(&self) -> Arc<JobManager> {
4834 self.jobs.clone()
4835 }
4836
4837 pub fn vfs(&self) -> Arc<VfsRouter> {
4841 self.vfs.clone()
4842 }
4843
4844 pub async fn reset(&self) -> Result<()> {
4851 {
4852 let mut scope = self.scope.write().await;
4853 *scope = Scope::new();
4854 }
4855 {
4856 let mut ctx = self.exec_ctx.write().await;
4857 ctx.cwd = PathBuf::from("/");
4858 }
4859 Ok(())
4860 }
4861
4862 pub async fn shutdown(self) -> Result<()> {
4864 self.jobs.wait_all().await;
4866 Ok(())
4867 }
4868
4869 async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
4880 if let Some(d) = self.dispatcher() {
4885 ctx.dispatcher = Some(d);
4886 }
4887
4888 {
4890 let mut scope = self.scope.write().await;
4891 *scope = ctx.scope.clone();
4892 }
4893 {
4894 let mut ec = self.exec_ctx.write().await;
4895 ec.cwd = ctx.cwd.clone();
4896 ec.prev_cwd = ctx.prev_cwd.clone();
4897 ec.stdin = ctx.stdin.take();
4898 ec.stdin_data = ctx.stdin_data.take();
4899 ec.pipe_stdin = ctx.pipe_stdin.take();
4905 ec.pipe_stdout = ctx.pipe_stdout.take();
4906 if let Some(stderr) = ctx.stderr.clone() {
4907 ec.stderr = Some(stderr);
4908 }
4909 ec.aliases = ctx.aliases.clone();
4910 ec.ignore_config = ctx.ignore_config.clone();
4911 ec.output_limit = ctx.output_limit.clone();
4912 ec.pipeline_position = ctx.pipeline_position;
4913 ec.cancel = ctx.cancel.clone();
4918 ec.watchdog = ctx.watchdog.clone();
4922 }
4923
4924 let result = self.execute_command(&cmd.name, &cmd.args).await?;
4926
4927 {
4929 let scope = self.scope.read().await;
4930 ctx.scope = scope.clone();
4931 }
4932 {
4933 let mut ec = self.exec_ctx.write().await;
4934 ctx.cwd = ec.cwd.clone();
4935 ctx.prev_cwd = ec.prev_cwd.clone();
4936 ctx.aliases = ec.aliases.clone();
4937 ctx.ignore_config = ec.ignore_config.clone();
4938 ctx.output_limit = ec.output_limit.clone();
4939 ctx.pipe_stdin = ec.pipe_stdin.take();
4944 ctx.pipe_stdout = ec.pipe_stdout.take();
4945 }
4946
4947 Ok(result)
4948 }
4949}
4950
4951#[async_trait]
4952impl CommandDispatcher for Kernel {
4953 async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
4959 self.dispatch_command(cmd, ctx).await
4960 }
4961
4962 async fn eval_expr(&self, expr: &Expr, _ctx: &ExecContext) -> Result<Value> {
4969 self.eval_expr_async(expr).await
4970 }
4971
4972 async fn fork(&self) -> Arc<dyn CommandDispatcher> {
4978 let fork: Arc<Kernel> = Kernel::fork(self).await;
4979 fork
4980 }
4981
4982 async fn fork_attached(&self) -> Arc<dyn CommandDispatcher> {
4984 let fork: Arc<Kernel> = Kernel::fork_attached(self).await;
4985 fork
4986 }
4987}
4988
4989fn finalize_output(
4997 result: ExecResult,
4998 format: Option<crate::interpreter::OutputFormat>,
4999 owns_output: bool,
5000) -> ExecResult {
5001 match format {
5002 Some(_) if owns_output => result,
5003 Some(format) => apply_output_format(result, format),
5004 None => result,
5005 }
5006}
5007
5008fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
5017 accumulated.materialize();
5021 match new.out_bytes() {
5022 Some(new_bytes) => {
5026 let mut combined: Vec<u8> = match accumulated.out_bytes() {
5027 Some(b) => b.to_vec(),
5028 None => accumulated.text_out().into_owned().into_bytes(),
5029 };
5030 combined.extend_from_slice(new_bytes);
5031 accumulated.set_out_bytes(combined);
5032 }
5033 None => accumulated.push_out(&new.text_out()),
5034 }
5035 accumulated.err.push_str(&new.err);
5036 accumulated.code = new.code;
5037 accumulated.data = new.data.clone();
5038 accumulated.did_spill = new.did_spill;
5039 accumulated.original_code = new.original_code;
5040 accumulated.content_type = new.content_type.clone();
5041 accumulated.baggage.clone_from(&new.baggage);
5042}
5043
5044fn fold_loop_output_into_flow(loop_output: ExecResult, flow: &mut ControlFlow) {
5050 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5051 let mut merged = loop_output;
5052 accumulate_result(&mut merged, result);
5053 *result = merged;
5054 }
5055}
5056
5057fn accumulate_flow_output(accumulated: &mut ExecResult, flow: &ControlFlow) {
5061 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5062 accumulate_result(accumulated, result);
5063 }
5064}
5065
5066fn is_truthy(value: &Value) -> bool {
5068 match value {
5069 Value::Null => false,
5070 Value::Bool(b) => *b,
5071 Value::Int(i) => *i != 0,
5072 Value::Float(f) => *f != 0.0,
5073 Value::String(s) => !s.is_empty(),
5074 Value::Json(json) => match json {
5075 serde_json::Value::Null => false,
5076 serde_json::Value::Array(arr) => !arr.is_empty(),
5077 serde_json::Value::Object(obj) => !obj.is_empty(),
5078 serde_json::Value::Bool(b) => *b,
5079 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
5080 serde_json::Value::String(s) => !s.is_empty(),
5081 },
5082 Value::Bytes(b) => !b.is_empty(), }
5084}
5085
5086fn apply_tilde_expansion(value: Value, home: Option<&str>) -> Value {
5092 match value {
5093 Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s, home)),
5094 _ => value,
5095 }
5096}
5097
5098fn push_repeatable_value(
5107 tool_args: &mut ToolArgs,
5108 flag_name: &str,
5109 canonical: &str,
5110 v: Value,
5111) -> anyhow::Result<()> {
5112 let occ = crate::interpreter::value_to_json(&v);
5113 let entry = tool_args
5114 .named
5115 .entry(canonical.to_string())
5116 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
5117 if let Value::Json(serde_json::Value::Array(items)) = entry {
5118 items.push(occ);
5119 Ok(())
5120 } else {
5121 anyhow::bail!("--{flag_name}: named[{canonical}] already holds a non-array value")
5122 }
5123}
5124
5125#[cfg(all(unix, feature = "subprocess"))]
5131pub(crate) async fn wait_or_kill(
5132 child: &mut tokio::process::Child,
5133 target: Option<&crate::pidfd::KillTarget>,
5134 cancel: &tokio_util::sync::CancellationToken,
5135 grace: Duration,
5136) -> std::io::Result<std::process::ExitStatus> {
5137 tokio::select! {
5138 biased;
5139 status = child.wait() => status,
5140 _ = cancel.cancelled() => kill_with_grace(child, target, grace).await,
5141 }
5142}
5143
5144#[cfg(all(not(unix), feature = "subprocess"))]
5145pub(crate) async fn wait_or_kill(
5146 child: &mut tokio::process::Child,
5147 _target: Option<&()>,
5148 cancel: &tokio_util::sync::CancellationToken,
5149 _grace: Duration,
5150) -> std::io::Result<std::process::ExitStatus> {
5151 tokio::select! {
5152 biased;
5153 status = child.wait() => status,
5154 _ = cancel.cancelled() => {
5155 let _ = child.start_kill();
5156 child.wait().await
5157 }
5158 }
5159}
5160
5161#[cfg(all(unix, feature = "subprocess"))]
5167pub(crate) async fn kill_with_grace(
5168 child: &mut tokio::process::Child,
5169 target: Option<&crate::pidfd::KillTarget>,
5170 grace: Duration,
5171) -> std::io::Result<std::process::ExitStatus> {
5172 use nix::sys::signal::Signal;
5173
5174 if let Some(t) = target {
5175 t.signal(Signal::SIGTERM);
5176 t.signal_pg(Signal::SIGTERM);
5177 if grace > Duration::ZERO
5178 && let Ok(status) = tokio::time::timeout(grace, child.wait()).await
5179 {
5180 return status;
5181 }
5182 t.signal(Signal::SIGKILL);
5183 t.signal_pg(Signal::SIGKILL);
5184 }
5185 child.wait().await
5186}
5187
5188#[cfg(all(test, feature = "subprocess"))]
5189#[allow(clippy::expect_used)]
5190mod tests {
5191 use super::*;
5192
5193 #[tokio::test]
5194 async fn test_kernel_transient() {
5195 let kernel = Kernel::transient().expect("failed to create kernel");
5196 assert_eq!(kernel.name(), "transient");
5197 }
5198
5199 #[tokio::test]
5200 async fn test_kernel_execute_echo() {
5201 let kernel = Kernel::transient().expect("failed to create kernel");
5202 let result = kernel.execute("echo hello").await.expect("execution failed");
5203 assert!(result.ok());
5204 assert_eq!(result.text_out().trim(), "hello");
5205 }
5206
5207 #[tokio::test]
5208 async fn test_multiple_statements_accumulate_output() {
5209 let kernel = Kernel::transient().expect("failed to create kernel");
5210 let result = kernel
5211 .execute("echo one\necho two\necho three")
5212 .await
5213 .expect("execution failed");
5214 assert!(result.ok());
5215 assert!(result.text_out().contains("one"), "missing 'one': {}", result.text_out());
5217 assert!(result.text_out().contains("two"), "missing 'two': {}", result.text_out());
5218 assert!(result.text_out().contains("three"), "missing 'three': {}", result.text_out());
5219 }
5220
5221 #[tokio::test]
5222 async fn test_and_chain_accumulates_output() {
5223 let kernel = Kernel::transient().expect("failed to create kernel");
5224 let result = kernel
5225 .execute("echo first && echo second")
5226 .await
5227 .expect("execution failed");
5228 assert!(result.ok());
5229 assert!(result.text_out().contains("first"), "missing 'first': {}", result.text_out());
5230 assert!(result.text_out().contains("second"), "missing 'second': {}", result.text_out());
5231 }
5232
5233 #[tokio::test]
5234 async fn test_for_loop_accumulates_output() {
5235 let kernel = Kernel::transient().expect("failed to create kernel");
5236 let result = kernel
5237 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
5238 .await
5239 .expect("execution failed");
5240 assert!(result.ok());
5241 assert!(result.text_out().contains("item: a"), "missing 'item: a': {}", result.text_out());
5242 assert!(result.text_out().contains("item: b"), "missing 'item: b': {}", result.text_out());
5243 assert!(result.text_out().contains("item: c"), "missing 'item: c': {}", result.text_out());
5244 }
5245
5246 #[tokio::test]
5247 async fn test_while_loop_accumulates_output() {
5248 let kernel = Kernel::transient().expect("failed to create kernel");
5249 let result = kernel
5250 .execute(r#"
5251 N=3
5252 while [[ ${N} -gt 0 ]]; do
5253 echo "N=${N}"
5254 N=$((N - 1))
5255 done
5256 "#)
5257 .await
5258 .expect("execution failed");
5259 assert!(result.ok());
5260 assert!(result.text_out().contains("N=3"), "missing 'N=3': {}", result.text_out());
5261 assert!(result.text_out().contains("N=2"), "missing 'N=2': {}", result.text_out());
5262 assert!(result.text_out().contains("N=1"), "missing 'N=1': {}", result.text_out());
5263 }
5264
5265 #[tokio::test]
5266 async fn test_kernel_set_var() {
5267 let kernel = Kernel::transient().expect("failed to create kernel");
5268
5269 kernel.execute("X=42").await.expect("set failed");
5270
5271 let value = kernel.get_var("X").await;
5272 assert_eq!(value, Some(Value::Int(42)));
5273 }
5274
5275 #[tokio::test]
5276 async fn test_kernel_var_expansion() {
5277 let kernel = Kernel::transient().expect("failed to create kernel");
5278
5279 kernel.execute("NAME=\"world\"").await.expect("set failed");
5280 let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
5281
5282 assert!(result.ok());
5283 assert_eq!(result.text_out().trim(), "hello world");
5284 }
5285
5286 #[tokio::test]
5287 async fn test_kernel_last_result() {
5288 let kernel = Kernel::transient().expect("failed to create kernel");
5289
5290 kernel.execute("echo test").await.expect("echo failed");
5291
5292 let last = kernel.last_result().await;
5293 assert!(last.ok());
5294 assert_eq!(last.text_out().trim(), "test");
5295 }
5296
5297 #[tokio::test]
5298 async fn test_kernel_tool_not_found() {
5299 let kernel = Kernel::transient().expect("failed to create kernel");
5300
5301 let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
5302 assert!(!result.ok());
5303 assert_eq!(result.code, 127);
5304 assert!(result.err.contains("command not found"));
5305 }
5306
5307 #[tokio::test]
5308 async fn test_external_command_true() {
5309 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
5311
5312 let result = kernel.execute("true").await.expect("execution failed");
5314 assert!(result.ok(), "true should succeed: {:?}", result);
5316 }
5317
5318 #[tokio::test]
5319 async fn test_external_command_basic() {
5320 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
5322
5323 let path_var = std::env::var("PATH").unwrap_or_default();
5328 eprintln!("System PATH: {}", path_var);
5329
5330 kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
5332
5333 let result = kernel.execute("uname").await.expect("execution failed");
5336 eprintln!("uname result: {:?}", result);
5337 assert!(result.ok() || result.code == 127, "uname: {:?}", result);
5339 }
5340
5341 #[tokio::test]
5342 async fn test_kernel_reset() {
5343 let kernel = Kernel::transient().expect("failed to create kernel");
5344
5345 kernel.execute("X=1").await.expect("set failed");
5346 assert!(kernel.get_var("X").await.is_some());
5347
5348 kernel.reset().await.expect("reset failed");
5349 assert!(kernel.get_var("X").await.is_none());
5350 }
5351
5352 #[tokio::test]
5353 async fn test_kernel_cwd() {
5354 let kernel = Kernel::transient().expect("failed to create kernel");
5355
5356 let cwd = kernel.cwd().await;
5358 let home = std::env::var("HOME")
5359 .map(PathBuf::from)
5360 .unwrap_or_else(|_| PathBuf::from("/"));
5361 assert_eq!(cwd, home);
5362
5363 kernel.set_cwd(PathBuf::from("/tmp")).await;
5364 assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
5365 }
5366
5367 #[tokio::test]
5368 async fn test_kernel_list_vars() {
5369 let kernel = Kernel::transient().expect("failed to create kernel");
5370
5371 kernel.execute("A=1").await.ok();
5372 kernel.execute("B=2").await.ok();
5373
5374 let vars = kernel.list_vars().await;
5375 assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
5376 assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
5377 }
5378
5379 #[tokio::test]
5380 async fn test_is_truthy() {
5381 assert!(!is_truthy(&Value::Null));
5382 assert!(!is_truthy(&Value::Bool(false)));
5383 assert!(is_truthy(&Value::Bool(true)));
5384 assert!(!is_truthy(&Value::Int(0)));
5385 assert!(is_truthy(&Value::Int(1)));
5386 assert!(!is_truthy(&Value::String("".into())));
5387 assert!(is_truthy(&Value::String("x".into())));
5388 }
5389
5390 #[tokio::test]
5391 async fn test_jq_in_pipeline() {
5392 let kernel = Kernel::transient().expect("failed to create kernel");
5393 let result = kernel
5395 .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
5396 .await
5397 .expect("execution failed");
5398 assert!(result.ok(), "jq pipeline failed: {}", result.err);
5399 assert_eq!(result.text_out().trim(), "Alice");
5400 }
5401
5402 #[tokio::test]
5403 async fn test_user_defined_tool() {
5404 let kernel = Kernel::transient().expect("failed to create kernel");
5405
5406 kernel
5408 .execute(r#"greet() { echo "Hello, $1!" }"#)
5409 .await
5410 .expect("function definition failed");
5411
5412 let result = kernel
5414 .execute(r#"greet "World""#)
5415 .await
5416 .expect("function call failed");
5417
5418 assert!(result.ok(), "greet failed: {}", result.err);
5419 assert_eq!(result.text_out().trim(), "Hello, World!");
5420 }
5421
5422 #[tokio::test]
5423 async fn test_user_tool_positional_args() {
5424 let kernel = Kernel::transient().expect("failed to create kernel");
5425
5426 kernel
5428 .execute(r#"greet() { echo "Hi $1" }"#)
5429 .await
5430 .expect("function definition failed");
5431
5432 let result = kernel
5434 .execute(r#"greet "Amy""#)
5435 .await
5436 .expect("function call failed");
5437
5438 assert!(result.ok(), "greet failed: {}", result.err);
5439 assert_eq!(result.text_out().trim(), "Hi Amy");
5440 }
5441
5442 #[tokio::test]
5443 async fn test_function_shared_scope() {
5444 let kernel = Kernel::transient().expect("failed to create kernel");
5445
5446 kernel
5448 .execute(r#"SECRET="hidden""#)
5449 .await
5450 .expect("set failed");
5451
5452 kernel
5454 .execute(r#"access_parent() {
5455 echo "${SECRET}"
5456 SECRET="modified"
5457 }"#)
5458 .await
5459 .expect("function definition failed");
5460
5461 let result = kernel.execute("access_parent").await.expect("function call failed");
5463
5464 assert!(
5466 result.text_out().contains("hidden"),
5467 "Function should access parent scope, got: {}",
5468 result.text_out()
5469 );
5470
5471 let secret = kernel.get_var("SECRET").await;
5473 assert_eq!(
5474 secret,
5475 Some(Value::String("modified".into())),
5476 "Function should modify parent scope"
5477 );
5478 }
5479
5480 #[tokio::test]
5481 #[ignore = "exec replaces the test binary via CommandExt::exec, hangs libtest; cannot be run under cargo test"]
5482 async fn test_exec_builtin() {
5483 let kernel = Kernel::transient().expect("failed to create kernel");
5484 let result = kernel
5486 .execute(r#"exec command="/bin/echo" argv="hello world""#)
5487 .await
5488 .expect("exec failed");
5489
5490 assert!(result.ok(), "exec failed: {}", result.err);
5491 assert_eq!(result.text_out().trim(), "hello world");
5492 }
5493
5494 #[tokio::test]
5495 async fn test_while_false_never_runs() {
5496 let kernel = Kernel::transient().expect("failed to create kernel");
5497
5498 let result = kernel
5500 .execute(r#"
5501 while false; do
5502 echo "should not run"
5503 done
5504 "#)
5505 .await
5506 .expect("while false failed");
5507
5508 assert!(result.ok());
5509 assert!(result.text_out().is_empty(), "while false should not execute body: {}", result.text_out());
5510 }
5511
5512 #[tokio::test]
5513 async fn test_while_string_comparison() {
5514 let kernel = Kernel::transient().expect("failed to create kernel");
5515
5516 kernel.execute(r#"FLAG="go""#).await.expect("set failed");
5518
5519 let result = kernel
5522 .execute(r#"
5523 while [[ ${FLAG} == "go" ]]; do
5524 FLAG="stop"
5525 echo "running"
5526 done
5527 "#)
5528 .await
5529 .expect("while with string cmp failed");
5530
5531 assert!(result.ok());
5532 assert!(result.text_out().contains("running"), "should have run once: {}", result.text_out());
5533
5534 let flag = kernel.get_var("FLAG").await;
5536 assert_eq!(flag, Some(Value::String("stop".into())));
5537 }
5538
5539 #[tokio::test]
5540 async fn test_while_numeric_comparison() {
5541 let kernel = Kernel::transient().expect("failed to create kernel");
5542
5543 kernel.execute("N=5").await.expect("set failed");
5545
5546 let result = kernel
5548 .execute(r#"
5549 while [[ ${N} -gt 3 ]]; do
5550 N=3
5551 echo "N was greater"
5552 done
5553 "#)
5554 .await
5555 .expect("while with > failed");
5556
5557 assert!(result.ok());
5558 assert!(result.text_out().contains("N was greater"), "should have run once: {}", result.text_out());
5559 }
5560
5561 #[tokio::test]
5562 async fn test_break_in_while_loop() {
5563 let kernel = Kernel::transient().expect("failed to create kernel");
5564
5565 let result = kernel
5566 .execute(r#"
5567 I=0
5568 while true; do
5569 I=1
5570 echo "before break"
5571 break
5572 echo "after break"
5573 done
5574 "#)
5575 .await
5576 .expect("while with break failed");
5577
5578 assert!(result.ok());
5579 assert!(result.text_out().contains("before break"), "should see before break: {}", result.text_out());
5580 assert!(!result.text_out().contains("after break"), "should not see after break: {}", result.text_out());
5581
5582 let i = kernel.get_var("I").await;
5584 assert_eq!(i, Some(Value::Int(1)));
5585 }
5586
5587 #[tokio::test]
5588 async fn test_continue_in_while_loop() {
5589 let kernel = Kernel::transient().expect("failed to create kernel");
5590
5591 let result = kernel
5596 .execute(r#"
5597 STATE="start"
5598 AFTER_CONTINUE="no"
5599 while [[ ${STATE} != "done" ]]; do
5600 if [[ ${STATE} == "start" ]]; then
5601 STATE="middle"
5602 continue
5603 AFTER_CONTINUE="yes"
5604 fi
5605 if [[ ${STATE} == "middle" ]]; then
5606 STATE="done"
5607 fi
5608 done
5609 "#)
5610 .await
5611 .expect("while with continue failed");
5612
5613 assert!(result.ok());
5614
5615 let state = kernel.get_var("STATE").await;
5617 assert_eq!(state, Some(Value::String("done".into())));
5618
5619 let after = kernel.get_var("AFTER_CONTINUE").await;
5621 assert_eq!(after, Some(Value::String("no".into())));
5622 }
5623
5624 #[tokio::test]
5625 async fn test_break_with_level() {
5626 let kernel = Kernel::transient().expect("failed to create kernel");
5627
5628 let result = kernel
5633 .execute(r#"
5634 OUTER=0
5635 while true; do
5636 OUTER=1
5637 for X in "1 2"; do
5638 break 2
5639 done
5640 OUTER=2
5641 done
5642 "#)
5643 .await
5644 .expect("nested break failed");
5645
5646 assert!(result.ok());
5647
5648 let outer = kernel.get_var("OUTER").await;
5650 assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
5651 }
5652
5653 #[tokio::test]
5654 async fn test_return_from_tool() {
5655 let kernel = Kernel::transient().expect("failed to create kernel");
5656
5657 kernel
5659 .execute(r#"early_return() {
5660 if [[ $1 == 1 ]]; then
5661 return 42
5662 fi
5663 echo "not returned"
5664 }"#)
5665 .await
5666 .expect("function definition failed");
5667
5668 let result = kernel
5671 .execute("early_return 1")
5672 .await
5673 .expect("function call failed");
5674
5675 assert_eq!(result.code, 42);
5677 assert!(result.text_out().is_empty());
5679 }
5680
5681 #[tokio::test]
5682 async fn test_return_without_value() {
5683 let kernel = Kernel::transient().expect("failed to create kernel");
5684
5685 kernel
5687 .execute(r#"early_exit() {
5688 if [[ $1 == "stop" ]]; then
5689 return
5690 fi
5691 echo "continued"
5692 }"#)
5693 .await
5694 .expect("function definition failed");
5695
5696 let result = kernel
5698 .execute(r#"early_exit "stop""#)
5699 .await
5700 .expect("function call failed");
5701
5702 assert!(result.ok());
5703 assert!(result.text_out().is_empty() || result.text_out().trim().is_empty());
5704 }
5705
5706 #[tokio::test]
5707 async fn test_exit_stops_execution() {
5708 let kernel = Kernel::transient().expect("failed to create kernel");
5709
5710 kernel
5712 .execute(r#"
5713 BEFORE="yes"
5714 exit 0
5715 AFTER="yes"
5716 "#)
5717 .await
5718 .expect("execution failed");
5719
5720 let before = kernel.get_var("BEFORE").await;
5722 assert_eq!(before, Some(Value::String("yes".into())));
5723
5724 let after = kernel.get_var("AFTER").await;
5725 assert!(after.is_none(), "AFTER should not be set after exit");
5726 }
5727
5728 #[tokio::test]
5729 async fn test_exit_with_code() {
5730 let kernel = Kernel::transient().expect("failed to create kernel");
5731
5732 let result = kernel
5734 .execute("exit 42")
5735 .await
5736 .expect("exit failed");
5737
5738 assert_eq!(result.code, 42);
5739 assert!(result.text_out().is_empty(), "exit should not produce stdout");
5740 }
5741
5742 #[tokio::test]
5743 async fn test_set_e_stops_on_failure() {
5744 let kernel = Kernel::transient().expect("failed to create kernel");
5745
5746 kernel.execute("set -e").await.expect("set -e failed");
5748
5749 kernel
5751 .execute(r#"
5752 STEP1="done"
5753 false
5754 STEP2="done"
5755 "#)
5756 .await
5757 .expect("execution failed");
5758
5759 let step1 = kernel.get_var("STEP1").await;
5761 assert_eq!(step1, Some(Value::String("done".into())));
5762
5763 let step2 = kernel.get_var("STEP2").await;
5764 assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
5765 }
5766
5767 #[tokio::test]
5768 async fn test_set_plus_e_disables_error_exit() {
5769 let kernel = Kernel::transient().expect("failed to create kernel");
5770
5771 kernel.execute("set -e").await.expect("set -e failed");
5773 kernel.execute("set +e").await.expect("set +e failed");
5774
5775 kernel
5777 .execute(r#"
5778 STEP1="done"
5779 false
5780 STEP2="done"
5781 "#)
5782 .await
5783 .expect("execution failed");
5784
5785 let step1 = kernel.get_var("STEP1").await;
5787 assert_eq!(step1, Some(Value::String("done".into())));
5788
5789 let step2 = kernel.get_var("STEP2").await;
5790 assert_eq!(step2, Some(Value::String("done".into())));
5791 }
5792
5793 #[tokio::test]
5794 async fn test_set_ignores_unknown_options() {
5795 let kernel = Kernel::transient().expect("failed to create kernel");
5796
5797 let result = kernel
5799 .execute("set -e -u -o pipefail")
5800 .await
5801 .expect("set with unknown options failed");
5802
5803 assert!(result.ok(), "set should succeed with unknown options");
5804
5805 kernel
5807 .execute(r#"
5808 BEFORE="yes"
5809 false
5810 AFTER="yes"
5811 "#)
5812 .await
5813 .ok();
5814
5815 let after = kernel.get_var("AFTER").await;
5816 assert!(after.is_none(), "-e should be enabled despite unknown options");
5817 }
5818
5819 #[tokio::test]
5820 async fn test_set_no_args_shows_settings() {
5821 let kernel = Kernel::transient().expect("failed to create kernel");
5822
5823 kernel.execute("set -e").await.expect("set -e failed");
5825
5826 let result = kernel.execute("set").await.expect("set failed");
5828
5829 assert!(result.ok());
5830 assert!(result.text_out().contains("set -e"), "should show -e is enabled: {}", result.text_out());
5831 }
5832
5833 #[tokio::test]
5834 async fn test_set_e_in_pipeline() {
5835 let kernel = Kernel::transient().expect("failed to create kernel");
5836
5837 kernel.execute("set -e").await.expect("set -e failed");
5838
5839 kernel
5841 .execute(r#"
5842 BEFORE="yes"
5843 false | cat
5844 AFTER="yes"
5845 "#)
5846 .await
5847 .ok();
5848
5849 let before = kernel.get_var("BEFORE").await;
5850 assert_eq!(before, Some(Value::String("yes".into())));
5851
5852 }
5857
5858 #[tokio::test]
5859 async fn test_set_e_with_and_chain() {
5860 let kernel = Kernel::transient().expect("failed to create kernel");
5861
5862 kernel.execute("set -e").await.expect("set -e failed");
5863
5864 kernel
5867 .execute(r#"
5868 RESULT="initial"
5869 false && RESULT="chained"
5870 RESULT="continued"
5871 "#)
5872 .await
5873 .ok();
5874
5875 let result = kernel.get_var("RESULT").await;
5878 assert!(result.is_some(), "RESULT should be set");
5881 }
5882
5883 #[tokio::test]
5884 async fn test_set_e_exits_in_for_loop() {
5885 let kernel = Kernel::transient().expect("failed to create kernel");
5886
5887 kernel.execute("set -e").await.expect("set -e failed");
5888
5889 kernel
5890 .execute(r#"
5891 REACHED="no"
5892 for x in 1 2 3; do
5893 false
5894 REACHED="yes"
5895 done
5896 "#)
5897 .await
5898 .ok();
5899
5900 let reached = kernel.get_var("REACHED").await;
5902 assert_eq!(reached, Some(Value::String("no".into())),
5903 "set -e should exit on failure in for loop body");
5904 }
5905
5906 #[tokio::test]
5907 async fn test_for_loop_continues_without_set_e() {
5908 let kernel = Kernel::transient().expect("failed to create kernel");
5909
5910 kernel
5912 .execute(r#"
5913 COUNT=0
5914 for x in 1 2 3; do
5915 false
5916 COUNT=$((COUNT + 1))
5917 done
5918 "#)
5919 .await
5920 .ok();
5921
5922 let count = kernel.get_var("COUNT").await;
5923 let count_val = match &count {
5925 Some(Value::Int(n)) => *n,
5926 Some(Value::String(s)) => s.parse().unwrap_or(-1),
5927 _ => -1,
5928 };
5929 assert_eq!(count_val, 3,
5930 "without set -e, loop should complete all iterations (got {:?})", count);
5931 }
5932
5933 #[tokio::test]
5938 async fn test_source_sets_variables() {
5939 let kernel = Kernel::transient().expect("failed to create kernel");
5940
5941 kernel
5943 .execute(r#"write "/test.kai" 'FOO="bar"'"#)
5944 .await
5945 .expect("write failed");
5946
5947 let result = kernel
5949 .execute(r#"source "/test.kai""#)
5950 .await
5951 .expect("source failed");
5952
5953 assert!(result.ok(), "source should succeed");
5954
5955 let foo = kernel.get_var("FOO").await;
5957 assert_eq!(foo, Some(Value::String("bar".into())));
5958 }
5959
5960 #[tokio::test]
5961 async fn test_source_with_dot_alias() {
5962 let kernel = Kernel::transient().expect("failed to create kernel");
5963
5964 kernel
5966 .execute(r#"write "/vars.kai" 'X=42'"#)
5967 .await
5968 .expect("write failed");
5969
5970 let result = kernel
5972 .execute(r#". "/vars.kai""#)
5973 .await
5974 .expect(". failed");
5975
5976 assert!(result.ok(), ". should succeed");
5977
5978 let x = kernel.get_var("X").await;
5980 assert_eq!(x, Some(Value::Int(42)));
5981 }
5982
5983 #[tokio::test]
5984 async fn test_source_not_found() {
5985 let kernel = Kernel::transient().expect("failed to create kernel");
5986
5987 let result = kernel
5989 .execute(r#"source "/nonexistent.kai""#)
5990 .await
5991 .expect("source should not fail with error");
5992
5993 assert!(!result.ok(), "source of non-existent file should fail");
5994 assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
5995 }
5996
5997 #[tokio::test]
5998 async fn test_source_missing_filename() {
5999 let kernel = Kernel::transient().expect("failed to create kernel");
6000
6001 let result = kernel
6003 .execute("source")
6004 .await
6005 .expect("source should not fail with error");
6006
6007 assert!(!result.ok(), "source without filename should fail");
6008 assert!(result.err.contains("missing filename"), "error should mention missing filename");
6009 }
6010
6011 #[tokio::test]
6012 async fn test_source_executes_multiple_statements() {
6013 let kernel = Kernel::transient().expect("failed to create kernel");
6014
6015 kernel
6017 .execute(r#"write "/multi.kai" 'A=1
6018B=2
6019C=3'"#)
6020 .await
6021 .expect("write failed");
6022
6023 kernel
6025 .execute(r#"source "/multi.kai""#)
6026 .await
6027 .expect("source failed");
6028
6029 assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
6031 assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
6032 assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
6033 }
6034
6035 #[tokio::test]
6036 async fn test_source_can_define_functions() {
6037 let kernel = Kernel::transient().expect("failed to create kernel");
6038
6039 kernel
6041 .execute(r#"write "/functions.kai" 'greet() {
6042 echo "Hello, $1!"
6043}'"#)
6044 .await
6045 .expect("write failed");
6046
6047 kernel
6049 .execute(r#"source "/functions.kai""#)
6050 .await
6051 .expect("source failed");
6052
6053 let result = kernel
6055 .execute(r#"greet "World""#)
6056 .await
6057 .expect("greet failed");
6058
6059 assert!(result.ok());
6060 assert!(result.text_out().contains("Hello, World!"));
6061 }
6062
6063 #[tokio::test]
6064 async fn test_source_inherits_error_exit() {
6065 let kernel = Kernel::transient().expect("failed to create kernel");
6066
6067 kernel.execute("set -e").await.expect("set -e failed");
6069
6070 kernel
6072 .execute(r#"write "/fail.kai" 'BEFORE="yes"
6073false
6074AFTER="yes"'"#)
6075 .await
6076 .expect("write failed");
6077
6078 kernel
6080 .execute(r#"source "/fail.kai""#)
6081 .await
6082 .ok();
6083
6084 let before = kernel.get_var("BEFORE").await;
6086 assert_eq!(before, Some(Value::String("yes".into())));
6087
6088 }
6091
6092 #[tokio::test]
6097 async fn test_set_e_and_chain_left_fails() {
6098 let kernel = Kernel::transient().expect("failed to create kernel");
6100 kernel.execute("set -e").await.expect("set -e failed");
6101
6102 kernel
6103 .execute("false && echo hi; REACHED=1")
6104 .await
6105 .expect("execution failed");
6106
6107 let reached = kernel.get_var("REACHED").await;
6108 assert_eq!(
6109 reached,
6110 Some(Value::Int(1)),
6111 "set -e should not trigger on left side of &&"
6112 );
6113 }
6114
6115 #[tokio::test]
6116 async fn test_set_e_and_chain_right_fails() {
6117 let kernel = Kernel::transient().expect("failed to create kernel");
6119 kernel.execute("set -e").await.expect("set -e failed");
6120
6121 kernel
6122 .execute("true && false; REACHED=1")
6123 .await
6124 .expect("execution failed");
6125
6126 let reached = kernel.get_var("REACHED").await;
6127 assert!(
6128 reached.is_none(),
6129 "set -e should trigger when right side of && fails"
6130 );
6131 }
6132
6133 #[tokio::test]
6134 async fn test_set_e_or_chain_recovers() {
6135 let kernel = Kernel::transient().expect("failed to create kernel");
6137 kernel.execute("set -e").await.expect("set -e failed");
6138
6139 kernel
6140 .execute("false || echo recovered; REACHED=1")
6141 .await
6142 .expect("execution failed");
6143
6144 let reached = kernel.get_var("REACHED").await;
6145 assert_eq!(
6146 reached,
6147 Some(Value::Int(1)),
6148 "set -e should not trigger when || recovers the failure"
6149 );
6150 }
6151
6152 #[tokio::test]
6153 async fn test_set_e_or_chain_both_fail() {
6154 let kernel = Kernel::transient().expect("failed to create kernel");
6156 kernel.execute("set -e").await.expect("set -e failed");
6157
6158 kernel
6159 .execute("false || false; REACHED=1")
6160 .await
6161 .expect("execution failed");
6162
6163 let reached = kernel.get_var("REACHED").await;
6164 assert!(
6165 reached.is_none(),
6166 "set -e should trigger when || chain ultimately fails"
6167 );
6168 }
6169
6170 fn schedule_cancel(kernel: &Arc<Kernel>, delay: std::time::Duration) {
6177 let k = Arc::clone(kernel);
6178 std::thread::spawn(move || {
6179 std::thread::sleep(delay);
6180 k.cancel();
6181 });
6182 }
6183
6184 #[tokio::test]
6185 async fn test_cancel_interrupts_for_loop() {
6186 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6187
6188 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
6190
6191 let result = kernel
6192 .execute("for i in $(seq 1 100000); do X=$i; done")
6193 .await
6194 .expect("execute failed");
6195
6196 assert_eq!(result.code, 130, "cancelled execution should exit with code 130");
6197
6198 let x = kernel.get_var("X").await;
6200 if let Some(Value::Int(n)) = x {
6201 assert!(n < 100000, "loop should have been interrupted before finishing, got X={n}");
6202 }
6203 }
6204
6205 #[tokio::test]
6206 async fn test_cancel_interrupts_while_loop() {
6207 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6208 kernel.execute("COUNT=0").await.expect("init failed");
6209
6210 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
6211
6212 let result = kernel
6213 .execute("while true; do COUNT=$((COUNT + 1)); done")
6214 .await
6215 .expect("execute failed");
6216
6217 assert_eq!(result.code, 130);
6218
6219 let count = kernel.get_var("COUNT").await;
6220 if let Some(Value::Int(n)) = count {
6221 assert!(n > 0, "loop should have run at least once");
6222 }
6223 }
6224
6225 #[tokio::test]
6226 async fn test_reset_after_cancel() {
6227 let kernel = Kernel::transient().expect("failed to create kernel");
6229 kernel.cancel(); let result = kernel.execute("echo hello").await.expect("execute failed");
6232 assert!(result.ok(), "execute after cancel should succeed");
6233 assert_eq!(result.text_out().trim(), "hello");
6234 }
6235
6236 #[tokio::test]
6237 async fn test_cancel_interrupts_statement_sequence() {
6238 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6239
6240 schedule_cancel(&kernel, std::time::Duration::from_millis(50));
6242
6243 let result = kernel
6244 .execute("STEP=1; sleep 5; STEP=2; sleep 5; STEP=3")
6245 .await
6246 .expect("execute failed");
6247
6248 assert_eq!(result.code, 130);
6249
6250 let step = kernel.get_var("STEP").await;
6252 assert_eq!(step, Some(Value::Int(1)), "cancel should stop before STEP=2");
6253 }
6254
6255 #[tokio::test]
6260 async fn test_case_simple_match() {
6261 let kernel = Kernel::transient().expect("failed to create kernel");
6262
6263 let result = kernel
6264 .execute(r#"
6265 case "hello" in
6266 hello) echo "matched hello" ;;
6267 world) echo "matched world" ;;
6268 esac
6269 "#)
6270 .await
6271 .expect("case failed");
6272
6273 assert!(result.ok());
6274 assert_eq!(result.text_out().trim(), "matched hello");
6275 }
6276
6277 #[tokio::test]
6278 async fn test_case_wildcard_match() {
6279 let kernel = Kernel::transient().expect("failed to create kernel");
6280
6281 let result = kernel
6282 .execute(r#"
6283 case "main.rs" in
6284 *.py) echo "Python" ;;
6285 *.rs) echo "Rust" ;;
6286 *) echo "Unknown" ;;
6287 esac
6288 "#)
6289 .await
6290 .expect("case failed");
6291
6292 assert!(result.ok());
6293 assert_eq!(result.text_out().trim(), "Rust");
6294 }
6295
6296 #[tokio::test]
6297 async fn test_case_default_match() {
6298 let kernel = Kernel::transient().expect("failed to create kernel");
6299
6300 let result = kernel
6301 .execute(r#"
6302 case "unknown.xyz" in
6303 *.py) echo "Python" ;;
6304 *.rs) echo "Rust" ;;
6305 *) echo "Default" ;;
6306 esac
6307 "#)
6308 .await
6309 .expect("case failed");
6310
6311 assert!(result.ok());
6312 assert_eq!(result.text_out().trim(), "Default");
6313 }
6314
6315 #[tokio::test]
6316 async fn test_case_no_match() {
6317 let kernel = Kernel::transient().expect("failed to create kernel");
6318
6319 let result = kernel
6321 .execute(r#"
6322 case "nope" in
6323 "yes") echo "yes" ;;
6324 "no") echo "no" ;;
6325 esac
6326 "#)
6327 .await
6328 .expect("case failed");
6329
6330 assert!(result.ok());
6331 assert!(result.text_out().is_empty(), "no match should produce empty output");
6332 }
6333
6334 #[tokio::test]
6335 async fn test_case_with_variable() {
6336 let kernel = Kernel::transient().expect("failed to create kernel");
6337
6338 kernel.execute(r#"LANG="rust""#).await.expect("set failed");
6339
6340 let result = kernel
6341 .execute(r#"
6342 case ${LANG} in
6343 python) echo "snake" ;;
6344 rust) echo "crab" ;;
6345 go) echo "gopher" ;;
6346 esac
6347 "#)
6348 .await
6349 .expect("case failed");
6350
6351 assert!(result.ok());
6352 assert_eq!(result.text_out().trim(), "crab");
6353 }
6354
6355 #[tokio::test]
6356 async fn test_case_multiple_patterns() {
6357 let kernel = Kernel::transient().expect("failed to create kernel");
6358
6359 let result = kernel
6360 .execute(r#"
6361 case "yes" in
6362 "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
6363 "n"|"no"|"N"|"NO") echo "negative" ;;
6364 esac
6365 "#)
6366 .await
6367 .expect("case failed");
6368
6369 assert!(result.ok());
6370 assert_eq!(result.text_out().trim(), "affirmative");
6371 }
6372
6373 #[tokio::test]
6374 async fn test_case_glob_question_mark() {
6375 let kernel = Kernel::transient().expect("failed to create kernel");
6376
6377 let result = kernel
6378 .execute(r#"
6379 case "test1" in
6380 test?) echo "matched test?" ;;
6381 *) echo "default" ;;
6382 esac
6383 "#)
6384 .await
6385 .expect("case failed");
6386
6387 assert!(result.ok());
6388 assert_eq!(result.text_out().trim(), "matched test?");
6389 }
6390
6391 #[tokio::test]
6392 async fn test_case_char_class() {
6393 let kernel = Kernel::transient().expect("failed to create kernel");
6394
6395 let result = kernel
6396 .execute(r#"
6397 case "Yes" in
6398 [Yy]*) echo "yes-like" ;;
6399 [Nn]*) echo "no-like" ;;
6400 esac
6401 "#)
6402 .await
6403 .expect("case failed");
6404
6405 assert!(result.ok());
6406 assert_eq!(result.text_out().trim(), "yes-like");
6407 }
6408
6409 #[tokio::test]
6414 async fn test_cat_from_pipeline() {
6415 let kernel = Kernel::transient().expect("failed to create kernel");
6416
6417 let result = kernel
6418 .execute(r#"echo "piped text" | cat"#)
6419 .await
6420 .expect("cat pipeline failed");
6421
6422 assert!(result.ok(), "cat failed: {}", result.err);
6423 assert_eq!(result.text_out().trim(), "piped text");
6424 }
6425
6426 #[tokio::test]
6427 async fn test_cat_from_pipeline_multiline() {
6428 let kernel = Kernel::transient().expect("failed to create kernel");
6429
6430 let result = kernel
6431 .execute(r#"echo "line1\nline2" | cat -n"#)
6432 .await
6433 .expect("cat pipeline failed");
6434
6435 assert!(result.ok(), "cat failed: {}", result.err);
6436 assert!(result.text_out().contains("1\t"), "output: {}", result.text_out());
6437 }
6438
6439 #[tokio::test]
6444 async fn test_heredoc_basic() {
6445 let kernel = Kernel::transient().expect("failed to create kernel");
6446
6447 let result = kernel
6448 .execute("cat <<EOF\nhello\nEOF")
6449 .await
6450 .expect("heredoc failed");
6451
6452 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
6453 assert_eq!(result.text_out().trim(), "hello");
6454 }
6455
6456 #[tokio::test]
6457 async fn test_arithmetic_in_string() {
6458 let kernel = Kernel::transient().expect("failed to create kernel");
6459
6460 let result = kernel
6461 .execute(r#"echo "result: $((1 + 2))""#)
6462 .await
6463 .expect("arithmetic in string failed");
6464
6465 assert!(result.ok(), "echo failed: {}", result.err);
6466 assert_eq!(result.text_out().trim(), "result: 3");
6467 }
6468
6469 #[tokio::test]
6470 async fn test_heredoc_multiline() {
6471 let kernel = Kernel::transient().expect("failed to create kernel");
6472
6473 let result = kernel
6474 .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
6475 .await
6476 .expect("heredoc failed");
6477
6478 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
6479 assert!(result.text_out().contains("line1"), "output: {}", result.text_out());
6480 assert!(result.text_out().contains("line2"), "output: {}", result.text_out());
6481 assert!(result.text_out().contains("line3"), "output: {}", result.text_out());
6482 }
6483
6484 #[tokio::test]
6485 async fn test_heredoc_variable_expansion() {
6486 let kernel = Kernel::transient().expect("failed to create kernel");
6488
6489 kernel.execute("GREETING=hello").await.expect("set var");
6490
6491 let result = kernel
6492 .execute("cat <<EOF\n$GREETING world\nEOF")
6493 .await
6494 .expect("heredoc expansion failed");
6495
6496 assert!(result.ok(), "heredoc expansion failed: {}", result.err);
6497 assert_eq!(result.text_out().trim(), "hello world");
6498 }
6499
6500 #[tokio::test]
6501 async fn test_heredoc_quoted_no_expansion() {
6502 let kernel = Kernel::transient().expect("failed to create kernel");
6504
6505 kernel.execute("GREETING=hello").await.expect("set var");
6506
6507 let result = kernel
6508 .execute("cat <<'EOF'\n$GREETING world\nEOF")
6509 .await
6510 .expect("quoted heredoc failed");
6511
6512 assert!(result.ok(), "quoted heredoc failed: {}", result.err);
6513 assert_eq!(result.text_out().trim(), "$GREETING world");
6514 }
6515
6516 #[tokio::test]
6517 async fn test_heredoc_default_value_expansion() {
6518 let kernel = Kernel::transient().expect("failed to create kernel");
6520
6521 let result = kernel
6522 .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
6523 .await
6524 .expect("heredoc default expansion failed");
6525
6526 assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
6527 assert_eq!(result.text_out().trim(), "fallback");
6528 }
6529
6530 #[tokio::test]
6535 async fn test_read_from_pipeline() {
6536 let kernel = Kernel::transient().expect("failed to create kernel");
6537
6538 let result = kernel
6540 .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
6541 .await
6542 .expect("read pipeline failed");
6543
6544 assert!(result.ok(), "read failed: {}", result.err);
6545 assert!(result.text_out().contains("Hello, Alice"), "output: {}", result.text_out());
6546 }
6547
6548 #[tokio::test]
6549 async fn test_read_multiple_vars_from_pipeline() {
6550 let kernel = Kernel::transient().expect("failed to create kernel");
6551
6552 let result = kernel
6553 .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
6554 .await
6555 .expect("read pipeline failed");
6556
6557 assert!(result.ok(), "read failed: {}", result.err);
6558 assert!(result.text_out().contains("John is 42"), "output: {}", result.text_out());
6559 }
6560
6561 #[tokio::test]
6566 async fn test_posix_function_with_positional_params() {
6567 let kernel = Kernel::transient().expect("failed to create kernel");
6568
6569 kernel
6571 .execute(r#"greet() { echo "Hello, $1!" }"#)
6572 .await
6573 .expect("function definition failed");
6574
6575 let result = kernel
6577 .execute(r#"greet "Amy""#)
6578 .await
6579 .expect("function call failed");
6580
6581 assert!(result.ok(), "greet failed: {}", result.err);
6582 assert_eq!(result.text_out().trim(), "Hello, Amy!");
6583 }
6584
6585 #[tokio::test]
6586 async fn test_posix_function_multiple_args() {
6587 let kernel = Kernel::transient().expect("failed to create kernel");
6588
6589 kernel
6591 .execute(r#"add_greeting() { echo "$1 $2!" }"#)
6592 .await
6593 .expect("function definition failed");
6594
6595 let result = kernel
6597 .execute(r#"add_greeting "Hello" "World""#)
6598 .await
6599 .expect("function call failed");
6600
6601 assert!(result.ok(), "function failed: {}", result.err);
6602 assert_eq!(result.text_out().trim(), "Hello World!");
6603 }
6604
6605 #[tokio::test]
6606 async fn test_bash_function_with_positional_params() {
6607 let kernel = Kernel::transient().expect("failed to create kernel");
6608
6609 kernel
6611 .execute(r#"function greet { echo "Hi $1" }"#)
6612 .await
6613 .expect("function definition failed");
6614
6615 let result = kernel
6617 .execute(r#"greet "Bob""#)
6618 .await
6619 .expect("function call failed");
6620
6621 assert!(result.ok(), "greet failed: {}", result.err);
6622 assert_eq!(result.text_out().trim(), "Hi Bob");
6623 }
6624
6625 #[tokio::test]
6626 async fn test_shell_function_with_all_args() {
6627 let kernel = Kernel::transient().expect("failed to create kernel");
6628
6629 kernel
6631 .execute(r#"echo_all() { echo "args: $@" }"#)
6632 .await
6633 .expect("function definition failed");
6634
6635 let result = kernel
6637 .execute(r#"echo_all "a" "b" "c""#)
6638 .await
6639 .expect("function call failed");
6640
6641 assert!(result.ok(), "function failed: {}", result.err);
6642 assert_eq!(result.text_out().trim(), "args: a b c");
6643 }
6644
6645 #[tokio::test]
6646 async fn test_shell_function_with_arg_count() {
6647 let kernel = Kernel::transient().expect("failed to create kernel");
6648
6649 kernel
6651 .execute(r#"count_args() { echo "count: $#" }"#)
6652 .await
6653 .expect("function definition failed");
6654
6655 let result = kernel
6657 .execute(r#"count_args "x" "y" "z""#)
6658 .await
6659 .expect("function call failed");
6660
6661 assert!(result.ok(), "function failed: {}", result.err);
6662 assert_eq!(result.text_out().trim(), "count: 3");
6663 }
6664
6665 #[tokio::test]
6666 async fn test_shell_function_shared_scope() {
6667 let kernel = Kernel::transient().expect("failed to create kernel");
6668
6669 kernel
6671 .execute(r#"PARENT_VAR="visible""#)
6672 .await
6673 .expect("set failed");
6674
6675 kernel
6677 .execute(r#"modify_parent() {
6678 echo "saw: ${PARENT_VAR}"
6679 PARENT_VAR="changed by function"
6680 }"#)
6681 .await
6682 .expect("function definition failed");
6683
6684 let result = kernel.execute("modify_parent").await.expect("function failed");
6686
6687 assert!(
6688 result.text_out().contains("visible"),
6689 "Shell function should access parent scope, got: {}",
6690 result.text_out()
6691 );
6692
6693 let var = kernel.get_var("PARENT_VAR").await;
6695 assert_eq!(
6696 var,
6697 Some(Value::String("changed by function".into())),
6698 "Shell function should modify parent scope"
6699 );
6700 }
6701
6702 #[tokio::test]
6707 async fn test_script_execution_from_path() {
6708 let kernel = Kernel::transient().expect("failed to create kernel");
6709
6710 kernel.execute(r#"mkdir "/bin""#).await.ok();
6712 kernel
6713 .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
6714 .await
6715 .expect("write script failed");
6716
6717 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
6719
6720 let result = kernel
6722 .execute("hello")
6723 .await
6724 .expect("script execution failed");
6725
6726 assert!(result.ok(), "script failed: {}", result.err);
6727 assert_eq!(result.text_out().trim(), "Hello from script!");
6728 }
6729
6730 #[tokio::test]
6731 async fn test_script_with_args() {
6732 let kernel = Kernel::transient().expect("failed to create kernel");
6733
6734 kernel.execute(r#"mkdir "/bin""#).await.ok();
6736 kernel
6737 .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
6738 .await
6739 .expect("write script failed");
6740
6741 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
6743
6744 let result = kernel
6746 .execute(r#"greet "World""#)
6747 .await
6748 .expect("script execution failed");
6749
6750 assert!(result.ok(), "script failed: {}", result.err);
6751 assert_eq!(result.text_out().trim(), "Hello, World!");
6752 }
6753
6754 #[tokio::test]
6755 async fn test_script_not_found() {
6756 let kernel = Kernel::transient().expect("failed to create kernel");
6757
6758 kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
6760
6761 let result = kernel
6763 .execute("noscript")
6764 .await
6765 .expect("execution failed");
6766
6767 assert!(!result.ok(), "should fail with command not found");
6768 assert_eq!(result.code, 127);
6769 assert!(result.err.contains("command not found"));
6770 }
6771
6772 #[tokio::test]
6773 async fn test_script_path_search_order() {
6774 let kernel = Kernel::transient().expect("failed to create kernel");
6775
6776 kernel.execute(r#"mkdir "/first""#).await.ok();
6779 kernel.execute(r#"mkdir "/second""#).await.ok();
6780 kernel
6781 .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
6782 .await
6783 .expect("write failed");
6784 kernel
6785 .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
6786 .await
6787 .expect("write failed");
6788
6789 kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
6791
6792 let result = kernel
6794 .execute("myscript")
6795 .await
6796 .expect("script execution failed");
6797
6798 assert!(result.ok(), "script failed: {}", result.err);
6799 assert_eq!(result.text_out().trim(), "from first");
6800 }
6801
6802 #[tokio::test]
6807 async fn test_last_exit_code_success() {
6808 let kernel = Kernel::transient().expect("failed to create kernel");
6809
6810 let result = kernel.execute("true; echo $?").await.expect("execution failed");
6812 assert!(result.text_out().contains("0"), "expected 0, got: {}", result.text_out());
6813 }
6814
6815 #[tokio::test]
6816 async fn test_last_exit_code_failure() {
6817 let kernel = Kernel::transient().expect("failed to create kernel");
6818
6819 let result = kernel.execute("false; echo $?").await.expect("execution failed");
6821 assert!(result.text_out().contains("1"), "expected 1, got: {}", result.text_out());
6822 }
6823
6824 #[tokio::test]
6825 async fn test_current_pid() {
6826 let kernel = Kernel::transient().expect("failed to create kernel");
6827
6828 let result = kernel.execute("echo $$").await.expect("execution failed");
6829 let pid: u32 = result.text_out().trim().parse().expect("PID should be a number");
6831 assert!(pid > 0, "PID should be positive");
6832 }
6833
6834 #[tokio::test]
6835 async fn test_unset_variable_expands_to_empty() {
6836 let kernel = Kernel::transient().expect("failed to create kernel");
6837
6838 let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
6840 assert_eq!(result.text_out().trim(), "prefix::suffix");
6841 }
6842
6843 #[tokio::test]
6844 async fn test_eq_ne_operators() {
6845 let kernel = Kernel::transient().expect("failed to create kernel");
6846
6847 let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
6849 assert_eq!(result.text_out().trim(), "eq works");
6850
6851 let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
6853 assert_eq!(result.text_out().trim(), "ne works");
6854
6855 let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
6857 assert_eq!(result.text_out().trim(), "correct");
6858 }
6859
6860 #[tokio::test]
6861 async fn test_escaped_dollar_in_string() {
6862 let kernel = Kernel::transient().expect("failed to create kernel");
6863
6864 let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
6866 assert_eq!(result.text_out().trim(), "$100");
6867 }
6868
6869 #[tokio::test]
6870 async fn test_special_vars_in_interpolation() {
6871 let kernel = Kernel::transient().expect("failed to create kernel");
6872
6873 let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
6875 assert_eq!(result.text_out().trim(), "exit: 0");
6876
6877 let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
6879 assert!(result.text_out().starts_with("pid: "), "unexpected output: {}", result.text_out());
6880 let text = result.text_out();
6881 let pid_part = text.trim().strip_prefix("pid: ").unwrap();
6882 let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
6883 }
6884
6885 #[tokio::test]
6890 async fn test_command_subst_assignment() {
6891 let kernel = Kernel::transient().expect("failed to create kernel");
6892
6893 let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
6895 assert_eq!(result.text_out().trim(), "hello");
6896 }
6897
6898 #[tokio::test]
6899 async fn test_command_subst_with_args() {
6900 let kernel = Kernel::transient().expect("failed to create kernel");
6901
6902 let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
6904 assert_eq!(result.text_out().trim(), "a b c");
6905 }
6906
6907 #[tokio::test]
6908 async fn test_command_subst_nested_vars() {
6909 let kernel = Kernel::transient().expect("failed to create kernel");
6910
6911 let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
6913 assert_eq!(result.text_out().trim(), "hello world");
6914 }
6915
6916 #[tokio::test]
6917 async fn test_background_job_basic() {
6918 use std::time::Duration;
6919
6920 let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
6921
6922 let result = kernel.execute("echo hello &").await.expect("execution failed");
6924 assert!(result.ok(), "background command should succeed: {}", result.err);
6925 assert!(result.text_out().contains("[1]"), "should return job ID: {}", result.text_out());
6926
6927 tokio::time::sleep(Duration::from_millis(100)).await;
6929
6930 let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
6932 assert!(status.ok(), "status should succeed: {}", status.err);
6933 assert!(
6934 status.text_out().contains("done:") || status.text_out().contains("running"),
6935 "should have valid status: {}",
6936 status.text_out()
6937 );
6938
6939 let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
6941 assert!(stdout.ok());
6942 assert!(stdout.text_out().contains("hello"));
6943 }
6944
6945 #[tokio::test]
6946 async fn test_heredoc_piped_to_command() {
6947 let kernel = Kernel::transient().expect("kernel");
6949 let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
6950 assert!(result.ok(), "heredoc | cat failed: {}", result.err);
6951 assert_eq!(result.text_out().trim(), "hello world");
6952 }
6953
6954 fn transient_with_tempdir() -> (Kernel, tempfile::TempDir, String) {
6962 let kernel = Kernel::transient().expect("kernel");
6963 let tmp = tempfile::tempdir().expect("tempdir");
6964 let dir = tmp.path().display().to_string();
6965 (kernel, tmp, dir)
6966 }
6967
6968 #[tokio::test]
6969 async fn test_for_loop_glob_iterates() {
6970 let (kernel, _tmp, dir) = transient_with_tempdir();
6972 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
6973 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
6974 let result = kernel.execute(&format!(r#"
6975 N=0
6976 for F in $(glob "{dir}/*.txt"); do
6977 N=$((N + 1))
6978 done
6979 echo $N
6980 "#)).await.unwrap();
6981 assert!(result.ok(), "for glob failed: {}", result.err);
6982 assert_eq!(result.text_out().trim(), "2", "Should iterate 2 files, got: {}", result.text_out());
6983 }
6984
6985 #[tokio::test]
6986 async fn test_bare_glob_expansion_echo() {
6987 let (kernel, _tmp, dir) = transient_with_tempdir();
6988 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
6989 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
6990 kernel.execute(&format!("echo c > {dir}/c.rs")).await.unwrap();
6991 kernel.execute(&format!("cd {dir}")).await.unwrap();
6992 let result = kernel.execute("echo *.txt").await.unwrap();
6993 assert!(result.ok(), "echo *.txt failed: {}", result.err);
6994 let out = result.text_out();
6995 let out = out.trim();
6996 assert!(out.contains("a.txt"), "missing a.txt in: {}", out);
6998 assert!(out.contains("b.txt"), "missing b.txt in: {}", out);
6999 assert!(!out.contains("c.rs"), "should not contain c.rs in: {}", out);
7000 }
7001
7002 #[tokio::test]
7003 async fn test_bare_glob_no_matches_errors() {
7004 let (kernel, _tmp, dir) = transient_with_tempdir();
7005 kernel.execute(&format!("cd {dir}")).await.unwrap();
7006 let result = kernel.execute("echo *.nonexistent").await;
7007 match &result {
7008 Ok(exec) => {
7009 assert!(!exec.ok(), "expected failure, got success: out={}, err={}", exec.text_out(), exec.err);
7011 assert!(exec.err.contains("no matches"), "error should say no matches: {}", exec.err);
7012 }
7013 Err(e) => {
7014 assert!(e.to_string().contains("no matches"), "error should say no matches: {}", e);
7015 }
7016 }
7017 }
7018
7019 #[tokio::test]
7020 async fn test_bare_glob_disabled_with_set() {
7021 let (kernel, _tmp, dir) = transient_with_tempdir();
7022 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7023 kernel.execute(&format!("cd {dir}")).await.unwrap();
7024 kernel.execute("set +o glob").await.unwrap();
7026 let result = kernel.execute("echo *.txt").await.unwrap();
7027 assert!(result.ok(), "echo should succeed: {}", result.err);
7029 assert_eq!(result.text_out().trim(), "*.txt", "should be literal: {}", result.text_out());
7030 }
7031
7032 #[tokio::test]
7033 async fn test_bare_glob_quoted_not_expanded() {
7034 let (kernel, _tmp, dir) = transient_with_tempdir();
7035 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7036 kernel.execute(&format!("cd {dir}")).await.unwrap();
7037 let result = kernel.execute("echo \"*.txt\"").await.unwrap();
7039 assert!(result.ok(), "echo should succeed: {}", result.err);
7040 assert_eq!(result.text_out().trim(), "*.txt", "quoted should be literal: {}", result.text_out());
7041 }
7042
7043 #[tokio::test]
7044 async fn test_bare_glob_for_loop() {
7045 let (kernel, _tmp, dir) = transient_with_tempdir();
7046 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7047 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
7048 kernel.execute(&format!("cd {dir}")).await.unwrap();
7049 let result = kernel.execute(r#"
7050 N=0
7051 for f in *.txt; do
7052 N=$((N + 1))
7053 done
7054 echo $N
7055 "#).await.unwrap();
7056 assert!(result.ok(), "for loop failed: {}", result.err);
7057 assert_eq!(result.text_out().trim(), "2", "should iterate 2 files: {}", result.text_out());
7058 }
7059
7060 #[tokio::test]
7061 async fn test_glob_in_assignment_is_literal() {
7062 let kernel = Kernel::transient().expect("kernel");
7063 let result = kernel.execute("X=*.txt; echo $X").await.unwrap();
7064 assert!(result.ok());
7065 assert_eq!(result.text_out().trim(), "*.txt", "glob in assignment should be literal");
7066 }
7067
7068 #[tokio::test]
7069 async fn test_glob_in_test_expr_is_literal() {
7070 let kernel = Kernel::transient().expect("kernel");
7071 let result = kernel.execute(r#"
7072 if [[ *.txt == "*.txt" ]]; then
7073 echo "match"
7074 else
7075 echo "no"
7076 fi
7077 "#).await.unwrap();
7078 assert!(result.ok());
7079 assert_eq!(result.text_out().trim(), "match", "glob in test expr should be literal");
7080 }
7081
7082 #[tokio::test]
7083 async fn test_command_subst_echo_not_iterable() {
7084 let kernel = Kernel::transient().expect("kernel");
7086 let result = kernel.execute(r#"
7087 N=0
7088 for X in $(echo "a b c"); do N=$((N + 1)); done
7089 echo $N
7090 "#).await.unwrap();
7091 assert!(result.ok());
7092 assert_eq!(result.text_out().trim(), "1", "echo should be one item: {}", result.text_out());
7093 }
7094
7095 #[test]
7098 fn test_accumulate_preserves_own_newlines() {
7099 let mut acc = ExecResult::success("line1\n");
7102 let new = ExecResult::success("line2\n");
7103 accumulate_result(&mut acc, &new);
7104 assert_eq!(&*acc.text_out(), "line1\nline2\n");
7105 assert!(!acc.text_out().contains("\n\n"), "should not have double newlines: {:?}", acc.text_out());
7106 }
7107
7108 #[test]
7109 fn test_accumulate_inserts_no_separator() {
7110 let mut acc = ExecResult::success("line1");
7113 let new = ExecResult::success("line2");
7114 accumulate_result(&mut acc, &new);
7115 assert_eq!(&*acc.text_out(), "line1line2");
7116 }
7117
7118 #[test]
7119 fn test_accumulate_empty_into_nonempty() {
7120 let mut acc = ExecResult::success("");
7121 let new = ExecResult::success("hello\n");
7122 accumulate_result(&mut acc, &new);
7123 assert_eq!(&*acc.text_out(), "hello\n");
7124 }
7125
7126 #[test]
7127 fn test_accumulate_nonempty_into_empty() {
7128 let mut acc = ExecResult::success("hello\n");
7129 let new = ExecResult::success("");
7130 accumulate_result(&mut acc, &new);
7131 assert_eq!(&*acc.text_out(), "hello\n");
7132 }
7133
7134 #[test]
7135 fn test_accumulate_stderr_no_double_newlines() {
7136 let mut acc = ExecResult::failure(1, "err1\n");
7137 let new = ExecResult::failure(1, "err2\n");
7138 accumulate_result(&mut acc, &new);
7139 assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
7140 }
7141
7142 #[tokio::test]
7143 async fn test_multiple_echo_no_blank_lines() {
7144 let kernel = Kernel::transient().expect("kernel");
7145 let result = kernel
7146 .execute("echo one\necho two\necho three")
7147 .await
7148 .expect("execution failed");
7149 assert!(result.ok());
7150 assert_eq!(&*result.text_out(), "one\ntwo\nthree\n");
7151 }
7152
7153 #[tokio::test]
7154 async fn test_for_loop_no_blank_lines() {
7155 let kernel = Kernel::transient().expect("kernel");
7156 let result = kernel
7157 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
7158 .await
7159 .expect("execution failed");
7160 assert!(result.ok());
7161 assert_eq!(&*result.text_out(), "item: a\nitem: b\nitem: c\n");
7162 }
7163
7164 #[tokio::test]
7165 async fn test_for_command_subst_no_blank_lines() {
7166 let kernel = Kernel::transient().expect("kernel");
7167 let result = kernel
7168 .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
7169 .await
7170 .expect("execution failed");
7171 assert!(result.ok());
7172 assert_eq!(&*result.text_out(), "n=1\nn=2\nn=3\n");
7173 }
7174
7175 fn multi_consume_schema() -> crate::tools::ToolSchema {
7183 use crate::tools::{ParamSchema, ToolSchema};
7184 ToolSchema::new("test", "multi-consume smoke")
7185 .param(
7186 ParamSchema::optional("pair", "array", Value::Null, "name+value pair")
7187 .consumes(2),
7188 )
7189 }
7190
7191 fn pos(s: &str) -> Arg {
7192 Arg::Positional(Expr::Literal(Value::String(s.to_string())))
7193 }
7194
7195 #[tokio::test]
7196 async fn build_args_multi_consume_single_occurrence() {
7197 let kernel = Kernel::transient().expect("kernel");
7198 let schema = multi_consume_schema();
7199 let args = vec![
7201 Arg::LongFlag("pair".into()),
7202 pos("NAME"),
7203 pos("VALUE"),
7204 pos("filter"),
7205 ];
7206 let built = kernel
7207 .build_args_async(&args, Some(&schema))
7208 .await
7209 .expect("build_args should succeed");
7210
7211 let pair = built.named.get("pair").expect("named[pair] missing");
7214 match pair {
7215 Value::Json(serde_json::Value::Array(occurrences)) => {
7216 assert_eq!(occurrences.len(), 1, "expected one occurrence");
7217 match &occurrences[0] {
7218 serde_json::Value::Array(values) => {
7219 assert_eq!(values.len(), 2, "pair must have 2 values");
7220 assert_eq!(values[0], serde_json::Value::String("NAME".into()));
7221 assert_eq!(values[1], serde_json::Value::String("VALUE".into()));
7222 }
7223 other => panic!("expected inner array, got {other:?}"),
7224 }
7225 }
7226 other => panic!("expected Json(Array(...)) for named[pair], got {other:?}"),
7227 }
7228
7229 assert_eq!(built.positional.len(), 1);
7231 assert_eq!(built.positional[0], Value::String("filter".into()));
7232 }
7233 #[tokio::test]
7234 async fn build_args_multi_consume_two_occurrences_accumulate() {
7235 let kernel = Kernel::transient().expect("kernel");
7236 let schema = multi_consume_schema();
7237 let args = vec![
7239 Arg::LongFlag("pair".into()),
7240 pos("A"),
7241 pos("1"),
7242 Arg::LongFlag("pair".into()),
7243 pos("B"),
7244 pos("2"),
7245 pos("filter"),
7246 ];
7247 let built = kernel
7248 .build_args_async(&args, Some(&schema))
7249 .await
7250 .expect("build_args should succeed");
7251
7252 let pair = built.named.get("pair").expect("named[pair] missing");
7253 match pair {
7254 Value::Json(serde_json::Value::Array(occurrences)) => {
7255 assert_eq!(occurrences.len(), 2, "expected two occurrences");
7256 match &occurrences[0] {
7258 serde_json::Value::Array(values) => {
7259 assert_eq!(values[0], serde_json::Value::String("A".into()));
7260 assert_eq!(values[1], serde_json::Value::String("1".into()));
7261 }
7262 other => panic!("expected inner array, got {other:?}"),
7263 }
7264 match &occurrences[1] {
7265 serde_json::Value::Array(values) => {
7266 assert_eq!(values[0], serde_json::Value::String("B".into()));
7267 assert_eq!(values[1], serde_json::Value::String("2".into()));
7268 }
7269 other => panic!("expected inner array, got {other:?}"),
7270 }
7271 }
7272 other => panic!("expected Json(Array(...)), got {other:?}"),
7273 }
7274 }
7275
7276 use crate::tools::{ParamSchema, ToolSchema};
7284
7285 fn kj_like_schema() -> ToolSchema {
7288 ToolSchema::new("kj", "incomplete backend schema")
7289 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
7290 .with_positional_mapping()
7291 }
7292
7293 #[tokio::test]
7294 async fn build_args_undeclared_space_flag_errors_under_map_positionals() {
7295 let kernel = Kernel::transient().expect("kernel");
7296 let schema = kj_like_schema();
7297 let args = vec![
7299 pos("context"),
7300 pos("create"),
7301 pos("exp"),
7302 Arg::LongFlag("type".into()),
7303 pos("explorer"),
7304 ];
7305 let err = kernel
7306 .build_args_async(&args, Some(&schema))
7307 .await
7308 .expect_err("undeclared --type with a space value must fail loud");
7309 let msg = err.to_string();
7310 assert!(msg.contains("--type"), "message should name the flag: {msg}");
7311 assert!(msg.contains("--type=explorer"), "message should suggest the = form: {msg}");
7312 assert!(msg.contains("kj"), "message should name the tool: {msg}");
7313 }
7314
7315 #[tokio::test]
7316 async fn build_args_declared_space_flag_still_binds() {
7317 let kernel = Kernel::transient().expect("kernel");
7318 let schema = ToolSchema::new("kj", "complete schema")
7320 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
7321 .param(ParamSchema::optional("type", "string", Value::Null, "role type"))
7322 .with_positional_mapping();
7323 let args = vec![
7324 pos("exp"),
7325 Arg::LongFlag("type".into()),
7326 pos("explorer"),
7327 ];
7328 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7329 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7330 }
7331
7332 #[tokio::test]
7333 async fn build_args_equals_form_binds_for_undeclared_flag() {
7334 let kernel = Kernel::transient().expect("kernel");
7335 let schema = kj_like_schema();
7336 let args = vec![
7338 pos("exp"),
7339 Arg::Named { key: "type".into(), value: Expr::Literal(Value::String("explorer".into())) },
7340 ];
7341 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7342 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7343 }
7344
7345 #[tokio::test]
7346 async fn build_args_undeclared_bool_flag_at_end_is_ok() {
7347 let kernel = Kernel::transient().expect("kernel");
7348 let schema = kj_like_schema();
7349 let args = vec![pos("exp"), Arg::LongFlag("force".into())];
7351 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7352 assert!(built.flags.contains("force"));
7353 }
7354
7355 #[tokio::test]
7356 async fn build_args_undeclared_flag_before_another_flag_is_ok() {
7357 let kernel = Kernel::transient().expect("kernel");
7358 let schema = kj_like_schema();
7359 let args = vec![
7361 Arg::LongFlag("verbose".into()),
7362 Arg::Named { key: "name".into(), value: Expr::Literal(Value::String("x".into())) },
7363 ];
7364 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7365 assert!(built.flags.contains("verbose"));
7366 }
7367
7368 #[tokio::test]
7369 async fn build_args_undeclared_space_flag_ok_for_builtin_schema() {
7370 let kernel = Kernel::transient().expect("kernel");
7371 let schema = ToolSchema::new("frobnicate", "builtin-style")
7374 .param(ParamSchema::optional("name", "string", Value::Null, "name"));
7375 let args = vec![Arg::LongFlag("frob".into()), pos("value")];
7376 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7377 assert!(built.flags.contains("frob"));
7378 }
7379
7380 fn kj_tree_schema() -> ToolSchema {
7390 ToolSchema::new("kj", "subcommand tool").subcommand(
7391 ToolSchema::new("context", "context ops")
7392 .with_command_aliases(["ctx"])
7393 .subcommand(
7394 ToolSchema::new("create", "create context")
7395 .param(ParamSchema::new("type", "string").with_aliases(["t"]))
7396 .param(ParamSchema::new("force", "bool")),
7397 ),
7398 )
7399 }
7400
7401 #[tokio::test]
7402 async fn build_args_binds_deep_leaf_value_flag_space_form() {
7403 let kernel = Kernel::transient().expect("kernel");
7404 let schema = kj_tree_schema();
7405 let args = vec![
7407 pos("context"),
7408 pos("create"),
7409 Arg::LongFlag("type".into()),
7410 pos("explorer"),
7411 ];
7412 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7413 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7415 let positionals: Vec<&str> = built
7417 .positional
7418 .iter()
7419 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
7420 .collect();
7421 assert_eq!(positionals, vec!["context", "create"]);
7422 }
7423
7424 #[tokio::test]
7425 async fn build_args_leaf_bool_flag_does_not_swallow_positional() {
7426 let kernel = Kernel::transient().expect("kernel");
7427 let schema = kj_tree_schema();
7428 let args = vec![
7431 pos("context"),
7432 pos("create"),
7433 Arg::LongFlag("force".into()),
7434 pos("somearg"),
7435 ];
7436 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7437 assert!(built.flags.contains("force"), "force should be a bare flag");
7438 let positionals: Vec<&str> = built
7439 .positional
7440 .iter()
7441 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
7442 .collect();
7443 assert_eq!(positionals, vec!["context", "create", "somearg"]);
7444 }
7445
7446 #[tokio::test]
7447 async fn build_args_alias_routed_leaf_binds_value_flag() {
7448 let kernel = Kernel::transient().expect("kernel");
7449 let schema = kj_tree_schema();
7450 let args = vec![
7452 pos("ctx"),
7453 pos("create"),
7454 Arg::ShortFlag("t".into()),
7455 pos("explorer"),
7456 ];
7457 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7458 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7459 }
7460
7461 #[tokio::test]
7462 async fn build_args_computed_subcommand_selector_fails_loud() {
7463 let kernel = Kernel::transient().expect("kernel");
7464 let schema = kj_tree_schema();
7465 let args = vec![Arg::Positional(Expr::CommandSubst(vec![Stmt::Command(
7467 crate::ast::Command { name: "echo".into(), args: vec![], redirects: vec![] },
7468 )]))];
7469 let err = kernel
7470 .build_args_async(&args, Some(&schema))
7471 .await
7472 .expect_err("computed subcommand selector must error");
7473 assert!(
7474 err.to_string().contains("subcommand name is required"),
7475 "got: {err}"
7476 );
7477 }
7478
7479 #[test]
7482 fn finalize_output_renders_when_kernel_owns_it() {
7483 use crate::interpreter::{OutputData, OutputFormat};
7484 let r = ExecResult::with_output(OutputData::text("RAW"));
7485 let out = finalize_output(r, Some(OutputFormat::Json), false);
7486 assert_ne!(out.text_out(), "RAW", "kernel should reformat to JSON");
7488 }
7489
7490 #[test]
7491 fn finalize_output_skips_when_tool_owns_output() {
7492 use crate::interpreter::{OutputData, OutputFormat};
7493 let r = ExecResult::with_output(OutputData::text("RAW"));
7494 let out = finalize_output(r, Some(OutputFormat::Json), true);
7495 assert_eq!(out.text_out(), "RAW", "owned output must be left as-is");
7497 }
7498
7499 #[test]
7500 fn finalize_output_no_format_is_noop() {
7501 use crate::interpreter::OutputData;
7502 let r = ExecResult::with_output(OutputData::text("RAW"));
7503 let out = finalize_output(r, None, false);
7504 assert_eq!(out.text_out(), "RAW");
7505 }
7506
7507 #[tokio::test]
7510 async fn test_initial_vars_set_and_exported() {
7511 let config = KernelConfig::transient()
7512 .with_var("INIT_FOO", Value::String("bar".into()));
7513 let kernel = Kernel::new(config).expect("failed to create kernel");
7514
7515 assert_eq!(
7516 kernel.get_var("INIT_FOO").await,
7517 Some(Value::String("bar".into()))
7518 );
7519 assert!(
7520 kernel.scope.read().await.is_exported("INIT_FOO"),
7521 "initial_vars entries must be marked exported"
7522 );
7523 }
7524
7525 #[tokio::test]
7526 async fn test_execute_with_vars_overlay_visible() {
7527 let kernel = Kernel::transient().expect("failed to create kernel");
7528 let mut overlay = HashMap::new();
7529 overlay.insert("OVERLAY_X".to_string(), Value::String("yes".into()));
7530
7531 let result = kernel
7532 .execute_with_options(r#"echo "${OVERLAY_X}""#, ExecuteOptions::new().with_vars(overlay))
7533 .await
7534 .expect("execute failed");
7535
7536 assert!(result.ok());
7537 assert_eq!(result.text_out().trim(), "yes");
7538 }
7539
7540 #[tokio::test]
7541 async fn test_execute_with_vars_overlay_cleanup() {
7542 let kernel = Kernel::transient().expect("failed to create kernel");
7543 let mut overlay = HashMap::new();
7544 overlay.insert("EPHEMERAL".to_string(), Value::String("transient".into()));
7545
7546 kernel
7547 .execute_with_options("echo ignored", ExecuteOptions::new().with_vars(overlay))
7548 .await
7549 .expect("execute failed");
7550
7551 assert_eq!(kernel.get_var("EPHEMERAL").await, None);
7552 assert!(
7553 !kernel.scope.read().await.is_exported("EPHEMERAL"),
7554 "overlay-only export must be cleared on return"
7555 );
7556 }
7557
7558 #[tokio::test]
7559 async fn test_execute_with_vars_does_not_clobber_existing_export() {
7560 let kernel = Kernel::transient().expect("failed to create kernel");
7561 kernel
7562 .execute("export OUTER=outer")
7563 .await
7564 .expect("export failed");
7565
7566 let mut overlay = HashMap::new();
7567 overlay.insert("OUTER".to_string(), Value::String("inner".into()));
7568 let result = kernel
7569 .execute_with_options(r#"echo "${OUTER}""#, ExecuteOptions::new().with_vars(overlay))
7570 .await
7571 .expect("execute failed");
7572 assert_eq!(result.text_out().trim(), "inner");
7573
7574 assert_eq!(
7575 kernel.get_var("OUTER").await,
7576 Some(Value::String("outer".into())),
7577 "outer value must reappear after pop"
7578 );
7579 assert!(
7580 kernel.scope.read().await.is_exported("OUTER"),
7581 "outer export must survive overlay"
7582 );
7583 }
7584
7585 #[tokio::test]
7586 async fn test_execute_with_vars_inner_assignment_is_local() {
7587 let kernel = Kernel::transient().expect("failed to create kernel");
7588 let mut overlay = HashMap::new();
7589 overlay.insert("LOCAL_FOO".to_string(), Value::String("from-overlay".into()));
7590
7591 let result = kernel
7596 .execute_with_options(
7597 r#"LOCAL_FOO="reassigned"; echo "${LOCAL_FOO}""#,
7598 ExecuteOptions::new().with_vars(overlay),
7599 )
7600 .await
7601 .expect("execute failed");
7602 assert!(result.ok());
7603
7604 assert_eq!(kernel.get_var("LOCAL_FOO").await, None);
7607 }
7608
7609 #[tokio::test]
7610 async fn test_external_command_sees_exported_var() {
7611 let kernel = Kernel::transient().expect("failed to create kernel");
7612 let result = kernel
7613 .execute("export EXT_FOO=bar; printenv EXT_FOO")
7614 .await
7615 .expect("execute failed");
7616
7617 assert!(result.ok(), "printenv should succeed: stderr={}", result.err);
7618 assert_eq!(result.text_out().trim(), "bar");
7619 }
7620
7621 #[tokio::test]
7622 async fn test_external_command_does_not_see_unexported_var() {
7623 let kernel = Kernel::transient().expect("failed to create kernel");
7624
7625 let result = kernel
7628 .execute("EXT_BAR=hidden; printenv EXT_BAR")
7629 .await
7630 .expect("execute failed");
7631
7632 assert!(!result.ok(), "printenv should fail when var is unexported");
7633 assert!(
7634 result.text_out().trim().is_empty(),
7635 "no stdout when var is missing, got: {}",
7636 result.text_out()
7637 );
7638 }
7639
7640 #[tokio::test]
7641 async fn test_external_command_does_not_see_os_env() {
7642 assert!(
7648 std::env::var_os("PATH").is_some(),
7649 "test precondition: cargo should set PATH"
7650 );
7651
7652 let kernel = Kernel::transient().expect("failed to create kernel");
7653 let result = kernel
7654 .execute("printenv PATH")
7655 .await
7656 .expect("execute failed");
7657
7658 assert!(
7659 !result.ok(),
7660 "printenv PATH must fail in hermetic kernel, got stdout={:?}",
7661 result.text_out()
7662 );
7663 assert!(
7664 result.text_out().trim().is_empty(),
7665 "no PATH in subprocess env, got stdout={:?}",
7666 result.text_out()
7667 );
7668 }
7669
7670 #[tokio::test]
7671 async fn test_execute_with_vars_overlay_reaches_subprocess() {
7672 let kernel = Kernel::transient().expect("failed to create kernel");
7673 let mut overlay = HashMap::new();
7674 overlay.insert("SUB_FOO".to_string(), Value::String("subproc".into()));
7675
7676 let result = kernel
7677 .execute_with_options("printenv SUB_FOO", ExecuteOptions::new().with_vars(overlay))
7678 .await
7679 .expect("execute failed");
7680
7681 assert!(
7682 result.ok(),
7683 "printenv should succeed: code={} stdout={:?} stderr={:?}",
7684 result.code,
7685 result.text_out(),
7686 result.err
7687 );
7688 assert_eq!(result.text_out().trim(), "subproc");
7689 }
7690}