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::{
50 spread_non_list_message, Arg, BinaryOp, Command, Expr, FileTestOp, ListElem, RecordKey, Stmt,
51 StringPart, TestExpr, ToolDef, Value,
52};
53pub use kaish_types::{CommandKind, ExecuteOptions};
54use crate::backend::{BackendError, KernelBackend};
55use kaish_glob::glob_match;
56use crate::dispatch::{CommandDispatcher, PipelinePosition};
57use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value_no_envelope, value_to_bool, value_to_string, value_to_text_sink, ControlFlow, ExecResult, LatchRequest, PathError, Scope};
58use crate::parser::parse;
59use crate::scheduler::{is_bool_type, schema_param_lookup, select_leaf, stderr_stream, BoundedStream, JobManager, PipelineRunner, StderrReceiver};
60#[cfg(feature = "subprocess")]
61use crate::scheduler::{drain_to_stream, DEFAULT_STREAM_MAX_SIZE};
62use crate::tools::{register_builtins, ExecContext, GlobalFlags, ToolArgs, ToolRegistry};
63#[cfg(feature = "subprocess")]
64use crate::tools::resolve_in_path;
65use crate::validator::{Severity, Validator};
66#[cfg(feature = "localfs")]
67use crate::vfs::LocalFs;
68use crate::vfs::{BuiltinFs, DevFs, JobFs, MemoryFs, VfsRouter};
69use kaish_vfs::ByteBudget;
70#[cfg(all(feature = "localfs", feature = "overlay"))]
71use kaish_vfs::OverlayFs;
72
73#[derive(Debug, Clone)]
80pub enum VfsMountMode {
81 #[cfg(feature = "localfs")]
90 Passthrough,
91
92 #[cfg(feature = "localfs")]
108 Sandboxed {
109 root: Option<PathBuf>,
112 },
113
114 NoLocal,
130}
131
132#[allow(clippy::derivable_impls)] impl Default for VfsMountMode {
134 fn default() -> Self {
135 #[cfg(feature = "localfs")]
136 { VfsMountMode::Sandboxed { root: None } }
137 #[cfg(not(feature = "localfs"))]
138 { VfsMountMode::NoLocal }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct KernelConfig {
145 pub name: String,
147
148 pub vfs_mode: VfsMountMode,
150
151 pub cwd: PathBuf,
153
154 pub skip_validation: bool,
160
161 pub interactive: bool,
166
167 pub ignore_config: crate::ignore_config::IgnoreConfig,
169
170 pub output_limit: crate::output_limit::OutputLimitConfig,
172
173 pub allow_external_commands: bool,
183
184 pub latch_enabled: bool,
189
190 pub trash_enabled: bool,
196
197 pub nonce_store: Option<crate::nonce::NonceStore>,
203
204 pub initial_vars: HashMap<String, Value>,
212
213 pub request_timeout: Option<Duration>,
220
221 pub kill_grace: Duration,
227
228 pub vfs_budget_bytes: Option<u64>,
246
247 pub overlay: bool,
269}
270
271#[cfg(feature = "localfs")]
273fn default_sandbox_root() -> PathBuf {
274 std::env::var("HOME")
275 .map(PathBuf::from)
276 .unwrap_or_else(|_| PathBuf::from("/"))
277}
278
279impl Default for KernelConfig {
280 fn default() -> Self {
281 #[cfg(feature = "localfs")]
282 {
283 let home = default_sandbox_root();
284 Self {
285 name: "default".to_string(),
286 vfs_mode: VfsMountMode::Sandboxed { root: None },
287 cwd: home,
288 skip_validation: false,
289 interactive: false,
290 ignore_config: crate::ignore_config::IgnoreConfig::none(),
291 output_limit: crate::output_limit::OutputLimitConfig::none(),
292 allow_external_commands: cfg!(feature = "subprocess"),
293 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
294 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
295 nonce_store: None,
296 initial_vars: HashMap::new(),
297 request_timeout: None,
298 kill_grace: Duration::from_secs(2),
299 vfs_budget_bytes: None,
300 overlay: false,
301 }
302 }
303 #[cfg(not(feature = "localfs"))]
304 {
305 Self {
306 name: "default".to_string(),
307 vfs_mode: VfsMountMode::NoLocal,
308 cwd: PathBuf::from("/"),
309 skip_validation: false,
310 interactive: false,
311 ignore_config: crate::ignore_config::IgnoreConfig::none(),
312 output_limit: crate::output_limit::OutputLimitConfig::none(),
313 allow_external_commands: false,
314 latch_enabled: false,
315 trash_enabled: false,
316 nonce_store: None,
317 initial_vars: HashMap::new(),
318 request_timeout: None,
319 kill_grace: Duration::from_secs(2),
320 vfs_budget_bytes: None,
321 overlay: false,
322 }
323 }
324 }
325}
326
327impl KernelConfig {
328 #[cfg(feature = "localfs")]
330 pub fn transient() -> Self {
331 let home = default_sandbox_root();
332 Self {
333 name: "transient".to_string(),
334 vfs_mode: VfsMountMode::Sandboxed { root: None },
335 cwd: home,
336 skip_validation: false,
337 interactive: false,
338 ignore_config: crate::ignore_config::IgnoreConfig::none(),
339 output_limit: crate::output_limit::OutputLimitConfig::none(),
340 allow_external_commands: cfg!(feature = "subprocess"),
341 latch_enabled: false,
342 trash_enabled: false,
343 nonce_store: None,
344 initial_vars: HashMap::new(),
345 request_timeout: None,
346 kill_grace: Duration::from_secs(2),
347 vfs_budget_bytes: None,
348 overlay: false,
349 }
350 }
351
352 #[cfg(not(feature = "localfs"))]
354 pub fn transient() -> Self {
355 Self::isolated()
356 }
357
358 #[cfg(feature = "localfs")]
360 pub fn named(name: &str) -> Self {
361 let home = default_sandbox_root();
362 Self {
363 name: name.to_string(),
364 vfs_mode: VfsMountMode::Sandboxed { root: None },
365 cwd: home,
366 skip_validation: false,
367 interactive: false,
368 ignore_config: crate::ignore_config::IgnoreConfig::none(),
369 output_limit: crate::output_limit::OutputLimitConfig::none(),
370 allow_external_commands: cfg!(feature = "subprocess"),
371 latch_enabled: false,
372 trash_enabled: false,
373 nonce_store: None,
374 initial_vars: HashMap::new(),
375 request_timeout: None,
376 kill_grace: Duration::from_secs(2),
377 vfs_budget_bytes: None,
378 overlay: false,
379 }
380 }
381
382 #[cfg(not(feature = "localfs"))]
384 pub fn named(name: &str) -> Self {
385 Self {
386 name: name.to_string(),
387 ..Self::isolated()
388 }
389 }
390
391 #[cfg(feature = "localfs")]
396 pub fn repl() -> Self {
397 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
398 Self {
399 name: "repl".to_string(),
400 vfs_mode: VfsMountMode::Passthrough,
401 cwd,
402 skip_validation: false,
403 interactive: false,
404 ignore_config: crate::ignore_config::IgnoreConfig::none(),
405 output_limit: crate::output_limit::OutputLimitConfig::none(),
406 allow_external_commands: cfg!(feature = "subprocess"),
407 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
408 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
409 nonce_store: None,
410 initial_vars: HashMap::new(),
411 request_timeout: None,
412 kill_grace: Duration::from_secs(2),
413 vfs_budget_bytes: None,
414 overlay: false,
415 }
416 }
417
418 #[cfg(feature = "localfs")]
431 pub fn agent() -> Self {
432 let home = default_sandbox_root();
433 Self {
434 name: "agent".to_string(),
435 vfs_mode: VfsMountMode::Sandboxed { root: None },
436 cwd: home,
437 skip_validation: false,
438 interactive: false,
439 ignore_config: crate::ignore_config::IgnoreConfig::agent(),
440 output_limit: crate::output_limit::OutputLimitConfig::agent(),
441 allow_external_commands: cfg!(feature = "subprocess"),
442 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
443 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
444 nonce_store: None,
445 initial_vars: HashMap::new(),
446 request_timeout: None,
447 kill_grace: Duration::from_secs(2),
448 vfs_budget_bytes: Some(64 * 1024 * 1024),
449 overlay: false,
450 }
451 }
452
453 #[cfg(feature = "localfs")]
460 pub fn agent_with_root(root: PathBuf) -> Self {
461 Self {
462 name: "agent".to_string(),
463 vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
464 cwd: root,
465 skip_validation: false,
466 interactive: false,
467 ignore_config: crate::ignore_config::IgnoreConfig::agent(),
468 output_limit: crate::output_limit::OutputLimitConfig::agent(),
469 allow_external_commands: cfg!(feature = "subprocess"),
470 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
471 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
472 nonce_store: None,
473 initial_vars: HashMap::new(),
474 request_timeout: None,
475 kill_grace: Duration::from_secs(2),
476 vfs_budget_bytes: Some(64 * 1024 * 1024),
477 overlay: false,
478 }
479 }
480
481 pub fn isolated() -> Self {
486 Self {
487 name: "isolated".to_string(),
488 vfs_mode: VfsMountMode::NoLocal,
489 cwd: PathBuf::from("/"),
490 skip_validation: false,
491 interactive: false,
492 ignore_config: crate::ignore_config::IgnoreConfig::none(),
493 output_limit: crate::output_limit::OutputLimitConfig::none(),
494 allow_external_commands: false,
495 latch_enabled: false,
496 trash_enabled: false,
497 nonce_store: None,
498 initial_vars: HashMap::new(),
499 request_timeout: None,
500 kill_grace: Duration::from_secs(2),
501 vfs_budget_bytes: None,
502 overlay: false,
503 }
504 }
505
506 pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
508 self.vfs_mode = mode;
509 self
510 }
511
512 pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
514 self.cwd = cwd;
515 self
516 }
517
518 pub fn with_skip_validation(mut self, skip: bool) -> Self {
520 self.skip_validation = skip;
521 self
522 }
523
524 pub fn with_interactive(mut self, interactive: bool) -> Self {
526 self.interactive = interactive;
527 self
528 }
529
530 pub fn with_ignore_config(mut self, config: crate::ignore_config::IgnoreConfig) -> Self {
532 self.ignore_config = config;
533 self
534 }
535
536 pub fn with_output_limit(mut self, config: crate::output_limit::OutputLimitConfig) -> Self {
538 self.output_limit = config;
539 self
540 }
541
542 pub fn with_allow_external_commands(mut self, allow: bool) -> Self {
548 self.allow_external_commands = allow;
549 self
550 }
551
552 pub fn with_latch(mut self, enabled: bool) -> Self {
554 self.latch_enabled = enabled;
555 self
556 }
557
558 pub fn with_trash(mut self, enabled: bool) -> Self {
560 self.trash_enabled = enabled;
561 self
562 }
563
564 pub fn with_nonce_store(mut self, store: crate::nonce::NonceStore) -> Self {
569 self.nonce_store = Some(store);
570 self
571 }
572
573 pub fn with_var(mut self, name: impl Into<String>, value: Value) -> Self {
577 self.initial_vars.insert(name.into(), value);
578 self
579 }
580
581 pub fn with_initial_vars(mut self, vars: HashMap<String, Value>) -> Self {
583 self.initial_vars = vars;
584 self
585 }
586
587 pub fn with_vars(mut self, vars: HashMap<String, Value>) -> Self {
589 self.initial_vars.extend(vars);
590 self
591 }
592
593 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
598 self.request_timeout = Some(timeout);
599 self
600 }
601
602 pub fn with_kill_grace(mut self, grace: Duration) -> Self {
604 self.kill_grace = grace;
605 self
606 }
607
608 pub fn with_vfs_budget(mut self, bytes: u64) -> Self {
617 self.vfs_budget_bytes = Some(bytes);
618 self
619 }
620
621 pub fn without_vfs_budget(mut self) -> Self {
626 self.vfs_budget_bytes = None;
627 self
628 }
629
630 pub fn with_overlay(mut self, overlay: bool) -> Self {
637 self.overlay = overlay;
638 self
639 }
640}
641
642#[cfg(all(feature = "localfs", feature = "overlay"))]
649#[derive(Clone)]
650pub struct OverlayHandle {
651 pub fs: Arc<OverlayFs>,
654 pub mount_path: PathBuf,
656 pub commit_root: PathBuf,
658}
659
660pub struct Kernel {
665 name: String,
667 scope: RwLock<Scope>,
669 tools: Arc<ToolRegistry>,
671 user_tools: RwLock<HashMap<String, ToolDef>>,
673 vfs: Arc<VfsRouter>,
675 jobs: Arc<JobManager>,
677 runner: PipelineRunner,
679 exec_ctx: RwLock<ExecContext>,
681 initial_vars: HashMap<String, Value>,
685 skip_validation: bool,
687 interactive: bool,
689 allow_external_commands: bool,
691 vfs_budget: Option<Arc<kaish_vfs::ByteBudget>>,
698 #[cfg(all(feature = "localfs", feature = "overlay"))]
705 overlay_handle: Option<Arc<OverlayHandle>>,
706 request_timeout: Option<Duration>,
708 kill_grace: Duration,
710 stderr_receiver: tokio::sync::Mutex<StderrReceiver>,
715 cancel_token: std::sync::Mutex<tokio_util::sync::CancellationToken>,
721 #[cfg(all(unix, feature = "subprocess"))]
723 terminal_state: Option<Arc<crate::terminal::TerminalState>>,
724 self_weak: std::sync::OnceLock<std::sync::Weak<Self>>,
729 bg_job_id: Option<crate::scheduler::JobId>,
735 execute_lock: tokio::sync::Mutex<()>,
741}
742
743struct VfsSetupResult {
745 vfs: VfsRouter,
746 budget: Option<Arc<ByteBudget>>,
747 #[cfg(all(feature = "localfs", feature = "overlay"))]
748 overlay_handle: Option<Arc<OverlayHandle>>,
749}
750
751impl Kernel {
752 pub fn new(config: KernelConfig) -> Result<Self> {
754 let mut setup = Self::setup_vfs(&config)?;
755 let jobs = Arc::new(JobManager::new());
756
757 setup.vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
759
760 #[cfg(all(feature = "localfs", feature = "overlay"))]
761 let overlay_handle = setup.overlay_handle.take();
762
763 let kernel = Self::assemble(config, setup.vfs, jobs, false, setup.budget, |_| {}, |vfs_ref, tools| {
767 ExecContext::with_vfs_and_tools(vfs_ref.clone(), tools.clone())
768 })?;
769
770 #[cfg(all(feature = "localfs", feature = "overlay"))]
771 {
772 let mut kernel = kernel;
773 kernel.overlay_handle = overlay_handle;
774 if let Some(ref handle) = kernel.overlay_handle {
776 kernel.exec_ctx.get_mut().overlay_handle = Some(Arc::clone(handle));
777 }
778 return Ok(kernel);
779 }
780
781 #[allow(unreachable_code)]
782 Ok(kernel)
783 }
784
785 fn setup_vfs(config: &KernelConfig) -> Result<VfsSetupResult> {
799 let mut vfs = VfsRouter::new();
800
801 let budget: Option<Arc<ByteBudget>> = config
804 .vfs_budget_bytes
805 .map(|bytes| Arc::new(ByteBudget::labeled(bytes, "vfs-memory")));
806
807 fn mem(budget: &Option<Arc<ByteBudget>>) -> MemoryFs {
809 match budget {
810 Some(b) => MemoryFs::with_budget(Arc::clone(b)),
811 None => MemoryFs::new(),
812 }
813 }
814
815 #[cfg(all(feature = "localfs", feature = "overlay"))]
817 let mut overlay_handle: Option<Arc<OverlayHandle>> = None;
818
819 match &config.vfs_mode {
820 #[cfg(feature = "localfs")]
821 VfsMountMode::Passthrough => {
822 #[cfg(feature = "overlay")]
823 if config.overlay {
824 let lower = Arc::new(LocalFs::read_only(PathBuf::from("/")));
826 let overlay_fs = Arc::new(match &budget {
827 Some(b) => OverlayFs::over_with_budget(lower, Arc::clone(b)),
828 None => OverlayFs::over(lower),
829 });
830 let handle = Arc::new(OverlayHandle {
831 fs: Arc::clone(&overlay_fs),
832 mount_path: PathBuf::from("/"),
833 commit_root: PathBuf::from("/"),
834 });
835 vfs.mount_arc("/", overlay_fs as Arc<dyn kaish_vfs::Filesystem>);
836 overlay_handle = Some(handle);
837 } else {
838 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
840 }
841 #[cfg(not(feature = "overlay"))]
842 {
843 if config.overlay {
844 return Err(anyhow::anyhow!(
845 "overlay=true requires the `overlay` feature, but this build \
846 was compiled without it. Recompile with --features overlay \
847 (or the default feature set) to enable overlay mode."
848 ));
849 }
850 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
852 }
853 vfs.mount("/v", mem(&budget));
855 }
856 #[cfg(feature = "localfs")]
857 VfsMountMode::Sandboxed { root } => {
858 vfs.mount("/", mem(&budget));
864 vfs.mount("/v", mem(&budget));
865
866 vfs.mount("/dev", DevFs::new());
869
870 vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
872
873 let runtime = crate::paths::xdg_runtime_dir();
875 if runtime.exists() {
876 let runtime_str = runtime.to_string_lossy().to_string();
877 vfs.mount(&runtime_str, LocalFs::new(runtime));
878 }
879
880 let local_root = root.clone().unwrap_or_else(|| {
882 std::env::var("HOME")
883 .map(PathBuf::from)
884 .unwrap_or_else(|_| PathBuf::from("/"))
885 });
886
887 let mount_point = local_root.to_string_lossy().to_string();
888
889 #[cfg(feature = "overlay")]
890 if config.overlay {
891 let lower = Arc::new(LocalFs::read_only(local_root.clone()));
893 let overlay_fs = Arc::new(match &budget {
894 Some(b) => OverlayFs::over_with_budget(lower, Arc::clone(b)),
895 None => OverlayFs::over(lower),
896 });
897 let handle = Arc::new(OverlayHandle {
898 fs: Arc::clone(&overlay_fs),
899 mount_path: PathBuf::from(&mount_point),
900 commit_root: local_root,
901 });
902 vfs.mount_arc(&mount_point, overlay_fs as Arc<dyn kaish_vfs::Filesystem>);
903 overlay_handle = Some(handle);
904 } else {
905 vfs.mount(&mount_point, LocalFs::new(local_root));
909 }
910 #[cfg(not(feature = "overlay"))]
911 {
912 if config.overlay {
913 return Err(anyhow::anyhow!(
914 "overlay=true requires the `overlay` feature, but this build \
915 was compiled without it. Recompile with --features overlay \
916 (or the default feature set) to enable overlay mode."
917 ));
918 }
919 vfs.mount(&mount_point, LocalFs::new(local_root));
921 }
922 }
923 VfsMountMode::NoLocal => {
924 if config.overlay {
925 return Err(anyhow::anyhow!(
926 "overlay=true is incompatible with VfsMountMode::NoLocal: \
927 everything is already virtual, there is no real lower layer \
928 to wrap. Use with_overlay(false) or switch to a Passthrough \
929 or Sandboxed VFS mode."
930 ));
931 }
932 vfs.mount("/", mem(&budget));
934 vfs.mount("/tmp", mem(&budget));
935 vfs.mount("/v", mem(&budget));
936 vfs.mount("/dev", DevFs::new());
938 }
939 }
940
941 Ok(VfsSetupResult {
942 vfs,
943 budget,
944 #[cfg(all(feature = "localfs", feature = "overlay"))]
945 overlay_handle,
946 })
947 }
948
949 pub fn transient() -> Result<Self> {
951 Self::new(KernelConfig::transient())
952 }
953
954 pub fn with_backend(
991 backend: Arc<dyn KernelBackend>,
992 config: KernelConfig,
993 configure_vfs: impl FnOnce(&mut VfsRouter),
994 configure_tools: impl FnOnce(&mut ToolRegistry),
995 ) -> Result<Self> {
996 use crate::backend::VirtualOverlayBackend;
997
998 if config.overlay {
1002 return Err(anyhow::anyhow!(
1003 "overlay=true is incompatible with Kernel::with_backend: the embedder \
1004 controls the VFS; the kernel cannot wrap it with an OverlayFs without \
1005 bypassing the embedder's storage semantics. Use KernelConfig::with_overlay(false)."
1006 ));
1007 }
1008
1009 let mut vfs = VfsRouter::new();
1010 let jobs = Arc::new(JobManager::new());
1011
1012 let vfs_budget: Option<Arc<ByteBudget>> = config
1016 .vfs_budget_bytes
1017 .map(|bytes| Arc::new(ByteBudget::labeled(bytes, "vfs-memory")));
1018
1019 vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
1020 let blobs_fs = match &vfs_budget {
1021 Some(b) => MemoryFs::with_budget(Arc::clone(b)),
1022 None => MemoryFs::new(),
1023 };
1024 vfs.mount("/v/blobs", blobs_fs);
1025
1026 vfs.mount("/dev", DevFs::new());
1031
1032 configure_vfs(&mut vfs);
1034
1035 Self::assemble(config, vfs, jobs, true, vfs_budget, configure_tools, |vfs_arc: &Arc<VfsRouter>, _: &Arc<ToolRegistry>| {
1040 let overlay: Arc<dyn KernelBackend> =
1041 Arc::new(VirtualOverlayBackend::new(backend, vfs_arc.clone()));
1042 ExecContext::with_backend(overlay)
1043 })
1044 }
1045
1046 fn assemble(
1052 config: KernelConfig,
1053 mut vfs: VfsRouter,
1054 jobs: Arc<JobManager>,
1055 no_host_filesystem: bool,
1056 vfs_budget: Option<Arc<ByteBudget>>,
1057 configure_tools: impl FnOnce(&mut ToolRegistry),
1058 make_ctx: impl FnOnce(&Arc<VfsRouter>, &Arc<ToolRegistry>) -> ExecContext,
1059 ) -> Result<Self> {
1060 let no_host_side_channel =
1073 no_host_filesystem || matches!(config.vfs_mode, VfsMountMode::NoLocal);
1074
1075 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;
1076
1077 if no_host_side_channel {
1078 output_limit.set_spill_mode(crate::output_limit::SpillMode::Memory);
1079 jobs.set_persist_output_files(false);
1080 }
1081
1082 let mut tools = ToolRegistry::new();
1083 register_builtins(&mut tools);
1084 configure_tools(&mut tools);
1085 let tools = Arc::new(tools);
1086
1087 vfs.mount("/v/bin", BuiltinFs::new(tools.clone()));
1089
1090 let vfs = Arc::new(vfs);
1091
1092 let runner = PipelineRunner::new(tools.clone());
1093
1094 let (stderr_writer, stderr_receiver) = stderr_stream();
1095
1096 let mut exec_ctx = make_ctx(&vfs, &tools);
1097 exec_ctx.set_cwd(cwd);
1098 exec_ctx.set_job_manager(jobs.clone());
1099 exec_ctx.set_tool_schemas(tools.schemas());
1100 exec_ctx.set_tools(tools.clone());
1101 #[cfg(feature = "os-integration")]
1102 exec_ctx.set_trash_backend(Arc::new(crate::trash_system::SystemTrash));
1103 exec_ctx.stderr = Some(stderr_writer);
1104 exec_ctx.ignore_config = ignore_config;
1105 exec_ctx.output_limit = output_limit;
1106 exec_ctx.allow_external_commands = allow_external_commands;
1107 exec_ctx.vfs_budget = vfs_budget.clone();
1108 if let Some(store) = nonce_store {
1109 exec_ctx.nonce_store = store;
1110 }
1111
1112 Ok(Self {
1113 name,
1114 scope: RwLock::new({
1115 let mut scope = Scope::new();
1116 scope.set_pid(KERNEL_COUNTER.fetch_add(1, Ordering::Relaxed));
1117 for (name, value) in initial_vars.clone() {
1126 scope.set_exported(name, value);
1127 }
1128 scope.set_latch_enabled(latch_enabled);
1129 scope.set_trash_enabled(trash_enabled);
1130 scope
1131 }),
1132 initial_vars,
1133 tools,
1134 user_tools: RwLock::new(HashMap::new()),
1135 vfs,
1136 jobs,
1137 runner,
1138 exec_ctx: RwLock::new(exec_ctx),
1139 skip_validation,
1140 interactive,
1141 allow_external_commands,
1142 vfs_budget,
1143 request_timeout,
1144 kill_grace,
1145 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
1146 cancel_token: std::sync::Mutex::new(tokio_util::sync::CancellationToken::new()),
1147 #[cfg(all(unix, feature = "subprocess"))]
1148 terminal_state: None,
1149 self_weak: std::sync::OnceLock::new(),
1150 execute_lock: tokio::sync::Mutex::new(()),
1151 bg_job_id: None,
1152 #[cfg(all(feature = "localfs", feature = "overlay"))]
1156 overlay_handle: None,
1157 })
1158 }
1159
1160 pub fn name(&self) -> &str {
1162 &self.name
1163 }
1164
1165 pub fn into_arc(self) -> Arc<Self> {
1172 let arc = Arc::new(self);
1173 let _ = arc.self_weak.set(Arc::downgrade(&arc));
1174 arc
1175 }
1176
1177 pub async fn fork(&self) -> Arc<Self> {
1206 self.fork_inner(tokio_util::sync::CancellationToken::new(), self.bg_job_id)
1207 .await
1208 }
1209
1210 pub async fn fork_attached(&self) -> Arc<Self> {
1218 let child_token = {
1219 #[allow(clippy::expect_used)]
1220 let parent = self.cancel_token.lock().expect("cancel_token poisoned");
1221 parent.child_token()
1222 };
1223 self.fork_inner(child_token, self.bg_job_id).await
1224 }
1225
1226 pub async fn fork_for_background(
1231 &self,
1232 cancel: tokio_util::sync::CancellationToken,
1233 job_id: crate::scheduler::JobId,
1234 ) -> Arc<Self> {
1235 self.fork_inner(cancel, Some(job_id)).await
1236 }
1237
1238 async fn fork_inner(
1241 &self,
1242 cancel: tokio_util::sync::CancellationToken,
1243 bg_job_id: Option<crate::scheduler::JobId>,
1244 ) -> Arc<Self> {
1245 let scope_snapshot = self.scope.read().await.clone();
1246 let user_tools_snapshot = self.user_tools.read().await.clone();
1247
1248 let mut fork_ctx = {
1252 let parent_ctx = self.exec_ctx.read().await;
1253 parent_ctx.child_for_pipeline()
1254 };
1255 let (stderr_writer, stderr_receiver) = stderr_stream();
1256 fork_ctx.stderr = Some(stderr_writer);
1257 fork_ctx.dispatcher = None;
1260 fork_ctx.interactive = false;
1261 fork_ctx.cancel = cancel.clone();
1262 #[cfg(all(unix, feature = "subprocess"))]
1263 {
1264 fork_ctx.terminal_state = None;
1265 }
1266
1267 let fork = Self {
1268 name: format!("{}:fork", self.name),
1269 scope: RwLock::new(scope_snapshot),
1270 initial_vars: self.initial_vars.clone(),
1271 tools: Arc::clone(&self.tools),
1272 user_tools: RwLock::new(user_tools_snapshot),
1273 vfs: Arc::clone(&self.vfs),
1274 jobs: Arc::clone(&self.jobs),
1275 runner: self.runner.clone(),
1276 exec_ctx: RwLock::new(fork_ctx),
1277 skip_validation: self.skip_validation,
1278 interactive: false,
1280 allow_external_commands: self.allow_external_commands,
1281 vfs_budget: self.vfs_budget.clone(),
1285 request_timeout: self.request_timeout,
1286 kill_grace: self.kill_grace,
1287 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
1288 cancel_token: std::sync::Mutex::new(cancel),
1289 #[cfg(all(unix, feature = "subprocess"))]
1290 terminal_state: None,
1291 self_weak: std::sync::OnceLock::new(),
1292 execute_lock: tokio::sync::Mutex::new(()),
1293 bg_job_id,
1294 #[cfg(all(feature = "localfs", feature = "overlay"))]
1298 overlay_handle: self.overlay_handle.clone(),
1299 };
1300
1301 fork.into_arc()
1302 }
1303
1304 pub fn dispatcher(&self) -> Option<Arc<dyn CommandDispatcher>> {
1309 self.self_weak
1310 .get()
1311 .and_then(|weak| weak.upgrade())
1312 .map(|arc| arc as Arc<dyn CommandDispatcher>)
1313 }
1314
1315 #[cfg(all(unix, feature = "subprocess"))]
1320 pub fn init_terminal(&mut self) {
1321 if !self.interactive {
1322 return;
1323 }
1324 match crate::terminal::TerminalState::init() {
1325 Ok(state) => {
1326 let state = Arc::new(state);
1327 self.terminal_state = Some(state.clone());
1328 self.exec_ctx.get_mut().terminal_state = Some(state);
1330 tracing::debug!("terminal job control initialized");
1331 }
1332 Err(e) => {
1333 tracing::warn!("failed to initialize terminal job control: {}", e);
1334 }
1335 }
1336 }
1337
1338 pub fn set_trash_backend(&mut self, backend: Option<Arc<dyn crate::trash::TrashBackend>>) {
1346 self.exec_ctx.get_mut().trash_backend = backend;
1347 }
1348
1349 pub fn cancel(&self) {
1355 #[allow(clippy::expect_used)]
1356 let token = self.cancel_token.lock().expect("cancel_token poisoned");
1357 token.cancel();
1358 }
1359
1360 pub fn is_cancelled(&self) -> bool {
1362 #[allow(clippy::expect_used)]
1363 let token = self.cancel_token.lock().expect("cancel_token poisoned");
1364 token.is_cancelled()
1365 }
1366
1367 fn reset_cancel(&self) -> tokio_util::sync::CancellationToken {
1369 #[allow(clippy::expect_used)]
1370 let mut token = self.cancel_token.lock().expect("cancel_token poisoned");
1371 if token.is_cancelled() {
1372 *token = tokio_util::sync::CancellationToken::new();
1373 }
1374 token.clone()
1375 }
1376
1377 async fn acquire_execute_lock(&self) -> tokio::sync::MutexGuard<'_, ()> {
1383 match self.execute_lock.try_lock() {
1384 Ok(guard) => guard,
1385 Err(_) => {
1386 tracing::warn!(
1387 target: "kaish::kernel::concurrency",
1388 kernel = %self.name,
1389 "execute() contended — serializing concurrent caller; \
1390 use Kernel::fork() for parallelism instead of sharing"
1391 );
1392 self.execute_lock.lock().await
1393 }
1394 }
1395 }
1396
1397 pub async fn execute(&self, input: &str) -> Result<ExecResult> {
1402 self.run_inner(input, ExecuteOptions::default(), None, None).await
1403 }
1404
1405 #[tracing::instrument(level = "info", skip(self, argv), fields(cmd = name, argc = argv.len()))]
1446 pub async fn execute_argv(&self, name: &str, argv: &[Value]) -> Result<ExecResult> {
1447 let _guard = self.acquire_execute_lock().await;
1448 let cancel = self.reset_cancel();
1454
1455 let timeout = self.request_timeout;
1457 if timeout == Some(Duration::ZERO) {
1458 return Ok(ExecResult::failure(124, "timeout: timed out after 0s".to_string()));
1459 }
1460
1461 let pipeline = crate::ast::Pipeline {
1462 commands: vec![crate::ast::Command {
1463 name: name.to_string(),
1464 args: argv_to_args(argv),
1465 redirects: Vec::new(),
1466 }],
1467 background: false,
1468 };
1469 let result = self
1470 .run_under_watchdog(timeout, &cancel, self.execute_pipeline(&pipeline))
1471 .await?;
1472 self.update_last_result(&result).await;
1473 Ok(result)
1474 }
1475
1476 pub async fn confirm(&self, latch: &LatchRequest) -> Result<ExecResult> {
1491 if latch.tool.is_empty() {
1492 return Ok(ExecResult::failure(
1493 2,
1494 "confirm: latch carries no captured invocation to replay — \
1495 re-run the command with --confirm=<nonce> instead",
1496 ));
1497 }
1498 let mut argv: Vec<Value> = Vec::with_capacity(latch.argv.len() + 1);
1501 argv.push(Value::String(format!("--confirm={}", latch.nonce)));
1502 argv.extend(latch.argv.iter().map(|a| Value::String(a.clone())));
1503 self.execute_argv(&latch.tool, &argv).await
1504 }
1505
1506 async fn run_under_watchdog<F>(
1519 &self,
1520 timeout: Option<Duration>,
1521 cancel: &tokio_util::sync::CancellationToken,
1522 work: F,
1523 ) -> Result<ExecResult>
1524 where
1525 F: std::future::Future<Output = Result<ExecResult>>,
1526 {
1527 let watchdog = timeout.map(|d| Arc::new(crate::watchdog::Watchdog::new(d)));
1529 {
1530 let mut ec = self.exec_ctx.write().await;
1531 ec.watchdog = watchdog.clone();
1532 }
1533
1534 let result = if let Some(d) = timeout {
1535 #[allow(clippy::expect_used)]
1536 let watchdog = watchdog.clone().expect("watchdog constructed when timeout is set");
1537 let elapsed = Arc::new(std::sync::atomic::AtomicBool::new(false));
1538 let timer = tokio::spawn(watchdog.run(elapsed.clone(), cancel.clone()));
1539 let r = work.await;
1540 timer.abort();
1541 match r {
1542 Ok(mut res) => {
1543 if elapsed.load(std::sync::atomic::Ordering::SeqCst) {
1544 res.code = 124;
1545 if res.err.is_empty() {
1546 res.err = format!("timeout: timed out after {:?}", d);
1547 }
1548 }
1549 Ok(res)
1550 }
1551 Err(e) => Err(e),
1552 }
1553 } else {
1554 work.await
1555 };
1556
1557 {
1559 let mut ec = self.exec_ctx.write().await;
1560 ec.watchdog = None;
1561 }
1562 result
1563 }
1564
1565 pub async fn execute_with_options(
1585 &self,
1586 input: &str,
1587 opts: ExecuteOptions,
1588 ) -> Result<ExecResult> {
1589 self.run_inner(input, opts, None, None).await
1590 }
1591
1592 pub async fn execute_with_options_streaming(
1596 &self,
1597 input: &str,
1598 opts: ExecuteOptions,
1599 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1600 ) -> Result<ExecResult> {
1601 self.run_inner(input, opts, None, Some(on_output)).await
1602 }
1603
1604 pub async fn execute_with_pipe_stdin(
1616 &self,
1617 input: &str,
1618 opts: ExecuteOptions,
1619 pipe_stdin: crate::scheduler::PipeReader,
1620 ) -> Result<ExecResult> {
1621 self.run_inner(input, opts, Some(pipe_stdin), None).await
1622 }
1623
1624 pub async fn execute_with_pipe_stdin_streaming(
1628 &self,
1629 input: &str,
1630 opts: ExecuteOptions,
1631 pipe_stdin: crate::scheduler::PipeReader,
1632 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1633 ) -> Result<ExecResult> {
1634 self.run_inner(input, opts, Some(pipe_stdin), Some(on_output)).await
1635 }
1636
1637 #[deprecated(note = "use Kernel::execute_with_options with ExecuteOptions::with_vars")]
1643 pub async fn execute_with_vars(
1644 &self,
1645 input: &str,
1646 vars: HashMap<String, Value>,
1647 ) -> Result<ExecResult> {
1648 self.run_inner(input, ExecuteOptions::new().with_vars(vars), None, None).await
1649 }
1650
1651 #[deprecated(note = "use Kernel::execute_with_options_streaming")]
1656 pub async fn execute_streaming(
1657 &self,
1658 input: &str,
1659 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1660 ) -> Result<ExecResult> {
1661 self.run_inner(input, ExecuteOptions::default(), None, Some(on_output)).await
1662 }
1663
1664 async fn run_inner(
1675 &self,
1676 input: &str,
1677 opts: ExecuteOptions,
1678 pipe_stdin: Option<crate::scheduler::PipeReader>,
1679 on_output: Option<&mut (dyn FnMut(&ExecResult) + Send)>,
1680 ) -> Result<ExecResult> {
1681 use opentelemetry::context::FutureExt;
1682
1683 let embedder_baggage = opts.baggage.clone();
1686
1687 let result = match crate::telemetry::extract_parent(&opts) {
1688 Some(parent) => self
1689 .execute_with_options_inner(input, opts, pipe_stdin, on_output)
1690 .with_context(parent)
1691 .await,
1692 None => self.execute_with_options_inner(input, opts, pipe_stdin, on_output).await,
1693 };
1694
1695 result.map(|mut r| {
1696 crate::telemetry::merge_egress_baggage(&mut r, embedder_baggage);
1697 r
1698 })
1699 }
1700
1701 #[tracing::instrument(level = "info", skip(self, opts, pipe_stdin, on_output), fields(input_len = input.len()))]
1705 async fn execute_with_options_inner(
1706 &self,
1707 input: &str,
1708 opts: ExecuteOptions,
1709 pipe_stdin: Option<crate::scheduler::PipeReader>,
1710 on_output: Option<&mut (dyn FnMut(&ExecResult) + Send)>,
1711 ) -> Result<ExecResult> {
1712 let _guard = self.acquire_execute_lock().await;
1713
1714 let internal = self.reset_cancel();
1722 let (effective_cancel, watcher_handle): (
1727 tokio_util::sync::CancellationToken,
1728 Option<tokio::task::JoinHandle<()>>,
1729 ) = if let Some(ext) = opts.cancel_token {
1730 let combined = tokio_util::sync::CancellationToken::new();
1731 let combined_writer = combined.clone();
1732 let i = internal.clone();
1733 let handle = tokio::spawn(async move {
1734 tokio::select! {
1735 _ = i.cancelled() => combined_writer.cancel(),
1736 _ = ext.cancelled() => combined_writer.cancel(),
1737 }
1738 });
1739 (combined, Some(handle))
1740 } else {
1741 (internal, None)
1742 };
1743
1744 let timeout = opts.timeout.or(self.request_timeout);
1746
1747 if timeout == Some(Duration::ZERO) {
1749 if let Some(h) = watcher_handle {
1750 h.abort();
1751 }
1752 return Ok(ExecResult::failure(124, "timeout: timed out after 0s".to_string()));
1753 }
1754
1755 struct VarsFrameGuard<'a> {
1759 kernel: &'a Kernel,
1760 newly_exported: Vec<String>,
1761 }
1762 impl Drop for VarsFrameGuard<'_> {
1763 fn drop(&mut self) {
1764 let Ok(mut scope) = self.kernel.scope.try_write() else {
1773 tracing::error!(
1774 "vars frame guard: scope lock unexpectedly busy; \
1775 skipping pop_frame to avoid runtime deadlock — \
1776 transient vars may leak"
1777 );
1778 return;
1779 };
1780 scope.pop_frame();
1781 for name in self.newly_exported.drain(..) {
1782 scope.unexport(&name);
1783 }
1784 }
1785 }
1786
1787 struct CwdGuard<'a> {
1791 kernel: &'a Kernel,
1792 saved: PathBuf,
1793 }
1794 impl Drop for CwdGuard<'_> {
1795 fn drop(&mut self) {
1796 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1797 tracing::error!(
1798 "cwd guard: exec_ctx lock unexpectedly busy; \
1799 skipping cwd restore — kernel cwd may be wrong for next call"
1800 );
1801 return;
1802 };
1803 ec.cwd = std::mem::take(&mut self.saved);
1804 }
1805 }
1806 let _cwd_guard: Option<CwdGuard<'_>> = if let Some(new_cwd) = opts.cwd {
1807 let mut ec = self.exec_ctx.write().await;
1808 let saved = std::mem::replace(&mut ec.cwd, new_cwd);
1809 drop(ec);
1810 Some(CwdGuard { kernel: self, saved })
1811 } else {
1812 None
1813 };
1814
1815 struct StdinGuard<'a> {
1821 kernel: &'a Kernel,
1822 saved: Option<String>,
1823 }
1824 impl Drop for StdinGuard<'_> {
1825 fn drop(&mut self) {
1826 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1827 tracing::error!(
1828 "stdin guard: exec_ctx lock unexpectedly busy; \
1829 skipping stdin restore — stale stdin may leak to next call"
1830 );
1831 return;
1832 };
1833 ec.stdin = self.saved.take();
1834 }
1835 }
1836 let _stdin_guard: Option<StdinGuard<'_>> = if let Some(stdin) = opts.stdin {
1837 let mut ec = self.exec_ctx.write().await;
1838 let saved = ec.stdin.replace(stdin);
1839 drop(ec);
1840 Some(StdinGuard { kernel: self, saved })
1841 } else {
1842 None
1843 };
1844
1845 struct PipeStdinGuard<'a> {
1851 kernel: &'a Kernel,
1852 saved: Option<crate::scheduler::PipeReader>,
1853 }
1854 impl Drop for PipeStdinGuard<'_> {
1855 fn drop(&mut self) {
1856 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1857 tracing::error!(
1858 "pipe stdin guard: exec_ctx lock unexpectedly busy; \
1859 skipping restore — stale pipe stdin may leak to next call"
1860 );
1861 return;
1862 };
1863 ec.pipe_stdin = self.saved.take();
1864 }
1865 }
1866 let _pipe_stdin_guard: Option<PipeStdinGuard<'_>> = if let Some(reader) = pipe_stdin {
1867 let mut ec = self.exec_ctx.write().await;
1868 let saved = ec.pipe_stdin.replace(reader);
1869 drop(ec);
1870 Some(PipeStdinGuard { kernel: self, saved })
1871 } else {
1872 None
1873 };
1874
1875 let _vars_guard: Option<VarsFrameGuard<'_>> = if !opts.vars.is_empty() {
1876 let mut scope = self.scope.write().await;
1877 scope.push_frame();
1878 let mut newly = Vec::with_capacity(opts.vars.len());
1879 for (name, value) in opts.vars {
1880 if !scope.is_exported(&name) {
1881 newly.push(name.clone());
1882 }
1883 scope.set_exported(name, value);
1884 }
1885 drop(scope);
1886 Some(VarsFrameGuard { kernel: self, newly_exported: newly })
1887 } else {
1888 None
1889 };
1890
1891 {
1898 #[allow(clippy::expect_used)]
1899 let mut cur = self.cancel_token.lock().expect("cancel_token poisoned");
1900 *cur = effective_cancel.clone();
1901 }
1902
1903 let mut noop_cb: Box<dyn FnMut(&ExecResult) + Send> = Box::new(|_| {});
1909 let cb_ref: &mut (dyn FnMut(&ExecResult) + Send) = match on_output {
1910 Some(cb) => cb,
1911 None => &mut *noop_cb,
1912 };
1913
1914 let result = self
1915 .run_under_watchdog(timeout, &effective_cancel, self.execute_streaming_inner(input, cb_ref))
1916 .await;
1917
1918 {
1923 #[allow(clippy::expect_used)]
1924 let mut cur = self.cancel_token.lock().expect("cancel_token poisoned");
1925 *cur = tokio_util::sync::CancellationToken::new();
1926 }
1927
1928 if let Some(h) = watcher_handle {
1931 h.abort();
1932 }
1933
1934 result
1937 }
1938
1939 async fn execute_streaming_inner(
1945 &self,
1946 input: &str,
1947 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1948 ) -> Result<ExecResult> {
1949 let program = parse(input).map_err(|errors| {
1950 let msg = errors
1951 .iter()
1952 .map(|e| e.format(input))
1953 .collect::<Vec<_>>()
1954 .join("\n");
1955 anyhow::anyhow!("parse error:\n{}", msg)
1956 })?;
1957
1958 {
1960 let scope = self.scope.read().await;
1961 if scope.show_ast() {
1962 let output = format!("{:#?}\n", program);
1963 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(output)));
1964 }
1965 }
1966
1967 let mut surfaced_warnings = String::new();
1972 if !self.skip_validation {
1973 let user_tools = self.user_tools.read().await;
1974 let validator = Validator::new(&self.tools, &user_tools);
1975 let issues = validator.validate(&program);
1976
1977 let errors: Vec<_> = issues
1979 .iter()
1980 .filter(|i| i.severity == Severity::Error)
1981 .collect();
1982
1983 if !errors.is_empty() {
1984 let error_msg = errors
1985 .iter()
1986 .map(|e| e.format(input))
1987 .collect::<Vec<_>>()
1988 .join("\n");
1989 return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
1990 }
1991
1992 for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
1995 tracing::trace!("validation: {}", warning.format(input));
1996 if warning.code.surfaces_to_agent() {
1997 surfaced_warnings.push_str(&warning.format(input));
1998 surfaced_warnings.push('\n');
1999 }
2000 }
2001 }
2002
2003 if !surfaced_warnings.is_empty() {
2010 let mut advisory = ExecResult::success("");
2011 advisory.err = surfaced_warnings.clone();
2012 on_output(&advisory);
2013 }
2014
2015 let mut result = ExecResult::success("");
2016
2017 let cancel = self.reset_cancel();
2019
2020 for stmt in program.statements {
2021 if matches!(stmt, Stmt::Empty) {
2022 continue;
2023 }
2024
2025 if cancel.is_cancelled() {
2027 result.code = 130;
2028 return Ok(result);
2029 }
2030
2031 let flow = self.execute_stmt_flow(&stmt).await?;
2032
2033 let drained_stderr = {
2037 let mut receiver = self.stderr_receiver.lock().await;
2038 receiver.drain_lossy()
2039 };
2040
2041 match flow {
2042 ControlFlow::Normal(mut r) => {
2043 if !drained_stderr.is_empty() {
2044 if !r.err.is_empty() && !r.err.ends_with('\n') {
2045 r.err.push('\n');
2046 }
2047 let combined = format!("{}{}", drained_stderr, r.err);
2049 r.err = combined;
2050 }
2051 on_output(&r);
2052 let last_output = r.output().cloned();
2056 accumulate_result(&mut result, &r);
2057 result.set_output(last_output);
2058 }
2059 ControlFlow::Exit { code } => {
2060 if !drained_stderr.is_empty() {
2061 result.err.push_str(&drained_stderr);
2062 }
2063 result.code = code;
2064 if !surfaced_warnings.is_empty() {
2065 result.err = format!("{surfaced_warnings}{}", result.err);
2066 }
2067 return Ok(result);
2068 }
2069 ControlFlow::Return { mut value } => {
2070 if !drained_stderr.is_empty() {
2071 value.err = format!("{}{}", drained_stderr, value.err);
2072 }
2073 on_output(&value);
2074 accumulate_result(&mut result, &value);
2078 if !surfaced_warnings.is_empty() {
2079 result.err = format!("{surfaced_warnings}{}", result.err);
2080 }
2081 return Ok(result);
2082 }
2083 ControlFlow::Break { result: mut r, .. } | ControlFlow::Continue { result: mut r, .. } => {
2084 if !drained_stderr.is_empty() {
2085 r.err = format!("{}{}", drained_stderr, r.err);
2086 }
2087 on_output(&r);
2088 accumulate_result(&mut result, &r);
2089 }
2090 }
2091 }
2092
2093 if !surfaced_warnings.is_empty() {
2094 result.err = format!("{surfaced_warnings}{}", result.err);
2095 }
2096 Ok(result)
2097 }
2098
2099 fn execute_stmt_flow<'a>(
2101 &'a self,
2102 stmt: &'a Stmt,
2103 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + Send + 'a>> {
2104 use tracing::Instrument;
2105 let span = tracing::debug_span!("execute_stmt_flow", stmt_type = %stmt.kind_name());
2106 Box::pin(async move {
2107 match stmt {
2108 Stmt::Assignment(assign) => {
2109 let value = self.eval_expr_async(&assign.value).await
2111 .context("failed to evaluate assignment")?;
2112 let mut scope = self.scope.write().await;
2113 if assign.path.segments.len() == 1 {
2114 if assign.local {
2116 scope.set(assign.name(), value.clone());
2118 } else {
2119 scope.set_global(assign.name(), value.clone());
2121 }
2122 } else {
2123 scope.walk_write(&assign.path, value.clone()).map_err(|e| match e {
2128 PathError::UndefinedRoot(name) => anyhow::anyhow!(
2129 "{name}: undefined — create it first, e.g. `{name}={{}}` or `{name}=[]`"
2130 ),
2131 PathError::Absence(msg) | PathError::Shape(msg) => anyhow::anyhow!(msg),
2132 })?;
2133 }
2134 drop(scope);
2135
2136 Ok(ControlFlow::ok(ExecResult::success("")))
2138 }
2139 Stmt::Command(cmd) => {
2140 let pipeline = crate::ast::Pipeline {
2143 commands: vec![cmd.clone()],
2144 background: false,
2145 };
2146 let result = self.execute_pipeline(&pipeline).await?;
2147 self.update_last_result(&result).await;
2148
2149 if !result.ok() {
2151 let scope = self.scope.read().await;
2152 if scope.error_exit_enabled() {
2153 return Ok(ControlFlow::exit_code(result.code));
2154 }
2155 }
2156
2157 Ok(ControlFlow::ok(result))
2158 }
2159 Stmt::Pipeline(pipeline) => {
2160 let result = self.execute_pipeline(pipeline).await?;
2161 self.update_last_result(&result).await;
2162
2163 if !result.ok() {
2165 let scope = self.scope.read().await;
2166 if scope.error_exit_enabled() {
2167 return Ok(ControlFlow::exit_code(result.code));
2168 }
2169 }
2170
2171 Ok(ControlFlow::ok(result))
2172 }
2173 Stmt::If(if_stmt) => {
2174 let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
2176
2177 let branch = if is_truthy(&cond_value) {
2178 &if_stmt.then_branch
2179 } else {
2180 if_stmt.else_branch.as_deref().unwrap_or(&[])
2181 };
2182
2183 let mut result = ExecResult::success("");
2184 for stmt in branch {
2185 let flow = self.execute_stmt_flow(stmt).await?;
2186 match flow {
2187 ControlFlow::Normal(r) => {
2188 accumulate_result(&mut result, &r);
2189 self.drain_stderr_into(&mut result).await;
2190 }
2191 other => {
2192 self.drain_stderr_into(&mut result).await;
2193 return Ok(other);
2194 }
2195 }
2196 }
2197 Ok(ControlFlow::ok(result))
2198 }
2199 Stmt::For(for_loop) => {
2200 let mut items: Vec<Value> = Vec::new();
2203 for item_expr in &for_loop.items {
2204 if let Expr::GlobPattern(pattern) = item_expr {
2206 let glob_enabled = {
2207 let scope = self.scope.read().await;
2208 scope.glob_enabled()
2209 };
2210 if glob_enabled {
2211 let (paths, cwd) = {
2212 let ctx = self.exec_ctx.read().await;
2213 let paths = ctx.expand_glob(pattern).await
2214 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
2215 let cwd = ctx.resolve_path(".");
2216 (paths, cwd)
2217 };
2218 if paths.is_empty() {
2219 return Err(anyhow::anyhow!("no matches: {}", pattern));
2220 }
2221 for path in paths {
2222 let display = if !pattern.starts_with('/') {
2223 path.strip_prefix(&cwd)
2224 .unwrap_or(&path)
2225 .to_string_lossy().into_owned()
2226 } else {
2227 path.to_string_lossy().into_owned()
2228 };
2229 items.push(Value::String(display));
2230 }
2231 continue;
2232 }
2233 }
2234 let from_command_subst = matches!(item_expr, Expr::CommandSubst(_));
2240 let item = self.eval_expr_async(item_expr).await?;
2241 match item {
2242 Value::Json(serde_json::Value::Array(arr)) => {
2245 for elem in arr {
2246 items.push(json_to_value_no_envelope(elem));
2251 }
2252 }
2253 Value::String(s) if from_command_subst => {
2261 let trimmed = s.trim_end_matches(['\n', '\r']);
2262 if trimmed.is_empty() {
2263 continue;
2264 }
2265 if trimmed.contains('\n') {
2266 for line in trimmed.split('\n') {
2267 let line = line.trim_end_matches('\r');
2268 items.push(Value::String(line.to_string()));
2269 }
2270 } else {
2271 items.push(Value::String(trimmed.to_string()));
2272 }
2273 }
2274 Value::Bytes(_) => {
2277 anyhow::bail!(
2278 "for: cannot iterate over binary data — decode it \
2279 (base64/xxd) first"
2280 );
2281 }
2282 other => items.push(other),
2284 }
2285 }
2286
2287 let mut result = ExecResult::success("");
2288 {
2289 let mut scope = self.scope.write().await;
2290 scope.push_frame();
2291 }
2292
2293 'outer: for item in items {
2294 if self.is_cancelled() {
2296 let mut scope = self.scope.write().await;
2297 scope.pop_frame();
2298 result.code = 130;
2299 return Ok(ControlFlow::ok(result));
2300 }
2301 {
2302 let mut scope = self.scope.write().await;
2303 scope.set(&for_loop.variable, item);
2304 }
2305 for stmt in &for_loop.body {
2306 let mut flow = match self.execute_stmt_flow(stmt).await {
2307 Ok(f) => f,
2308 Err(e) => {
2309 let mut scope = self.scope.write().await;
2310 scope.pop_frame();
2311 return Err(e);
2312 }
2313 };
2314 self.drain_stderr_into(&mut result).await;
2315 match &mut flow {
2316 ControlFlow::Normal(r) => {
2317 accumulate_result(&mut result, r);
2318 if !r.ok() {
2319 let scope = self.scope.read().await;
2320 if scope.error_exit_enabled() {
2321 drop(scope);
2322 let mut scope = self.scope.write().await;
2323 scope.pop_frame();
2324 return Ok(ControlFlow::exit_code(r.code));
2325 }
2326 }
2327 }
2328 ControlFlow::Break { .. } => {
2329 if flow.decrement_level() {
2330 accumulate_flow_output(&mut result, &flow);
2331 break 'outer;
2332 }
2333 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2334 let mut scope = self.scope.write().await;
2335 scope.pop_frame();
2336 return Ok(flow);
2337 }
2338 ControlFlow::Continue { .. } => {
2339 if flow.decrement_level() {
2340 accumulate_flow_output(&mut result, &flow);
2341 continue 'outer;
2342 }
2343 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2344 let mut scope = self.scope.write().await;
2345 scope.pop_frame();
2346 return Ok(flow);
2347 }
2348 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
2349 let mut scope = self.scope.write().await;
2350 scope.pop_frame();
2351 return Ok(flow);
2352 }
2353 }
2354 }
2355 }
2356
2357 {
2358 let mut scope = self.scope.write().await;
2359 scope.pop_frame();
2360 }
2361 Ok(ControlFlow::ok(result))
2362 }
2363 Stmt::While(while_loop) => {
2364 let mut result = ExecResult::success("");
2365
2366 'outer: loop {
2367 if self.is_cancelled() {
2370 result.code = 130;
2371 return Ok(ControlFlow::ok(result));
2372 }
2373
2374 let cond_value = self.eval_expr_async(&while_loop.condition).await?;
2375
2376 if !is_truthy(&cond_value) {
2377 break;
2378 }
2379
2380 for stmt in &while_loop.body {
2382 let mut flow = self.execute_stmt_flow(stmt).await?;
2383 self.drain_stderr_into(&mut result).await;
2384 match &mut flow {
2385 ControlFlow::Normal(r) => {
2386 accumulate_result(&mut result, r);
2387 if !r.ok() {
2388 let scope = self.scope.read().await;
2389 if scope.error_exit_enabled() {
2390 return Ok(ControlFlow::exit_code(r.code));
2391 }
2392 }
2393 }
2394 ControlFlow::Break { .. } => {
2395 if flow.decrement_level() {
2396 accumulate_flow_output(&mut result, &flow);
2397 break 'outer;
2398 }
2399 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2400 return Ok(flow);
2401 }
2402 ControlFlow::Continue { .. } => {
2403 if flow.decrement_level() {
2404 accumulate_flow_output(&mut result, &flow);
2405 continue 'outer;
2406 }
2407 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2408 return Ok(flow);
2409 }
2410 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
2411 return Ok(flow);
2412 }
2413 }
2414 }
2415 }
2416
2417 Ok(ControlFlow::ok(result))
2418 }
2419 Stmt::Case(case_stmt) => {
2420 let match_value = {
2422 let value = self.eval_expr_async(&case_stmt.expr).await?;
2423 value_to_string(&value)
2424 };
2425
2426 for branch in &case_stmt.branches {
2428 let matched = branch.patterns.iter().any(|pattern| {
2429 glob_match(pattern, &match_value)
2430 });
2431
2432 if matched {
2433 let mut result = ExecResult::success("");
2435 for stmt in &branch.body {
2436 let flow = self.execute_stmt_flow(stmt).await?;
2437 match flow {
2438 ControlFlow::Normal(r) => {
2439 accumulate_result(&mut result, &r);
2440 self.drain_stderr_into(&mut result).await;
2441 }
2442 other => {
2443 self.drain_stderr_into(&mut result).await;
2444 return Ok(other);
2445 }
2446 }
2447 }
2448 return Ok(ControlFlow::ok(result));
2449 }
2450 }
2451
2452 Ok(ControlFlow::ok(ExecResult::success("")))
2454 }
2455 Stmt::Break(levels) => {
2456 Ok(ControlFlow::break_n(levels.unwrap_or(1)))
2457 }
2458 Stmt::Continue(levels) => {
2459 Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
2460 }
2461 Stmt::Return(expr) => {
2462 let result = if let Some(e) = expr {
2465 let val = self.eval_expr_async(e).await?;
2466 let code = crate::interpreter::value_to_exit_code(&val)
2467 .map_err(|e| anyhow::anyhow!("return: {}", e))?;
2468 ExecResult::from_parts(code, String::new(), String::new(), None)
2469 } else {
2470 ExecResult::success("")
2471 };
2472 Ok(ControlFlow::return_value(result))
2473 }
2474 Stmt::Exit(expr) => {
2475 let code = if let Some(e) = expr {
2476 let val = self.eval_expr_async(e).await?;
2477 crate::interpreter::value_to_exit_code(&val)
2478 .map_err(|e| anyhow::anyhow!("exit: {}", e))?
2479 } else {
2480 0
2481 };
2482 Ok(ControlFlow::exit_code(code))
2483 }
2484 Stmt::ToolDef(tool_def) => {
2485 let mut user_tools = self.user_tools.write().await;
2486 user_tools.insert(tool_def.name.clone(), tool_def.clone());
2487 Ok(ControlFlow::ok(ExecResult::success("")))
2488 }
2489 Stmt::AndChain { left, right } => {
2490 {
2493 let mut scope = self.scope.write().await;
2494 scope.suppress_errexit();
2495 }
2496 let left_flow = match self.execute_stmt_flow(left).await {
2497 Ok(f) => f,
2498 Err(e) => {
2499 let mut scope = self.scope.write().await;
2500 scope.unsuppress_errexit();
2501 return Err(e);
2502 }
2503 };
2504 {
2505 let mut scope = self.scope.write().await;
2506 scope.unsuppress_errexit();
2507 }
2508 match left_flow {
2509 ControlFlow::Normal(mut left_result) => {
2510 self.drain_stderr_into(&mut left_result).await;
2511 self.update_last_result(&left_result).await;
2512 if left_result.ok() {
2513 let right_flow = self.execute_stmt_flow(right).await?;
2514 match right_flow {
2515 ControlFlow::Normal(mut right_result) => {
2516 self.drain_stderr_into(&mut right_result).await;
2517 self.update_last_result(&right_result).await;
2518 let mut combined = left_result;
2519 accumulate_result(&mut combined, &right_result);
2520 Ok(ControlFlow::ok(combined))
2521 }
2522 other => Ok(other),
2523 }
2524 } else {
2525 Ok(ControlFlow::ok(left_result))
2526 }
2527 }
2528 _ => Ok(left_flow),
2529 }
2530 }
2531 Stmt::OrChain { left, right } => {
2532 {
2535 let mut scope = self.scope.write().await;
2536 scope.suppress_errexit();
2537 }
2538 let left_flow = match self.execute_stmt_flow(left).await {
2539 Ok(f) => f,
2540 Err(e) => {
2541 let mut scope = self.scope.write().await;
2542 scope.unsuppress_errexit();
2543 return Err(e);
2544 }
2545 };
2546 {
2547 let mut scope = self.scope.write().await;
2548 scope.unsuppress_errexit();
2549 }
2550 match left_flow {
2551 ControlFlow::Normal(mut left_result) => {
2552 self.drain_stderr_into(&mut left_result).await;
2553 self.update_last_result(&left_result).await;
2554 if !left_result.ok() {
2555 let right_flow = self.execute_stmt_flow(right).await?;
2556 match right_flow {
2557 ControlFlow::Normal(mut right_result) => {
2558 self.drain_stderr_into(&mut right_result).await;
2559 self.update_last_result(&right_result).await;
2560 let mut combined = left_result;
2561 accumulate_result(&mut combined, &right_result);
2562 Ok(ControlFlow::ok(combined))
2563 }
2564 other => Ok(other),
2565 }
2566 } else {
2567 Ok(ControlFlow::ok(left_result))
2568 }
2569 }
2570 _ => Ok(left_flow), }
2572 }
2573 Stmt::Test(test_expr) => {
2574 let is_true = self.eval_test_async(test_expr).await?;
2575 let result = if is_true {
2576 ExecResult::success("")
2577 } else {
2578 ExecResult::failure(1, "")
2579 };
2580 self.update_last_result(&result).await;
2586 if !result.ok() {
2587 let scope = self.scope.read().await;
2588 if scope.error_exit_enabled() {
2589 return Ok(ControlFlow::exit_code(result.code));
2590 }
2591 }
2592 Ok(ControlFlow::ok(result))
2593 }
2594 Stmt::EnvScoped { assignments, body } => {
2595 {
2602 let mut scope = self.scope.write().await;
2603 scope.push_frame();
2604 }
2605 let mut prior_export: Vec<(String, bool)> =
2606 Vec::with_capacity(assignments.len());
2607 let mut setup_err: Option<anyhow::Error> = None;
2608 for assign in assignments {
2609 match self.eval_expr_async(&assign.value).await {
2610 Ok(value) => {
2611 let mut scope = self.scope.write().await;
2612 prior_export
2613 .push((assign.name().to_string(), scope.is_exported(assign.name())));
2614 scope.set_exported(assign.name(), value);
2615 }
2616 Err(e) => {
2617 setup_err = Some(e);
2618 break;
2619 }
2620 }
2621 }
2622
2623 let flow = if setup_err.is_none() {
2624 self.execute_stmt_flow(body).await
2625 } else {
2626 Ok(ControlFlow::ok(ExecResult::success("")))
2627 };
2628
2629 {
2632 let mut scope = self.scope.write().await;
2633 scope.pop_frame();
2634 for (name, was_exported) in &prior_export {
2635 if !*was_exported {
2636 scope.unexport(name);
2637 }
2638 }
2639 }
2640
2641 match setup_err {
2642 Some(e) => Err(e),
2643 None => flow,
2644 }
2645 }
2646 Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
2647 }
2648 }.instrument(span))
2649 }
2650
2651 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(background = pipeline.background, command_count = pipeline.commands.len()))]
2653 async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
2654 if pipeline.commands.is_empty() {
2655 return Ok(ExecResult::success(""));
2656 }
2657
2658 if pipeline.background {
2660 return self.execute_background(pipeline).await;
2661 }
2662
2663 let (mut ctx, has_pipe_stdin) = {
2671 let ec = self.exec_ctx.read().await;
2672 let scope = self.scope.read().await;
2673 let has_pipe_stdin = ec.pipe_stdin.is_some();
2677 (ExecContext {
2678 backend: ec.backend.clone(),
2679 scope: scope.clone(),
2680 cwd: ec.cwd.clone(),
2681 prev_cwd: ec.prev_cwd.clone(),
2682 stdin: ec.stdin.clone(),
2687 stdin_data: ec.stdin_data.clone(),
2688 stdin_data_rx: None,
2689 pipe_stdin: None,
2690 pipe_stdout: None,
2691 stderr: ec.stderr.clone(),
2692 tool_schemas: ec.tool_schemas.clone(),
2693 tools: ec.tools.clone(),
2694 job_manager: ec.job_manager.clone(),
2695 pipeline_position: PipelinePosition::Only,
2696 interactive: self.interactive,
2697 aliases: ec.aliases.clone(),
2698 ignore_config: ec.ignore_config.clone(),
2699 output_limit: ec.output_limit.clone(),
2700 allow_external_commands: self.allow_external_commands,
2701 nonce_store: ec.nonce_store.clone(),
2702 trash_backend: ec.trash_backend.clone(),
2703 #[cfg(all(unix, feature = "subprocess"))]
2704 terminal_state: ec.terminal_state.clone(),
2705 dispatcher: self.dispatcher(),
2706 cancel: {
2707 #[allow(clippy::expect_used)]
2708 let token = self.cancel_token.lock().expect("cancel_token poisoned");
2709 token.clone()
2710 },
2711 output_format: None,
2712 current_invocation: None,
2713 vfs_budget: self.vfs_budget.clone(),
2714 watchdog: ec.watchdog.clone(),
2715 #[cfg(all(feature = "localfs", feature = "overlay"))]
2716 overlay_handle: self.overlay_handle.clone(),
2717 }, has_pipe_stdin)
2718 }; if ctx.stdin.is_some() || ctx.stdin_data.is_some() || has_pipe_stdin {
2726 let mut ec = self.exec_ctx.write().await;
2727 ctx.pipe_stdin = ec.pipe_stdin.take();
2728 ec.stdin = None;
2729 ec.stdin_data = None;
2730 }
2731
2732 let mut result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
2733
2734 if ctx.output_limit.is_enabled() {
2736 let _ = crate::output_limit::spill_if_needed(&mut result, &ctx.output_limit).await;
2737 }
2738
2739 if result.did_spill {
2742 result.original_code = Some(result.code);
2743 result.code = 3;
2744 }
2745
2746 {
2748 let mut ec = self.exec_ctx.write().await;
2749 ec.cwd = ctx.cwd.clone();
2750 ec.prev_cwd = ctx.prev_cwd.clone();
2751 ec.aliases = ctx.aliases.clone();
2752 ec.ignore_config = ctx.ignore_config.clone();
2753 ec.output_limit = ctx.output_limit.clone();
2754 }
2755 {
2756 let mut scope = self.scope.write().await;
2757 *scope = ctx.scope.clone();
2758 }
2759
2760 Ok(result)
2761 }
2762
2763 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
2771 async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
2772 use tokio::sync::oneshot;
2773
2774 let command_str = self.format_pipeline(pipeline);
2776
2777 let stdout = Arc::new(BoundedStream::default_size());
2779 let stderr = Arc::new(BoundedStream::default_size());
2780
2781 let (tx, rx) = oneshot::channel();
2783
2784 let job_id = self.jobs.register_with_streams(
2786 command_str.clone(),
2787 rx,
2788 stdout.clone(),
2789 stderr.clone(),
2790 ).await;
2791
2792 let cancel = tokio_util::sync::CancellationToken::new();
2803 self.jobs.set_cancel_token(job_id, cancel.clone()).await;
2804 let fork = self.fork_for_background(cancel, job_id).await;
2805 let runner = self.runner.clone();
2806 let commands = pipeline.commands.clone();
2807
2808 let mut bg_ctx = {
2812 let ec = fork.exec_ctx.read().await;
2813 ec.child_for_pipeline()
2814 };
2815 bg_ctx.scope = fork.scope.read().await.clone();
2816 bg_ctx.dispatcher = fork.dispatcher();
2820
2821 tokio::spawn(crate::telemetry::bind_current_context(async move {
2824 let result = runner.run(&commands, &mut bg_ctx, fork.as_ref()).await;
2827
2828 let text = result.text_out();
2830 if !text.is_empty() {
2831 stdout.write(text.as_bytes()).await;
2832 }
2833 if !result.err.is_empty() {
2834 stderr.write(result.err.as_bytes()).await;
2835 }
2836
2837 stdout.close().await;
2839 stderr.close().await;
2840
2841 let _ = tx.send(result);
2843 }));
2844
2845 Ok(ExecResult::success(format!("[{}]", job_id)))
2846 }
2847
2848 fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
2850 pipeline.commands
2851 .iter()
2852 .map(|cmd| {
2853 let mut parts = vec![cmd.name.clone()];
2854 for arg in &cmd.args {
2855 match arg {
2856 Arg::Positional(expr) => {
2857 parts.push(self.format_expr(expr));
2858 }
2859 Arg::Named { key, value } => {
2860 parts.push(format!("--{}={}", key, self.format_expr(value)));
2861 }
2862 Arg::WordAssign { key, value } => {
2863 parts.push(format!("{}={}", key, self.format_expr(value)));
2864 }
2865 Arg::ShortFlag(name) => {
2866 parts.push(format!("-{}", name));
2867 }
2868 Arg::LongFlag(name) => {
2869 parts.push(format!("--{}", name));
2870 }
2871 Arg::DoubleDash => {
2872 parts.push("--".to_string());
2873 }
2874 }
2875 }
2876 parts.join(" ")
2877 })
2878 .collect::<Vec<_>>()
2879 .join(" | ")
2880 }
2881
2882 fn format_expr(&self, expr: &Expr) -> String {
2884 match expr {
2885 Expr::Literal(Value::String(s)) => {
2886 if s.contains(' ') || s.contains('"') {
2887 format!("'{}'", s.replace('\'', "\\'"))
2888 } else {
2889 s.clone()
2890 }
2891 }
2892 Expr::Literal(Value::Int(i)) => i.to_string(),
2893 Expr::Literal(Value::Float(f)) => f.to_string(),
2894 Expr::Literal(Value::Bool(b)) => b.to_string(),
2895 Expr::Literal(Value::Null) => "null".to_string(),
2896 Expr::VarRef(path) => {
2897 let mut name = String::new();
2898 for (i, seg) in path.segments.iter().enumerate() {
2899 match seg {
2900 crate::ast::VarSegment::Field(f) => {
2901 if i > 0 {
2902 name.push('.');
2903 }
2904 name.push_str(f);
2905 }
2906 crate::ast::VarSegment::Index(idx) => name.push_str(&format!("[{idx}]")),
2907 crate::ast::VarSegment::Key(k) => name.push_str(&format!("[{k}]")),
2908 crate::ast::VarSegment::Dynamic(v) => name.push_str(&format!("[${v}]")),
2909 crate::ast::VarSegment::Slice(a, b) => name.push_str(&format!(
2910 "[{}:{}]",
2911 a.map(|n| n.to_string()).unwrap_or_default(),
2912 b.map(|n| n.to_string()).unwrap_or_default()
2913 )),
2914 }
2915 }
2916 format!("${{{}}}", name)
2917 }
2918 Expr::Interpolated(_) => "\"...\"".to_string(),
2919 Expr::HereDocBody { .. } => "<<heredoc".to_string(),
2920 _ => "...".to_string(),
2921 }
2922 }
2923
2924 async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
2926 self.execute_command_depth(name, args, 0).await
2927 }
2928
2929 #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
2930 async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
2931 if let Some(form) = crate::validator::SpecialForm::from_name(name) {
2938 return match form {
2939 crate::validator::SpecialForm::True => Ok(ExecResult::success("")),
2940 crate::validator::SpecialForm::False => Ok(ExecResult::failure(1, "")),
2941 crate::validator::SpecialForm::Source => self.execute_source(args).await,
2942 };
2943 }
2944
2945 if alias_depth < 10 {
2947 let alias_value = {
2948 let ctx = self.exec_ctx.read().await;
2949 ctx.aliases.get(name).cloned()
2950 };
2951 if let Some(alias_val) = alias_value {
2952 let parts: Vec<&str> = alias_val.split_whitespace().collect();
2954 if let Some((alias_cmd, alias_args)) = parts.split_first() {
2955 let mut new_args: Vec<Arg> = alias_args
2956 .iter()
2957 .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
2958 .collect();
2959 new_args.extend_from_slice(args);
2960 return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
2961 }
2962 }
2963 }
2964
2965 if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
2967 return match self.tools.get(builtin_name) {
2968 Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
2969 None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
2970 };
2971 }
2972
2973 {
2975 let user_tools = self.user_tools.read().await;
2976 if let Some(tool_def) = user_tools.get(name) {
2977 let tool_def = tool_def.clone();
2978 drop(user_tools);
2979 return self.execute_user_tool(tool_def, args).await;
2980 }
2981 }
2982
2983 let tool = match self.tools.get(name) {
2985 Some(t) => t,
2986 None => {
2987 if let Some(result) = self.try_execute_script(name, args).await? {
2989 return Ok(result);
2990 }
2991 if let Some(result) = self.try_execute_external(name, args).await? {
2993 return Ok(result);
2994 }
2995
2996 let backend = self.exec_ctx.read().await.backend.clone();
3001 let tool_schema = backend
3002 .get_tool(name)
3003 .await
3004 .unwrap_or_else(|e| {
3005 tracing::debug!("backend get_tool error for {name}: {e}");
3010 None
3011 })
3012 .map(|t| {
3013 let mut s = t.schema;
3014 if s.subcommands.is_empty() {
3020 s.map_positionals = true;
3021 }
3022 s
3023 });
3024 let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
3025 let mut ctx = self.exec_ctx.write().await;
3026 {
3027 let scope = self.scope.read().await;
3028 ctx.scope = scope.clone();
3029 }
3030 let backend = ctx.backend.clone();
3031 match backend.call_tool(name, tool_args, &mut *ctx).await {
3032 Ok(tool_result) => {
3033 let mut scope = self.scope.write().await;
3034 *scope = ctx.scope.clone();
3035 return Ok(ExecResult::from(tool_result));
3041 }
3042 Err(BackendError::ToolNotFound(_)) => {
3043 }
3046 Err(e) => {
3047 return Ok(ExecResult::failure(1, format!("{}: {}", name, e)));
3052 }
3053 }
3054
3055 return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
3056 }
3057 };
3058
3059 let schema = tool.schema();
3061 let tool_args = self.build_args_async(args, Some(&schema)).await?;
3062
3063 let schema_claims = |flag: &str| -> bool {
3071 let bare = flag.trim_start_matches('-');
3072 schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
3073 };
3074 let wants_help = !schema.owns_output
3075 && ((tool_args.flags.contains("help") && !schema_claims("help"))
3076 || (tool_args.flags.contains("h") && !schema_claims("-h")));
3077 if wants_help {
3078 let help_topic = crate::help::HelpTopic::Tool(name.to_string());
3079 let ctx = self.exec_ctx.read().await;
3080 let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
3081 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
3082 }
3083
3084 let mut ctx = {
3090 let ec = self.exec_ctx.write().await;
3091 let scope = self.scope.read().await;
3092 ExecContext {
3093 backend: ec.backend.clone(),
3094 scope: scope.clone(),
3095 cwd: ec.cwd.clone(),
3096 prev_cwd: ec.prev_cwd.clone(),
3097 stdin: ec.stdin.clone(),
3098 stdin_data: ec.stdin_data.clone(),
3099 stdin_data_rx: None,
3100 pipe_stdin: None, pipe_stdout: None,
3102 stderr: ec.stderr.clone(),
3103 tool_schemas: ec.tool_schemas.clone(),
3104 tools: ec.tools.clone(),
3105 job_manager: ec.job_manager.clone(),
3106 pipeline_position: ec.pipeline_position,
3107 interactive: self.interactive,
3108 aliases: ec.aliases.clone(),
3109 ignore_config: ec.ignore_config.clone(),
3110 output_limit: ec.output_limit.clone(),
3111 allow_external_commands: self.allow_external_commands,
3112 nonce_store: ec.nonce_store.clone(),
3113 trash_backend: ec.trash_backend.clone(),
3114 #[cfg(all(unix, feature = "subprocess"))]
3115 terminal_state: ec.terminal_state.clone(),
3116 dispatcher: self.dispatcher(),
3117 cancel: ec.cancel.clone(),
3123 output_format: None,
3124 current_invocation: None,
3125 vfs_budget: self.vfs_budget.clone(),
3126 watchdog: ec.watchdog.clone(),
3127 #[cfg(all(feature = "localfs", feature = "overlay"))]
3128 overlay_handle: self.overlay_handle.clone(),
3129 }
3130 }; {
3136 let mut ec = self.exec_ctx.write().await;
3137 ctx.stdin = ec.stdin.take();
3138 ctx.stdin_data = ec.stdin_data.take();
3139 ctx.stdin_data_rx = ec.stdin_data_rx.take();
3140 ctx.pipe_stdin = ec.pipe_stdin.take();
3141 ctx.pipe_stdout = ec.pipe_stdout.take();
3142 }
3143
3144 GlobalFlags::apply_from_args(&tool_args, &mut ctx);
3149
3150 ctx.current_invocation = Some(Box::new((name.to_string(), tool_args.to_argv())));
3165
3166 let result = tool.execute(tool_args, &mut ctx).await;
3167
3168 {
3175 let mut scope = self.scope.write().await;
3176 *scope = ctx.scope.clone();
3177 }
3178 {
3179 let mut ec = self.exec_ctx.write().await;
3180 ec.cwd = ctx.cwd;
3181 ec.prev_cwd = ctx.prev_cwd;
3182 ec.aliases = ctx.aliases;
3183 ec.output_limit = ctx.output_limit.clone();
3188 ec.pipe_stdin = ctx.pipe_stdin.take();
3189 ec.pipe_stdout = ctx.pipe_stdout.take();
3190 }
3191
3192 let result = finalize_output(result, ctx.output_format, schema.owns_output);
3197
3198 Ok(result)
3199 }
3200
3201 async fn scope_home(&self) -> Option<String> {
3206 match self.scope.read().await.get("HOME") {
3207 Some(Value::String(s)) => Some(s.clone()),
3208 _ => None,
3209 }
3210 }
3211
3212 #[allow(clippy::too_many_arguments)]
3233 async fn consume_flag_positionals(
3234 &self,
3235 args: &[Arg],
3236 flag_name: &str,
3237 canonical: &str,
3238 consumes: usize,
3239 repeatable: bool,
3240 positional_indices: &[usize],
3241 consumed: &mut std::collections::HashSet<usize>,
3242 current_idx: usize,
3243 tool_args: &mut ToolArgs,
3244 ) -> Result<()> {
3245 let home = self.scope_home().await;
3246 let mut collected: Vec<Value> = Vec::with_capacity(consumes.max(1));
3247 for _ in 0..consumes.max(1) {
3248 let allow_word_assign = consumes <= 1;
3254 let next_pos = positional_indices
3255 .iter()
3256 .find(|idx| {
3257 **idx > current_idx
3258 && !consumed.contains(idx)
3259 && (allow_word_assign || matches!(args[**idx], Arg::Positional(_)))
3260 })
3261 .copied();
3262 match next_pos {
3263 Some(pos_idx) => match &args[pos_idx] {
3264 Arg::Positional(expr) => {
3265 let value = self.eval_expr_async(expr).await?;
3266 let value = apply_tilde_expansion(value, home.as_deref());
3267 collected.push(value);
3268 consumed.insert(pos_idx);
3269 }
3270 Arg::WordAssign { key, value } => {
3273 let val = self.eval_expr_async(value).await?;
3274 let val = apply_tilde_expansion(val, home.as_deref());
3275 let val_str = crate::interpreter::value_to_string(&val);
3276 collected.push(Value::String(format!("{key}={val_str}")));
3277 consumed.insert(pos_idx);
3278 }
3279 _ => {}
3280 },
3281 None => {
3282 if consumes <= 1 && collected.is_empty() {
3283 tool_args.flags.insert(flag_name.to_string());
3287 return Ok(());
3288 }
3289 anyhow::bail!(
3290 "--{flag_name} requires {consumes} argument{}, got {}",
3291 if consumes == 1 { "" } else { "s" },
3292 collected.len()
3293 );
3294 }
3295 }
3296 }
3297
3298 if consumes <= 1 {
3299 if let Some(v) = collected.pop() {
3300 if repeatable {
3301 push_repeatable_value(tool_args, flag_name, canonical, v)?;
3302 } else {
3303 tool_args.named.insert(canonical.to_string(), v);
3304 }
3305 }
3306 return Ok(());
3307 }
3308
3309 let occ: Vec<serde_json::Value> = collected
3311 .into_iter()
3312 .map(|v| crate::interpreter::value_to_json(&v))
3313 .collect();
3314 let entry = tool_args
3315 .named
3316 .entry(canonical.to_string())
3317 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
3318 if let Value::Json(serde_json::Value::Array(outer)) = entry {
3319 outer.push(serde_json::Value::Array(occ));
3320 } else {
3321 anyhow::bail!(
3322 "--{flag_name}: named[{canonical}] already holds a non-array value"
3323 );
3324 }
3325 Ok(())
3326 }
3327
3328 async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
3332 let mut tool_args = ToolArgs::new();
3333 let home = self.scope_home().await;
3334
3335 if schema.is_some_and(|s| s.raw_argv) {
3345 for arg in args {
3346 match arg {
3347 Arg::Positional(expr) => {
3348 let glob = if let Expr::GlobPattern(p) = expr {
3349 self.scope.read().await.glob_enabled().then(|| p.clone())
3350 } else {
3351 None
3352 };
3353 if let Some(pattern) = glob {
3354 let (paths, cwd) = {
3355 let ctx = self.exec_ctx.read().await;
3356 let paths = ctx
3357 .expand_glob(&pattern)
3358 .await
3359 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3360 let cwd = ctx.resolve_path(".");
3361 (paths, cwd)
3362 };
3363 if paths.is_empty() {
3364 return Err(anyhow::anyhow!("no matches: {}", pattern));
3365 }
3366 for path in paths {
3367 let display = if !pattern.starts_with('/') {
3368 path.strip_prefix(&cwd)
3369 .unwrap_or(&path)
3370 .to_string_lossy()
3371 .into_owned()
3372 } else {
3373 path.to_string_lossy().into_owned()
3374 };
3375 tool_args.positional.push(Value::String(display));
3376 }
3377 } else {
3378 let value = self.eval_expr_async(expr).await?;
3379 let value = apply_tilde_expansion(value, home.as_deref());
3380 tool_args.positional.push(value);
3381 }
3382 }
3383 Arg::ShortFlag(name) => {
3384 tool_args.positional.push(Value::String(format!("-{name}")));
3385 }
3386 Arg::LongFlag(name) => {
3387 tool_args.positional.push(Value::String(format!("--{name}")));
3388 }
3389 Arg::Named { key, value } => {
3390 let val = self.eval_expr_async(value).await?;
3391 let val = apply_tilde_expansion(val, home.as_deref());
3392 tool_args.positional.push(Value::String(format!(
3393 "--{key}={}",
3394 crate::interpreter::value_to_string(&val)
3395 )));
3396 }
3397 Arg::WordAssign { key, value } => {
3398 let val = self.eval_expr_async(value).await?;
3399 let val = apply_tilde_expansion(val, home.as_deref());
3400 tool_args.positional.push(Value::String(format!(
3401 "{key}={}",
3402 crate::interpreter::value_to_string(&val)
3403 )));
3404 }
3405 Arg::DoubleDash => {
3406 tool_args.positional.push(Value::String("--".to_string()));
3407 }
3408 }
3409 }
3410 return Ok(tool_args);
3411 }
3412
3413 let leaf = match schema {
3419 Some(s) => Some(select_leaf(s, args)?),
3420 None => None,
3421 };
3422 let mut param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
3429 if let Some(l) = leaf {
3430 param_lookup.extend(schema_param_lookup(l));
3431 }
3432 let accepts_word_assign = schema
3435 .map(|s| crate::tools::accepts_word_assign(s.name.as_str()))
3436 .unwrap_or(false);
3437
3438 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
3440 let mut past_double_dash = false;
3441
3442 let positional_indices: Vec<usize> = args.iter().enumerate()
3450 .filter_map(|(i, a)| {
3451 let consumable = matches!(a, Arg::Positional(_))
3452 || (!accepts_word_assign && matches!(a, Arg::WordAssign { .. }));
3453 consumable.then_some(i)
3454 })
3455 .collect();
3456
3457 let mut i = 0;
3458 while i < args.len() {
3459 match &args[i] {
3460 Arg::DoubleDash => {
3461 past_double_dash = true;
3462 }
3463 Arg::Positional(expr) => {
3464 if !consumed.contains(&i) {
3465 if let Expr::GlobPattern(pattern) = expr {
3467 let glob_enabled = {
3468 let scope = self.scope.read().await;
3469 scope.glob_enabled()
3470 };
3471 if glob_enabled {
3472 let (paths, cwd) = {
3473 let ctx = self.exec_ctx.read().await;
3474 let paths = ctx.expand_glob(pattern).await
3475 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3476 let cwd = ctx.resolve_path(".");
3477 (paths, cwd)
3478 };
3479 if paths.is_empty() {
3480 return Err(anyhow::anyhow!("no matches: {}", pattern));
3481 }
3482 for path in paths {
3483 let display = if !pattern.starts_with('/') {
3484 path.strip_prefix(&cwd)
3485 .unwrap_or(&path)
3486 .to_string_lossy().into_owned()
3487 } else {
3488 path.to_string_lossy().into_owned()
3489 };
3490 tool_args.positional.push(Value::String(display));
3491 }
3492 i += 1;
3493 continue;
3494 }
3495 }
3496 let value = self.eval_expr_async(expr).await?;
3497 let value = apply_tilde_expansion(value, home.as_deref());
3498 tool_args.positional.push(value);
3499 }
3500 }
3501 Arg::Named { key, value } => {
3502 let val = self.eval_expr_async(value).await?;
3503 let val = apply_tilde_expansion(val, home.as_deref());
3504 if let Some(&(canonical, _, _, true)) = param_lookup.get(key.as_str()) {
3510 push_repeatable_value(&mut tool_args, key, canonical, val)?;
3511 } else {
3512 tool_args.named.insert(key.clone(), val);
3513 }
3514 }
3515 Arg::WordAssign { key, value } => {
3516 if consumed.contains(&i) {
3519 i += 1;
3520 continue;
3521 }
3522 let val = self.eval_expr_async(value).await?;
3523 let val = apply_tilde_expansion(val, home.as_deref());
3524 if accepts_word_assign {
3525 tool_args.named.insert(key.clone(), val);
3526 } else {
3527 let val_str = crate::interpreter::value_to_string(&val);
3530 tool_args.positional.push(Value::String(format!("{key}={val_str}")));
3531 }
3532 }
3533 Arg::ShortFlag(name) => {
3534 if past_double_dash {
3535 tool_args.positional.push(Value::String(format!("-{name}")));
3536 } else if name.len() == 1 {
3537 let flag_name = name.as_str();
3538 let lookup = param_lookup.get(flag_name);
3539 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3540
3541 if is_bool {
3542 tool_args.flags.insert(flag_name.to_string());
3543 } else {
3544 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(flag_name);
3546 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3547 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3548 self.consume_flag_positionals(
3549 args,
3550 name,
3551 canonical,
3552 consumes,
3553 repeatable,
3554 &positional_indices,
3555 &mut consumed,
3556 i,
3557 &mut tool_args,
3558 )
3559 .await?;
3560 }
3561 } else if let Some(&(canonical, typ, consumes, repeatable)) = param_lookup.get(name.as_str()) {
3562 if is_bool_type(typ) {
3564 tool_args.flags.insert(canonical.to_string());
3565 } else {
3566 self.consume_flag_positionals(
3567 args,
3568 name,
3569 canonical,
3570 consumes,
3571 repeatable,
3572 &positional_indices,
3573 &mut consumed,
3574 i,
3575 &mut tool_args,
3576 )
3577 .await?;
3578 }
3579 } else if let Some(&(canonical, _, consumes, repeatable)) = param_lookup
3580 .get(&name[..1])
3581 .filter(|(_, typ, ..)| !is_bool_type(typ))
3582 {
3583 bind_glued_short_value(
3590 &mut tool_args,
3591 &name[..1],
3592 canonical,
3593 consumes,
3594 repeatable,
3595 name[1..].to_string(),
3596 )?;
3597 } else {
3598 let bytes = name.as_bytes();
3612 let mut p = 0;
3613 while p < bytes.len() {
3614 let key = &name[p..p + 1];
3615 match param_lookup.get(key) {
3616 Some(&(canonical, typ, consumes, repeatable))
3617 if !is_bool_type(typ) =>
3618 {
3619 let glued = name[p + 1..].to_string();
3620 if glued.is_empty() {
3621 self.consume_flag_positionals(
3625 args,
3626 key,
3627 canonical,
3628 consumes,
3629 repeatable,
3630 &positional_indices,
3631 &mut consumed,
3632 i,
3633 &mut tool_args,
3634 )
3635 .await?;
3636 } else {
3637 bind_glued_short_value(
3638 &mut tool_args,
3639 key,
3640 canonical,
3641 consumes,
3642 repeatable,
3643 glued,
3644 )?;
3645 }
3646 break;
3647 }
3648 _ => {
3649 tool_args.flags.insert(key.to_string());
3650 p += 1;
3651 }
3652 }
3653 }
3654 }
3655 }
3656 Arg::LongFlag(name) => {
3657 if past_double_dash {
3658 tool_args.positional.push(Value::String(format!("--{name}")));
3659 } else {
3660 let lookup = param_lookup.get(name.as_str());
3661 let ambiguous_value = (lookup.is_none()
3670 && leaf.is_some_and(|s| s.map_positionals)
3671 && !consumed.contains(&(i + 1)))
3672 .then(|| match args.get(i + 1) {
3673 Some(Arg::Positional(Expr::Literal(Value::String(s)))) => {
3676 Some(s.clone())
3677 }
3678 Some(Arg::Positional(_)) => Some("VALUE".to_string()),
3679 _ => None,
3680 })
3681 .flatten();
3682 if let Some(val) = ambiguous_value {
3683 let tool = leaf.map(|s| s.name.as_str()).unwrap_or("command");
3684 anyhow::bail!(
3685 "{tool}: --{name} is not a declared flag, so the \
3686 space-separated value would be silently dropped. \
3687 Use --{name}={val}, or have {tool} declare --{name} \
3688 in its schema."
3689 );
3690 }
3691 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3692
3693 if is_bool {
3694 tool_args.flags.insert(name.clone());
3695 } else {
3696 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(name.as_str());
3697 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3698 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3699 self.consume_flag_positionals(
3700 args,
3701 name,
3702 canonical,
3703 consumes,
3704 repeatable,
3705 &positional_indices,
3706 &mut consumed,
3707 i,
3708 &mut tool_args,
3709 )
3710 .await?;
3711 }
3712 }
3713 }
3714 }
3715 i += 1;
3716 }
3717
3718 if let Some(schema) = leaf.filter(|s| s.map_positionals) {
3725 let pre_dash_count = if past_double_dash {
3726 let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
3727 positional_indices.iter()
3728 .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
3729 .count()
3730 } else {
3731 tool_args.positional.len()
3732 };
3733
3734 let mut remaining = Vec::new();
3735 let mut positional_iter = tool_args.positional.drain(..).enumerate();
3736
3737 for param in &schema.params {
3738 if tool_args.named.contains_key(¶m.name) || tool_args.flags.contains(¶m.name) {
3739 continue;
3740 }
3741 if is_bool_type(¶m.param_type) {
3742 continue;
3743 }
3744 loop {
3745 match positional_iter.next() {
3746 Some((idx, val)) if idx < pre_dash_count => {
3747 tool_args.named.insert(param.name.clone(), val);
3748 break;
3749 }
3750 Some((_, val)) => {
3751 remaining.push(val);
3752 }
3753 None => break,
3754 }
3755 }
3756 }
3757
3758 remaining.extend(positional_iter.map(|(_, v)| v));
3759 tool_args.positional = remaining;
3760 }
3761
3762 Ok(tool_args)
3763 }
3764
3765 #[cfg(feature = "subprocess")]
3775 async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
3776 let mut argv = Vec::new();
3777 let home = self.scope_home().await;
3778 for arg in args {
3779 match arg {
3780 Arg::Positional(expr) => {
3781 if let Expr::GlobPattern(pattern) = expr {
3783 let glob_enabled = {
3784 let scope = self.scope.read().await;
3785 scope.glob_enabled()
3786 };
3787 if glob_enabled {
3788 let (paths, cwd) = {
3789 let ctx = self.exec_ctx.read().await;
3790 let paths = ctx.expand_glob(pattern).await
3791 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3792 let cwd = ctx.resolve_path(".");
3793 (paths, cwd)
3794 };
3795 if paths.is_empty() {
3796 return Err(anyhow::anyhow!("no matches: {}", pattern));
3797 }
3798 for path in paths {
3799 let display = if !pattern.starts_with('/') {
3800 path.strip_prefix(&cwd)
3801 .unwrap_or(&path)
3802 .to_string_lossy().into_owned()
3803 } else {
3804 path.to_string_lossy().into_owned()
3805 };
3806 argv.push(display);
3807 }
3808 continue;
3809 }
3810 }
3811 let value = self.eval_expr_async(expr).await?;
3812 if let Some(msg) = crate::interpreter::structured_boundary_error("a command argument", &value) {
3818 return Err(anyhow::anyhow!(msg));
3819 }
3820 let value = apply_tilde_expansion(value, home.as_deref());
3821 argv.push(value_to_text_sink(&value).map_err(|e| anyhow::anyhow!("{e}"))?);
3824 }
3825 Arg::Named { key, value } => {
3826 let val = self.eval_expr_async(value).await?;
3827 if let Some(msg) = crate::interpreter::structured_boundary_error("a command argument", &val) {
3828 return Err(anyhow::anyhow!(msg));
3829 }
3830 let val = apply_tilde_expansion(val, home.as_deref());
3831 let val_str = value_to_text_sink(&val).map_err(|e| anyhow::anyhow!("{e}"))?;
3832 argv.push(format!("--{key}={val_str}"));
3833 }
3834 Arg::WordAssign { key, value } => {
3835 let val = self.eval_expr_async(value).await?;
3836 if let Some(msg) = crate::interpreter::structured_boundary_error("a command argument", &val) {
3837 return Err(anyhow::anyhow!(msg));
3838 }
3839 let val = apply_tilde_expansion(val, home.as_deref());
3840 let val_str = value_to_text_sink(&val).map_err(|e| anyhow::anyhow!("{e}"))?;
3841 argv.push(format!("{key}={val_str}"));
3842 }
3843 Arg::ShortFlag(name) => {
3844 argv.push(format!("-{}", name));
3846 }
3847 Arg::LongFlag(name) => {
3848 argv.push(format!("--{}", name));
3850 }
3851 Arg::DoubleDash => {
3852 argv.push("--".to_string());
3854 }
3855 }
3856 }
3857 Ok(argv)
3858 }
3859
3860 fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
3865 Box::pin(async move {
3866 match expr {
3867 Expr::Literal(value) => Ok(value.clone()),
3868 Expr::VarRef(path) => {
3869 let scope = self.scope.read().await;
3870 match scope.resolve_path(path) {
3871 Ok(v) => Ok(v),
3872 Err(PathError::UndefinedRoot(_)) => {
3873 Err(anyhow::anyhow!("undefined variable"))
3874 }
3875 Err(PathError::Absence(msg)) | Err(PathError::Shape(msg)) => {
3876 Err(anyhow::anyhow!(msg))
3877 }
3878 }
3879 }
3880 Expr::Interpolated(parts) => {
3881 let mut result = String::new();
3882 for part in parts {
3883 result.push_str(&self.eval_string_part_async(part).await?);
3884 }
3885 Ok(Value::String(result))
3886 }
3887 Expr::HereDocBody { parts, strip_tabs } => {
3888 let mut asm = crate::interpreter::HeredocAssembler::new(*strip_tabs);
3892 for sp in parts {
3893 match &sp.part {
3894 StringPart::Literal(s) => asm.push_literal(s),
3895 other => {
3896 asm.push_interpolated(&self.eval_string_part_async(other).await?)
3897 }
3898 }
3899 }
3900 Ok(Value::String(asm.into_string()))
3901 }
3902 Expr::BinaryOp { left, op, right } => match op {
3903 BinaryOp::And => {
3904 let left_val = self.eval_expr_async(left).await?;
3905 if !is_truthy(&left_val) {
3906 return Ok(left_val);
3907 }
3908 self.eval_expr_async(right).await
3909 }
3910 BinaryOp::Or => {
3911 let left_val = self.eval_expr_async(left).await?;
3912 if is_truthy(&left_val) {
3913 return Ok(left_val);
3914 }
3915 self.eval_expr_async(right).await
3916 }
3917 },
3918 Expr::CommandSubst(stmts) => {
3919 let saved_scope = { self.scope.read().await.clone() };
3922 let saved_cwd = {
3923 let ec = self.exec_ctx.read().await;
3924 (ec.cwd.clone(), ec.prev_cwd.clone())
3925 };
3926
3927 let run_result = self.execute_block_capturing(stmts).await;
3929
3930 {
3932 let mut scope = self.scope.write().await;
3933 *scope = saved_scope;
3934 if let Ok(ref r) = run_result {
3935 scope.set_last_result(r.clone());
3936 }
3937 }
3938 {
3939 let mut ec = self.exec_ctx.write().await;
3940 ec.cwd = saved_cwd.0;
3941 ec.prev_cwd = saved_cwd.1;
3942 }
3943
3944 let result = run_result?;
3946
3947 if let Some(bytes) = result.out_bytes() {
3950 Ok(Value::Bytes(bytes.to_vec()))
3951 } else if let Some(data) = &result.data {
3953 Ok(data.clone())
3954 } else if let Some(output) = result.output() {
3955 if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
3957 let items: Vec<serde_json::Value> = output.root.iter()
3958 .map(|n| serde_json::Value::String(n.display_name().to_string()))
3959 .collect();
3960 Ok(Value::Json(serde_json::Value::Array(items)))
3961 } else {
3962 Ok(Value::String(
3969 result.text_out().trim_end_matches('\n').to_string(),
3970 ))
3971 }
3972 } else {
3973 Ok(Value::String(
3975 result.text_out().trim_end_matches('\n').to_string(),
3976 ))
3977 }
3978 }
3979 Expr::Test(test_expr) => {
3980 Ok(Value::Bool(self.eval_test_async(test_expr).await?))
3981 }
3982 Expr::Positional(n) => {
3983 let scope = self.scope.read().await;
3984 match scope.get_positional(*n) {
3985 Some(s) => Ok(Value::String(s.to_string())),
3986 None => Ok(Value::String(String::new())),
3987 }
3988 }
3989 Expr::AllArgs => {
3990 let scope = self.scope.read().await;
3991 Ok(Value::String(scope.all_args().join(" ")))
3992 }
3993 Expr::ArgCount => {
3994 let scope = self.scope.read().await;
3995 Ok(Value::Int(scope.arg_count() as i64))
3996 }
3997 Expr::VarLength(path) => {
3998 let scope = self.scope.read().await;
3999 crate::interpreter::resolve_length(&scope, path)
4000 .map(Value::Int)
4001 .map_err(|msg| anyhow::anyhow!(msg))
4002 }
4003 Expr::VarWithDefault { path, default } => {
4004 let resolved = {
4007 let scope = self.scope.read().await;
4008 crate::interpreter::resolve_default(&scope, path)
4009 .map_err(|msg| anyhow::anyhow!(msg))?
4010 };
4011 match resolved {
4012 Some(value) => Ok(value),
4013 None => self.eval_string_parts_async(default).await.map(Value::String),
4014 }
4015 }
4016 Expr::Arithmetic(expr_str) => {
4017 let scope = self.scope.read().await;
4018 crate::arithmetic::eval_arithmetic(expr_str, &scope)
4019 .map(Value::Int)
4020 .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
4021 }
4022 Expr::Command(cmd) => {
4023 let result = self.execute_command(&cmd.name, &cmd.args).await?;
4025 Ok(Value::Bool(result.code == 0))
4026 }
4027 Expr::LastExitCode => {
4028 let scope = self.scope.read().await;
4029 Ok(Value::Int(scope.last_result().code))
4030 }
4031 Expr::CurrentPid => {
4032 let scope = self.scope.read().await;
4033 Ok(Value::Int(scope.pid() as i64))
4034 }
4035 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
4036 Expr::ListLiteral(elems) => {
4037 let mut out = Vec::with_capacity(elems.len());
4042 for elem in elems {
4043 match elem {
4044 ListElem::Item(e) => {
4045 let value = self.eval_expr_async(e).await?;
4046 out.push(crate::interpreter::value_to_json(&value));
4047 }
4048 ListElem::Spread(e) => {
4049 let value = self.eval_expr_async(e).await?;
4050 match value {
4051 Value::Json(serde_json::Value::Array(items)) => out.extend(items),
4052 other => return Err(anyhow::anyhow!(spread_non_list_message(&other))),
4053 }
4054 }
4055 }
4056 }
4057 Ok(Value::Json(serde_json::Value::Array(out)))
4058 }
4059 Expr::RecordLiteral(entries) => {
4060 let mut map = serde_json::Map::new();
4064 for entry in entries {
4065 let key = match &entry.key {
4066 RecordKey::Bare(s) | RecordKey::Quoted(s) => s.clone(),
4067 RecordKey::Interpolated(parts) => {
4070 self.eval_string_parts_async(parts).await?
4071 }
4072 };
4073 let value = self.eval_expr_async(&entry.value).await?;
4074 map.insert(key, crate::interpreter::value_to_json(&value));
4075 }
4076 Ok(Value::Json(serde_json::Value::Object(map)))
4077 }
4078 }
4079 })
4080 }
4081
4082 fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
4084 Box::pin(async move {
4085 let mut result = String::new();
4086 for part in parts {
4087 result.push_str(&self.eval_string_part_async(part).await?);
4088 }
4089 Ok(result)
4090 })
4091 }
4092
4093 fn eval_test_async<'a>(&'a self, test_expr: &'a TestExpr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
4097 Box::pin(async move {
4098 match test_expr {
4099 TestExpr::FileTest { op, path } => {
4100 let path_value = self.eval_expr_async(path).await?;
4101 let home = self.scope_home().await;
4105 let path_value = apply_tilde_expansion(path_value, home.as_deref());
4106 let path_str = value_to_string(&path_value);
4107 let (resolved, backend) = {
4112 let ctx = self.exec_ctx.read().await;
4113 (ctx.resolve_path(&path_str), ctx.backend.clone())
4114 };
4115 let entry = backend.stat(&resolved).await.ok();
4116 Ok(match op {
4117 FileTestOp::Exists => entry.is_some(),
4118 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
4119 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
4120 FileTestOp::Readable => entry.is_some(),
4121 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
4122 e.permissions.is_none_or(|p| p & 0o222 != 0)
4123 }),
4124 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
4125 e.permissions.is_some_and(|p| p & 0o111 != 0)
4126 }),
4127 })
4128 }
4129 TestExpr::StringTest { op, value } => match op {
4130 crate::ast::StringTestOp::IsEmpty | crate::ast::StringTestOp::IsNonEmpty => {
4131 let val = self.eval_expr_async(value).await?;
4132 let symbol = match op {
4136 crate::ast::StringTestOp::IsEmpty => "-z",
4137 crate::ast::StringTestOp::IsNonEmpty => "-n",
4138 crate::ast::StringTestOp::IsList
4139 | crate::ast::StringTestOp::IsRecord => unreachable!(),
4140 };
4141 if let Some(msg) = crate::interpreter::scalar_test_operand_error(symbol, &val) {
4142 anyhow::bail!(msg);
4143 }
4144 let s = value_to_string(&val);
4145 Ok(match op {
4146 crate::ast::StringTestOp::IsEmpty => s.is_empty(),
4147 crate::ast::StringTestOp::IsNonEmpty => !s.is_empty(),
4148 crate::ast::StringTestOp::IsList
4149 | crate::ast::StringTestOp::IsRecord => unreachable!(),
4150 })
4151 }
4152 crate::ast::StringTestOp::IsList | crate::ast::StringTestOp::IsRecord => {
4157 let val = self.eval_expr_async(value).await?;
4158 Ok(op.matches_shape(&val))
4159 }
4160 },
4161 TestExpr::Comparison { left, op, right } => {
4162 let left_val = self.eval_expr_async(left).await?;
4164 let right_val = self.eval_expr_async(right).await?;
4165 let resolved = TestExpr::Comparison {
4166 left: Box::new(Expr::Literal(left_val)),
4167 op: *op,
4168 right: Box::new(Expr::Literal(right_val)),
4169 };
4170 let expr = Expr::Test(Box::new(resolved));
4171 let mut scope = self.scope.write().await;
4172 let value = eval_expr(&expr, &mut scope)
4173 .map_err(|e| anyhow::anyhow!("{}", e))?;
4174 Ok(value_to_bool(&value))
4175 }
4176 TestExpr::And { left, right } => {
4177 if !self.eval_test_async(left).await? {
4178 Ok(false)
4179 } else {
4180 self.eval_test_async(right).await
4181 }
4182 }
4183 TestExpr::Or { left, right } => {
4184 if self.eval_test_async(left).await? {
4185 Ok(true)
4186 } else {
4187 self.eval_test_async(right).await
4188 }
4189 }
4190 TestExpr::Not { expr } => {
4191 Ok(!self.eval_test_async(expr).await?)
4192 }
4193 TestExpr::In { left, right } => {
4194 let left_val = self.eval_expr_async(left).await?;
4195 let right_val = self.eval_expr_async(right).await?;
4196 let resolved = TestExpr::In {
4197 left: Box::new(Expr::Literal(left_val)),
4198 right: Box::new(Expr::Literal(right_val)),
4199 };
4200 let expr = Expr::Test(Box::new(resolved));
4201 let mut scope = self.scope.write().await;
4202 let value = eval_expr(&expr, &mut scope)
4203 .map_err(|e| anyhow::anyhow!("{}", e))?;
4204 Ok(value_to_bool(&value))
4205 }
4206 TestExpr::NotIn { left, right } => {
4207 let left_val = self.eval_expr_async(left).await?;
4208 let right_val = self.eval_expr_async(right).await?;
4209 let resolved = TestExpr::NotIn {
4210 left: Box::new(Expr::Literal(left_val)),
4211 right: Box::new(Expr::Literal(right_val)),
4212 };
4213 let expr = Expr::Test(Box::new(resolved));
4214 let mut scope = self.scope.write().await;
4215 let value = eval_expr(&expr, &mut scope)
4216 .map_err(|e| anyhow::anyhow!("{}", e))?;
4217 Ok(value_to_bool(&value))
4218 }
4219 }
4220 })
4221 }
4222
4223 fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
4224 Box::pin(async move {
4225 match part {
4226 StringPart::Literal(s) => Ok(s.clone()),
4227 StringPart::Var(path) => {
4228 let scope = self.scope.read().await;
4229 match scope.resolve_path(path) {
4230 Ok(value) => value_to_text_sink(&value).map_err(|e| anyhow::anyhow!("{e}")),
4234 Err(PathError::UndefinedRoot(_)) => Ok(String::new()),
4236 Err(PathError::Absence(msg)) | Err(PathError::Shape(msg)) => {
4237 Err(anyhow::anyhow!(msg))
4238 }
4239 }
4240 }
4241 StringPart::VarWithDefault { path, default } => {
4242 let resolved = {
4243 let scope = self.scope.read().await;
4244 crate::interpreter::resolve_default(&scope, path)
4245 .map_err(|msg| anyhow::anyhow!(msg))?
4246 };
4247 match resolved {
4248 Some(value) => value_to_text_sink(&value).map_err(|e| anyhow::anyhow!("{e}")),
4249 None => self.eval_string_parts_async(default).await,
4250 }
4251 }
4252 StringPart::VarLength(path) => {
4253 let scope = self.scope.read().await;
4254 crate::interpreter::resolve_length(&scope, path)
4255 .map(|n| n.to_string())
4256 .map_err(|msg| anyhow::anyhow!(msg))
4257 }
4258 StringPart::Positional(n) => {
4259 let scope = self.scope.read().await;
4260 match scope.get_positional(*n) {
4261 Some(s) => Ok(s.to_string()),
4262 None => Ok(String::new()),
4263 }
4264 }
4265 StringPart::AllArgs => {
4266 let scope = self.scope.read().await;
4267 Ok(scope.all_args().join(" "))
4268 }
4269 StringPart::ArgCount => {
4270 let scope = self.scope.read().await;
4271 Ok(scope.arg_count().to_string())
4272 }
4273 StringPart::Arithmetic(expr) => {
4274 let scope = self.scope.read().await;
4275 match crate::arithmetic::eval_arithmetic(expr, &scope) {
4276 Ok(value) => Ok(value.to_string()),
4277 Err(_) => Ok(String::new()),
4278 }
4279 }
4280 StringPart::CommandSubst(stmts) => {
4281 let saved_scope = { self.scope.read().await.clone() };
4284 let saved_cwd = {
4285 let ec = self.exec_ctx.read().await;
4286 (ec.cwd.clone(), ec.prev_cwd.clone())
4287 };
4288
4289 let run_result = self.execute_block_capturing(stmts).await;
4291
4292 {
4294 let mut scope = self.scope.write().await;
4295 *scope = saved_scope;
4296 if let Ok(ref r) = run_result {
4297 scope.set_last_result(r.clone());
4298 }
4299 }
4300 {
4301 let mut ec = self.exec_ctx.write().await;
4302 ec.cwd = saved_cwd.0;
4303 ec.prev_cwd = saved_cwd.1;
4304 }
4305
4306 let result = run_result?;
4308
4309 match result.try_text_out() {
4312 Ok(s) if !s.is_empty() => Ok(s.trim_end_matches('\n').to_string()),
4314 Ok(_) => match &result.data {
4325 Some(data) => Ok(value_to_string(data)),
4326 None => Ok(String::new()),
4327 },
4328 Err(e) => anyhow::bail!(
4329 "command substitution in a string produced binary data ({e}) — \
4330 pipe through base64/xxd"
4331 ),
4332 }
4333 }
4334 StringPart::LastExitCode => {
4335 let scope = self.scope.read().await;
4336 Ok(scope.last_result().code.to_string())
4337 }
4338 StringPart::CurrentPid => {
4339 let scope = self.scope.read().await;
4340 Ok(scope.pid().to_string())
4341 }
4342 }
4343 })
4344 }
4345
4346 async fn update_last_result(&self, result: &ExecResult) {
4348 let mut scope = self.scope.write().await;
4349 scope.set_last_result(result.clone());
4350 }
4351
4352 async fn drain_stderr_into(&self, result: &mut ExecResult) {
4358 let drained = {
4359 let mut receiver = self.stderr_receiver.lock().await;
4360 receiver.drain_lossy()
4361 };
4362 if !drained.is_empty() {
4363 if !result.err.is_empty() && !result.err.ends_with('\n') {
4364 result.err.push('\n');
4365 }
4366 result.err.push_str(&drained);
4367 }
4368 }
4369
4370 async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
4376 let tool_args = self.build_args_async(args, None).await?;
4378
4379 {
4381 let mut scope = self.scope.write().await;
4382 scope.push_frame();
4383 }
4384
4385 let saved_positional = {
4387 let mut scope = self.scope.write().await;
4388 let saved = scope.save_positional();
4389
4390 let positional_args: Vec<String> = tool_args.positional
4392 .iter()
4393 .map(value_to_string)
4394 .collect();
4395 scope.set_positional(&def.name, positional_args);
4396
4397 saved
4398 };
4399
4400 let mut accumulated_out: Vec<u8> = Vec::new();
4405 let mut accumulated_err = String::new();
4406 let mut last_code = 0i64;
4407 let mut last_data: Option<Value> = None;
4408
4409 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
4410 match r.out_bytes() {
4411 Some(b) => buf.extend_from_slice(b),
4412 None => buf.extend_from_slice(r.text_out().as_bytes()),
4413 }
4414 }
4415
4416 let mut exec_error: Option<anyhow::Error> = None;
4418 let mut exit_code: Option<i64> = None;
4419
4420 for stmt in &def.body {
4421 match self.execute_stmt_flow(stmt).await {
4422 Ok(flow) => {
4423 let drained = {
4425 let mut receiver = self.stderr_receiver.lock().await;
4426 receiver.drain_lossy()
4427 };
4428 if !drained.is_empty() {
4429 accumulated_err.push_str(&drained);
4430 }
4431
4432 match flow {
4433 ControlFlow::Normal(r) => {
4434 push_out(&mut accumulated_out, &r);
4435 accumulated_err.push_str(&r.err);
4436 last_code = r.code;
4437 last_data = r.data;
4438 }
4439 ControlFlow::Return { value } => {
4440 push_out(&mut accumulated_out, &value);
4441 accumulated_err.push_str(&value.err);
4442 last_code = value.code;
4443 last_data = value.data;
4444 break;
4445 }
4446 ControlFlow::Exit { code } => {
4447 exit_code = Some(code);
4448 break;
4449 }
4450 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
4451 push_out(&mut accumulated_out, &r);
4452 accumulated_err.push_str(&r.err);
4453 last_code = r.code;
4454 last_data = r.data;
4455 }
4456 }
4457 }
4458 Err(e) => {
4459 exec_error = Some(e);
4460 break;
4461 }
4462 }
4463 }
4464
4465 {
4467 let mut scope = self.scope.write().await;
4468 scope.pop_frame();
4469 scope.set_positional(saved_positional.0, saved_positional.1);
4470 }
4471
4472 if let Some(e) = exec_error {
4474 return Err(e);
4475 }
4476 let code = exit_code.unwrap_or(last_code);
4477 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(code);
4478 result.err = accumulated_err;
4479 result.data = last_data;
4480 Ok(result)
4481 }
4482
4483 async fn execute_block_capturing(&self, stmts: &[Stmt]) -> Result<ExecResult> {
4491 let mut accumulated_out: Vec<u8> = Vec::new();
4495 let mut accumulated_err = String::new();
4496 let mut last_code = 0i64;
4497 let mut last_data: Option<Value> = None;
4498
4499 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
4501 match r.out_bytes() {
4502 Some(b) => buf.extend_from_slice(b),
4503 None => buf.extend_from_slice(r.text_out().as_bytes()),
4504 }
4505 }
4506
4507 for stmt in stmts {
4508 let flow = self.execute_stmt_flow(stmt).await?;
4509
4510 let drained = {
4513 let mut receiver = self.stderr_receiver.lock().await;
4514 receiver.drain_lossy()
4515 };
4516 if !drained.is_empty() {
4517 accumulated_err.push_str(&drained);
4518 }
4519
4520 match flow {
4521 ControlFlow::Normal(r)
4522 | ControlFlow::Break { result: r, .. }
4523 | ControlFlow::Continue { result: r, .. } => {
4524 push_out(&mut accumulated_out, &r);
4525 accumulated_err.push_str(&r.err);
4526 last_code = r.code;
4527 last_data = r.data;
4528 }
4529 ControlFlow::Return { value } => {
4530 push_out(&mut accumulated_out, &value);
4531 accumulated_err.push_str(&value.err);
4532 last_code = value.code;
4533 last_data = value.data;
4534 break;
4535 }
4536 ControlFlow::Exit { code } => {
4537 last_code = code;
4538 break;
4539 }
4540 }
4541 }
4542
4543 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(last_code);
4544 result.err = accumulated_err;
4545 result.data = last_data;
4546 Ok(result)
4547 }
4548
4549 async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
4554 let tool_args = self.build_args_async(args, None).await?;
4556 let path = match tool_args.positional.first() {
4557 Some(Value::String(s)) => s.clone(),
4558 Some(v) => value_to_string(v),
4559 None => {
4560 return Ok(ExecResult::failure(1, "source: missing filename"));
4561 }
4562 };
4563
4564 let full_path = {
4566 let ctx = self.exec_ctx.read().await;
4567 if path.starts_with('/') {
4568 std::path::PathBuf::from(&path)
4569 } else {
4570 ctx.cwd.join(&path)
4571 }
4572 };
4573
4574 let content = {
4576 let ctx = self.exec_ctx.read().await;
4577 match ctx.backend.read(&full_path, None).await {
4578 Ok(bytes) => {
4579 String::from_utf8(bytes).map_err(|e| {
4580 anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
4581 })?
4582 }
4583 Err(e) => {
4584 return Ok(ExecResult::failure(
4585 1,
4586 format!("source: {}: {}", path, e),
4587 ));
4588 }
4589 }
4590 };
4591
4592 let program = match crate::parser::parse(&content) {
4594 Ok(p) => p,
4595 Err(errors) => {
4596 let msg = errors
4597 .iter()
4598 .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
4599 .collect::<Vec<_>>()
4600 .join("\n");
4601 return Ok(ExecResult::failure(1, format!("source: {}", msg)));
4602 }
4603 };
4604
4605 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
4610 match r.out_bytes() {
4611 Some(b) => buf.extend_from_slice(b),
4612 None => buf.extend_from_slice(r.text_out().as_bytes()),
4613 }
4614 }
4615
4616 let mut accumulated_out: Vec<u8> = Vec::new();
4617 let mut accumulated_err = String::new();
4618 let mut last_code = 0i64;
4619 let mut last_data: Option<Value> = None;
4620
4621 for stmt in program.statements {
4622 if matches!(stmt, crate::ast::Stmt::Empty) {
4623 continue;
4624 }
4625
4626 match self.execute_stmt_flow(&stmt).await {
4627 Ok(flow) => {
4628 let drained = {
4629 let mut receiver = self.stderr_receiver.lock().await;
4630 receiver.drain_lossy()
4631 };
4632 if !drained.is_empty() {
4633 accumulated_err.push_str(&drained);
4634 }
4635 match flow {
4636 ControlFlow::Normal(r) => {
4637 push_out(&mut accumulated_out, &r);
4638 accumulated_err.push_str(&r.err);
4639 last_code = r.code;
4640 last_data = r.data.clone();
4641 self.update_last_result(&r).await;
4642 }
4643 ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
4644 return Err(anyhow::anyhow!(
4645 "source: {}: unexpected break/continue outside loop",
4646 path
4647 ));
4648 }
4649 ControlFlow::Return { value } => {
4650 push_out(&mut accumulated_out, &value);
4651 accumulated_err.push_str(&value.err);
4652 let mut result = ExecResult::success_text_or_bytes(accumulated_out)
4653 .with_code(value.code);
4654 result.err = accumulated_err;
4655 result.data = value.data;
4656 return Ok(result);
4657 }
4658 ControlFlow::Exit { code } => {
4659 let mut result =
4660 ExecResult::success_text_or_bytes(accumulated_out).with_code(code);
4661 result.err = accumulated_err;
4662 result.data = last_data;
4663 return Ok(result);
4664 }
4665 }
4666 }
4667 Err(e) => {
4668 return Err(e.context(format!("source: {}", path)));
4669 }
4670 }
4671 }
4672
4673 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(last_code);
4674 result.err = accumulated_err;
4675 result.data = last_data;
4676 Ok(result)
4677 }
4678
4679 async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4684 let path_value = {
4686 let scope = self.scope.read().await;
4687 scope
4688 .get("PATH")
4689 .map(value_to_string)
4690 .unwrap_or_else(|| "/bin".to_string())
4691 };
4692
4693 for dir in path_value.split(':') {
4695 if dir.is_empty() {
4696 continue;
4697 }
4698
4699 let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
4701
4702 let exists = {
4704 let ctx = self.exec_ctx.read().await;
4705 ctx.backend.exists(&script_path).await
4706 };
4707
4708 if !exists {
4709 continue;
4710 }
4711
4712 let content = {
4714 let ctx = self.exec_ctx.read().await;
4715 match ctx.backend.read(&script_path, None).await {
4716 Ok(bytes) => match String::from_utf8(bytes) {
4717 Ok(s) => s,
4718 Err(e) => {
4719 return Ok(Some(ExecResult::failure(
4720 1,
4721 format!("{}: invalid UTF-8: {}", script_path.display(), e),
4722 )));
4723 }
4724 },
4725 Err(e) => {
4726 return Ok(Some(ExecResult::failure(
4727 1,
4728 format!("{}: {}", script_path.display(), e),
4729 )));
4730 }
4731 }
4732 };
4733
4734 let program = match crate::parser::parse(&content) {
4736 Ok(p) => p,
4737 Err(errors) => {
4738 let msg = errors
4739 .iter()
4740 .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
4741 .collect::<Vec<_>>()
4742 .join("\n");
4743 return Ok(Some(ExecResult::failure(1, msg)));
4744 }
4745 };
4746
4747 let tool_args = self.build_args_async(args, None).await?;
4749
4750 let mut isolated_scope = Scope::new();
4752
4753 let positional_args: Vec<String> = tool_args.positional
4755 .iter()
4756 .map(value_to_string)
4757 .collect();
4758 isolated_scope.set_positional(name, positional_args);
4759
4760 let original_scope = {
4762 let mut scope = self.scope.write().await;
4763 std::mem::replace(&mut *scope, isolated_scope)
4764 };
4765
4766 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
4770 match r.out_bytes() {
4771 Some(b) => buf.extend_from_slice(b),
4772 None => buf.extend_from_slice(r.text_out().as_bytes()),
4773 }
4774 }
4775
4776 let mut accumulated_out: Vec<u8> = Vec::new();
4777 let mut accumulated_err = String::new();
4778 let mut last_code = 0i64;
4779 let mut last_data: Option<Value> = None;
4780 let mut exec_error: Option<anyhow::Error> = None;
4781 let mut exit_code: Option<i64> = None;
4782
4783 for stmt in program.statements {
4784 if matches!(stmt, crate::ast::Stmt::Empty) {
4785 continue;
4786 }
4787
4788 match self.execute_stmt_flow(&stmt).await {
4789 Ok(flow) => {
4790 let drained = {
4791 let mut receiver = self.stderr_receiver.lock().await;
4792 receiver.drain_lossy()
4793 };
4794 if !drained.is_empty() {
4795 accumulated_err.push_str(&drained);
4796 }
4797 match flow {
4798 ControlFlow::Normal(r) => {
4799 push_out(&mut accumulated_out, &r);
4800 accumulated_err.push_str(&r.err);
4801 last_code = r.code;
4802 last_data = r.data;
4803 }
4804 ControlFlow::Return { value } => {
4805 push_out(&mut accumulated_out, &value);
4806 accumulated_err.push_str(&value.err);
4807 last_code = value.code;
4808 last_data = value.data;
4809 break;
4810 }
4811 ControlFlow::Exit { code } => {
4812 exit_code = Some(code);
4813 break;
4814 }
4815 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
4816 push_out(&mut accumulated_out, &r);
4817 accumulated_err.push_str(&r.err);
4818 last_code = r.code;
4819 last_data = r.data;
4820 }
4821 }
4822 }
4823 Err(e) => {
4824 exec_error = Some(e);
4825 break;
4826 }
4827 }
4828 }
4829
4830 {
4832 let mut scope = self.scope.write().await;
4833 *scope = original_scope;
4834 }
4835
4836 if let Some(e) = exec_error {
4838 return Err(e.context(format!("script: {}", script_path.display())));
4839 }
4840 let code = exit_code.unwrap_or(last_code);
4841 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(code);
4842 result.err = accumulated_err;
4843 result.data = last_data;
4844 return Ok(Some(result));
4845 }
4846
4847 Ok(None)
4849 }
4850
4851 #[cfg(not(feature = "subprocess"))]
4865 async fn try_execute_external(&self, _name: &str, _args: &[Arg]) -> Result<Option<ExecResult>> {
4866 Ok(None)
4867 }
4868
4869 #[cfg(feature = "subprocess")]
4871 #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
4872 async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4873 let cancel = {
4879 let ec = self.exec_ctx.read().await;
4880 ec.cancel.clone()
4881 };
4882 let kill_grace = self.kill_grace;
4883 if !self.allow_external_commands {
4884 return Ok(None);
4885 }
4886
4887 let real_cwd = {
4892 let ctx = self.exec_ctx.read().await;
4893 match ctx.backend.resolve_real_path(&ctx.cwd) {
4894 Some(p) => p,
4895 None => return Ok(None),
4896 }
4897 };
4898
4899 let executable = if name.contains('/') {
4900 let resolved = if std::path::Path::new(name).is_absolute() {
4902 std::path::PathBuf::from(name)
4903 } else {
4904 real_cwd.join(name)
4905 };
4906 if !resolved.exists() {
4907 return Ok(Some(ExecResult::failure(
4908 127,
4909 format!("{}: No such file or directory", name),
4910 )));
4911 }
4912 if !resolved.is_file() {
4913 return Ok(Some(ExecResult::failure(
4914 126,
4915 format!("{}: Is a directory", name),
4916 )));
4917 }
4918 #[cfg(unix)]
4919 {
4920 use std::os::unix::fs::PermissionsExt;
4921 let mode = std::fs::metadata(&resolved)
4922 .map(|m| m.permissions().mode())
4923 .unwrap_or(0);
4924 if mode & 0o111 == 0 {
4925 return Ok(Some(ExecResult::failure(
4926 126,
4927 format!("{}: Permission denied", name),
4928 )));
4929 }
4930 }
4931 resolved.to_string_lossy().into_owned()
4932 } else {
4933 let path_var = {
4937 let scope = self.scope.read().await;
4938 scope.get("PATH").map(value_to_string).unwrap_or_default()
4939 };
4940
4941 match resolve_in_path(name, &path_var) {
4943 Some(path) => path,
4944 None => return Ok(None), }
4946 };
4947
4948 tracing::debug!(executable = %executable, "resolved external command");
4949
4950 let argv = self.build_args_flat(args).await?;
4952
4953 let (pipe_stdin, stdin_string) = {
4962 let mut ctx = self.exec_ctx.write().await;
4963 (ctx.pipe_stdin.take(), ctx.take_stdin())
4964 };
4965 let has_stdin = pipe_stdin.is_some() || stdin_string.is_some();
4966
4967 use tokio::process::Command;
4969
4970 let mut cmd = Command::new(&executable);
4971 cmd.args(&argv);
4972 cmd.current_dir(&real_cwd);
4973
4974 cmd.env_clear();
4978 {
4979 let scope = self.scope.read().await;
4980 let exported = scope.exported_vars();
4981 if let Some(msg) = crate::interpreter::structured_export_error(&exported) {
4984 return Err(anyhow::anyhow!(msg));
4985 }
4986 for (var_name, value) in exported {
4987 cmd.env(var_name, value_to_string(&value));
4988 }
4989 }
4990
4991 cmd.stdin(if has_stdin {
4993 std::process::Stdio::piped()
4994 } else if self.interactive {
4995 std::process::Stdio::inherit()
4996 } else {
4997 std::process::Stdio::null()
4998 });
4999
5000 let pipeline_position = {
5004 let ctx = self.exec_ctx.read().await;
5005 ctx.pipeline_position
5006 };
5007 let inherit_output = self.interactive
5008 && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
5009
5010 if inherit_output {
5011 cmd.stdout(std::process::Stdio::inherit());
5012 cmd.stderr(std::process::Stdio::inherit());
5013 } else {
5014 cmd.stdout(std::process::Stdio::piped());
5015 cmd.stderr(std::process::Stdio::piped());
5016 }
5017
5018 #[cfg(unix)]
5024 {
5025 let restore_jc_signals = self.terminal_state.is_some() && inherit_output;
5026 #[allow(unsafe_code)]
5028 unsafe {
5029 cmd.pre_exec(move || {
5030 nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
5032 .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
5033 if restore_jc_signals {
5034 use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
5035 let mut sa: nix::libc::sigaction = std::mem::zeroed();
5036 sa.sa_sigaction = SIG_DFL;
5037 if sigaction(SIGTSTP, &sa, std::ptr::null_mut()) != 0 {
5038 return Err(std::io::Error::last_os_error());
5039 }
5040 if sigaction(SIGTTOU, &sa, std::ptr::null_mut()) != 0 {
5041 return Err(std::io::Error::last_os_error());
5042 }
5043 if sigaction(SIGTTIN, &sa, std::ptr::null_mut()) != 0 {
5044 return Err(std::io::Error::last_os_error());
5045 }
5046 if sigaction(SIGINT, &sa, std::ptr::null_mut()) != 0 {
5047 return Err(std::io::Error::last_os_error());
5048 }
5049 }
5050 Ok(())
5051 });
5052 }
5053 }
5054
5055 let in_jc_inherit_path = inherit_output && self.terminal_state.is_some();
5062 if !in_jc_inherit_path {
5063 cmd.kill_on_drop(true);
5064 }
5065
5066 let mut child = match cmd.spawn() {
5071 Ok(child) => child,
5072 Err(e) => {
5073 return Ok(Some(ExecResult::failure(
5074 127,
5075 format!("{}: {}", name, e),
5076 )));
5077 }
5078 };
5079 let kill_target = crate::pidfd::KillTarget::from_child(&child);
5080
5081 if let Some(job_id) = self.bg_job_id
5086 && let Some(pid) = child.id()
5087 {
5088 self.jobs.add_pgid(job_id, pid).await;
5089 }
5090
5091 let stdin_task: Option<tokio::task::JoinHandle<()>> = if let Some(mut pipe_in) = pipe_stdin {
5098 child.stdin.take().map(|mut child_stdin| {
5099 tokio::spawn(async move {
5100 use tokio::io::{AsyncReadExt, AsyncWriteExt};
5101 let mut buf = [0u8; 8192];
5102 loop {
5103 match pipe_in.read(&mut buf).await {
5104 Ok(0) => break, Ok(n) => {
5106 if child_stdin.write_all(&buf[..n]).await.is_err() {
5107 break; }
5109 }
5110 Err(_) => break,
5111 }
5112 }
5113 })
5115 })
5116 } else if let Some(data) = stdin_string {
5117 child.stdin.take().map(|mut child_stdin| {
5125 tokio::spawn(async move {
5126 use tokio::io::AsyncWriteExt;
5127 let _ = child_stdin.write_all(data.as_bytes()).await;
5128 })
5129 })
5130 } else {
5131 None
5132 };
5133
5134 struct AbortStdinCopyOnDrop(Option<tokio::task::JoinHandle<()>>);
5142 impl Drop for AbortStdinCopyOnDrop {
5143 fn drop(&mut self) {
5144 if let Some(t) = self.0.take() {
5145 t.abort();
5146 }
5147 }
5148 }
5149 let _stdin_copy_guard = AbortStdinCopyOnDrop(stdin_task);
5150
5151 if inherit_output {
5152 #[cfg(unix)]
5154 if let Some(ref term) = self.terminal_state {
5155 let child_id = child.id().unwrap_or(0);
5156 let pid = nix::unistd::Pid::from_raw(child_id as i32);
5157 let pgid = pid; if let Err(e) = term.give_terminal_to(pgid) {
5161 tracing::warn!("failed to give terminal to child: {}", e);
5162 }
5163
5164 let term_clone = term.clone();
5165 let cmd_name = name.to_string();
5166 let cmd_display = format!("{} {}", name, argv.join(" "));
5167 let jobs = self.jobs.clone();
5168
5169 let wait_complete = std::sync::Arc::new(
5183 std::sync::atomic::AtomicBool::new(false)
5184 );
5185 let cancel_watcher = {
5186 let cancel = cancel.clone();
5187 let wc = wait_complete.clone();
5188 let target = kill_target.as_ref().map(|t| {
5196 crate::pidfd::KillTarget::from_pid(t.pid())
5208 });
5209 tokio::spawn(async move {
5210 cancel.cancelled().await;
5211 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
5212 use nix::sys::signal::Signal;
5213 if let Some(t) = &target {
5214 t.signal(Signal::SIGTERM);
5215 t.signal_pg(Signal::SIGTERM);
5216 } else {
5217 let _ = nix::sys::signal::kill(pid, Signal::SIGTERM);
5218 let _ = nix::sys::signal::killpg(pid, Signal::SIGTERM);
5219 }
5220 if kill_grace > Duration::ZERO {
5221 tokio::time::sleep(kill_grace).await;
5222 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
5223 }
5224 if let Some(t) = &target {
5225 t.signal(Signal::SIGKILL);
5226 t.signal_pg(Signal::SIGKILL);
5227 } else {
5228 let _ = nix::sys::signal::kill(pid, Signal::SIGKILL);
5229 let _ = nix::sys::signal::killpg(pid, Signal::SIGKILL);
5230 }
5231 })
5232 };
5233 struct AbortOnDrop(tokio::task::JoinHandle<()>);
5234 impl Drop for AbortOnDrop {
5235 fn drop(&mut self) {
5236 self.0.abort();
5237 }
5238 }
5239 let _watcher_guard = AbortOnDrop(cancel_watcher);
5240
5241 let wait_complete_setter = wait_complete.clone();
5242 let code = tokio::task::block_in_place(move || {
5243 let result = term_clone.wait_for_foreground(pid);
5244 wait_complete_setter.store(true, std::sync::atomic::Ordering::SeqCst);
5246
5247 if let Err(e) = term_clone.reclaim_terminal() {
5249 tracing::warn!("failed to reclaim terminal: {}", e);
5250 }
5251
5252 match result {
5253 crate::terminal::WaitResult::Exited(code) => code as i64,
5254 crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
5255 crate::terminal::WaitResult::Stopped(_sig) => {
5256 let rt = tokio::runtime::Handle::current();
5258 let job_id = rt.block_on(jobs.register_stopped(
5259 cmd_display,
5260 child_id,
5261 child_id, ));
5263 eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
5264 148 }
5266 }
5267 });
5268
5269 return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
5270 }
5271
5272 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
5274 Ok(s) => s,
5275 Err(e) => {
5276 return Ok(Some(ExecResult::failure(
5277 1,
5278 format!("{}: failed to wait: {}", name, e),
5279 )));
5280 }
5281 };
5282
5283 let code = status.code().unwrap_or_else(|| {
5284 #[cfg(unix)]
5285 {
5286 use std::os::unix::process::ExitStatusExt;
5287 128 + status.signal().unwrap_or(0)
5288 }
5289 #[cfg(not(unix))]
5290 {
5291 -1
5292 }
5293 }) as i64;
5294
5295 Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
5297 } else {
5298 let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
5300 let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
5301
5302 let stdout_pipe = child.stdout.take();
5303 let stderr_pipe = child.stderr.take();
5304
5305 let stdout_clone = stdout_stream.clone();
5306 let stderr_clone = stderr_stream.clone();
5307
5308 let stdout_task = stdout_pipe.map(|pipe| {
5309 tokio::spawn(async move {
5310 drain_to_stream(pipe, stdout_clone).await;
5311 })
5312 });
5313
5314 let stderr_task = stderr_pipe.map(|pipe| {
5315 tokio::spawn(async move {
5316 drain_to_stream(pipe, stderr_clone).await;
5317 })
5318 });
5319
5320 let cancelled_before_wait = cancel.is_cancelled();
5321 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
5322 Ok(s) => s,
5323 Err(e) => {
5324 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
5326 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
5327 return Ok(Some(ExecResult::failure(
5328 1,
5329 format!("{}: failed to wait: {}", name, e),
5330 )));
5331 }
5332 };
5333
5334 if cancelled_before_wait || cancel.is_cancelled() {
5338 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
5339 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
5340 } else {
5341 if let Some(task) = stdout_task {
5342 let _ = task.await;
5344 }
5345 if let Some(task) = stderr_task {
5346 let _ = task.await;
5347 }
5348 }
5349
5350 let code = status.code().unwrap_or_else(|| {
5351 #[cfg(unix)]
5352 {
5353 use std::os::unix::process::ExitStatusExt;
5354 128 + status.signal().unwrap_or(0)
5355 }
5356 #[cfg(not(unix))]
5357 {
5358 -1
5359 }
5360 }) as i64;
5361
5362 let stdout = stdout_stream.read().await;
5366 let stderr = stderr_stream.read_string().await;
5367 let mut result = ExecResult::success_text_or_bytes(stdout).with_code(code);
5368 result.err = stderr;
5369 Ok(Some(result))
5370 }
5371 }
5372
5373 pub async fn get_var(&self, name: &str) -> Option<Value> {
5377 let scope = self.scope.read().await;
5378 scope.get(name).cloned()
5379 }
5380
5381 #[cfg(test)]
5383 pub async fn error_exit_enabled(&self) -> bool {
5384 let scope = self.scope.read().await;
5385 scope.error_exit_enabled()
5386 }
5387
5388 pub async fn set_var(&self, name: &str, value: Value) {
5390 let mut scope = self.scope.write().await;
5391 scope.set(name.to_string(), value);
5392 }
5393
5394 pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
5396 let mut scope = self.scope.write().await;
5397 scope.set_positional(script_name, args);
5398 }
5399
5400 pub async fn list_vars(&self) -> Vec<(String, Value)> {
5402 let scope = self.scope.read().await;
5403 scope.all()
5404 }
5405
5406 pub async fn exported_vars(&self) -> Vec<(String, Value)> {
5409 let scope = self.scope.read().await;
5410 scope.exported_vars()
5411 }
5412
5413 pub async fn cwd(&self) -> PathBuf {
5417 self.exec_ctx.read().await.cwd.clone()
5418 }
5419
5420 pub async fn set_cwd(&self, path: PathBuf) {
5422 let mut ctx = self.exec_ctx.write().await;
5423 ctx.set_cwd(path);
5424 }
5425
5426 pub async fn try_set_cwd(&self, path: PathBuf) -> bool {
5432 let backend = self.exec_ctx.read().await.backend.clone();
5435 let is_dir = matches!(backend.stat(&path).await, Ok(entry) if entry.is_dir());
5436 if is_dir {
5437 self.exec_ctx.write().await.set_cwd(path);
5438 }
5439 is_dir
5440 }
5441
5442 pub async fn last_result(&self) -> ExecResult {
5446 let scope = self.scope.read().await;
5447 scope.last_result().clone()
5448 }
5449
5450 pub async fn has_function(&self, name: &str) -> bool {
5454 self.user_tools.read().await.contains_key(name)
5455 }
5456
5457 pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
5459 self.tools.schemas()
5460 }
5461
5462 pub async fn classify_command(&self, name: &str) -> CommandKind {
5488 let mut name = name.to_string();
5493 let mut alias_depth = 0u8;
5494 loop {
5495 if !crate::validator::is_static_command_name(&name) {
5496 return CommandKind::Dynamic;
5497 }
5498 if crate::validator::is_runtime_special_form(&name) {
5499 return CommandKind::Special;
5500 }
5501 if alias_depth >= 10 {
5502 break;
5503 }
5504 let alias_value = {
5505 let ctx = self.exec_ctx.read().await;
5506 ctx.aliases.get(&name).cloned()
5507 };
5508 match alias_value
5511 .as_deref()
5512 .and_then(|v| v.split_whitespace().next())
5513 {
5514 Some(head) => {
5515 name = head.to_string();
5516 alias_depth += 1;
5517 }
5518 None => break,
5519 }
5520 }
5521
5522 let is_user_tool = self.user_tools.read().await.contains_key(&name);
5523 let is_builtin = self.tools.contains(&name);
5524 crate::validator::classify_command_name(&name, is_builtin, is_user_tool)
5525 }
5526
5527 pub fn jobs(&self) -> Arc<JobManager> {
5531 self.jobs.clone()
5532 }
5533
5534 pub fn vfs(&self) -> Arc<VfsRouter> {
5538 self.vfs.clone()
5539 }
5540
5541 pub async fn reset(&self) -> Result<()> {
5553 {
5554 let mut scope = self.scope.write().await;
5555 let pid = scope.pid();
5556 let latch_enabled = scope.latch_enabled();
5557 let trash_enabled = scope.trash_enabled();
5558 let mut fresh = Scope::new();
5559 fresh.set_pid(pid);
5560 for (name, value) in self.initial_vars.clone() {
5561 fresh.set_exported(name, value);
5562 }
5563 fresh.set_latch_enabled(latch_enabled);
5564 fresh.set_trash_enabled(trash_enabled);
5565 *scope = fresh;
5566 }
5567 {
5568 let mut ctx = self.exec_ctx.write().await;
5569 ctx.cwd = PathBuf::from("/");
5570 }
5571 Ok(())
5572 }
5573
5574 pub async fn shutdown(self) -> Result<()> {
5576 self.jobs.wait_all().await;
5578 Ok(())
5579 }
5580
5581 async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
5592 if let Some(d) = self.dispatcher() {
5597 ctx.dispatcher = Some(d);
5598 }
5599
5600 {
5602 let mut scope = self.scope.write().await;
5603 *scope = ctx.scope.clone();
5604 }
5605 {
5606 let mut ec = self.exec_ctx.write().await;
5607 ec.cwd = ctx.cwd.clone();
5608 ec.prev_cwd = ctx.prev_cwd.clone();
5609 ec.stdin = ctx.stdin.take();
5610 ec.stdin_data = ctx.stdin_data.take();
5611 ec.stdin_data_rx = ctx.stdin_data_rx.take();
5616 ec.pipe_stdin = ctx.pipe_stdin.take();
5622 ec.pipe_stdout = ctx.pipe_stdout.take();
5623 if let Some(stderr) = ctx.stderr.clone() {
5624 ec.stderr = Some(stderr);
5625 }
5626 ec.aliases = ctx.aliases.clone();
5627 ec.ignore_config = ctx.ignore_config.clone();
5628 ec.output_limit = ctx.output_limit.clone();
5629 ec.pipeline_position = ctx.pipeline_position;
5630 ec.cancel = ctx.cancel.clone();
5635 ec.watchdog = ctx.watchdog.clone();
5639 }
5640
5641 let result = self.execute_command(&cmd.name, &cmd.args).await?;
5643
5644 {
5646 let scope = self.scope.read().await;
5647 ctx.scope = scope.clone();
5648 }
5649 {
5650 let mut ec = self.exec_ctx.write().await;
5651 ctx.cwd = ec.cwd.clone();
5652 ctx.prev_cwd = ec.prev_cwd.clone();
5653 ctx.aliases = ec.aliases.clone();
5654 ctx.ignore_config = ec.ignore_config.clone();
5655 ctx.output_limit = ec.output_limit.clone();
5656 ctx.pipe_stdin = ec.pipe_stdin.take();
5661 ctx.pipe_stdout = ec.pipe_stdout.take();
5662 }
5663
5664 Ok(result)
5665 }
5666}
5667
5668#[async_trait]
5669impl CommandDispatcher for Kernel {
5670 async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
5676 self.dispatch_command(cmd, ctx).await
5677 }
5678
5679 async fn eval_expr(&self, expr: &Expr, _ctx: &ExecContext) -> Result<Value> {
5686 self.eval_expr_async(expr).await
5687 }
5688
5689 async fn fork(&self) -> Arc<dyn CommandDispatcher> {
5695 let fork: Arc<Kernel> = Kernel::fork(self).await;
5696 fork
5697 }
5698
5699 async fn fork_attached(&self) -> Arc<dyn CommandDispatcher> {
5701 let fork: Arc<Kernel> = Kernel::fork_attached(self).await;
5702 fork
5703 }
5704}
5705
5706fn finalize_output(
5714 result: ExecResult,
5715 format: Option<crate::interpreter::OutputFormat>,
5716 owns_output: bool,
5717) -> ExecResult {
5718 match format {
5719 Some(_) if owns_output => result,
5720 Some(format) => apply_output_format(result, format),
5721 None => result,
5722 }
5723}
5724
5725fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
5734 accumulated.materialize();
5738 match new.out_bytes() {
5739 Some(new_bytes) => {
5743 let mut combined: Vec<u8> = match accumulated.out_bytes() {
5744 Some(b) => b.to_vec(),
5745 None => accumulated.text_out().into_owned().into_bytes(),
5746 };
5747 combined.extend_from_slice(new_bytes);
5748 accumulated.set_out_bytes(combined);
5749 }
5750 None => accumulated.push_out(&new.text_out()),
5751 }
5752 accumulated.err.push_str(&new.err);
5753 accumulated.code = new.code;
5754 accumulated.data = new.data.clone();
5755 accumulated.did_spill = new.did_spill;
5756 accumulated.original_code = new.original_code;
5757 accumulated.content_type = new.content_type.clone();
5758 accumulated.baggage.clone_from(&new.baggage);
5759 accumulated.latch = new.latch.clone();
5762}
5763
5764fn fold_loop_output_into_flow(loop_output: ExecResult, flow: &mut ControlFlow) {
5770 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5771 let mut merged = loop_output;
5772 accumulate_result(&mut merged, result);
5773 *result = merged;
5774 }
5775}
5776
5777fn accumulate_flow_output(accumulated: &mut ExecResult, flow: &ControlFlow) {
5781 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5782 accumulate_result(accumulated, result);
5783 }
5784}
5785
5786fn is_truthy(value: &Value) -> bool {
5788 match value {
5789 Value::Null => false,
5790 Value::Bool(b) => *b,
5791 Value::Int(i) => *i != 0,
5792 Value::Float(f) => *f != 0.0,
5793 Value::String(s) => !s.is_empty(),
5794 Value::Json(json) => match json {
5795 serde_json::Value::Null => false,
5796 serde_json::Value::Array(arr) => !arr.is_empty(),
5797 serde_json::Value::Object(obj) => !obj.is_empty(),
5798 serde_json::Value::Bool(b) => *b,
5799 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
5800 serde_json::Value::String(s) => !s.is_empty(),
5801 },
5802 Value::Bytes(b) => !b.is_empty(), }
5804}
5805
5806fn apply_tilde_expansion(value: Value, home: Option<&str>) -> Value {
5812 match value {
5813 Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s, home)),
5814 _ => value,
5815 }
5816}
5817
5818pub(crate) fn argv_to_args(argv: &[Value]) -> Vec<Arg> {
5841 argv.iter().map(classify_argv_token).collect()
5842}
5843
5844fn classify_argv_token(token: &Value) -> Arg {
5845 let Value::String(s) = token else {
5846 return Arg::Positional(Expr::Literal(token.clone()));
5847 };
5848
5849 if s == "--" {
5850 return Arg::DoubleDash;
5851 }
5852
5853 if let Some(rest) = s.strip_prefix("--") {
5858 if rest.starts_with(|c: char| c.is_ascii_alphabetic()) {
5859 return match rest.split_once('=') {
5860 Some((key, val)) => Arg::Named {
5861 key: key.to_string(),
5862 value: Expr::Literal(Value::String(val.to_string())),
5863 },
5864 None => Arg::LongFlag(rest.to_string()),
5865 };
5866 }
5867 } else if let Some(rest) = s.strip_prefix('-') {
5868 if is_short_flag_body(rest) {
5874 return Arg::ShortFlag(rest.to_string());
5875 }
5876 }
5877
5878 if let Some((key, val)) = s.split_once('=') {
5879 if is_shell_identifier(key) {
5880 return Arg::WordAssign {
5881 key: key.to_string(),
5882 value: Expr::Literal(Value::String(val.to_string())),
5883 };
5884 }
5885 }
5886
5887 Arg::Positional(Expr::Literal(Value::String(s.clone())))
5888}
5889
5890fn is_short_flag_body(s: &str) -> bool {
5899 s.starts_with(|c: char| c.is_ascii_alphabetic())
5900 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == ':')
5901}
5902
5903fn is_shell_identifier(s: &str) -> bool {
5905 let mut chars = s.chars();
5906 match chars.next() {
5907 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
5908 _ => return false,
5909 }
5910 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
5911}
5912
5913pub(crate) fn push_repeatable_value(
5922 tool_args: &mut ToolArgs,
5923 flag_name: &str,
5924 canonical: &str,
5925 v: Value,
5926) -> anyhow::Result<()> {
5927 let occ = crate::interpreter::value_to_json(&v);
5928 let entry = tool_args
5929 .named
5930 .entry(canonical.to_string())
5931 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
5932 if let Value::Json(serde_json::Value::Array(items)) = entry {
5933 items.push(occ);
5934 Ok(())
5935 } else {
5936 anyhow::bail!("--{flag_name}: named[{canonical}] already holds a non-array value")
5937 }
5938}
5939
5940pub(crate) fn bind_glued_short_value(
5947 tool_args: &mut ToolArgs,
5948 flag_name: &str,
5949 canonical: &str,
5950 consumes: usize,
5951 repeatable: bool,
5952 value: String,
5953) -> anyhow::Result<()> {
5954 if consumes > 1 {
5955 anyhow::bail!(
5956 "-{flag_name} takes {consumes} arguments; use the separated form, not a glued value"
5957 );
5958 }
5959 if repeatable {
5960 push_repeatable_value(tool_args, flag_name, canonical, Value::String(value))
5961 } else {
5962 tool_args
5963 .named
5964 .insert(canonical.to_string(), Value::String(value));
5965 Ok(())
5966 }
5967}
5968
5969#[cfg(all(unix, feature = "subprocess"))]
5975pub(crate) async fn wait_or_kill(
5976 child: &mut tokio::process::Child,
5977 target: Option<&crate::pidfd::KillTarget>,
5978 cancel: &tokio_util::sync::CancellationToken,
5979 grace: Duration,
5980) -> std::io::Result<std::process::ExitStatus> {
5981 tokio::select! {
5982 biased;
5983 status = child.wait() => status,
5984 _ = cancel.cancelled() => kill_with_grace(child, target, grace).await,
5985 }
5986}
5987
5988#[cfg(all(not(unix), feature = "subprocess"))]
5989pub(crate) async fn wait_or_kill(
5990 child: &mut tokio::process::Child,
5991 _target: Option<&()>,
5992 cancel: &tokio_util::sync::CancellationToken,
5993 _grace: Duration,
5994) -> std::io::Result<std::process::ExitStatus> {
5995 tokio::select! {
5996 biased;
5997 status = child.wait() => status,
5998 _ = cancel.cancelled() => {
5999 let _ = child.start_kill();
6000 child.wait().await
6001 }
6002 }
6003}
6004
6005#[cfg(all(unix, feature = "subprocess"))]
6011pub(crate) async fn kill_with_grace(
6012 child: &mut tokio::process::Child,
6013 target: Option<&crate::pidfd::KillTarget>,
6014 grace: Duration,
6015) -> std::io::Result<std::process::ExitStatus> {
6016 use nix::sys::signal::Signal;
6017
6018 if let Some(t) = target {
6019 t.signal(Signal::SIGTERM);
6020 t.signal_pg(Signal::SIGTERM);
6021 if grace > Duration::ZERO
6022 && let Ok(status) = tokio::time::timeout(grace, child.wait()).await
6023 {
6024 return status;
6025 }
6026 t.signal(Signal::SIGKILL);
6027 t.signal_pg(Signal::SIGKILL);
6028 }
6029 child.wait().await
6030}
6031
6032#[cfg(test)]
6033#[allow(clippy::unwrap_used, clippy::expect_used)]
6034mod argv_classify_tests {
6035 use super::*;
6036
6037 fn canonical(arg: &Arg) -> Option<(&'static str, String, String)> {
6063 let lit = |e: &Expr| match e {
6065 Expr::Literal(Value::String(s)) => Some(s.clone()),
6066 _ => None,
6067 };
6068 Some(match arg {
6069 Arg::DoubleDash => ("dash", String::new(), String::new()),
6070 Arg::ShortFlag(s) => ("short", s.clone(), String::new()),
6071 Arg::LongFlag(s) => ("long", s.clone(), String::new()),
6072 Arg::Positional(e) => ("pos", String::new(), lit(e)?),
6073 Arg::Named { key, value } => ("named", key.clone(), lit(value)?),
6074 Arg::WordAssign { key, value } => ("pos", String::new(), format!("{key}={}", lit(value)?)),
6075 })
6076 }
6077
6078 fn classify(token: &str) -> Arg {
6080 classify_argv_token(&Value::String(token.to_string()))
6081 }
6082
6083 #[test]
6084 fn classifies_each_word_class() {
6085 assert_eq!(classify("--"), Arg::DoubleDash);
6086 assert_eq!(classify("-l"), Arg::ShortFlag("l".into()));
6087 assert_eq!(classify("-la"), Arg::ShortFlag("la".into()));
6088 assert_eq!(classify("--force"), Arg::LongFlag("force".into()));
6089 assert_eq!(
6090 classify("--key=value"),
6091 Arg::Named { key: "key".into(), value: Expr::Literal(Value::String("value".into())) }
6092 );
6093 assert_eq!(
6094 classify("NAME=val"),
6095 Arg::WordAssign { key: "NAME".into(), value: Expr::Literal(Value::String("val".into())) }
6096 );
6097 assert_eq!(classify("-A1"), Arg::ShortFlag("A1".into()));
6099 assert_eq!(classify("--type2"), Arg::LongFlag("type2".into()));
6100 assert_eq!(classify("-1"), Arg::Positional(Expr::Literal(Value::String("-1".into()))));
6102 assert_eq!(classify("00"), Arg::Positional(Expr::Literal(Value::String("00".into()))));
6106 assert_eq!(classify("1.50"), Arg::Positional(Expr::Literal(Value::String("1.50".into()))));
6107 assert_eq!(classify("-"), Arg::Positional(Expr::Literal(Value::String("-".into()))));
6109 assert_eq!(classify("1=2"), Arg::Positional(Expr::Literal(Value::String("1=2".into()))));
6111 assert_eq!(classify("plain"), Arg::Positional(Expr::Literal(Value::String("plain".into()))));
6112 }
6113
6114 #[test]
6115 fn typed_values_pass_through_as_literal_positionals() {
6116 let bytes = Value::Bytes(vec![0u8, 159, 146, 150]); assert_eq!(
6121 classify_argv_token(&bytes),
6122 Arg::Positional(Expr::Literal(bytes.clone()))
6123 );
6124 let json = Value::Json(serde_json::json!({"a": 1, "b": [2, 3]}));
6125 assert_eq!(
6126 classify_argv_token(&json),
6127 Arg::Positional(Expr::Literal(json.clone()))
6128 );
6129 assert_eq!(
6132 classify_argv_token(&Value::Int(-9)),
6133 Arg::Positional(Expr::Literal(Value::Int(-9)))
6134 );
6135 }
6136
6137 #[test]
6138 fn double_dash_only_matches_exactly() {
6139 assert_eq!(classify("--"), Arg::DoubleDash);
6142 assert_eq!(classify("--x"), Arg::LongFlag("x".into()));
6143 assert_eq!(classify("---"), Arg::Positional(Expr::Literal(Value::String("---".into()))));
6144 }
6145
6146 #[test]
6147 fn malformed_flag_words_fall_back_to_literal_positionals() {
6148 let pos = |t: &str| Arg::Positional(Expr::Literal(Value::String(t.into())));
6153 assert_eq!(classify("-k=v"), pos("-k=v"));
6156 assert_eq!(classify("-="), pos("-="));
6157 assert_eq!(classify("--=v"), pos("--=v"));
6159 assert_eq!(classify("--1"), pos("--1"));
6161 assert_eq!(classify("-"), pos("-"));
6163 assert_eq!(classify("-9"), pos("-9"));
6164 assert_eq!(classify("-lé"), pos("-lé"));
6171 assert_eq!(classify("-é"), pos("-é"));
6172 }
6173
6174 #[tokio::test]
6175 async fn non_ascii_short_flag_bundle_does_not_panic() {
6176 let kernel = Kernel::transient().expect("failed to create kernel");
6182 let result = kernel
6183 .execute_argv("ls", &[Value::String("-lé".into())])
6184 .await
6185 .expect("execute_argv must not panic on a non-ASCII short-flag token");
6186 assert_ne!(result.code, 0);
6189 }
6190
6191 proptest::proptest! {
6192 #[test]
6198 fn classifier_matches_parser_on_clean_tokens(
6199 token in "[a-zA-Z_=./@:+-]{1,8}"
6206 ) {
6207 let parsed = match parse(&format!("cmd {token}")) {
6208 Ok(p) => p,
6209 Err(_) => return Ok(()), };
6211 let [Stmt::Command(cmd)] = parsed.statements.as_slice() else {
6212 return Ok(());
6213 };
6214 let [arg] = cmd.args.as_slice() else { return Ok(()); };
6216
6217 let (Some(theirs), Some(ours)) = (canonical(arg), canonical(&classify(&token))) else {
6218 return Ok(()); };
6220 proptest::prop_assert_eq!(
6221 ours, theirs,
6222 "classifier diverged from parser on token {:?}", token
6223 );
6224 }
6225 }
6226}
6227
6228#[cfg(all(test, feature = "subprocess"))]
6229#[allow(clippy::expect_used)]
6230mod tests {
6231 use super::*;
6232
6233 #[tokio::test]
6234 async fn test_kernel_transient() {
6235 let kernel = Kernel::transient().expect("failed to create kernel");
6236 assert_eq!(kernel.name(), "transient");
6237 }
6238
6239 #[tokio::test]
6240 async fn test_kernel_execute_echo() {
6241 let kernel = Kernel::transient().expect("failed to create kernel");
6242 let result = kernel.execute("echo hello").await.expect("execution failed");
6243 assert!(result.ok());
6244 assert_eq!(result.text_out().trim(), "hello");
6245 }
6246
6247 #[tokio::test]
6248 async fn test_multiple_statements_accumulate_output() {
6249 let kernel = Kernel::transient().expect("failed to create kernel");
6250 let result = kernel
6251 .execute("echo one\necho two\necho three")
6252 .await
6253 .expect("execution failed");
6254 assert!(result.ok());
6255 assert!(result.text_out().contains("one"), "missing 'one': {}", result.text_out());
6257 assert!(result.text_out().contains("two"), "missing 'two': {}", result.text_out());
6258 assert!(result.text_out().contains("three"), "missing 'three': {}", result.text_out());
6259 }
6260
6261 #[tokio::test]
6262 async fn test_and_chain_accumulates_output() {
6263 let kernel = Kernel::transient().expect("failed to create kernel");
6264 let result = kernel
6265 .execute("echo first && echo second")
6266 .await
6267 .expect("execution failed");
6268 assert!(result.ok());
6269 assert!(result.text_out().contains("first"), "missing 'first': {}", result.text_out());
6270 assert!(result.text_out().contains("second"), "missing 'second': {}", result.text_out());
6271 }
6272
6273 #[tokio::test]
6274 async fn test_for_loop_accumulates_output() {
6275 let kernel = Kernel::transient().expect("failed to create kernel");
6276 let result = kernel
6277 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
6278 .await
6279 .expect("execution failed");
6280 assert!(result.ok());
6281 assert!(result.text_out().contains("item: a"), "missing 'item: a': {}", result.text_out());
6282 assert!(result.text_out().contains("item: b"), "missing 'item: b': {}", result.text_out());
6283 assert!(result.text_out().contains("item: c"), "missing 'item: c': {}", result.text_out());
6284 }
6285
6286 #[tokio::test]
6287 async fn test_while_loop_accumulates_output() {
6288 let kernel = Kernel::transient().expect("failed to create kernel");
6289 let result = kernel
6290 .execute(r#"
6291 N=3
6292 while [[ ${N} -gt 0 ]]; do
6293 echo "N=${N}"
6294 N=$((N - 1))
6295 done
6296 "#)
6297 .await
6298 .expect("execution failed");
6299 assert!(result.ok());
6300 assert!(result.text_out().contains("N=3"), "missing 'N=3': {}", result.text_out());
6301 assert!(result.text_out().contains("N=2"), "missing 'N=2': {}", result.text_out());
6302 assert!(result.text_out().contains("N=1"), "missing 'N=1': {}", result.text_out());
6303 }
6304
6305 #[tokio::test]
6306 async fn test_kernel_set_var() {
6307 let kernel = Kernel::transient().expect("failed to create kernel");
6308
6309 kernel.execute("X=42").await.expect("set failed");
6310
6311 let value = kernel.get_var("X").await;
6312 assert_eq!(value, Some(Value::Int(42)));
6313 }
6314
6315 #[tokio::test]
6316 async fn test_kernel_var_expansion() {
6317 let kernel = Kernel::transient().expect("failed to create kernel");
6318
6319 kernel.execute("NAME=\"world\"").await.expect("set failed");
6320 let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
6321
6322 assert!(result.ok());
6323 assert_eq!(result.text_out().trim(), "hello world");
6324 }
6325
6326 #[tokio::test]
6327 async fn test_kernel_last_result() {
6328 let kernel = Kernel::transient().expect("failed to create kernel");
6329
6330 kernel.execute("echo test").await.expect("echo failed");
6331
6332 let last = kernel.last_result().await;
6333 assert!(last.ok());
6334 assert_eq!(last.text_out().trim(), "test");
6335 }
6336
6337 #[tokio::test]
6338 async fn test_kernel_tool_not_found() {
6339 let kernel = Kernel::transient().expect("failed to create kernel");
6340
6341 let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
6342 assert!(!result.ok());
6343 assert_eq!(result.code, 127);
6344 assert!(result.err.contains("command not found"));
6345 }
6346
6347 #[tokio::test]
6348 async fn backend_tool_data_content_type_and_baggage_survive_into_exec_result() {
6349 use crate::backend::testing::MockBackend;
6355 use crate::backend::ToolResult;
6356 let (mock, _calls) = MockBackend::new();
6357 let backend = mock.with_tool_result(|_name| {
6358 let mut baggage = std::collections::BTreeMap::new();
6359 baggage.insert("trace_id".to_string(), "abc123".to_string());
6360 Ok(ToolResult {
6361 code: 0,
6362 stdout: String::new(),
6363 stderr: String::new(),
6364 data: Some(serde_json::json!({"key": "value"})),
6365 output: None,
6366 content_type: Some("application/json".to_string()),
6367 baggage,
6368 latch: None,
6369 })
6370 });
6371 let backend: Arc<dyn crate::backend::KernelBackend> = Arc::new(backend);
6372 let kernel = Kernel::with_backend(backend, KernelConfig::isolated(), |_| {}, |_| {})
6373 .expect("with_backend kernel");
6374
6375 let result = kernel
6376 .execute("embedder_tool")
6377 .await
6378 .expect("execution failed");
6379 assert!(result.ok(), "backend tool call should succeed: {result:?}");
6380 assert_eq!(
6381 result.data,
6382 Some(Value::Json(serde_json::json!({"key": "value"}))),
6383 "backend tool's structured data must survive into ExecResult, not be dropped"
6384 );
6385 assert_eq!(
6386 result.content_type.as_deref(),
6387 Some("application/json"),
6388 "backend tool's content_type must survive into ExecResult"
6389 );
6390 assert_eq!(
6391 result.baggage.get("trace_id").map(String::as_str),
6392 Some("abc123"),
6393 "backend tool's baggage must survive into ExecResult"
6394 );
6395 }
6396
6397 #[tokio::test]
6398 async fn backend_tool_execution_error_is_not_reported_as_command_not_found() {
6399 use crate::backend::testing::MockBackend;
6404 let (mock, _calls) = MockBackend::new();
6405 let backend = mock.with_tool_result(|_name| Err(BackendError::Io("disk exploded".to_string())));
6406 let backend: Arc<dyn crate::backend::KernelBackend> = Arc::new(backend);
6407 let kernel = Kernel::with_backend(backend, KernelConfig::isolated(), |_| {}, |_| {})
6408 .expect("with_backend kernel");
6409
6410 let result = kernel
6411 .execute("embedder_tool")
6412 .await
6413 .expect("execution failed");
6414 assert_ne!(result.code, 127, "a real execution error must not look like command-not-found: {result:?}");
6415 assert!(!result.ok());
6416 assert!(
6417 result.err.contains("disk exploded"),
6418 "the real backend error must be visible, not masked: {result:?}"
6419 );
6420 }
6421
6422 #[tokio::test]
6423 async fn test_external_command_true() {
6424 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
6426
6427 let result = kernel.execute("true").await.expect("execution failed");
6429 assert!(result.ok(), "true should succeed: {:?}", result);
6431 }
6432
6433 #[tokio::test]
6434 async fn test_external_command_basic() {
6435 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
6437
6438 let path_var = std::env::var("PATH").unwrap_or_default();
6443 eprintln!("System PATH: {}", path_var);
6444
6445 kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
6447
6448 let result = kernel.execute("uname").await.expect("execution failed");
6451 eprintln!("uname result: {:?}", result);
6452 assert!(result.ok() || result.code == 127, "uname: {:?}", result);
6454 }
6455
6456 #[tokio::test]
6457 async fn test_kernel_reset() {
6458 let kernel = Kernel::transient().expect("failed to create kernel");
6459
6460 kernel.execute("X=1").await.expect("set failed");
6461 assert!(kernel.get_var("X").await.is_some());
6462
6463 kernel.reset().await.expect("reset failed");
6464 assert!(kernel.get_var("X").await.is_none());
6465 }
6466
6467 #[tokio::test]
6468 async fn test_kernel_reset_preserves_latch_and_trash_config() {
6469 let kernel = Kernel::new(
6474 KernelConfig::transient()
6475 .with_latch(true)
6476 .with_skip_validation(true),
6477 )
6478 .expect("failed to create kernel");
6479
6480 kernel.execute("cd /; echo hi > latch-probe.txt").await.expect("setup write failed");
6483
6484 let before = kernel.execute("rm latch-probe.txt").await.expect("execute failed");
6485 assert_eq!(before.code, 2, "latch should require confirmation before reset: {before:?}");
6486
6487 kernel.reset().await.expect("reset failed");
6488
6489 let after = kernel.execute("rm latch-probe.txt").await.expect("execute failed");
6493 assert_eq!(
6494 after.code, 2,
6495 "latch must still require confirmation after reset, not silently disable: {after:?}"
6496 );
6497 }
6498
6499 #[tokio::test]
6500 async fn test_kernel_reset_preserves_pid_and_initial_vars() {
6501 let kernel = Kernel::new(KernelConfig::transient().with_var("HOME", Value::String("/home/probe".into())))
6502 .expect("failed to create kernel");
6503
6504 let pid_before = kernel.execute("echo $$").await.expect("execute failed").text_out().trim().to_string();
6505 assert_eq!(kernel.get_var("HOME").await, Some(Value::String("/home/probe".into())));
6506
6507 kernel.reset().await.expect("reset failed");
6508
6509 let pid_after = kernel.execute("echo $$").await.expect("execute failed").text_out().trim().to_string();
6510 assert_eq!(pid_before, pid_after, "$$ must stay stable across reset(), not silently renumber");
6511 assert_eq!(
6512 kernel.get_var("HOME").await,
6513 Some(Value::String("/home/probe".into())),
6514 "frontend-seeded initial vars (HOME/PATH) must survive reset(), not silently vanish"
6515 );
6516 }
6517
6518 #[tokio::test]
6519 async fn test_kernel_cwd() {
6520 let kernel = Kernel::transient().expect("failed to create kernel");
6521
6522 let cwd = kernel.cwd().await;
6524 let home = std::env::var("HOME")
6525 .map(PathBuf::from)
6526 .unwrap_or_else(|_| PathBuf::from("/"));
6527 assert_eq!(cwd, home);
6528
6529 kernel.set_cwd(PathBuf::from("/tmp")).await;
6530 assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
6531 }
6532
6533 #[tokio::test]
6534 async fn test_kernel_list_vars() {
6535 let kernel = Kernel::transient().expect("failed to create kernel");
6536
6537 kernel.execute("A=1").await.ok();
6538 kernel.execute("B=2").await.ok();
6539
6540 let vars = kernel.list_vars().await;
6541 assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
6542 assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
6543 }
6544
6545 #[tokio::test]
6546 async fn test_is_truthy() {
6547 assert!(!is_truthy(&Value::Null));
6548 assert!(!is_truthy(&Value::Bool(false)));
6549 assert!(is_truthy(&Value::Bool(true)));
6550 assert!(!is_truthy(&Value::Int(0)));
6551 assert!(is_truthy(&Value::Int(1)));
6552 assert!(!is_truthy(&Value::String("".into())));
6553 assert!(is_truthy(&Value::String("x".into())));
6554 }
6555
6556 #[tokio::test]
6557 async fn test_jq_in_pipeline() {
6558 let kernel = Kernel::transient().expect("failed to create kernel");
6559 let result = kernel
6561 .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
6562 .await
6563 .expect("execution failed");
6564 assert!(result.ok(), "jq pipeline failed: {}", result.err);
6565 assert_eq!(result.text_out().trim(), "Alice");
6566 }
6567
6568 #[tokio::test]
6569 async fn test_user_defined_tool() {
6570 let kernel = Kernel::transient().expect("failed to create kernel");
6571
6572 kernel
6574 .execute(r#"greet() { echo "Hello, $1!" }"#)
6575 .await
6576 .expect("function definition failed");
6577
6578 let result = kernel
6580 .execute(r#"greet "World""#)
6581 .await
6582 .expect("function call failed");
6583
6584 assert!(result.ok(), "greet failed: {}", result.err);
6585 assert_eq!(result.text_out().trim(), "Hello, World!");
6586 }
6587
6588 #[tokio::test]
6589 async fn test_user_tool_positional_args() {
6590 let kernel = Kernel::transient().expect("failed to create kernel");
6591
6592 kernel
6594 .execute(r#"greet() { echo "Hi $1" }"#)
6595 .await
6596 .expect("function definition failed");
6597
6598 let result = kernel
6600 .execute(r#"greet "Amy""#)
6601 .await
6602 .expect("function call failed");
6603
6604 assert!(result.ok(), "greet failed: {}", result.err);
6605 assert_eq!(result.text_out().trim(), "Hi Amy");
6606 }
6607
6608 #[tokio::test]
6609 async fn test_function_shared_scope() {
6610 let kernel = Kernel::transient().expect("failed to create kernel");
6611
6612 kernel
6614 .execute(r#"SECRET="hidden""#)
6615 .await
6616 .expect("set failed");
6617
6618 kernel
6620 .execute(r#"access_parent() {
6621 echo "${SECRET}"
6622 SECRET="modified"
6623 }"#)
6624 .await
6625 .expect("function definition failed");
6626
6627 let result = kernel.execute("access_parent").await.expect("function call failed");
6629
6630 assert!(
6632 result.text_out().contains("hidden"),
6633 "Function should access parent scope, got: {}",
6634 result.text_out()
6635 );
6636
6637 let secret = kernel.get_var("SECRET").await;
6639 assert_eq!(
6640 secret,
6641 Some(Value::String("modified".into())),
6642 "Function should modify parent scope"
6643 );
6644 }
6645
6646 #[tokio::test]
6647 #[ignore = "exec replaces the test binary via CommandExt::exec, hangs libtest; cannot be run under cargo test"]
6648 async fn test_exec_builtin() {
6649 let kernel = Kernel::transient().expect("failed to create kernel");
6650 let result = kernel
6652 .execute(r#"exec command="/bin/echo" argv="hello world""#)
6653 .await
6654 .expect("exec failed");
6655
6656 assert!(result.ok(), "exec failed: {}", result.err);
6657 assert_eq!(result.text_out().trim(), "hello world");
6658 }
6659
6660 #[tokio::test]
6661 async fn test_while_false_never_runs() {
6662 let kernel = Kernel::transient().expect("failed to create kernel");
6663
6664 let result = kernel
6666 .execute(r#"
6667 while false; do
6668 echo "should not run"
6669 done
6670 "#)
6671 .await
6672 .expect("while false failed");
6673
6674 assert!(result.ok());
6675 assert!(result.text_out().is_empty(), "while false should not execute body: {}", result.text_out());
6676 }
6677
6678 #[tokio::test]
6679 async fn test_while_string_comparison() {
6680 let kernel = Kernel::transient().expect("failed to create kernel");
6681
6682 kernel.execute(r#"FLAG="go""#).await.expect("set failed");
6684
6685 let result = kernel
6688 .execute(r#"
6689 while [[ ${FLAG} == "go" ]]; do
6690 FLAG="stop"
6691 echo "running"
6692 done
6693 "#)
6694 .await
6695 .expect("while with string cmp failed");
6696
6697 assert!(result.ok());
6698 assert!(result.text_out().contains("running"), "should have run once: {}", result.text_out());
6699
6700 let flag = kernel.get_var("FLAG").await;
6702 assert_eq!(flag, Some(Value::String("stop".into())));
6703 }
6704
6705 #[tokio::test]
6706 async fn test_while_numeric_comparison() {
6707 let kernel = Kernel::transient().expect("failed to create kernel");
6708
6709 kernel.execute("N=5").await.expect("set failed");
6711
6712 let result = kernel
6714 .execute(r#"
6715 while [[ ${N} -gt 3 ]]; do
6716 N=3
6717 echo "N was greater"
6718 done
6719 "#)
6720 .await
6721 .expect("while with > failed");
6722
6723 assert!(result.ok());
6724 assert!(result.text_out().contains("N was greater"), "should have run once: {}", result.text_out());
6725 }
6726
6727 #[tokio::test]
6728 async fn test_break_in_while_loop() {
6729 let kernel = Kernel::transient().expect("failed to create kernel");
6730
6731 let result = kernel
6732 .execute(r#"
6733 I=0
6734 while true; do
6735 I=1
6736 echo "before break"
6737 break
6738 echo "after break"
6739 done
6740 "#)
6741 .await
6742 .expect("while with break failed");
6743
6744 assert!(result.ok());
6745 assert!(result.text_out().contains("before break"), "should see before break: {}", result.text_out());
6746 assert!(!result.text_out().contains("after break"), "should not see after break: {}", result.text_out());
6747
6748 let i = kernel.get_var("I").await;
6750 assert_eq!(i, Some(Value::Int(1)));
6751 }
6752
6753 #[tokio::test]
6754 async fn test_continue_in_while_loop() {
6755 let kernel = Kernel::transient().expect("failed to create kernel");
6756
6757 let result = kernel
6762 .execute(r#"
6763 STATE="start"
6764 AFTER_CONTINUE="no"
6765 while [[ ${STATE} != "done" ]]; do
6766 if [[ ${STATE} == "start" ]]; then
6767 STATE="middle"
6768 continue
6769 AFTER_CONTINUE="yes"
6770 fi
6771 if [[ ${STATE} == "middle" ]]; then
6772 STATE="done"
6773 fi
6774 done
6775 "#)
6776 .await
6777 .expect("while with continue failed");
6778
6779 assert!(result.ok());
6780
6781 let state = kernel.get_var("STATE").await;
6783 assert_eq!(state, Some(Value::String("done".into())));
6784
6785 let after = kernel.get_var("AFTER_CONTINUE").await;
6787 assert_eq!(after, Some(Value::String("no".into())));
6788 }
6789
6790 #[tokio::test]
6791 async fn test_break_with_level() {
6792 let kernel = Kernel::transient().expect("failed to create kernel");
6793
6794 let result = kernel
6799 .execute(r#"
6800 OUTER=0
6801 while true; do
6802 OUTER=1
6803 for X in "1 2"; do
6804 break 2
6805 done
6806 OUTER=2
6807 done
6808 "#)
6809 .await
6810 .expect("nested break failed");
6811
6812 assert!(result.ok());
6813
6814 let outer = kernel.get_var("OUTER").await;
6816 assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
6817 }
6818
6819 #[tokio::test]
6820 async fn test_return_from_tool() {
6821 let kernel = Kernel::transient().expect("failed to create kernel");
6822
6823 kernel
6825 .execute(r#"early_return() {
6826 if [[ $1 == 1 ]]; then
6827 return 42
6828 fi
6829 echo "not returned"
6830 }"#)
6831 .await
6832 .expect("function definition failed");
6833
6834 let result = kernel
6837 .execute("early_return 1")
6838 .await
6839 .expect("function call failed");
6840
6841 assert_eq!(result.code, 42);
6843 assert!(result.text_out().is_empty());
6845 }
6846
6847 #[tokio::test]
6848 async fn test_return_without_value() {
6849 let kernel = Kernel::transient().expect("failed to create kernel");
6850
6851 kernel
6853 .execute(r#"early_exit() {
6854 if [[ $1 == "stop" ]]; then
6855 return
6856 fi
6857 echo "continued"
6858 }"#)
6859 .await
6860 .expect("function definition failed");
6861
6862 let result = kernel
6864 .execute(r#"early_exit "stop""#)
6865 .await
6866 .expect("function call failed");
6867
6868 assert!(result.ok());
6869 assert!(result.text_out().is_empty() || result.text_out().trim().is_empty());
6870 }
6871
6872 #[tokio::test]
6873 async fn test_exit_stops_execution() {
6874 let kernel = Kernel::transient().expect("failed to create kernel");
6875
6876 kernel
6878 .execute(r#"
6879 BEFORE="yes"
6880 exit 0
6881 AFTER="yes"
6882 "#)
6883 .await
6884 .expect("execution failed");
6885
6886 let before = kernel.get_var("BEFORE").await;
6888 assert_eq!(before, Some(Value::String("yes".into())));
6889
6890 let after = kernel.get_var("AFTER").await;
6891 assert!(after.is_none(), "AFTER should not be set after exit");
6892 }
6893
6894 #[tokio::test]
6895 async fn test_exit_with_code() {
6896 let kernel = Kernel::transient().expect("failed to create kernel");
6897
6898 let result = kernel
6900 .execute("exit 42")
6901 .await
6902 .expect("exit failed");
6903
6904 assert_eq!(result.code, 42);
6905 assert!(result.text_out().is_empty(), "exit should not produce stdout");
6906 }
6907
6908 #[tokio::test]
6909 async fn test_set_e_stops_on_failure() {
6910 let kernel = Kernel::transient().expect("failed to create kernel");
6911
6912 kernel.execute("set -e").await.expect("set -e failed");
6914
6915 kernel
6917 .execute(r#"
6918 STEP1="done"
6919 false
6920 STEP2="done"
6921 "#)
6922 .await
6923 .expect("execution failed");
6924
6925 let step1 = kernel.get_var("STEP1").await;
6927 assert_eq!(step1, Some(Value::String("done".into())));
6928
6929 let step2 = kernel.get_var("STEP2").await;
6930 assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
6931 }
6932
6933 #[tokio::test]
6934 async fn test_set_plus_e_disables_error_exit() {
6935 let kernel = Kernel::transient().expect("failed to create kernel");
6936
6937 kernel.execute("set -e").await.expect("set -e failed");
6939 kernel.execute("set +e").await.expect("set +e failed");
6940
6941 kernel
6943 .execute(r#"
6944 STEP1="done"
6945 false
6946 STEP2="done"
6947 "#)
6948 .await
6949 .expect("execution failed");
6950
6951 let step1 = kernel.get_var("STEP1").await;
6953 assert_eq!(step1, Some(Value::String("done".into())));
6954
6955 let step2 = kernel.get_var("STEP2").await;
6956 assert_eq!(step2, Some(Value::String("done".into())));
6957 }
6958
6959 #[tokio::test]
6960 async fn test_set_ignores_unknown_options() {
6961 let kernel = Kernel::transient().expect("failed to create kernel");
6962
6963 let result = kernel
6965 .execute("set -e -u -o pipefail")
6966 .await
6967 .expect("set with unknown options failed");
6968
6969 assert!(result.ok(), "set should succeed with unknown options");
6970
6971 kernel
6973 .execute(r#"
6974 BEFORE="yes"
6975 false
6976 AFTER="yes"
6977 "#)
6978 .await
6979 .ok();
6980
6981 let after = kernel.get_var("AFTER").await;
6982 assert!(after.is_none(), "-e should be enabled despite unknown options");
6983 }
6984
6985 #[tokio::test]
6986 async fn test_set_no_args_shows_settings() {
6987 let kernel = Kernel::transient().expect("failed to create kernel");
6988
6989 kernel.execute("set -e").await.expect("set -e failed");
6991
6992 let result = kernel.execute("set").await.expect("set failed");
6994
6995 assert!(result.ok());
6996 assert!(result.text_out().contains("set -e"), "should show -e is enabled: {}", result.text_out());
6997 }
6998
6999 #[tokio::test]
7000 async fn test_set_e_in_pipeline() {
7001 let kernel = Kernel::transient().expect("failed to create kernel");
7002
7003 kernel.execute("set -e").await.expect("set -e failed");
7004
7005 kernel
7007 .execute(r#"
7008 BEFORE="yes"
7009 false | cat
7010 AFTER="yes"
7011 "#)
7012 .await
7013 .ok();
7014
7015 let before = kernel.get_var("BEFORE").await;
7016 assert_eq!(before, Some(Value::String("yes".into())));
7017
7018 }
7023
7024 #[tokio::test]
7025 async fn test_set_e_with_and_chain() {
7026 let kernel = Kernel::transient().expect("failed to create kernel");
7027
7028 kernel.execute("set -e").await.expect("set -e failed");
7029
7030 kernel
7033 .execute(r#"
7034 RESULT="initial"
7035 false && RESULT="chained"
7036 RESULT="continued"
7037 "#)
7038 .await
7039 .ok();
7040
7041 let result = kernel.get_var("RESULT").await;
7044 assert!(result.is_some(), "RESULT should be set");
7047 }
7048
7049 #[tokio::test]
7050 async fn test_set_e_exits_in_for_loop() {
7051 let kernel = Kernel::transient().expect("failed to create kernel");
7052
7053 kernel.execute("set -e").await.expect("set -e failed");
7054
7055 kernel
7056 .execute(r#"
7057 REACHED="no"
7058 for x in 1 2 3; do
7059 false
7060 REACHED="yes"
7061 done
7062 "#)
7063 .await
7064 .ok();
7065
7066 let reached = kernel.get_var("REACHED").await;
7068 assert_eq!(reached, Some(Value::String("no".into())),
7069 "set -e should exit on failure in for loop body");
7070 }
7071
7072 #[tokio::test]
7073 async fn test_for_loop_continues_without_set_e() {
7074 let kernel = Kernel::transient().expect("failed to create kernel");
7075
7076 kernel
7078 .execute(r#"
7079 COUNT=0
7080 for x in 1 2 3; do
7081 false
7082 COUNT=$((COUNT + 1))
7083 done
7084 "#)
7085 .await
7086 .ok();
7087
7088 let count = kernel.get_var("COUNT").await;
7089 let count_val = match &count {
7091 Some(Value::Int(n)) => *n,
7092 Some(Value::String(s)) => s.parse().unwrap_or(-1),
7093 _ => -1,
7094 };
7095 assert_eq!(count_val, 3,
7096 "without set -e, loop should complete all iterations (got {:?})", count);
7097 }
7098
7099 #[tokio::test]
7104 async fn test_source_sets_variables() {
7105 let kernel = Kernel::transient().expect("failed to create kernel");
7106
7107 kernel
7109 .execute(r#"write "/test.kai" 'FOO="bar"'"#)
7110 .await
7111 .expect("write failed");
7112
7113 let result = kernel
7115 .execute(r#"source "/test.kai""#)
7116 .await
7117 .expect("source failed");
7118
7119 assert!(result.ok(), "source should succeed");
7120
7121 let foo = kernel.get_var("FOO").await;
7123 assert_eq!(foo, Some(Value::String("bar".into())));
7124 }
7125
7126 #[tokio::test]
7127 async fn test_source_with_dot_alias() {
7128 let kernel = Kernel::transient().expect("failed to create kernel");
7129
7130 kernel
7132 .execute(r#"write "/vars.kai" 'X=42'"#)
7133 .await
7134 .expect("write failed");
7135
7136 let result = kernel
7138 .execute(r#". "/vars.kai""#)
7139 .await
7140 .expect(". failed");
7141
7142 assert!(result.ok(), ". should succeed");
7143
7144 let x = kernel.get_var("X").await;
7146 assert_eq!(x, Some(Value::Int(42)));
7147 }
7148
7149 #[tokio::test]
7150 async fn test_source_not_found() {
7151 let kernel = Kernel::transient().expect("failed to create kernel");
7152
7153 let result = kernel
7155 .execute(r#"source "/nonexistent.kai""#)
7156 .await
7157 .expect("source should not fail with error");
7158
7159 assert!(!result.ok(), "source of non-existent file should fail");
7160 assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
7161 }
7162
7163 #[tokio::test]
7164 async fn test_source_missing_filename() {
7165 let kernel = Kernel::transient().expect("failed to create kernel");
7166
7167 let result = kernel
7169 .execute("source")
7170 .await
7171 .expect("source should not fail with error");
7172
7173 assert!(!result.ok(), "source without filename should fail");
7174 assert!(result.err.contains("missing filename"), "error should mention missing filename");
7175 }
7176
7177 #[tokio::test]
7178 async fn test_source_executes_multiple_statements() {
7179 let kernel = Kernel::transient().expect("failed to create kernel");
7180
7181 kernel
7183 .execute(r#"write "/multi.kai" 'A=1
7184B=2
7185C=3'"#)
7186 .await
7187 .expect("write failed");
7188
7189 kernel
7191 .execute(r#"source "/multi.kai""#)
7192 .await
7193 .expect("source failed");
7194
7195 assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
7197 assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
7198 assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
7199 }
7200
7201 #[tokio::test]
7202 async fn test_source_can_define_functions() {
7203 let kernel = Kernel::transient().expect("failed to create kernel");
7204
7205 kernel
7207 .execute(r#"write "/functions.kai" 'greet() {
7208 echo "Hello, $1!"
7209}'"#)
7210 .await
7211 .expect("write failed");
7212
7213 kernel
7215 .execute(r#"source "/functions.kai""#)
7216 .await
7217 .expect("source failed");
7218
7219 let result = kernel
7221 .execute(r#"greet "World""#)
7222 .await
7223 .expect("greet failed");
7224
7225 assert!(result.ok());
7226 assert!(result.text_out().contains("Hello, World!"));
7227 }
7228
7229 #[tokio::test]
7230 async fn test_source_inherits_error_exit() {
7231 let kernel = Kernel::transient().expect("failed to create kernel");
7232
7233 kernel.execute("set -e").await.expect("set -e failed");
7235
7236 kernel
7238 .execute(r#"write "/fail.kai" 'BEFORE="yes"
7239false
7240AFTER="yes"'"#)
7241 .await
7242 .expect("write failed");
7243
7244 kernel
7246 .execute(r#"source "/fail.kai""#)
7247 .await
7248 .ok();
7249
7250 let before = kernel.get_var("BEFORE").await;
7252 assert_eq!(before, Some(Value::String("yes".into())));
7253
7254 }
7257
7258 #[tokio::test]
7263 async fn test_set_e_and_chain_left_fails() {
7264 let kernel = Kernel::transient().expect("failed to create kernel");
7266 kernel.execute("set -e").await.expect("set -e failed");
7267
7268 kernel
7269 .execute("false && echo hi; REACHED=1")
7270 .await
7271 .expect("execution failed");
7272
7273 let reached = kernel.get_var("REACHED").await;
7274 assert_eq!(
7275 reached,
7276 Some(Value::Int(1)),
7277 "set -e should not trigger on left side of &&"
7278 );
7279 }
7280
7281 #[tokio::test]
7282 async fn test_set_e_and_chain_right_fails() {
7283 let kernel = Kernel::transient().expect("failed to create kernel");
7285 kernel.execute("set -e").await.expect("set -e failed");
7286
7287 kernel
7288 .execute("true && false; REACHED=1")
7289 .await
7290 .expect("execution failed");
7291
7292 let reached = kernel.get_var("REACHED").await;
7293 assert!(
7294 reached.is_none(),
7295 "set -e should trigger when right side of && fails"
7296 );
7297 }
7298
7299 #[tokio::test]
7300 async fn test_set_e_or_chain_recovers() {
7301 let kernel = Kernel::transient().expect("failed to create kernel");
7303 kernel.execute("set -e").await.expect("set -e failed");
7304
7305 kernel
7306 .execute("false || echo recovered; REACHED=1")
7307 .await
7308 .expect("execution failed");
7309
7310 let reached = kernel.get_var("REACHED").await;
7311 assert_eq!(
7312 reached,
7313 Some(Value::Int(1)),
7314 "set -e should not trigger when || recovers the failure"
7315 );
7316 }
7317
7318 #[tokio::test]
7319 async fn test_set_e_or_chain_both_fail() {
7320 let kernel = Kernel::transient().expect("failed to create kernel");
7322 kernel.execute("set -e").await.expect("set -e failed");
7323
7324 kernel
7325 .execute("false || false; REACHED=1")
7326 .await
7327 .expect("execution failed");
7328
7329 let reached = kernel.get_var("REACHED").await;
7330 assert!(
7331 reached.is_none(),
7332 "set -e should trigger when || chain ultimately fails"
7333 );
7334 }
7335
7336 fn schedule_cancel(kernel: &Arc<Kernel>, delay: std::time::Duration) {
7343 let k = Arc::clone(kernel);
7344 std::thread::spawn(move || {
7345 std::thread::sleep(delay);
7346 k.cancel();
7347 });
7348 }
7349
7350 #[tokio::test]
7351 async fn test_cancel_interrupts_for_loop() {
7352 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
7353
7354 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
7356
7357 let result = kernel
7358 .execute("for i in $(seq 1 100000); do X=$i; done")
7359 .await
7360 .expect("execute failed");
7361
7362 assert_eq!(result.code, 130, "cancelled execution should exit with code 130");
7363
7364 let x = kernel.get_var("X").await;
7366 if let Some(Value::Int(n)) = x {
7367 assert!(n < 100000, "loop should have been interrupted before finishing, got X={n}");
7368 }
7369 }
7370
7371 #[tokio::test]
7372 async fn test_cancel_interrupts_while_loop() {
7373 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
7374 kernel.execute("COUNT=0").await.expect("init failed");
7375
7376 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
7377
7378 let result = kernel
7379 .execute("while true; do COUNT=$((COUNT + 1)); done")
7380 .await
7381 .expect("execute failed");
7382
7383 assert_eq!(result.code, 130);
7384
7385 let count = kernel.get_var("COUNT").await;
7386 if let Some(Value::Int(n)) = count {
7387 assert!(n > 0, "loop should have run at least once");
7388 }
7389 }
7390
7391 #[tokio::test]
7392 async fn test_reset_after_cancel() {
7393 let kernel = Kernel::transient().expect("failed to create kernel");
7395 kernel.cancel(); let result = kernel.execute("echo hello").await.expect("execute failed");
7398 assert!(result.ok(), "execute after cancel should succeed");
7399 assert_eq!(result.text_out().trim(), "hello");
7400 }
7401
7402 #[tokio::test]
7403 async fn test_cancel_interrupts_statement_sequence() {
7404 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
7405
7406 schedule_cancel(&kernel, std::time::Duration::from_millis(50));
7408
7409 let result = kernel
7410 .execute("STEP=1; sleep 5; STEP=2; sleep 5; STEP=3")
7411 .await
7412 .expect("execute failed");
7413
7414 assert_eq!(result.code, 130);
7415
7416 let step = kernel.get_var("STEP").await;
7418 assert_eq!(step, Some(Value::Int(1)), "cancel should stop before STEP=2");
7419 }
7420
7421 #[tokio::test]
7426 async fn test_case_simple_match() {
7427 let kernel = Kernel::transient().expect("failed to create kernel");
7428
7429 let result = kernel
7430 .execute(r#"
7431 case "hello" in
7432 hello) echo "matched hello" ;;
7433 world) echo "matched world" ;;
7434 esac
7435 "#)
7436 .await
7437 .expect("case failed");
7438
7439 assert!(result.ok());
7440 assert_eq!(result.text_out().trim(), "matched hello");
7441 }
7442
7443 #[tokio::test]
7444 async fn test_case_wildcard_match() {
7445 let kernel = Kernel::transient().expect("failed to create kernel");
7446
7447 let result = kernel
7448 .execute(r#"
7449 case "main.rs" in
7450 *.py) echo "Python" ;;
7451 *.rs) echo "Rust" ;;
7452 *) echo "Unknown" ;;
7453 esac
7454 "#)
7455 .await
7456 .expect("case failed");
7457
7458 assert!(result.ok());
7459 assert_eq!(result.text_out().trim(), "Rust");
7460 }
7461
7462 #[tokio::test]
7463 async fn test_case_default_match() {
7464 let kernel = Kernel::transient().expect("failed to create kernel");
7465
7466 let result = kernel
7467 .execute(r#"
7468 case "unknown.xyz" in
7469 *.py) echo "Python" ;;
7470 *.rs) echo "Rust" ;;
7471 *) echo "Default" ;;
7472 esac
7473 "#)
7474 .await
7475 .expect("case failed");
7476
7477 assert!(result.ok());
7478 assert_eq!(result.text_out().trim(), "Default");
7479 }
7480
7481 #[tokio::test]
7482 async fn test_case_no_match() {
7483 let kernel = Kernel::transient().expect("failed to create kernel");
7484
7485 let result = kernel
7487 .execute(r#"
7488 case "nope" in
7489 "yes") echo "yes" ;;
7490 "no") echo "no" ;;
7491 esac
7492 "#)
7493 .await
7494 .expect("case failed");
7495
7496 assert!(result.ok());
7497 assert!(result.text_out().is_empty(), "no match should produce empty output");
7498 }
7499
7500 #[tokio::test]
7501 async fn test_case_with_variable() {
7502 let kernel = Kernel::transient().expect("failed to create kernel");
7503
7504 kernel.execute(r#"LANG="rust""#).await.expect("set failed");
7505
7506 let result = kernel
7507 .execute(r#"
7508 case ${LANG} in
7509 python) echo "snake" ;;
7510 rust) echo "crab" ;;
7511 go) echo "gopher" ;;
7512 esac
7513 "#)
7514 .await
7515 .expect("case failed");
7516
7517 assert!(result.ok());
7518 assert_eq!(result.text_out().trim(), "crab");
7519 }
7520
7521 #[tokio::test]
7522 async fn test_case_multiple_patterns() {
7523 let kernel = Kernel::transient().expect("failed to create kernel");
7524
7525 let result = kernel
7526 .execute(r#"
7527 case "yes" in
7528 "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
7529 "n"|"no"|"N"|"NO") echo "negative" ;;
7530 esac
7531 "#)
7532 .await
7533 .expect("case failed");
7534
7535 assert!(result.ok());
7536 assert_eq!(result.text_out().trim(), "affirmative");
7537 }
7538
7539 #[tokio::test]
7540 async fn test_case_glob_question_mark() {
7541 let kernel = Kernel::transient().expect("failed to create kernel");
7542
7543 let result = kernel
7544 .execute(r#"
7545 case "test1" in
7546 test?) echo "matched test?" ;;
7547 *) echo "default" ;;
7548 esac
7549 "#)
7550 .await
7551 .expect("case failed");
7552
7553 assert!(result.ok());
7554 assert_eq!(result.text_out().trim(), "matched test?");
7555 }
7556
7557 #[tokio::test]
7558 async fn test_case_char_class() {
7559 let kernel = Kernel::transient().expect("failed to create kernel");
7560
7561 let result = kernel
7562 .execute(r#"
7563 case "Yes" in
7564 [Yy]*) echo "yes-like" ;;
7565 [Nn]*) echo "no-like" ;;
7566 esac
7567 "#)
7568 .await
7569 .expect("case failed");
7570
7571 assert!(result.ok());
7572 assert_eq!(result.text_out().trim(), "yes-like");
7573 }
7574
7575 #[tokio::test]
7580 async fn test_cat_from_pipeline() {
7581 let kernel = Kernel::transient().expect("failed to create kernel");
7582
7583 let result = kernel
7584 .execute(r#"echo "piped text" | cat"#)
7585 .await
7586 .expect("cat pipeline failed");
7587
7588 assert!(result.ok(), "cat failed: {}", result.err);
7589 assert_eq!(result.text_out().trim(), "piped text");
7590 }
7591
7592 #[tokio::test]
7593 async fn test_cat_from_pipeline_multiline() {
7594 let kernel = Kernel::transient().expect("failed to create kernel");
7595
7596 let result = kernel
7597 .execute(r#"echo "line1\nline2" | cat -n"#)
7598 .await
7599 .expect("cat pipeline failed");
7600
7601 assert!(result.ok(), "cat failed: {}", result.err);
7602 assert!(result.text_out().contains("1\t"), "output: {}", result.text_out());
7603 }
7604
7605 #[tokio::test]
7610 async fn test_heredoc_basic() {
7611 let kernel = Kernel::transient().expect("failed to create kernel");
7612
7613 let result = kernel
7614 .execute("cat <<EOF\nhello\nEOF")
7615 .await
7616 .expect("heredoc failed");
7617
7618 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
7619 assert_eq!(result.text_out().trim(), "hello");
7620 }
7621
7622 #[tokio::test]
7623 async fn test_arithmetic_in_string() {
7624 let kernel = Kernel::transient().expect("failed to create kernel");
7625
7626 let result = kernel
7627 .execute(r#"echo "result: $((1 + 2))""#)
7628 .await
7629 .expect("arithmetic in string failed");
7630
7631 assert!(result.ok(), "echo failed: {}", result.err);
7632 assert_eq!(result.text_out().trim(), "result: 3");
7633 }
7634
7635 #[tokio::test]
7636 async fn test_heredoc_multiline() {
7637 let kernel = Kernel::transient().expect("failed to create kernel");
7638
7639 let result = kernel
7640 .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
7641 .await
7642 .expect("heredoc failed");
7643
7644 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
7645 assert!(result.text_out().contains("line1"), "output: {}", result.text_out());
7646 assert!(result.text_out().contains("line2"), "output: {}", result.text_out());
7647 assert!(result.text_out().contains("line3"), "output: {}", result.text_out());
7648 }
7649
7650 #[tokio::test]
7651 async fn test_heredoc_variable_expansion() {
7652 let kernel = Kernel::transient().expect("failed to create kernel");
7654
7655 kernel.execute("GREETING=hello").await.expect("set var");
7656
7657 let result = kernel
7658 .execute("cat <<EOF\n$GREETING world\nEOF")
7659 .await
7660 .expect("heredoc expansion failed");
7661
7662 assert!(result.ok(), "heredoc expansion failed: {}", result.err);
7663 assert_eq!(result.text_out().trim(), "hello world");
7664 }
7665
7666 #[tokio::test]
7667 async fn test_heredoc_quoted_no_expansion() {
7668 let kernel = Kernel::transient().expect("failed to create kernel");
7670
7671 kernel.execute("GREETING=hello").await.expect("set var");
7672
7673 let result = kernel
7674 .execute("cat <<'EOF'\n$GREETING world\nEOF")
7675 .await
7676 .expect("quoted heredoc failed");
7677
7678 assert!(result.ok(), "quoted heredoc failed: {}", result.err);
7679 assert_eq!(result.text_out().trim(), "$GREETING world");
7680 }
7681
7682 #[tokio::test]
7683 async fn test_heredoc_default_value_expansion() {
7684 let kernel = Kernel::transient().expect("failed to create kernel");
7686
7687 let result = kernel
7688 .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
7689 .await
7690 .expect("heredoc default expansion failed");
7691
7692 assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
7693 assert_eq!(result.text_out().trim(), "fallback");
7694 }
7695
7696 #[tokio::test]
7701 async fn test_read_from_pipeline() {
7702 let kernel = Kernel::transient().expect("failed to create kernel");
7703
7704 let result = kernel
7706 .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
7707 .await
7708 .expect("read pipeline failed");
7709
7710 assert!(result.ok(), "read failed: {}", result.err);
7711 assert!(result.text_out().contains("Hello, Alice"), "output: {}", result.text_out());
7712 }
7713
7714 #[tokio::test]
7715 async fn test_read_multiple_vars_from_pipeline() {
7716 let kernel = Kernel::transient().expect("failed to create kernel");
7717
7718 let result = kernel
7719 .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
7720 .await
7721 .expect("read pipeline failed");
7722
7723 assert!(result.ok(), "read failed: {}", result.err);
7724 assert!(result.text_out().contains("John is 42"), "output: {}", result.text_out());
7725 }
7726
7727 #[tokio::test]
7732 async fn test_posix_function_with_positional_params() {
7733 let kernel = Kernel::transient().expect("failed to create kernel");
7734
7735 kernel
7737 .execute(r#"greet() { echo "Hello, $1!" }"#)
7738 .await
7739 .expect("function definition failed");
7740
7741 let result = kernel
7743 .execute(r#"greet "Amy""#)
7744 .await
7745 .expect("function call failed");
7746
7747 assert!(result.ok(), "greet failed: {}", result.err);
7748 assert_eq!(result.text_out().trim(), "Hello, Amy!");
7749 }
7750
7751 #[tokio::test]
7752 async fn test_posix_function_multiple_args() {
7753 let kernel = Kernel::transient().expect("failed to create kernel");
7754
7755 kernel
7757 .execute(r#"add_greeting() { echo "$1 $2!" }"#)
7758 .await
7759 .expect("function definition failed");
7760
7761 let result = kernel
7763 .execute(r#"add_greeting "Hello" "World""#)
7764 .await
7765 .expect("function call failed");
7766
7767 assert!(result.ok(), "function failed: {}", result.err);
7768 assert_eq!(result.text_out().trim(), "Hello World!");
7769 }
7770
7771 #[tokio::test]
7772 async fn test_bash_function_with_positional_params() {
7773 let kernel = Kernel::transient().expect("failed to create kernel");
7774
7775 kernel
7777 .execute(r#"function greet { echo "Hi $1" }"#)
7778 .await
7779 .expect("function definition failed");
7780
7781 let result = kernel
7783 .execute(r#"greet "Bob""#)
7784 .await
7785 .expect("function call failed");
7786
7787 assert!(result.ok(), "greet failed: {}", result.err);
7788 assert_eq!(result.text_out().trim(), "Hi Bob");
7789 }
7790
7791 #[tokio::test]
7792 async fn test_shell_function_with_all_args() {
7793 let kernel = Kernel::transient().expect("failed to create kernel");
7794
7795 kernel
7797 .execute(r#"echo_all() { echo "args: $@" }"#)
7798 .await
7799 .expect("function definition failed");
7800
7801 let result = kernel
7803 .execute(r#"echo_all "a" "b" "c""#)
7804 .await
7805 .expect("function call failed");
7806
7807 assert!(result.ok(), "function failed: {}", result.err);
7808 assert_eq!(result.text_out().trim(), "args: a b c");
7809 }
7810
7811 #[tokio::test]
7812 async fn test_shell_function_with_arg_count() {
7813 let kernel = Kernel::transient().expect("failed to create kernel");
7814
7815 kernel
7817 .execute(r#"count_args() { echo "count: $#" }"#)
7818 .await
7819 .expect("function definition failed");
7820
7821 let result = kernel
7823 .execute(r#"count_args "x" "y" "z""#)
7824 .await
7825 .expect("function call failed");
7826
7827 assert!(result.ok(), "function failed: {}", result.err);
7828 assert_eq!(result.text_out().trim(), "count: 3");
7829 }
7830
7831 #[tokio::test]
7832 async fn test_shell_function_shared_scope() {
7833 let kernel = Kernel::transient().expect("failed to create kernel");
7834
7835 kernel
7837 .execute(r#"PARENT_VAR="visible""#)
7838 .await
7839 .expect("set failed");
7840
7841 kernel
7843 .execute(r#"modify_parent() {
7844 echo "saw: ${PARENT_VAR}"
7845 PARENT_VAR="changed by function"
7846 }"#)
7847 .await
7848 .expect("function definition failed");
7849
7850 let result = kernel.execute("modify_parent").await.expect("function failed");
7852
7853 assert!(
7854 result.text_out().contains("visible"),
7855 "Shell function should access parent scope, got: {}",
7856 result.text_out()
7857 );
7858
7859 let var = kernel.get_var("PARENT_VAR").await;
7861 assert_eq!(
7862 var,
7863 Some(Value::String("changed by function".into())),
7864 "Shell function should modify parent scope"
7865 );
7866 }
7867
7868 #[tokio::test]
7873 async fn test_script_execution_from_path() {
7874 let kernel = Kernel::transient().expect("failed to create kernel");
7875
7876 kernel.execute(r#"mkdir "/bin""#).await.ok();
7878 kernel
7879 .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
7880 .await
7881 .expect("write script failed");
7882
7883 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
7885
7886 let result = kernel
7888 .execute("hello")
7889 .await
7890 .expect("script execution failed");
7891
7892 assert!(result.ok(), "script failed: {}", result.err);
7893 assert_eq!(result.text_out().trim(), "Hello from script!");
7894 }
7895
7896 #[tokio::test]
7897 async fn test_script_with_args() {
7898 let kernel = Kernel::transient().expect("failed to create kernel");
7899
7900 kernel.execute(r#"mkdir "/bin""#).await.ok();
7902 kernel
7903 .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
7904 .await
7905 .expect("write script failed");
7906
7907 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
7909
7910 let result = kernel
7912 .execute(r#"greet "World""#)
7913 .await
7914 .expect("script execution failed");
7915
7916 assert!(result.ok(), "script failed: {}", result.err);
7917 assert_eq!(result.text_out().trim(), "Hello, World!");
7918 }
7919
7920 #[tokio::test]
7921 async fn test_script_not_found() {
7922 let kernel = Kernel::transient().expect("failed to create kernel");
7923
7924 kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
7926
7927 let result = kernel
7929 .execute("noscript")
7930 .await
7931 .expect("execution failed");
7932
7933 assert!(!result.ok(), "should fail with command not found");
7934 assert_eq!(result.code, 127);
7935 assert!(result.err.contains("command not found"));
7936 }
7937
7938 #[tokio::test]
7939 async fn test_script_path_search_order() {
7940 let kernel = Kernel::transient().expect("failed to create kernel");
7941
7942 kernel.execute(r#"mkdir "/first""#).await.ok();
7945 kernel.execute(r#"mkdir "/second""#).await.ok();
7946 kernel
7947 .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
7948 .await
7949 .expect("write failed");
7950 kernel
7951 .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
7952 .await
7953 .expect("write failed");
7954
7955 kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
7957
7958 let result = kernel
7960 .execute("myscript")
7961 .await
7962 .expect("script execution failed");
7963
7964 assert!(result.ok(), "script failed: {}", result.err);
7965 assert_eq!(result.text_out().trim(), "from first");
7966 }
7967
7968 #[tokio::test]
7973 async fn test_last_exit_code_success() {
7974 let kernel = Kernel::transient().expect("failed to create kernel");
7975
7976 let result = kernel.execute("true; echo $?").await.expect("execution failed");
7978 assert!(result.text_out().contains("0"), "expected 0, got: {}", result.text_out());
7979 }
7980
7981 #[tokio::test]
7982 async fn test_last_exit_code_failure() {
7983 let kernel = Kernel::transient().expect("failed to create kernel");
7984
7985 let result = kernel.execute("false; echo $?").await.expect("execution failed");
7987 assert!(result.text_out().contains("1"), "expected 1, got: {}", result.text_out());
7988 }
7989
7990 #[tokio::test]
7991 async fn test_current_pid() {
7992 let kernel = Kernel::transient().expect("failed to create kernel");
7993
7994 let result = kernel.execute("echo $$").await.expect("execution failed");
7995 let pid: u32 = result.text_out().trim().parse().expect("PID should be a number");
7997 assert!(pid > 0, "PID should be positive");
7998 }
7999
8000 #[tokio::test]
8001 async fn test_unset_variable_expands_to_empty() {
8002 let kernel = Kernel::transient().expect("failed to create kernel");
8003
8004 let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
8006 assert_eq!(result.text_out().trim(), "prefix::suffix");
8007 }
8008
8009 #[tokio::test]
8010 async fn test_eq_ne_operators() {
8011 let kernel = Kernel::transient().expect("failed to create kernel");
8012
8013 let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
8015 assert_eq!(result.text_out().trim(), "eq works");
8016
8017 let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
8019 assert_eq!(result.text_out().trim(), "ne works");
8020
8021 let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
8023 assert_eq!(result.text_out().trim(), "correct");
8024 }
8025
8026 #[tokio::test]
8027 async fn test_escaped_dollar_in_string() {
8028 let kernel = Kernel::transient().expect("failed to create kernel");
8029
8030 let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
8032 assert_eq!(result.text_out().trim(), "$100");
8033 }
8034
8035 #[tokio::test]
8036 async fn test_special_vars_in_interpolation() {
8037 let kernel = Kernel::transient().expect("failed to create kernel");
8038
8039 let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
8041 assert_eq!(result.text_out().trim(), "exit: 0");
8042
8043 let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
8045 assert!(result.text_out().starts_with("pid: "), "unexpected output: {}", result.text_out());
8046 let text = result.text_out();
8047 let pid_part = text.trim().strip_prefix("pid: ").unwrap();
8048 let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
8049 }
8050
8051 #[tokio::test]
8056 async fn test_command_subst_assignment() {
8057 let kernel = Kernel::transient().expect("failed to create kernel");
8058
8059 let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
8061 assert_eq!(result.text_out().trim(), "hello");
8062 }
8063
8064 #[tokio::test]
8065 async fn test_command_subst_with_args() {
8066 let kernel = Kernel::transient().expect("failed to create kernel");
8067
8068 let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
8070 assert_eq!(result.text_out().trim(), "a b c");
8071 }
8072
8073 #[tokio::test]
8074 async fn test_command_subst_nested_vars() {
8075 let kernel = Kernel::transient().expect("failed to create kernel");
8076
8077 let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
8079 assert_eq!(result.text_out().trim(), "hello world");
8080 }
8081
8082 #[tokio::test]
8083 async fn test_background_job_basic() {
8084 use std::time::Duration;
8085
8086 let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
8087
8088 let result = kernel.execute("echo hello &").await.expect("execution failed");
8090 assert!(result.ok(), "background command should succeed: {}", result.err);
8091 assert!(result.text_out().contains("[1]"), "should return job ID: {}", result.text_out());
8092
8093 tokio::time::sleep(Duration::from_millis(100)).await;
8095
8096 let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
8098 assert!(status.ok(), "status should succeed: {}", status.err);
8099 assert!(
8100 status.text_out().contains("done:") || status.text_out().contains("running"),
8101 "should have valid status: {}",
8102 status.text_out()
8103 );
8104
8105 let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
8107 assert!(stdout.ok());
8108 assert!(stdout.text_out().contains("hello"));
8109 }
8110
8111 #[tokio::test]
8112 async fn test_heredoc_piped_to_command() {
8113 let kernel = Kernel::transient().expect("kernel");
8115 let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
8116 assert!(result.ok(), "heredoc | cat failed: {}", result.err);
8117 assert_eq!(result.text_out().trim(), "hello world");
8118 }
8119
8120 fn transient_with_tempdir() -> (Kernel, tempfile::TempDir, String) {
8128 let kernel = Kernel::transient().expect("kernel");
8129 let tmp = tempfile::tempdir().expect("tempdir");
8130 let dir = tmp.path().display().to_string();
8131 (kernel, tmp, dir)
8132 }
8133
8134 #[tokio::test]
8135 async fn test_for_loop_glob_iterates() {
8136 let (kernel, _tmp, dir) = transient_with_tempdir();
8138 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
8139 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
8140 let result = kernel.execute(&format!(r#"
8141 N=0
8142 for F in $(glob "{dir}/*.txt"); do
8143 N=$((N + 1))
8144 done
8145 echo $N
8146 "#)).await.unwrap();
8147 assert!(result.ok(), "for glob failed: {}", result.err);
8148 assert_eq!(result.text_out().trim(), "2", "Should iterate 2 files, got: {}", result.text_out());
8149 }
8150
8151 #[tokio::test]
8152 async fn test_bare_glob_expansion_echo() {
8153 let (kernel, _tmp, dir) = transient_with_tempdir();
8154 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
8155 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
8156 kernel.execute(&format!("echo c > {dir}/c.rs")).await.unwrap();
8157 kernel.execute(&format!("cd {dir}")).await.unwrap();
8158 let result = kernel.execute("echo *.txt").await.unwrap();
8159 assert!(result.ok(), "echo *.txt failed: {}", result.err);
8160 let out = result.text_out();
8161 let out = out.trim();
8162 assert!(out.contains("a.txt"), "missing a.txt in: {}", out);
8164 assert!(out.contains("b.txt"), "missing b.txt in: {}", out);
8165 assert!(!out.contains("c.rs"), "should not contain c.rs in: {}", out);
8166 }
8167
8168 #[tokio::test]
8169 async fn test_bare_glob_no_matches_errors() {
8170 let (kernel, _tmp, dir) = transient_with_tempdir();
8171 kernel.execute(&format!("cd {dir}")).await.unwrap();
8172 let result = kernel.execute("echo *.nonexistent").await;
8173 match &result {
8174 Ok(exec) => {
8175 assert!(!exec.ok(), "expected failure, got success: out={}, err={}", exec.text_out(), exec.err);
8177 assert!(exec.err.contains("no matches"), "error should say no matches: {}", exec.err);
8178 }
8179 Err(e) => {
8180 assert!(e.to_string().contains("no matches"), "error should say no matches: {}", e);
8181 }
8182 }
8183 }
8184
8185 #[tokio::test]
8186 async fn test_bare_glob_disabled_with_set() {
8187 let (kernel, _tmp, dir) = transient_with_tempdir();
8188 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
8189 kernel.execute(&format!("cd {dir}")).await.unwrap();
8190 kernel.execute("set +o glob").await.unwrap();
8192 let result = kernel.execute("echo *.txt").await.unwrap();
8193 assert!(result.ok(), "echo should succeed: {}", result.err);
8195 assert_eq!(result.text_out().trim(), "*.txt", "should be literal: {}", result.text_out());
8196 }
8197
8198 #[tokio::test]
8199 async fn test_bare_glob_quoted_not_expanded() {
8200 let (kernel, _tmp, dir) = transient_with_tempdir();
8201 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
8202 kernel.execute(&format!("cd {dir}")).await.unwrap();
8203 let result = kernel.execute("echo \"*.txt\"").await.unwrap();
8205 assert!(result.ok(), "echo should succeed: {}", result.err);
8206 assert_eq!(result.text_out().trim(), "*.txt", "quoted should be literal: {}", result.text_out());
8207 }
8208
8209 #[tokio::test]
8210 async fn test_bare_glob_for_loop() {
8211 let (kernel, _tmp, dir) = transient_with_tempdir();
8212 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
8213 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
8214 kernel.execute(&format!("cd {dir}")).await.unwrap();
8215 let result = kernel.execute(r#"
8216 N=0
8217 for f in *.txt; do
8218 N=$((N + 1))
8219 done
8220 echo $N
8221 "#).await.unwrap();
8222 assert!(result.ok(), "for loop failed: {}", result.err);
8223 assert_eq!(result.text_out().trim(), "2", "should iterate 2 files: {}", result.text_out());
8224 }
8225
8226 #[tokio::test]
8227 async fn test_glob_in_assignment_is_literal() {
8228 let kernel = Kernel::transient().expect("kernel");
8229 let result = kernel.execute("X=*.txt; echo $X").await.unwrap();
8230 assert!(result.ok());
8231 assert_eq!(result.text_out().trim(), "*.txt", "glob in assignment should be literal");
8232 }
8233
8234 #[tokio::test]
8235 async fn test_glob_in_test_expr_is_literal() {
8236 let kernel = Kernel::transient().expect("kernel");
8237 let result = kernel.execute(r#"
8238 if [[ *.txt == "*.txt" ]]; then
8239 echo "match"
8240 else
8241 echo "no"
8242 fi
8243 "#).await.unwrap();
8244 assert!(result.ok());
8245 assert_eq!(result.text_out().trim(), "match", "glob in test expr should be literal");
8246 }
8247
8248 #[tokio::test]
8249 async fn test_command_subst_echo_not_iterable() {
8250 let kernel = Kernel::transient().expect("kernel");
8252 let result = kernel.execute(r#"
8253 N=0
8254 for X in $(echo "a b c"); do N=$((N + 1)); done
8255 echo $N
8256 "#).await.unwrap();
8257 assert!(result.ok());
8258 assert_eq!(result.text_out().trim(), "1", "echo should be one item: {}", result.text_out());
8259 }
8260
8261 #[test]
8264 fn test_accumulate_preserves_own_newlines() {
8265 let mut acc = ExecResult::success("line1\n");
8268 let new = ExecResult::success("line2\n");
8269 accumulate_result(&mut acc, &new);
8270 assert_eq!(&*acc.text_out(), "line1\nline2\n");
8271 assert!(!acc.text_out().contains("\n\n"), "should not have double newlines: {:?}", acc.text_out());
8272 }
8273
8274 #[test]
8275 fn test_accumulate_inserts_no_separator() {
8276 let mut acc = ExecResult::success("line1");
8279 let new = ExecResult::success("line2");
8280 accumulate_result(&mut acc, &new);
8281 assert_eq!(&*acc.text_out(), "line1line2");
8282 }
8283
8284 #[test]
8285 fn test_accumulate_empty_into_nonempty() {
8286 let mut acc = ExecResult::success("");
8287 let new = ExecResult::success("hello\n");
8288 accumulate_result(&mut acc, &new);
8289 assert_eq!(&*acc.text_out(), "hello\n");
8290 }
8291
8292 #[test]
8293 fn test_accumulate_nonempty_into_empty() {
8294 let mut acc = ExecResult::success("hello\n");
8295 let new = ExecResult::success("");
8296 accumulate_result(&mut acc, &new);
8297 assert_eq!(&*acc.text_out(), "hello\n");
8298 }
8299
8300 #[test]
8301 fn test_accumulate_stderr_no_double_newlines() {
8302 let mut acc = ExecResult::failure(1, "err1\n");
8303 let new = ExecResult::failure(1, "err2\n");
8304 accumulate_result(&mut acc, &new);
8305 assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
8306 }
8307
8308 #[tokio::test]
8309 async fn test_multiple_echo_no_blank_lines() {
8310 let kernel = Kernel::transient().expect("kernel");
8311 let result = kernel
8312 .execute("echo one\necho two\necho three")
8313 .await
8314 .expect("execution failed");
8315 assert!(result.ok());
8316 assert_eq!(&*result.text_out(), "one\ntwo\nthree\n");
8317 }
8318
8319 #[tokio::test]
8320 async fn test_for_loop_no_blank_lines() {
8321 let kernel = Kernel::transient().expect("kernel");
8322 let result = kernel
8323 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
8324 .await
8325 .expect("execution failed");
8326 assert!(result.ok());
8327 assert_eq!(&*result.text_out(), "item: a\nitem: b\nitem: c\n");
8328 }
8329
8330 #[tokio::test]
8331 async fn test_for_command_subst_no_blank_lines() {
8332 let kernel = Kernel::transient().expect("kernel");
8333 let result = kernel
8334 .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
8335 .await
8336 .expect("execution failed");
8337 assert!(result.ok());
8338 assert_eq!(&*result.text_out(), "n=1\nn=2\nn=3\n");
8339 }
8340
8341 fn multi_consume_schema() -> crate::tools::ToolSchema {
8349 use crate::tools::{ParamSchema, ToolSchema};
8350 ToolSchema::new("test", "multi-consume smoke")
8351 .param(
8352 ParamSchema::optional("pair", "array", Value::Null, "name+value pair")
8353 .consumes(2),
8354 )
8355 }
8356
8357 fn pos(s: &str) -> Arg {
8358 Arg::Positional(Expr::Literal(Value::String(s.to_string())))
8359 }
8360
8361 #[tokio::test]
8362 async fn build_args_multi_consume_single_occurrence() {
8363 let kernel = Kernel::transient().expect("kernel");
8364 let schema = multi_consume_schema();
8365 let args = vec![
8367 Arg::LongFlag("pair".into()),
8368 pos("NAME"),
8369 pos("VALUE"),
8370 pos("filter"),
8371 ];
8372 let built = kernel
8373 .build_args_async(&args, Some(&schema))
8374 .await
8375 .expect("build_args should succeed");
8376
8377 let pair = built.named.get("pair").expect("named[pair] missing");
8380 match pair {
8381 Value::Json(serde_json::Value::Array(occurrences)) => {
8382 assert_eq!(occurrences.len(), 1, "expected one occurrence");
8383 match &occurrences[0] {
8384 serde_json::Value::Array(values) => {
8385 assert_eq!(values.len(), 2, "pair must have 2 values");
8386 assert_eq!(values[0], serde_json::Value::String("NAME".into()));
8387 assert_eq!(values[1], serde_json::Value::String("VALUE".into()));
8388 }
8389 other => panic!("expected inner array, got {other:?}"),
8390 }
8391 }
8392 other => panic!("expected Json(Array(...)) for named[pair], got {other:?}"),
8393 }
8394
8395 assert_eq!(built.positional.len(), 1);
8397 assert_eq!(built.positional[0], Value::String("filter".into()));
8398 }
8399 #[tokio::test]
8400 async fn build_args_multi_consume_two_occurrences_accumulate() {
8401 let kernel = Kernel::transient().expect("kernel");
8402 let schema = multi_consume_schema();
8403 let args = vec![
8405 Arg::LongFlag("pair".into()),
8406 pos("A"),
8407 pos("1"),
8408 Arg::LongFlag("pair".into()),
8409 pos("B"),
8410 pos("2"),
8411 pos("filter"),
8412 ];
8413 let built = kernel
8414 .build_args_async(&args, Some(&schema))
8415 .await
8416 .expect("build_args should succeed");
8417
8418 let pair = built.named.get("pair").expect("named[pair] missing");
8419 match pair {
8420 Value::Json(serde_json::Value::Array(occurrences)) => {
8421 assert_eq!(occurrences.len(), 2, "expected two occurrences");
8422 match &occurrences[0] {
8424 serde_json::Value::Array(values) => {
8425 assert_eq!(values[0], serde_json::Value::String("A".into()));
8426 assert_eq!(values[1], serde_json::Value::String("1".into()));
8427 }
8428 other => panic!("expected inner array, got {other:?}"),
8429 }
8430 match &occurrences[1] {
8431 serde_json::Value::Array(values) => {
8432 assert_eq!(values[0], serde_json::Value::String("B".into()));
8433 assert_eq!(values[1], serde_json::Value::String("2".into()));
8434 }
8435 other => panic!("expected inner array, got {other:?}"),
8436 }
8437 }
8438 other => panic!("expected Json(Array(...)), got {other:?}"),
8439 }
8440 }
8441
8442 use crate::tools::{ParamSchema, ToolSchema};
8450
8451 fn kj_like_schema() -> ToolSchema {
8454 ToolSchema::new("kj", "incomplete backend schema")
8455 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
8456 .with_positional_mapping()
8457 }
8458
8459 #[tokio::test]
8460 async fn build_args_undeclared_space_flag_errors_under_map_positionals() {
8461 let kernel = Kernel::transient().expect("kernel");
8462 let schema = kj_like_schema();
8463 let args = vec![
8465 pos("context"),
8466 pos("create"),
8467 pos("exp"),
8468 Arg::LongFlag("type".into()),
8469 pos("explorer"),
8470 ];
8471 let err = kernel
8472 .build_args_async(&args, Some(&schema))
8473 .await
8474 .expect_err("undeclared --type with a space value must fail loud");
8475 let msg = err.to_string();
8476 assert!(msg.contains("--type"), "message should name the flag: {msg}");
8477 assert!(msg.contains("--type=explorer"), "message should suggest the = form: {msg}");
8478 assert!(msg.contains("kj"), "message should name the tool: {msg}");
8479 }
8480
8481 #[tokio::test]
8482 async fn build_args_declared_space_flag_still_binds() {
8483 let kernel = Kernel::transient().expect("kernel");
8484 let schema = ToolSchema::new("kj", "complete schema")
8486 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
8487 .param(ParamSchema::optional("type", "string", Value::Null, "role type"))
8488 .with_positional_mapping();
8489 let args = vec![
8490 pos("exp"),
8491 Arg::LongFlag("type".into()),
8492 pos("explorer"),
8493 ];
8494 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
8495 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
8496 }
8497
8498 #[tokio::test]
8499 async fn build_args_equals_form_binds_for_undeclared_flag() {
8500 let kernel = Kernel::transient().expect("kernel");
8501 let schema = kj_like_schema();
8502 let args = vec![
8504 pos("exp"),
8505 Arg::Named { key: "type".into(), value: Expr::Literal(Value::String("explorer".into())) },
8506 ];
8507 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
8508 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
8509 }
8510
8511 #[tokio::test]
8512 async fn build_args_undeclared_bool_flag_at_end_is_ok() {
8513 let kernel = Kernel::transient().expect("kernel");
8514 let schema = kj_like_schema();
8515 let args = vec![pos("exp"), Arg::LongFlag("force".into())];
8517 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
8518 assert!(built.flags.contains("force"));
8519 }
8520
8521 #[tokio::test]
8522 async fn build_args_undeclared_flag_before_another_flag_is_ok() {
8523 let kernel = Kernel::transient().expect("kernel");
8524 let schema = kj_like_schema();
8525 let args = vec![
8527 Arg::LongFlag("verbose".into()),
8528 Arg::Named { key: "name".into(), value: Expr::Literal(Value::String("x".into())) },
8529 ];
8530 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
8531 assert!(built.flags.contains("verbose"));
8532 }
8533
8534 #[tokio::test]
8535 async fn build_args_undeclared_space_flag_ok_for_builtin_schema() {
8536 let kernel = Kernel::transient().expect("kernel");
8537 let schema = ToolSchema::new("frobnicate", "builtin-style")
8540 .param(ParamSchema::optional("name", "string", Value::Null, "name"));
8541 let args = vec![Arg::LongFlag("frob".into()), pos("value")];
8542 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
8543 assert!(built.flags.contains("frob"));
8544 }
8545
8546 fn kj_tree_schema() -> ToolSchema {
8556 ToolSchema::new("kj", "subcommand tool").subcommand(
8557 ToolSchema::new("context", "context ops")
8558 .with_command_aliases(["ctx"])
8559 .subcommand(
8560 ToolSchema::new("create", "create context")
8561 .param(ParamSchema::new("type", "string").with_aliases(["t"]))
8562 .param(ParamSchema::new("force", "bool")),
8563 ),
8564 )
8565 }
8566
8567 #[tokio::test]
8568 async fn build_args_binds_deep_leaf_value_flag_space_form() {
8569 let kernel = Kernel::transient().expect("kernel");
8570 let schema = kj_tree_schema();
8571 let args = vec![
8573 pos("context"),
8574 pos("create"),
8575 Arg::LongFlag("type".into()),
8576 pos("explorer"),
8577 ];
8578 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
8579 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
8581 let positionals: Vec<&str> = built
8583 .positional
8584 .iter()
8585 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
8586 .collect();
8587 assert_eq!(positionals, vec!["context", "create"]);
8588 }
8589
8590 #[tokio::test]
8591 async fn build_args_leaf_bool_flag_does_not_swallow_positional() {
8592 let kernel = Kernel::transient().expect("kernel");
8593 let schema = kj_tree_schema();
8594 let args = vec![
8597 pos("context"),
8598 pos("create"),
8599 Arg::LongFlag("force".into()),
8600 pos("somearg"),
8601 ];
8602 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
8603 assert!(built.flags.contains("force"), "force should be a bare flag");
8604 let positionals: Vec<&str> = built
8605 .positional
8606 .iter()
8607 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
8608 .collect();
8609 assert_eq!(positionals, vec!["context", "create", "somearg"]);
8610 }
8611
8612 #[tokio::test]
8613 async fn build_args_alias_routed_leaf_binds_value_flag() {
8614 let kernel = Kernel::transient().expect("kernel");
8615 let schema = kj_tree_schema();
8616 let args = vec![
8618 pos("ctx"),
8619 pos("create"),
8620 Arg::ShortFlag("t".into()),
8621 pos("explorer"),
8622 ];
8623 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
8624 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
8625 }
8626
8627 #[tokio::test]
8628 async fn build_args_computed_subcommand_selector_fails_loud() {
8629 let kernel = Kernel::transient().expect("kernel");
8630 let schema = kj_tree_schema();
8631 let args = vec![Arg::Positional(Expr::CommandSubst(vec![Stmt::Command(
8633 crate::ast::Command { name: "echo".into(), args: vec![], redirects: vec![] },
8634 )]))];
8635 let err = kernel
8636 .build_args_async(&args, Some(&schema))
8637 .await
8638 .expect_err("computed subcommand selector must error");
8639 assert!(
8640 err.to_string().contains("subcommand name is required"),
8641 "got: {err}"
8642 );
8643 }
8644
8645 #[test]
8648 fn finalize_output_renders_when_kernel_owns_it() {
8649 use crate::interpreter::{OutputData, OutputFormat};
8650 let r = ExecResult::with_output(OutputData::text("RAW"));
8651 let out = finalize_output(r, Some(OutputFormat::Json), false);
8652 assert_ne!(out.text_out(), "RAW", "kernel should reformat to JSON");
8654 }
8655
8656 #[test]
8657 fn finalize_output_skips_when_tool_owns_output() {
8658 use crate::interpreter::{OutputData, OutputFormat};
8659 let r = ExecResult::with_output(OutputData::text("RAW"));
8660 let out = finalize_output(r, Some(OutputFormat::Json), true);
8661 assert_eq!(out.text_out(), "RAW", "owned output must be left as-is");
8663 }
8664
8665 #[test]
8666 fn finalize_output_no_format_is_noop() {
8667 use crate::interpreter::OutputData;
8668 let r = ExecResult::with_output(OutputData::text("RAW"));
8669 let out = finalize_output(r, None, false);
8670 assert_eq!(out.text_out(), "RAW");
8671 }
8672
8673 #[tokio::test]
8676 async fn test_initial_vars_set_and_exported() {
8677 let config = KernelConfig::transient()
8678 .with_var("INIT_FOO", Value::String("bar".into()));
8679 let kernel = Kernel::new(config).expect("failed to create kernel");
8680
8681 assert_eq!(
8682 kernel.get_var("INIT_FOO").await,
8683 Some(Value::String("bar".into()))
8684 );
8685 assert!(
8686 kernel.scope.read().await.is_exported("INIT_FOO"),
8687 "initial_vars entries must be marked exported"
8688 );
8689 }
8690
8691 #[tokio::test]
8692 async fn test_execute_with_vars_overlay_visible() {
8693 let kernel = Kernel::transient().expect("failed to create kernel");
8694 let mut overlay = HashMap::new();
8695 overlay.insert("OVERLAY_X".to_string(), Value::String("yes".into()));
8696
8697 let result = kernel
8698 .execute_with_options(r#"echo "${OVERLAY_X}""#, ExecuteOptions::new().with_vars(overlay))
8699 .await
8700 .expect("execute failed");
8701
8702 assert!(result.ok());
8703 assert_eq!(result.text_out().trim(), "yes");
8704 }
8705
8706 #[tokio::test]
8707 async fn test_execute_with_vars_overlay_cleanup() {
8708 let kernel = Kernel::transient().expect("failed to create kernel");
8709 let mut overlay = HashMap::new();
8710 overlay.insert("EPHEMERAL".to_string(), Value::String("transient".into()));
8711
8712 kernel
8713 .execute_with_options("echo ignored", ExecuteOptions::new().with_vars(overlay))
8714 .await
8715 .expect("execute failed");
8716
8717 assert_eq!(kernel.get_var("EPHEMERAL").await, None);
8718 assert!(
8719 !kernel.scope.read().await.is_exported("EPHEMERAL"),
8720 "overlay-only export must be cleared on return"
8721 );
8722 }
8723
8724 #[tokio::test]
8725 async fn test_execute_with_vars_does_not_clobber_existing_export() {
8726 let kernel = Kernel::transient().expect("failed to create kernel");
8727 kernel
8728 .execute("export OUTER=outer")
8729 .await
8730 .expect("export failed");
8731
8732 let mut overlay = HashMap::new();
8733 overlay.insert("OUTER".to_string(), Value::String("inner".into()));
8734 let result = kernel
8735 .execute_with_options(r#"echo "${OUTER}""#, ExecuteOptions::new().with_vars(overlay))
8736 .await
8737 .expect("execute failed");
8738 assert_eq!(result.text_out().trim(), "inner");
8739
8740 assert_eq!(
8741 kernel.get_var("OUTER").await,
8742 Some(Value::String("outer".into())),
8743 "outer value must reappear after pop"
8744 );
8745 assert!(
8746 kernel.scope.read().await.is_exported("OUTER"),
8747 "outer export must survive overlay"
8748 );
8749 }
8750
8751 #[tokio::test]
8752 async fn test_execute_with_vars_inner_assignment_is_local() {
8753 let kernel = Kernel::transient().expect("failed to create kernel");
8754 let mut overlay = HashMap::new();
8755 overlay.insert("LOCAL_FOO".to_string(), Value::String("from-overlay".into()));
8756
8757 let result = kernel
8762 .execute_with_options(
8763 r#"LOCAL_FOO="reassigned"; echo "${LOCAL_FOO}""#,
8764 ExecuteOptions::new().with_vars(overlay),
8765 )
8766 .await
8767 .expect("execute failed");
8768 assert!(result.ok());
8769
8770 assert_eq!(kernel.get_var("LOCAL_FOO").await, None);
8773 }
8774
8775 #[tokio::test]
8776 async fn test_external_command_sees_exported_var() {
8777 let kernel = Kernel::transient().expect("failed to create kernel");
8778 let path = std::env::var("PATH").unwrap_or_default();
8782 let result = kernel
8783 .execute(&format!(
8784 "PATH=\"{path}\"; export EXT_FOO=bar; printenv EXT_FOO"
8785 ))
8786 .await
8787 .expect("execute failed");
8788
8789 assert!(result.ok(), "printenv should succeed: stderr={}", result.err);
8790 assert_eq!(result.text_out().trim(), "bar");
8791 }
8792
8793 #[tokio::test]
8794 async fn test_external_command_does_not_see_unexported_var() {
8795 let kernel = Kernel::transient().expect("failed to create kernel");
8796
8797 let result = kernel
8800 .execute("EXT_BAR=hidden; printenv EXT_BAR")
8801 .await
8802 .expect("execute failed");
8803
8804 assert!(!result.ok(), "printenv should fail when var is unexported");
8805 assert!(
8806 result.text_out().trim().is_empty(),
8807 "no stdout when var is missing, got: {}",
8808 result.text_out()
8809 );
8810 }
8811
8812 #[tokio::test]
8813 async fn test_external_command_does_not_see_os_env() {
8814 assert!(
8820 std::env::var_os("PATH").is_some(),
8821 "test precondition: cargo should set PATH"
8822 );
8823
8824 let kernel = Kernel::transient().expect("failed to create kernel");
8825 let result = kernel
8826 .execute("printenv PATH")
8827 .await
8828 .expect("execute failed");
8829
8830 assert!(
8831 !result.ok(),
8832 "printenv PATH must fail in hermetic kernel, got stdout={:?}",
8833 result.text_out()
8834 );
8835 assert!(
8836 result.text_out().trim().is_empty(),
8837 "no PATH in subprocess env, got stdout={:?}",
8838 result.text_out()
8839 );
8840 }
8841
8842 #[tokio::test]
8843 async fn test_execute_with_vars_overlay_reaches_subprocess() {
8844 let kernel = Kernel::transient().expect("failed to create kernel");
8845 let mut overlay = HashMap::new();
8846 overlay.insert("SUB_FOO".to_string(), Value::String("subproc".into()));
8847 overlay.insert(
8849 "PATH".to_string(),
8850 Value::String(std::env::var("PATH").unwrap_or_default()),
8851 );
8852
8853 let result = kernel
8854 .execute_with_options("printenv SUB_FOO", ExecuteOptions::new().with_vars(overlay))
8855 .await
8856 .expect("execute failed");
8857
8858 assert!(
8859 result.ok(),
8860 "printenv should succeed: code={} stdout={:?} stderr={:?}",
8861 result.code,
8862 result.text_out(),
8863 result.err
8864 );
8865 assert_eq!(result.text_out().trim(), "subproc");
8866 }
8867
8868 #[tokio::test]
8869 async fn test_classify_command_builtin() {
8870 let kernel = Kernel::transient().expect("failed to create kernel");
8871 assert_eq!(kernel.classify_command("cat").await, CommandKind::Builtin);
8872 assert_eq!(kernel.classify_command("grep").await, CommandKind::Builtin);
8873 }
8874
8875 #[tokio::test]
8876 async fn test_classify_command_special_forms() {
8877 let kernel = Kernel::transient().expect("failed to create kernel");
8878 for name in ["true", "false", "source", "."] {
8879 assert_eq!(
8880 kernel.classify_command(name).await,
8881 CommandKind::Special,
8882 "{name} should be a special-form",
8883 );
8884 }
8885 }
8886
8887 #[tokio::test]
8888 async fn test_classify_command_dynamic() {
8889 let kernel = Kernel::transient().expect("failed to create kernel");
8890 assert_eq!(kernel.classify_command("$cmd").await, CommandKind::Dynamic);
8891 assert_eq!(
8892 kernel.classify_command("$(pick)").await,
8893 CommandKind::Dynamic
8894 );
8895 }
8896
8897 #[tokio::test]
8898 async fn test_classify_command_external() {
8899 let kernel = Kernel::transient().expect("failed to create kernel");
8900 assert_eq!(
8902 kernel.classify_command("definitely_not_a_kaish_builtin").await,
8903 CommandKind::External
8904 );
8905 assert_eq!(
8910 kernel.classify_command("readonly").await,
8911 CommandKind::External
8912 );
8913 assert!(kernel.classify_command("readonly").await.escapes_kernel());
8914 }
8915
8916 #[tokio::test]
8917 async fn test_classify_command_user_tool_shadows_builtin() {
8918 let kernel = Kernel::transient().expect("failed to create kernel");
8919 kernel
8920 .execute(r#"greet() { echo "hi" }"#)
8921 .await
8922 .expect("function definition failed");
8923 assert_eq!(
8924 kernel.classify_command("greet").await,
8925 CommandKind::UserTool
8926 );
8927
8928 kernel
8931 .execute(r#"cat() { echo "shadowed" }"#)
8932 .await
8933 .expect("function definition failed");
8934 assert_eq!(kernel.classify_command("cat").await, CommandKind::UserTool);
8935 }
8936
8937 #[tokio::test]
8938 async fn test_classify_command_alias_to_external_is_external() {
8939 let kernel = Kernel::transient().expect("failed to create kernel");
8940 kernel
8944 .execute("alias cat='/usr/bin/whatever'")
8945 .await
8946 .expect("alias failed");
8947 assert_eq!(kernel.classify_command("cat").await, CommandKind::External);
8948 assert!(kernel.classify_command("cat").await.escapes_kernel());
8949 }
8950
8951 #[tokio::test]
8952 async fn test_classify_command_alias_to_builtin() {
8953 let kernel = Kernel::transient().expect("failed to create kernel");
8954 kernel.execute("alias g=grep").await.expect("alias failed");
8955 assert_eq!(kernel.classify_command("g").await, CommandKind::Builtin);
8956 }
8957
8958 #[tokio::test]
8959 async fn test_classify_command_alias_to_special_form() {
8960 let kernel = Kernel::transient().expect("failed to create kernel");
8961 kernel.execute("alias t=true").await.expect("alias failed");
8962 assert_eq!(kernel.classify_command("t").await, CommandKind::Special);
8963 }
8964
8965 #[tokio::test]
8966 async fn test_classify_command_braced_var_is_dynamic() {
8967 let kernel = Kernel::transient().expect("failed to create kernel");
8968 assert_eq!(
8971 kernel.classify_command("${CMD}").await,
8972 CommandKind::Dynamic
8973 );
8974 }
8975
8976 #[tokio::test]
8985 async fn classify_command_matches_executor() {
8986 let kernel = Kernel::transient().expect("failed to create kernel");
8987
8988 for name in ["true", "false", "source", "."] {
8998 assert_eq!(
8999 kernel.classify_command(name).await,
9000 CommandKind::Special,
9001 "{name} should classify Special",
9002 );
9003 }
9004 assert_eq!(kernel.execute("true").await.expect("run true").code, 0);
9005 assert_eq!(kernel.execute("false").await.expect("run false").code, 1);
9006 for name in ["source", "."] {
9009 let r = kernel.execute(name).await.expect("run source form");
9010 assert_ne!(r.code, 127, "{name} fell through to PATH instead of source");
9011 assert!(
9012 r.err.contains("source: missing filename"),
9013 "{name} did not route to execute_source: {:?}",
9014 r.err,
9015 );
9016 }
9017
9018 assert_eq!(kernel.classify_command("echo").await, CommandKind::Builtin);
9020 let r = kernel.execute("echo hi").await.expect("run echo");
9021 assert!(r.ok() && r.text_out().trim() == "hi", "echo builtin didn't run");
9022
9023 kernel
9026 .execute(r#"cat() { echo SHADOWED }"#)
9027 .await
9028 .expect("define cat()");
9029 assert_eq!(kernel.classify_command("cat").await, CommandKind::UserTool);
9030 let r = kernel.execute("cat").await.expect("run shadowed cat");
9031 assert_eq!(
9032 r.text_out().trim(),
9033 "SHADOWED",
9034 "executor ran the builtin instead of the shadowing function",
9035 );
9036
9037 kernel
9040 .execute("alias x='/nonexistent/binary'")
9041 .await
9042 .expect("define alias x");
9043 assert_eq!(kernel.classify_command("x").await, CommandKind::External);
9044 let r = kernel.execute("x").await.expect("run alias x");
9045 assert!(
9046 !r.ok(),
9047 "alias to a missing external should fail, not resolve internally",
9048 );
9049 }
9050}