1use std::collections::HashMap;
26use std::path::PathBuf;
27use std::sync::Arc;
28use std::sync::atomic::{AtomicU64, Ordering};
29use std::time::Duration;
30
31use anyhow::{Context, Result};
32use tokio::sync::RwLock;
33
34static KERNEL_COUNTER: AtomicU64 = AtomicU64::new(1);
46
47use async_trait::async_trait;
48
49use crate::ast::{Arg, Command, Expr, FileTestOp, Stmt, StringPart, TestExpr, ToolDef, Value, BinaryOp};
50pub use kaish_types::ExecuteOptions;
51use crate::backend::{BackendError, KernelBackend};
52use kaish_glob::glob_match;
53use crate::dispatch::{CommandDispatcher, PipelinePosition};
54use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value, value_to_bool, value_to_string, ControlFlow, ExecResult, Scope};
55use crate::parser::parse;
56use crate::scheduler::{is_bool_type, schema_param_lookup, select_leaf, stderr_stream, BoundedStream, JobManager, PipelineRunner, StderrReceiver};
57#[cfg(feature = "subprocess")]
58use crate::scheduler::{drain_to_stream, DEFAULT_STREAM_MAX_SIZE};
59use crate::tools::{register_builtins, ExecContext, GlobalFlags, ToolArgs, ToolRegistry};
60#[cfg(feature = "subprocess")]
61use crate::tools::resolve_in_path;
62use crate::validator::{Severity, Validator};
63#[cfg(feature = "localfs")]
64use crate::vfs::LocalFs;
65use crate::vfs::{BuiltinFs, DevFs, JobFs, MemoryFs, VfsRouter};
66use kaish_vfs::ByteBudget;
67#[cfg(all(feature = "localfs", feature = "overlay"))]
68use kaish_vfs::OverlayFs;
69
70#[derive(Debug, Clone)]
77pub enum VfsMountMode {
78 #[cfg(feature = "localfs")]
87 Passthrough,
88
89 #[cfg(feature = "localfs")]
105 Sandboxed {
106 root: Option<PathBuf>,
109 },
110
111 NoLocal,
127}
128
129#[allow(clippy::derivable_impls)] impl Default for VfsMountMode {
131 fn default() -> Self {
132 #[cfg(feature = "localfs")]
133 { VfsMountMode::Sandboxed { root: None } }
134 #[cfg(not(feature = "localfs"))]
135 { VfsMountMode::NoLocal }
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct KernelConfig {
142 pub name: String,
144
145 pub vfs_mode: VfsMountMode,
147
148 pub cwd: PathBuf,
150
151 pub skip_validation: bool,
157
158 pub interactive: bool,
163
164 pub ignore_config: crate::ignore_config::IgnoreConfig,
166
167 pub output_limit: crate::output_limit::OutputLimitConfig,
169
170 pub allow_external_commands: bool,
180
181 pub latch_enabled: bool,
186
187 pub trash_enabled: bool,
193
194 pub nonce_store: Option<crate::nonce::NonceStore>,
200
201 pub initial_vars: HashMap<String, Value>,
209
210 pub request_timeout: Option<Duration>,
217
218 pub kill_grace: Duration,
224
225 pub vfs_budget_bytes: Option<u64>,
243
244 pub overlay: bool,
266}
267
268#[cfg(feature = "localfs")]
270fn default_sandbox_root() -> PathBuf {
271 std::env::var("HOME")
272 .map(PathBuf::from)
273 .unwrap_or_else(|_| PathBuf::from("/"))
274}
275
276impl Default for KernelConfig {
277 fn default() -> Self {
278 #[cfg(feature = "localfs")]
279 {
280 let home = default_sandbox_root();
281 Self {
282 name: "default".to_string(),
283 vfs_mode: VfsMountMode::Sandboxed { root: None },
284 cwd: home,
285 skip_validation: false,
286 interactive: false,
287 ignore_config: crate::ignore_config::IgnoreConfig::none(),
288 output_limit: crate::output_limit::OutputLimitConfig::none(),
289 allow_external_commands: cfg!(feature = "subprocess"),
290 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
291 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
292 nonce_store: None,
293 initial_vars: HashMap::new(),
294 request_timeout: None,
295 kill_grace: Duration::from_secs(2),
296 vfs_budget_bytes: None,
297 overlay: false,
298 }
299 }
300 #[cfg(not(feature = "localfs"))]
301 {
302 Self {
303 name: "default".to_string(),
304 vfs_mode: VfsMountMode::NoLocal,
305 cwd: PathBuf::from("/"),
306 skip_validation: false,
307 interactive: false,
308 ignore_config: crate::ignore_config::IgnoreConfig::none(),
309 output_limit: crate::output_limit::OutputLimitConfig::none(),
310 allow_external_commands: false,
311 latch_enabled: false,
312 trash_enabled: false,
313 nonce_store: None,
314 initial_vars: HashMap::new(),
315 request_timeout: None,
316 kill_grace: Duration::from_secs(2),
317 vfs_budget_bytes: None,
318 overlay: false,
319 }
320 }
321 }
322}
323
324impl KernelConfig {
325 #[cfg(feature = "localfs")]
327 pub fn transient() -> Self {
328 let home = default_sandbox_root();
329 Self {
330 name: "transient".to_string(),
331 vfs_mode: VfsMountMode::Sandboxed { root: None },
332 cwd: home,
333 skip_validation: false,
334 interactive: false,
335 ignore_config: crate::ignore_config::IgnoreConfig::none(),
336 output_limit: crate::output_limit::OutputLimitConfig::none(),
337 allow_external_commands: cfg!(feature = "subprocess"),
338 latch_enabled: false,
339 trash_enabled: false,
340 nonce_store: None,
341 initial_vars: HashMap::new(),
342 request_timeout: None,
343 kill_grace: Duration::from_secs(2),
344 vfs_budget_bytes: None,
345 overlay: false,
346 }
347 }
348
349 #[cfg(not(feature = "localfs"))]
351 pub fn transient() -> Self {
352 Self::isolated()
353 }
354
355 #[cfg(feature = "localfs")]
357 pub fn named(name: &str) -> Self {
358 let home = default_sandbox_root();
359 Self {
360 name: name.to_string(),
361 vfs_mode: VfsMountMode::Sandboxed { root: None },
362 cwd: home,
363 skip_validation: false,
364 interactive: false,
365 ignore_config: crate::ignore_config::IgnoreConfig::none(),
366 output_limit: crate::output_limit::OutputLimitConfig::none(),
367 allow_external_commands: cfg!(feature = "subprocess"),
368 latch_enabled: false,
369 trash_enabled: false,
370 nonce_store: None,
371 initial_vars: HashMap::new(),
372 request_timeout: None,
373 kill_grace: Duration::from_secs(2),
374 vfs_budget_bytes: None,
375 overlay: false,
376 }
377 }
378
379 #[cfg(not(feature = "localfs"))]
381 pub fn named(name: &str) -> Self {
382 Self {
383 name: name.to_string(),
384 ..Self::isolated()
385 }
386 }
387
388 #[cfg(feature = "localfs")]
393 pub fn repl() -> Self {
394 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
395 Self {
396 name: "repl".to_string(),
397 vfs_mode: VfsMountMode::Passthrough,
398 cwd,
399 skip_validation: false,
400 interactive: false,
401 ignore_config: crate::ignore_config::IgnoreConfig::none(),
402 output_limit: crate::output_limit::OutputLimitConfig::none(),
403 allow_external_commands: cfg!(feature = "subprocess"),
404 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
405 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
406 nonce_store: None,
407 initial_vars: HashMap::new(),
408 request_timeout: None,
409 kill_grace: Duration::from_secs(2),
410 vfs_budget_bytes: None,
411 overlay: false,
412 }
413 }
414
415 #[cfg(feature = "localfs")]
428 pub fn agent() -> Self {
429 let home = default_sandbox_root();
430 Self {
431 name: "agent".to_string(),
432 vfs_mode: VfsMountMode::Sandboxed { root: None },
433 cwd: home,
434 skip_validation: false,
435 interactive: false,
436 ignore_config: crate::ignore_config::IgnoreConfig::agent(),
437 output_limit: crate::output_limit::OutputLimitConfig::agent(),
438 allow_external_commands: cfg!(feature = "subprocess"),
439 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
440 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
441 nonce_store: None,
442 initial_vars: HashMap::new(),
443 request_timeout: None,
444 kill_grace: Duration::from_secs(2),
445 vfs_budget_bytes: Some(64 * 1024 * 1024),
446 overlay: false,
447 }
448 }
449
450 #[cfg(feature = "localfs")]
457 pub fn agent_with_root(root: PathBuf) -> Self {
458 Self {
459 name: "agent".to_string(),
460 vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
461 cwd: root,
462 skip_validation: false,
463 interactive: false,
464 ignore_config: crate::ignore_config::IgnoreConfig::agent(),
465 output_limit: crate::output_limit::OutputLimitConfig::agent(),
466 allow_external_commands: cfg!(feature = "subprocess"),
467 latch_enabled: std::env::var("KAISH_LATCH").is_ok_and(|v| v == "1"),
468 trash_enabled: std::env::var("KAISH_TRASH").is_ok_and(|v| v == "1"),
469 nonce_store: None,
470 initial_vars: HashMap::new(),
471 request_timeout: None,
472 kill_grace: Duration::from_secs(2),
473 vfs_budget_bytes: Some(64 * 1024 * 1024),
474 overlay: false,
475 }
476 }
477
478 pub fn isolated() -> Self {
483 Self {
484 name: "isolated".to_string(),
485 vfs_mode: VfsMountMode::NoLocal,
486 cwd: PathBuf::from("/"),
487 skip_validation: false,
488 interactive: false,
489 ignore_config: crate::ignore_config::IgnoreConfig::none(),
490 output_limit: crate::output_limit::OutputLimitConfig::none(),
491 allow_external_commands: false,
492 latch_enabled: false,
493 trash_enabled: false,
494 nonce_store: None,
495 initial_vars: HashMap::new(),
496 request_timeout: None,
497 kill_grace: Duration::from_secs(2),
498 vfs_budget_bytes: None,
499 overlay: false,
500 }
501 }
502
503 pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
505 self.vfs_mode = mode;
506 self
507 }
508
509 pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
511 self.cwd = cwd;
512 self
513 }
514
515 pub fn with_skip_validation(mut self, skip: bool) -> Self {
517 self.skip_validation = skip;
518 self
519 }
520
521 pub fn with_interactive(mut self, interactive: bool) -> Self {
523 self.interactive = interactive;
524 self
525 }
526
527 pub fn with_ignore_config(mut self, config: crate::ignore_config::IgnoreConfig) -> Self {
529 self.ignore_config = config;
530 self
531 }
532
533 pub fn with_output_limit(mut self, config: crate::output_limit::OutputLimitConfig) -> Self {
535 self.output_limit = config;
536 self
537 }
538
539 pub fn with_allow_external_commands(mut self, allow: bool) -> Self {
545 self.allow_external_commands = allow;
546 self
547 }
548
549 pub fn with_latch(mut self, enabled: bool) -> Self {
551 self.latch_enabled = enabled;
552 self
553 }
554
555 pub fn with_trash(mut self, enabled: bool) -> Self {
557 self.trash_enabled = enabled;
558 self
559 }
560
561 pub fn with_nonce_store(mut self, store: crate::nonce::NonceStore) -> Self {
566 self.nonce_store = Some(store);
567 self
568 }
569
570 pub fn with_var(mut self, name: impl Into<String>, value: Value) -> Self {
574 self.initial_vars.insert(name.into(), value);
575 self
576 }
577
578 pub fn with_initial_vars(mut self, vars: HashMap<String, Value>) -> Self {
580 self.initial_vars = vars;
581 self
582 }
583
584 pub fn with_vars(mut self, vars: HashMap<String, Value>) -> Self {
586 self.initial_vars.extend(vars);
587 self
588 }
589
590 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
595 self.request_timeout = Some(timeout);
596 self
597 }
598
599 pub fn with_kill_grace(mut self, grace: Duration) -> Self {
601 self.kill_grace = grace;
602 self
603 }
604
605 pub fn with_vfs_budget(mut self, bytes: u64) -> Self {
614 self.vfs_budget_bytes = Some(bytes);
615 self
616 }
617
618 pub fn without_vfs_budget(mut self) -> Self {
623 self.vfs_budget_bytes = None;
624 self
625 }
626
627 pub fn with_overlay(mut self, overlay: bool) -> Self {
634 self.overlay = overlay;
635 self
636 }
637}
638
639#[cfg(all(feature = "localfs", feature = "overlay"))]
646#[derive(Clone)]
647pub struct OverlayHandle {
648 pub fs: Arc<OverlayFs>,
651 pub mount_path: PathBuf,
653 pub commit_root: PathBuf,
655}
656
657pub struct Kernel {
662 name: String,
664 scope: RwLock<Scope>,
666 tools: Arc<ToolRegistry>,
668 user_tools: RwLock<HashMap<String, ToolDef>>,
670 vfs: Arc<VfsRouter>,
672 jobs: Arc<JobManager>,
674 runner: PipelineRunner,
676 exec_ctx: RwLock<ExecContext>,
678 skip_validation: bool,
680 interactive: bool,
682 allow_external_commands: bool,
684 vfs_budget: Option<Arc<kaish_vfs::ByteBudget>>,
691 #[cfg(all(feature = "localfs", feature = "overlay"))]
698 overlay_handle: Option<Arc<OverlayHandle>>,
699 request_timeout: Option<Duration>,
701 kill_grace: Duration,
703 stderr_receiver: tokio::sync::Mutex<StderrReceiver>,
708 cancel_token: std::sync::Mutex<tokio_util::sync::CancellationToken>,
714 #[cfg(all(unix, feature = "subprocess"))]
716 terminal_state: Option<Arc<crate::terminal::TerminalState>>,
717 self_weak: std::sync::OnceLock<std::sync::Weak<Self>>,
722 bg_job_id: Option<crate::scheduler::JobId>,
728 execute_lock: tokio::sync::Mutex<()>,
734}
735
736struct VfsSetupResult {
738 vfs: VfsRouter,
739 budget: Option<Arc<ByteBudget>>,
740 #[cfg(all(feature = "localfs", feature = "overlay"))]
741 overlay_handle: Option<Arc<OverlayHandle>>,
742}
743
744impl Kernel {
745 pub fn new(config: KernelConfig) -> Result<Self> {
747 let mut setup = Self::setup_vfs(&config)?;
748 let jobs = Arc::new(JobManager::new());
749
750 setup.vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
752
753 #[cfg(all(feature = "localfs", feature = "overlay"))]
754 let overlay_handle = setup.overlay_handle.take();
755
756 let kernel = Self::assemble(config, setup.vfs, jobs, false, setup.budget, |_| {}, |vfs_ref, tools| {
760 ExecContext::with_vfs_and_tools(vfs_ref.clone(), tools.clone())
761 })?;
762
763 #[cfg(all(feature = "localfs", feature = "overlay"))]
764 {
765 let mut kernel = kernel;
766 kernel.overlay_handle = overlay_handle;
767 if let Some(ref handle) = kernel.overlay_handle {
769 kernel.exec_ctx.get_mut().overlay_handle = Some(Arc::clone(handle));
770 }
771 return Ok(kernel);
772 }
773
774 #[allow(unreachable_code)]
775 Ok(kernel)
776 }
777
778 fn setup_vfs(config: &KernelConfig) -> Result<VfsSetupResult> {
792 let mut vfs = VfsRouter::new();
793
794 let budget: Option<Arc<ByteBudget>> = config
797 .vfs_budget_bytes
798 .map(|bytes| Arc::new(ByteBudget::labeled(bytes, "vfs-memory")));
799
800 fn mem(budget: &Option<Arc<ByteBudget>>) -> MemoryFs {
802 match budget {
803 Some(b) => MemoryFs::with_budget(Arc::clone(b)),
804 None => MemoryFs::new(),
805 }
806 }
807
808 #[cfg(all(feature = "localfs", feature = "overlay"))]
810 let mut overlay_handle: Option<Arc<OverlayHandle>> = None;
811
812 match &config.vfs_mode {
813 #[cfg(feature = "localfs")]
814 VfsMountMode::Passthrough => {
815 #[cfg(feature = "overlay")]
816 if config.overlay {
817 let lower = Arc::new(LocalFs::read_only(PathBuf::from("/")));
819 let overlay_fs = Arc::new(match &budget {
820 Some(b) => OverlayFs::over_with_budget(lower, Arc::clone(b)),
821 None => OverlayFs::over(lower),
822 });
823 let handle = Arc::new(OverlayHandle {
824 fs: Arc::clone(&overlay_fs),
825 mount_path: PathBuf::from("/"),
826 commit_root: PathBuf::from("/"),
827 });
828 vfs.mount_arc("/", overlay_fs as Arc<dyn kaish_vfs::Filesystem>);
829 overlay_handle = Some(handle);
830 } else {
831 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
833 }
834 #[cfg(not(feature = "overlay"))]
835 {
836 if config.overlay {
837 return Err(anyhow::anyhow!(
838 "overlay=true requires the `overlay` feature, but this build \
839 was compiled without it. Recompile with --features overlay \
840 (or the default feature set) to enable overlay mode."
841 ));
842 }
843 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
845 }
846 vfs.mount("/v", mem(&budget));
848 }
849 #[cfg(feature = "localfs")]
850 VfsMountMode::Sandboxed { root } => {
851 vfs.mount("/", mem(&budget));
857 vfs.mount("/v", mem(&budget));
858
859 vfs.mount("/dev", DevFs::new());
862
863 vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
865
866 let runtime = crate::paths::xdg_runtime_dir();
868 if runtime.exists() {
869 let runtime_str = runtime.to_string_lossy().to_string();
870 vfs.mount(&runtime_str, LocalFs::new(runtime));
871 }
872
873 let local_root = root.clone().unwrap_or_else(|| {
875 std::env::var("HOME")
876 .map(PathBuf::from)
877 .unwrap_or_else(|_| PathBuf::from("/"))
878 });
879
880 let mount_point = local_root.to_string_lossy().to_string();
881
882 #[cfg(feature = "overlay")]
883 if config.overlay {
884 let lower = Arc::new(LocalFs::read_only(local_root.clone()));
886 let overlay_fs = Arc::new(match &budget {
887 Some(b) => OverlayFs::over_with_budget(lower, Arc::clone(b)),
888 None => OverlayFs::over(lower),
889 });
890 let handle = Arc::new(OverlayHandle {
891 fs: Arc::clone(&overlay_fs),
892 mount_path: PathBuf::from(&mount_point),
893 commit_root: local_root,
894 });
895 vfs.mount_arc(&mount_point, overlay_fs as Arc<dyn kaish_vfs::Filesystem>);
896 overlay_handle = Some(handle);
897 } else {
898 vfs.mount(&mount_point, LocalFs::new(local_root));
902 }
903 #[cfg(not(feature = "overlay"))]
904 {
905 if config.overlay {
906 return Err(anyhow::anyhow!(
907 "overlay=true requires the `overlay` feature, but this build \
908 was compiled without it. Recompile with --features overlay \
909 (or the default feature set) to enable overlay mode."
910 ));
911 }
912 vfs.mount(&mount_point, LocalFs::new(local_root));
914 }
915 }
916 VfsMountMode::NoLocal => {
917 if config.overlay {
918 return Err(anyhow::anyhow!(
919 "overlay=true is incompatible with VfsMountMode::NoLocal: \
920 everything is already virtual, there is no real lower layer \
921 to wrap. Use with_overlay(false) or switch to a Passthrough \
922 or Sandboxed VFS mode."
923 ));
924 }
925 vfs.mount("/", mem(&budget));
927 vfs.mount("/tmp", mem(&budget));
928 vfs.mount("/v", mem(&budget));
929 vfs.mount("/dev", DevFs::new());
931 }
932 }
933
934 Ok(VfsSetupResult {
935 vfs,
936 budget,
937 #[cfg(all(feature = "localfs", feature = "overlay"))]
938 overlay_handle,
939 })
940 }
941
942 pub fn transient() -> Result<Self> {
944 Self::new(KernelConfig::transient())
945 }
946
947 pub fn with_backend(
981 backend: Arc<dyn KernelBackend>,
982 config: KernelConfig,
983 configure_vfs: impl FnOnce(&mut VfsRouter),
984 configure_tools: impl FnOnce(&mut ToolRegistry),
985 ) -> Result<Self> {
986 use crate::backend::VirtualOverlayBackend;
987
988 if config.overlay {
992 return Err(anyhow::anyhow!(
993 "overlay=true is incompatible with Kernel::with_backend: the embedder \
994 controls the VFS; the kernel cannot wrap it with an OverlayFs without \
995 bypassing the embedder's storage semantics. Use KernelConfig::with_overlay(false)."
996 ));
997 }
998
999 let mut vfs = VfsRouter::new();
1000 let jobs = Arc::new(JobManager::new());
1001
1002 let vfs_budget: Option<Arc<ByteBudget>> = config
1006 .vfs_budget_bytes
1007 .map(|bytes| Arc::new(ByteBudget::labeled(bytes, "vfs-memory")));
1008
1009 vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
1010 let blobs_fs = match &vfs_budget {
1011 Some(b) => MemoryFs::with_budget(Arc::clone(b)),
1012 None => MemoryFs::new(),
1013 };
1014 vfs.mount("/v/blobs", blobs_fs);
1015
1016 configure_vfs(&mut vfs);
1018
1019 Self::assemble(config, vfs, jobs, true, vfs_budget, configure_tools, |vfs_arc: &Arc<VfsRouter>, _: &Arc<ToolRegistry>| {
1024 let overlay: Arc<dyn KernelBackend> =
1025 Arc::new(VirtualOverlayBackend::new(backend, vfs_arc.clone()));
1026 ExecContext::with_backend(overlay)
1027 })
1028 }
1029
1030 fn assemble(
1036 config: KernelConfig,
1037 mut vfs: VfsRouter,
1038 jobs: Arc<JobManager>,
1039 no_host_filesystem: bool,
1040 vfs_budget: Option<Arc<ByteBudget>>,
1041 configure_tools: impl FnOnce(&mut ToolRegistry),
1042 make_ctx: impl FnOnce(&Arc<VfsRouter>, &Arc<ToolRegistry>) -> ExecContext,
1043 ) -> Result<Self> {
1044 let no_host_side_channel =
1057 no_host_filesystem || matches!(config.vfs_mode, VfsMountMode::NoLocal);
1058
1059 let KernelConfig { name, cwd, skip_validation, interactive, ignore_config, mut output_limit, allow_external_commands, latch_enabled, trash_enabled, nonce_store, initial_vars, request_timeout, kill_grace, .. } = config;
1060
1061 if no_host_side_channel {
1062 output_limit.set_spill_mode(crate::output_limit::SpillMode::Memory);
1063 jobs.set_persist_output_files(false);
1064 }
1065
1066 let mut tools = ToolRegistry::new();
1067 register_builtins(&mut tools);
1068 configure_tools(&mut tools);
1069 let tools = Arc::new(tools);
1070
1071 vfs.mount("/v/bin", BuiltinFs::new(tools.clone()));
1073
1074 let vfs = Arc::new(vfs);
1075
1076 let runner = PipelineRunner::new(tools.clone());
1077
1078 let (stderr_writer, stderr_receiver) = stderr_stream();
1079
1080 let mut exec_ctx = make_ctx(&vfs, &tools);
1081 exec_ctx.set_cwd(cwd);
1082 exec_ctx.set_job_manager(jobs.clone());
1083 exec_ctx.set_tool_schemas(tools.schemas());
1084 exec_ctx.set_tools(tools.clone());
1085 #[cfg(feature = "os-integration")]
1086 exec_ctx.set_trash_backend(Arc::new(crate::trash_system::SystemTrash));
1087 exec_ctx.stderr = Some(stderr_writer);
1088 exec_ctx.ignore_config = ignore_config;
1089 exec_ctx.output_limit = output_limit;
1090 exec_ctx.allow_external_commands = allow_external_commands;
1091 exec_ctx.vfs_budget = vfs_budget.clone();
1092 if let Some(store) = nonce_store {
1093 exec_ctx.nonce_store = store;
1094 }
1095
1096 Ok(Self {
1097 name,
1098 scope: RwLock::new({
1099 let mut scope = Scope::new();
1100 scope.set_pid(KERNEL_COUNTER.fetch_add(1, Ordering::Relaxed));
1101 for (name, value) in initial_vars {
1110 scope.set_exported(name, value);
1111 }
1112 scope.set_latch_enabled(latch_enabled);
1113 scope.set_trash_enabled(trash_enabled);
1114 scope
1115 }),
1116 tools,
1117 user_tools: RwLock::new(HashMap::new()),
1118 vfs,
1119 jobs,
1120 runner,
1121 exec_ctx: RwLock::new(exec_ctx),
1122 skip_validation,
1123 interactive,
1124 allow_external_commands,
1125 vfs_budget,
1126 request_timeout,
1127 kill_grace,
1128 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
1129 cancel_token: std::sync::Mutex::new(tokio_util::sync::CancellationToken::new()),
1130 #[cfg(all(unix, feature = "subprocess"))]
1131 terminal_state: None,
1132 self_weak: std::sync::OnceLock::new(),
1133 execute_lock: tokio::sync::Mutex::new(()),
1134 bg_job_id: None,
1135 #[cfg(all(feature = "localfs", feature = "overlay"))]
1139 overlay_handle: None,
1140 })
1141 }
1142
1143 pub fn name(&self) -> &str {
1145 &self.name
1146 }
1147
1148 pub fn into_arc(self) -> Arc<Self> {
1155 let arc = Arc::new(self);
1156 let _ = arc.self_weak.set(Arc::downgrade(&arc));
1157 arc
1158 }
1159
1160 pub async fn fork(&self) -> Arc<Self> {
1189 self.fork_inner(tokio_util::sync::CancellationToken::new(), self.bg_job_id)
1190 .await
1191 }
1192
1193 pub async fn fork_attached(&self) -> Arc<Self> {
1201 let child_token = {
1202 #[allow(clippy::expect_used)]
1203 let parent = self.cancel_token.lock().expect("cancel_token poisoned");
1204 parent.child_token()
1205 };
1206 self.fork_inner(child_token, self.bg_job_id).await
1207 }
1208
1209 pub async fn fork_for_background(
1214 &self,
1215 cancel: tokio_util::sync::CancellationToken,
1216 job_id: crate::scheduler::JobId,
1217 ) -> Arc<Self> {
1218 self.fork_inner(cancel, Some(job_id)).await
1219 }
1220
1221 async fn fork_inner(
1224 &self,
1225 cancel: tokio_util::sync::CancellationToken,
1226 bg_job_id: Option<crate::scheduler::JobId>,
1227 ) -> Arc<Self> {
1228 let scope_snapshot = self.scope.read().await.clone();
1229 let user_tools_snapshot = self.user_tools.read().await.clone();
1230
1231 let mut fork_ctx = {
1235 let parent_ctx = self.exec_ctx.read().await;
1236 parent_ctx.child_for_pipeline()
1237 };
1238 let (stderr_writer, stderr_receiver) = stderr_stream();
1239 fork_ctx.stderr = Some(stderr_writer);
1240 fork_ctx.dispatcher = None;
1243 fork_ctx.interactive = false;
1244 fork_ctx.cancel = cancel.clone();
1245 #[cfg(all(unix, feature = "subprocess"))]
1246 {
1247 fork_ctx.terminal_state = None;
1248 }
1249
1250 let fork = Self {
1251 name: format!("{}:fork", self.name),
1252 scope: RwLock::new(scope_snapshot),
1253 tools: Arc::clone(&self.tools),
1254 user_tools: RwLock::new(user_tools_snapshot),
1255 vfs: Arc::clone(&self.vfs),
1256 jobs: Arc::clone(&self.jobs),
1257 runner: self.runner.clone(),
1258 exec_ctx: RwLock::new(fork_ctx),
1259 skip_validation: self.skip_validation,
1260 interactive: false,
1262 allow_external_commands: self.allow_external_commands,
1263 vfs_budget: self.vfs_budget.clone(),
1267 request_timeout: self.request_timeout,
1268 kill_grace: self.kill_grace,
1269 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
1270 cancel_token: std::sync::Mutex::new(cancel),
1271 #[cfg(all(unix, feature = "subprocess"))]
1272 terminal_state: None,
1273 self_weak: std::sync::OnceLock::new(),
1274 execute_lock: tokio::sync::Mutex::new(()),
1275 bg_job_id,
1276 #[cfg(all(feature = "localfs", feature = "overlay"))]
1280 overlay_handle: self.overlay_handle.clone(),
1281 };
1282
1283 fork.into_arc()
1284 }
1285
1286 pub fn dispatcher(&self) -> Option<Arc<dyn CommandDispatcher>> {
1291 self.self_weak
1292 .get()
1293 .and_then(|weak| weak.upgrade())
1294 .map(|arc| arc as Arc<dyn CommandDispatcher>)
1295 }
1296
1297 #[cfg(all(unix, feature = "subprocess"))]
1302 pub fn init_terminal(&mut self) {
1303 if !self.interactive {
1304 return;
1305 }
1306 match crate::terminal::TerminalState::init() {
1307 Ok(state) => {
1308 let state = Arc::new(state);
1309 self.terminal_state = Some(state.clone());
1310 self.exec_ctx.get_mut().terminal_state = Some(state);
1312 tracing::debug!("terminal job control initialized");
1313 }
1314 Err(e) => {
1315 tracing::warn!("failed to initialize terminal job control: {}", e);
1316 }
1317 }
1318 }
1319
1320 pub fn set_trash_backend(&mut self, backend: Option<Arc<dyn crate::trash::TrashBackend>>) {
1328 self.exec_ctx.get_mut().trash_backend = backend;
1329 }
1330
1331 pub fn cancel(&self) {
1337 #[allow(clippy::expect_used)]
1338 let token = self.cancel_token.lock().expect("cancel_token poisoned");
1339 token.cancel();
1340 }
1341
1342 pub fn is_cancelled(&self) -> bool {
1344 #[allow(clippy::expect_used)]
1345 let token = self.cancel_token.lock().expect("cancel_token poisoned");
1346 token.is_cancelled()
1347 }
1348
1349 fn reset_cancel(&self) -> tokio_util::sync::CancellationToken {
1351 #[allow(clippy::expect_used)]
1352 let mut token = self.cancel_token.lock().expect("cancel_token poisoned");
1353 if token.is_cancelled() {
1354 *token = tokio_util::sync::CancellationToken::new();
1355 }
1356 token.clone()
1357 }
1358
1359 async fn acquire_execute_lock(&self) -> tokio::sync::MutexGuard<'_, ()> {
1365 match self.execute_lock.try_lock() {
1366 Ok(guard) => guard,
1367 Err(_) => {
1368 tracing::warn!(
1369 target: "kaish::kernel::concurrency",
1370 kernel = %self.name,
1371 "execute() contended — serializing concurrent caller; \
1372 use Kernel::fork() for parallelism instead of sharing"
1373 );
1374 self.execute_lock.lock().await
1375 }
1376 }
1377 }
1378
1379 pub async fn execute(&self, input: &str) -> Result<ExecResult> {
1384 self.run_inner(input, ExecuteOptions::default(), None, None).await
1385 }
1386
1387 #[tracing::instrument(level = "info", skip(self, argv), fields(cmd = name, argc = argv.len()))]
1428 pub async fn execute_argv(&self, name: &str, argv: &[Value]) -> Result<ExecResult> {
1429 let _guard = self.acquire_execute_lock().await;
1430 let cancel = self.reset_cancel();
1436
1437 let timeout = self.request_timeout;
1439 if timeout == Some(Duration::ZERO) {
1440 return Ok(ExecResult::failure(124, "timeout: timed out after 0s".to_string()));
1441 }
1442
1443 let pipeline = crate::ast::Pipeline {
1444 commands: vec![crate::ast::Command {
1445 name: name.to_string(),
1446 args: argv_to_args(argv),
1447 redirects: Vec::new(),
1448 }],
1449 background: false,
1450 };
1451 let result = self
1452 .run_under_watchdog(timeout, &cancel, self.execute_pipeline(&pipeline))
1453 .await?;
1454 self.update_last_result(&result).await;
1455 Ok(result)
1456 }
1457
1458 async fn run_under_watchdog<F>(
1471 &self,
1472 timeout: Option<Duration>,
1473 cancel: &tokio_util::sync::CancellationToken,
1474 work: F,
1475 ) -> Result<ExecResult>
1476 where
1477 F: std::future::Future<Output = Result<ExecResult>>,
1478 {
1479 let watchdog = timeout.map(|d| Arc::new(crate::watchdog::Watchdog::new(d)));
1481 {
1482 let mut ec = self.exec_ctx.write().await;
1483 ec.watchdog = watchdog.clone();
1484 }
1485
1486 let result = if let Some(d) = timeout {
1487 #[allow(clippy::expect_used)]
1488 let watchdog = watchdog.clone().expect("watchdog constructed when timeout is set");
1489 let elapsed = Arc::new(std::sync::atomic::AtomicBool::new(false));
1490 let timer = tokio::spawn(watchdog.run(elapsed.clone(), cancel.clone()));
1491 let r = work.await;
1492 timer.abort();
1493 match r {
1494 Ok(mut res) => {
1495 if elapsed.load(std::sync::atomic::Ordering::SeqCst) {
1496 res.code = 124;
1497 if res.err.is_empty() {
1498 res.err = format!("timeout: timed out after {:?}", d);
1499 }
1500 }
1501 Ok(res)
1502 }
1503 Err(e) => Err(e),
1504 }
1505 } else {
1506 work.await
1507 };
1508
1509 {
1511 let mut ec = self.exec_ctx.write().await;
1512 ec.watchdog = None;
1513 }
1514 result
1515 }
1516
1517 pub async fn execute_with_options(
1537 &self,
1538 input: &str,
1539 opts: ExecuteOptions,
1540 ) -> Result<ExecResult> {
1541 self.run_inner(input, opts, None, None).await
1542 }
1543
1544 pub async fn execute_with_options_streaming(
1548 &self,
1549 input: &str,
1550 opts: ExecuteOptions,
1551 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1552 ) -> Result<ExecResult> {
1553 self.run_inner(input, opts, None, Some(on_output)).await
1554 }
1555
1556 pub async fn execute_with_pipe_stdin(
1568 &self,
1569 input: &str,
1570 opts: ExecuteOptions,
1571 pipe_stdin: crate::scheduler::PipeReader,
1572 ) -> Result<ExecResult> {
1573 self.run_inner(input, opts, Some(pipe_stdin), None).await
1574 }
1575
1576 pub async fn execute_with_pipe_stdin_streaming(
1580 &self,
1581 input: &str,
1582 opts: ExecuteOptions,
1583 pipe_stdin: crate::scheduler::PipeReader,
1584 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1585 ) -> Result<ExecResult> {
1586 self.run_inner(input, opts, Some(pipe_stdin), Some(on_output)).await
1587 }
1588
1589 #[deprecated(note = "use Kernel::execute_with_options with ExecuteOptions::with_vars")]
1595 pub async fn execute_with_vars(
1596 &self,
1597 input: &str,
1598 vars: HashMap<String, Value>,
1599 ) -> Result<ExecResult> {
1600 self.run_inner(input, ExecuteOptions::new().with_vars(vars), None, None).await
1601 }
1602
1603 #[deprecated(note = "use Kernel::execute_with_options_streaming")]
1608 pub async fn execute_streaming(
1609 &self,
1610 input: &str,
1611 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1612 ) -> Result<ExecResult> {
1613 self.run_inner(input, ExecuteOptions::default(), None, Some(on_output)).await
1614 }
1615
1616 async fn run_inner(
1627 &self,
1628 input: &str,
1629 opts: ExecuteOptions,
1630 pipe_stdin: Option<crate::scheduler::PipeReader>,
1631 on_output: Option<&mut (dyn FnMut(&ExecResult) + Send)>,
1632 ) -> Result<ExecResult> {
1633 use opentelemetry::context::FutureExt;
1634
1635 let embedder_baggage = opts.baggage.clone();
1638
1639 let result = match crate::telemetry::extract_parent(&opts) {
1640 Some(parent) => self
1641 .execute_with_options_inner(input, opts, pipe_stdin, on_output)
1642 .with_context(parent)
1643 .await,
1644 None => self.execute_with_options_inner(input, opts, pipe_stdin, on_output).await,
1645 };
1646
1647 result.map(|mut r| {
1648 crate::telemetry::merge_egress_baggage(&mut r, embedder_baggage);
1649 r
1650 })
1651 }
1652
1653 #[tracing::instrument(level = "info", skip(self, opts, pipe_stdin, on_output), fields(input_len = input.len()))]
1657 async fn execute_with_options_inner(
1658 &self,
1659 input: &str,
1660 opts: ExecuteOptions,
1661 pipe_stdin: Option<crate::scheduler::PipeReader>,
1662 on_output: Option<&mut (dyn FnMut(&ExecResult) + Send)>,
1663 ) -> Result<ExecResult> {
1664 let _guard = self.acquire_execute_lock().await;
1665
1666 let internal = self.reset_cancel();
1674 let (effective_cancel, watcher_handle): (
1679 tokio_util::sync::CancellationToken,
1680 Option<tokio::task::JoinHandle<()>>,
1681 ) = if let Some(ext) = opts.cancel_token {
1682 let combined = tokio_util::sync::CancellationToken::new();
1683 let combined_writer = combined.clone();
1684 let i = internal.clone();
1685 let handle = tokio::spawn(async move {
1686 tokio::select! {
1687 _ = i.cancelled() => combined_writer.cancel(),
1688 _ = ext.cancelled() => combined_writer.cancel(),
1689 }
1690 });
1691 (combined, Some(handle))
1692 } else {
1693 (internal, None)
1694 };
1695
1696 let timeout = opts.timeout.or(self.request_timeout);
1698
1699 if timeout == Some(Duration::ZERO) {
1701 if let Some(h) = watcher_handle {
1702 h.abort();
1703 }
1704 return Ok(ExecResult::failure(124, "timeout: timed out after 0s".to_string()));
1705 }
1706
1707 struct VarsFrameGuard<'a> {
1711 kernel: &'a Kernel,
1712 newly_exported: Vec<String>,
1713 }
1714 impl Drop for VarsFrameGuard<'_> {
1715 fn drop(&mut self) {
1716 let Ok(mut scope) = self.kernel.scope.try_write() else {
1725 tracing::error!(
1726 "vars frame guard: scope lock unexpectedly busy; \
1727 skipping pop_frame to avoid runtime deadlock — \
1728 transient vars may leak"
1729 );
1730 return;
1731 };
1732 scope.pop_frame();
1733 for name in self.newly_exported.drain(..) {
1734 scope.unexport(&name);
1735 }
1736 }
1737 }
1738
1739 struct CwdGuard<'a> {
1743 kernel: &'a Kernel,
1744 saved: PathBuf,
1745 }
1746 impl Drop for CwdGuard<'_> {
1747 fn drop(&mut self) {
1748 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1749 tracing::error!(
1750 "cwd guard: exec_ctx lock unexpectedly busy; \
1751 skipping cwd restore — kernel cwd may be wrong for next call"
1752 );
1753 return;
1754 };
1755 ec.cwd = std::mem::take(&mut self.saved);
1756 }
1757 }
1758 let _cwd_guard: Option<CwdGuard<'_>> = if let Some(new_cwd) = opts.cwd {
1759 let mut ec = self.exec_ctx.write().await;
1760 let saved = std::mem::replace(&mut ec.cwd, new_cwd);
1761 drop(ec);
1762 Some(CwdGuard { kernel: self, saved })
1763 } else {
1764 None
1765 };
1766
1767 struct StdinGuard<'a> {
1773 kernel: &'a Kernel,
1774 saved: Option<String>,
1775 }
1776 impl Drop for StdinGuard<'_> {
1777 fn drop(&mut self) {
1778 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1779 tracing::error!(
1780 "stdin guard: exec_ctx lock unexpectedly busy; \
1781 skipping stdin restore — stale stdin may leak to next call"
1782 );
1783 return;
1784 };
1785 ec.stdin = self.saved.take();
1786 }
1787 }
1788 let _stdin_guard: Option<StdinGuard<'_>> = if let Some(stdin) = opts.stdin {
1789 let mut ec = self.exec_ctx.write().await;
1790 let saved = ec.stdin.replace(stdin);
1791 drop(ec);
1792 Some(StdinGuard { kernel: self, saved })
1793 } else {
1794 None
1795 };
1796
1797 struct PipeStdinGuard<'a> {
1803 kernel: &'a Kernel,
1804 saved: Option<crate::scheduler::PipeReader>,
1805 }
1806 impl Drop for PipeStdinGuard<'_> {
1807 fn drop(&mut self) {
1808 let Ok(mut ec) = self.kernel.exec_ctx.try_write() else {
1809 tracing::error!(
1810 "pipe stdin guard: exec_ctx lock unexpectedly busy; \
1811 skipping restore — stale pipe stdin may leak to next call"
1812 );
1813 return;
1814 };
1815 ec.pipe_stdin = self.saved.take();
1816 }
1817 }
1818 let _pipe_stdin_guard: Option<PipeStdinGuard<'_>> = if let Some(reader) = pipe_stdin {
1819 let mut ec = self.exec_ctx.write().await;
1820 let saved = ec.pipe_stdin.replace(reader);
1821 drop(ec);
1822 Some(PipeStdinGuard { kernel: self, saved })
1823 } else {
1824 None
1825 };
1826
1827 let _vars_guard: Option<VarsFrameGuard<'_>> = if !opts.vars.is_empty() {
1828 let mut scope = self.scope.write().await;
1829 scope.push_frame();
1830 let mut newly = Vec::with_capacity(opts.vars.len());
1831 for (name, value) in opts.vars {
1832 if !scope.is_exported(&name) {
1833 newly.push(name.clone());
1834 }
1835 scope.set_exported(name, value);
1836 }
1837 drop(scope);
1838 Some(VarsFrameGuard { kernel: self, newly_exported: newly })
1839 } else {
1840 None
1841 };
1842
1843 {
1850 #[allow(clippy::expect_used)]
1851 let mut cur = self.cancel_token.lock().expect("cancel_token poisoned");
1852 *cur = effective_cancel.clone();
1853 }
1854
1855 let mut noop_cb: Box<dyn FnMut(&ExecResult) + Send> = Box::new(|_| {});
1861 let cb_ref: &mut (dyn FnMut(&ExecResult) + Send) = match on_output {
1862 Some(cb) => cb,
1863 None => &mut *noop_cb,
1864 };
1865
1866 let result = self
1867 .run_under_watchdog(timeout, &effective_cancel, self.execute_streaming_inner(input, cb_ref))
1868 .await;
1869
1870 {
1875 #[allow(clippy::expect_used)]
1876 let mut cur = self.cancel_token.lock().expect("cancel_token poisoned");
1877 *cur = tokio_util::sync::CancellationToken::new();
1878 }
1879
1880 if let Some(h) = watcher_handle {
1883 h.abort();
1884 }
1885
1886 result
1889 }
1890
1891 async fn execute_streaming_inner(
1897 &self,
1898 input: &str,
1899 on_output: &mut (dyn FnMut(&ExecResult) + Send),
1900 ) -> Result<ExecResult> {
1901 let program = parse(input).map_err(|errors| {
1902 let msg = errors
1903 .iter()
1904 .map(|e| e.format(input))
1905 .collect::<Vec<_>>()
1906 .join("\n");
1907 anyhow::anyhow!("parse error:\n{}", msg)
1908 })?;
1909
1910 {
1912 let scope = self.scope.read().await;
1913 if scope.show_ast() {
1914 let output = format!("{:#?}\n", program);
1915 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(output)));
1916 }
1917 }
1918
1919 let mut surfaced_warnings = String::new();
1924 if !self.skip_validation {
1925 let user_tools = self.user_tools.read().await;
1926 let validator = Validator::new(&self.tools, &user_tools);
1927 let issues = validator.validate(&program);
1928
1929 let errors: Vec<_> = issues
1931 .iter()
1932 .filter(|i| i.severity == Severity::Error)
1933 .collect();
1934
1935 if !errors.is_empty() {
1936 let error_msg = errors
1937 .iter()
1938 .map(|e| e.format(input))
1939 .collect::<Vec<_>>()
1940 .join("\n");
1941 return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
1942 }
1943
1944 for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
1947 tracing::trace!("validation: {}", warning.format(input));
1948 if warning.code.surfaces_to_agent() {
1949 surfaced_warnings.push_str(&warning.format(input));
1950 surfaced_warnings.push('\n');
1951 }
1952 }
1953 }
1954
1955 if !surfaced_warnings.is_empty() {
1962 let mut advisory = ExecResult::success("");
1963 advisory.err = surfaced_warnings.clone();
1964 on_output(&advisory);
1965 }
1966
1967 let mut result = ExecResult::success("");
1968
1969 let cancel = self.reset_cancel();
1971
1972 for stmt in program.statements {
1973 if matches!(stmt, Stmt::Empty) {
1974 continue;
1975 }
1976
1977 if cancel.is_cancelled() {
1979 result.code = 130;
1980 return Ok(result);
1981 }
1982
1983 let flow = self.execute_stmt_flow(&stmt).await?;
1984
1985 let drained_stderr = {
1989 let mut receiver = self.stderr_receiver.lock().await;
1990 receiver.drain_lossy()
1991 };
1992
1993 match flow {
1994 ControlFlow::Normal(mut r) => {
1995 if !drained_stderr.is_empty() {
1996 if !r.err.is_empty() && !r.err.ends_with('\n') {
1997 r.err.push('\n');
1998 }
1999 let combined = format!("{}{}", drained_stderr, r.err);
2001 r.err = combined;
2002 }
2003 on_output(&r);
2004 let last_output = r.output().cloned();
2008 accumulate_result(&mut result, &r);
2009 result.set_output(last_output);
2010 }
2011 ControlFlow::Exit { code } => {
2012 if !drained_stderr.is_empty() {
2013 result.err.push_str(&drained_stderr);
2014 }
2015 result.code = code;
2016 if !surfaced_warnings.is_empty() {
2017 result.err = format!("{surfaced_warnings}{}", result.err);
2018 }
2019 return Ok(result);
2020 }
2021 ControlFlow::Return { mut value } => {
2022 if !drained_stderr.is_empty() {
2023 value.err = format!("{}{}", drained_stderr, value.err);
2024 }
2025 on_output(&value);
2026 result = value;
2027 }
2028 ControlFlow::Break { result: mut r, .. } | ControlFlow::Continue { result: mut r, .. } => {
2029 if !drained_stderr.is_empty() {
2030 r.err = format!("{}{}", drained_stderr, r.err);
2031 }
2032 on_output(&r);
2033 result = r;
2034 }
2035 }
2036 }
2037
2038 if !surfaced_warnings.is_empty() {
2039 result.err = format!("{surfaced_warnings}{}", result.err);
2040 }
2041 Ok(result)
2042 }
2043
2044 fn execute_stmt_flow<'a>(
2046 &'a self,
2047 stmt: &'a Stmt,
2048 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + Send + 'a>> {
2049 use tracing::Instrument;
2050 let span = tracing::debug_span!("execute_stmt_flow", stmt_type = %stmt.kind_name());
2051 Box::pin(async move {
2052 match stmt {
2053 Stmt::Assignment(assign) => {
2054 let value = self.eval_expr_async(&assign.value).await
2056 .context("failed to evaluate assignment")?;
2057 let mut scope = self.scope.write().await;
2058 if assign.local {
2059 scope.set(&assign.name, value.clone());
2061 } else {
2062 scope.set_global(&assign.name, value.clone());
2064 }
2065 drop(scope);
2066
2067 Ok(ControlFlow::ok(ExecResult::success("")))
2069 }
2070 Stmt::Command(cmd) => {
2071 let pipeline = crate::ast::Pipeline {
2074 commands: vec![cmd.clone()],
2075 background: false,
2076 };
2077 let result = self.execute_pipeline(&pipeline).await?;
2078 self.update_last_result(&result).await;
2079
2080 if !result.ok() {
2082 let scope = self.scope.read().await;
2083 if scope.error_exit_enabled() {
2084 return Ok(ControlFlow::exit_code(result.code));
2085 }
2086 }
2087
2088 Ok(ControlFlow::ok(result))
2089 }
2090 Stmt::Pipeline(pipeline) => {
2091 let result = self.execute_pipeline(pipeline).await?;
2092 self.update_last_result(&result).await;
2093
2094 if !result.ok() {
2096 let scope = self.scope.read().await;
2097 if scope.error_exit_enabled() {
2098 return Ok(ControlFlow::exit_code(result.code));
2099 }
2100 }
2101
2102 Ok(ControlFlow::ok(result))
2103 }
2104 Stmt::If(if_stmt) => {
2105 let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
2107
2108 let branch = if is_truthy(&cond_value) {
2109 &if_stmt.then_branch
2110 } else {
2111 if_stmt.else_branch.as_deref().unwrap_or(&[])
2112 };
2113
2114 let mut result = ExecResult::success("");
2115 for stmt in branch {
2116 let flow = self.execute_stmt_flow(stmt).await?;
2117 match flow {
2118 ControlFlow::Normal(r) => {
2119 accumulate_result(&mut result, &r);
2120 self.drain_stderr_into(&mut result).await;
2121 }
2122 other => {
2123 self.drain_stderr_into(&mut result).await;
2124 return Ok(other);
2125 }
2126 }
2127 }
2128 Ok(ControlFlow::ok(result))
2129 }
2130 Stmt::For(for_loop) => {
2131 let mut items: Vec<Value> = Vec::new();
2134 for item_expr in &for_loop.items {
2135 if let Expr::GlobPattern(pattern) = item_expr {
2137 let glob_enabled = {
2138 let scope = self.scope.read().await;
2139 scope.glob_enabled()
2140 };
2141 if glob_enabled {
2142 let (paths, cwd) = {
2143 let ctx = self.exec_ctx.read().await;
2144 let paths = ctx.expand_glob(pattern).await
2145 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
2146 let cwd = ctx.resolve_path(".");
2147 (paths, cwd)
2148 };
2149 if paths.is_empty() {
2150 return Err(anyhow::anyhow!("no matches: {}", pattern));
2151 }
2152 for path in paths {
2153 let display = if !pattern.starts_with('/') {
2154 path.strip_prefix(&cwd)
2155 .unwrap_or(&path)
2156 .to_string_lossy().into_owned()
2157 } else {
2158 path.to_string_lossy().into_owned()
2159 };
2160 items.push(Value::String(display));
2161 }
2162 continue;
2163 }
2164 }
2165 let from_command_subst = matches!(item_expr, Expr::CommandSubst(_));
2171 let item = self.eval_expr_async(item_expr).await?;
2172 match item {
2173 Value::Json(serde_json::Value::Array(arr)) => {
2176 for elem in arr {
2177 items.push(json_to_value(elem));
2178 }
2179 }
2180 Value::String(s) if from_command_subst => {
2188 let trimmed = s.trim_end_matches(['\n', '\r']);
2189 if trimmed.is_empty() {
2190 continue;
2191 }
2192 if trimmed.contains('\n') {
2193 for line in trimmed.split('\n') {
2194 let line = line.trim_end_matches('\r');
2195 items.push(Value::String(line.to_string()));
2196 }
2197 } else {
2198 items.push(Value::String(trimmed.to_string()));
2199 }
2200 }
2201 Value::Bytes(_) => {
2204 anyhow::bail!(
2205 "for: cannot iterate over binary data — decode it \
2206 (base64/xxd) first"
2207 );
2208 }
2209 other => items.push(other),
2211 }
2212 }
2213
2214 let mut result = ExecResult::success("");
2215 {
2216 let mut scope = self.scope.write().await;
2217 scope.push_frame();
2218 }
2219
2220 'outer: for item in items {
2221 if self.is_cancelled() {
2223 let mut scope = self.scope.write().await;
2224 scope.pop_frame();
2225 result.code = 130;
2226 return Ok(ControlFlow::ok(result));
2227 }
2228 {
2229 let mut scope = self.scope.write().await;
2230 scope.set(&for_loop.variable, item);
2231 }
2232 for stmt in &for_loop.body {
2233 let mut flow = match self.execute_stmt_flow(stmt).await {
2234 Ok(f) => f,
2235 Err(e) => {
2236 let mut scope = self.scope.write().await;
2237 scope.pop_frame();
2238 return Err(e);
2239 }
2240 };
2241 self.drain_stderr_into(&mut result).await;
2242 match &mut flow {
2243 ControlFlow::Normal(r) => {
2244 accumulate_result(&mut result, r);
2245 if !r.ok() {
2246 let scope = self.scope.read().await;
2247 if scope.error_exit_enabled() {
2248 drop(scope);
2249 let mut scope = self.scope.write().await;
2250 scope.pop_frame();
2251 return Ok(ControlFlow::exit_code(r.code));
2252 }
2253 }
2254 }
2255 ControlFlow::Break { .. } => {
2256 if flow.decrement_level() {
2257 accumulate_flow_output(&mut result, &flow);
2258 break 'outer;
2259 }
2260 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2261 let mut scope = self.scope.write().await;
2262 scope.pop_frame();
2263 return Ok(flow);
2264 }
2265 ControlFlow::Continue { .. } => {
2266 if flow.decrement_level() {
2267 accumulate_flow_output(&mut result, &flow);
2268 continue 'outer;
2269 }
2270 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2271 let mut scope = self.scope.write().await;
2272 scope.pop_frame();
2273 return Ok(flow);
2274 }
2275 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
2276 let mut scope = self.scope.write().await;
2277 scope.pop_frame();
2278 return Ok(flow);
2279 }
2280 }
2281 }
2282 }
2283
2284 {
2285 let mut scope = self.scope.write().await;
2286 scope.pop_frame();
2287 }
2288 Ok(ControlFlow::ok(result))
2289 }
2290 Stmt::While(while_loop) => {
2291 let mut result = ExecResult::success("");
2292
2293 'outer: loop {
2294 if self.is_cancelled() {
2297 result.code = 130;
2298 return Ok(ControlFlow::ok(result));
2299 }
2300
2301 let cond_value = self.eval_expr_async(&while_loop.condition).await?;
2302
2303 if !is_truthy(&cond_value) {
2304 break;
2305 }
2306
2307 for stmt in &while_loop.body {
2309 let mut flow = self.execute_stmt_flow(stmt).await?;
2310 self.drain_stderr_into(&mut result).await;
2311 match &mut flow {
2312 ControlFlow::Normal(r) => {
2313 accumulate_result(&mut result, r);
2314 if !r.ok() {
2315 let scope = self.scope.read().await;
2316 if scope.error_exit_enabled() {
2317 return Ok(ControlFlow::exit_code(r.code));
2318 }
2319 }
2320 }
2321 ControlFlow::Break { .. } => {
2322 if flow.decrement_level() {
2323 accumulate_flow_output(&mut result, &flow);
2324 break 'outer;
2325 }
2326 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2327 return Ok(flow);
2328 }
2329 ControlFlow::Continue { .. } => {
2330 if flow.decrement_level() {
2331 accumulate_flow_output(&mut result, &flow);
2332 continue 'outer;
2333 }
2334 fold_loop_output_into_flow(std::mem::take(&mut result), &mut flow);
2335 return Ok(flow);
2336 }
2337 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
2338 return Ok(flow);
2339 }
2340 }
2341 }
2342 }
2343
2344 Ok(ControlFlow::ok(result))
2345 }
2346 Stmt::Case(case_stmt) => {
2347 let match_value = {
2349 let value = self.eval_expr_async(&case_stmt.expr).await?;
2350 value_to_string(&value)
2351 };
2352
2353 for branch in &case_stmt.branches {
2355 let matched = branch.patterns.iter().any(|pattern| {
2356 glob_match(pattern, &match_value)
2357 });
2358
2359 if matched {
2360 let mut result = ExecResult::success("");
2362 for stmt in &branch.body {
2363 let flow = self.execute_stmt_flow(stmt).await?;
2364 match flow {
2365 ControlFlow::Normal(r) => {
2366 accumulate_result(&mut result, &r);
2367 self.drain_stderr_into(&mut result).await;
2368 }
2369 other => {
2370 self.drain_stderr_into(&mut result).await;
2371 return Ok(other);
2372 }
2373 }
2374 }
2375 return Ok(ControlFlow::ok(result));
2376 }
2377 }
2378
2379 Ok(ControlFlow::ok(ExecResult::success("")))
2381 }
2382 Stmt::Break(levels) => {
2383 Ok(ControlFlow::break_n(levels.unwrap_or(1)))
2384 }
2385 Stmt::Continue(levels) => {
2386 Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
2387 }
2388 Stmt::Return(expr) => {
2389 let result = if let Some(e) = expr {
2392 let val = self.eval_expr_async(e).await?;
2393 let code = crate::interpreter::value_to_exit_code(&val)
2394 .map_err(|e| anyhow::anyhow!("return: {}", e))?;
2395 ExecResult::from_parts(code, String::new(), String::new(), None)
2396 } else {
2397 ExecResult::success("")
2398 };
2399 Ok(ControlFlow::return_value(result))
2400 }
2401 Stmt::Exit(expr) => {
2402 let code = if let Some(e) = expr {
2403 let val = self.eval_expr_async(e).await?;
2404 crate::interpreter::value_to_exit_code(&val)
2405 .map_err(|e| anyhow::anyhow!("exit: {}", e))?
2406 } else {
2407 0
2408 };
2409 Ok(ControlFlow::exit_code(code))
2410 }
2411 Stmt::ToolDef(tool_def) => {
2412 let mut user_tools = self.user_tools.write().await;
2413 user_tools.insert(tool_def.name.clone(), tool_def.clone());
2414 Ok(ControlFlow::ok(ExecResult::success("")))
2415 }
2416 Stmt::AndChain { left, right } => {
2417 {
2420 let mut scope = self.scope.write().await;
2421 scope.suppress_errexit();
2422 }
2423 let left_flow = match self.execute_stmt_flow(left).await {
2424 Ok(f) => f,
2425 Err(e) => {
2426 let mut scope = self.scope.write().await;
2427 scope.unsuppress_errexit();
2428 return Err(e);
2429 }
2430 };
2431 {
2432 let mut scope = self.scope.write().await;
2433 scope.unsuppress_errexit();
2434 }
2435 match left_flow {
2436 ControlFlow::Normal(mut left_result) => {
2437 self.drain_stderr_into(&mut left_result).await;
2438 self.update_last_result(&left_result).await;
2439 if left_result.ok() {
2440 let right_flow = self.execute_stmt_flow(right).await?;
2441 match right_flow {
2442 ControlFlow::Normal(mut right_result) => {
2443 self.drain_stderr_into(&mut right_result).await;
2444 self.update_last_result(&right_result).await;
2445 let mut combined = left_result;
2446 accumulate_result(&mut combined, &right_result);
2447 Ok(ControlFlow::ok(combined))
2448 }
2449 other => Ok(other),
2450 }
2451 } else {
2452 Ok(ControlFlow::ok(left_result))
2453 }
2454 }
2455 _ => Ok(left_flow),
2456 }
2457 }
2458 Stmt::OrChain { left, right } => {
2459 {
2462 let mut scope = self.scope.write().await;
2463 scope.suppress_errexit();
2464 }
2465 let left_flow = match self.execute_stmt_flow(left).await {
2466 Ok(f) => f,
2467 Err(e) => {
2468 let mut scope = self.scope.write().await;
2469 scope.unsuppress_errexit();
2470 return Err(e);
2471 }
2472 };
2473 {
2474 let mut scope = self.scope.write().await;
2475 scope.unsuppress_errexit();
2476 }
2477 match left_flow {
2478 ControlFlow::Normal(mut left_result) => {
2479 self.drain_stderr_into(&mut left_result).await;
2480 self.update_last_result(&left_result).await;
2481 if !left_result.ok() {
2482 let right_flow = self.execute_stmt_flow(right).await?;
2483 match right_flow {
2484 ControlFlow::Normal(mut right_result) => {
2485 self.drain_stderr_into(&mut right_result).await;
2486 self.update_last_result(&right_result).await;
2487 let mut combined = left_result;
2488 accumulate_result(&mut combined, &right_result);
2489 Ok(ControlFlow::ok(combined))
2490 }
2491 other => Ok(other),
2492 }
2493 } else {
2494 Ok(ControlFlow::ok(left_result))
2495 }
2496 }
2497 _ => Ok(left_flow), }
2499 }
2500 Stmt::Test(test_expr) => {
2501 let is_true = self.eval_test_async(test_expr).await?;
2502 if is_true {
2503 Ok(ControlFlow::ok(ExecResult::success("")))
2504 } else {
2505 Ok(ControlFlow::ok(ExecResult::failure(1, "")))
2506 }
2507 }
2508 Stmt::EnvScoped { assignments, body } => {
2509 {
2516 let mut scope = self.scope.write().await;
2517 scope.push_frame();
2518 }
2519 let mut prior_export: Vec<(String, bool)> =
2520 Vec::with_capacity(assignments.len());
2521 let mut setup_err: Option<anyhow::Error> = None;
2522 for assign in assignments {
2523 match self.eval_expr_async(&assign.value).await {
2524 Ok(value) => {
2525 let mut scope = self.scope.write().await;
2526 prior_export
2527 .push((assign.name.clone(), scope.is_exported(&assign.name)));
2528 scope.set_exported(&assign.name, value);
2529 }
2530 Err(e) => {
2531 setup_err = Some(e);
2532 break;
2533 }
2534 }
2535 }
2536
2537 let flow = if setup_err.is_none() {
2538 self.execute_stmt_flow(body).await
2539 } else {
2540 Ok(ControlFlow::ok(ExecResult::success("")))
2541 };
2542
2543 {
2546 let mut scope = self.scope.write().await;
2547 scope.pop_frame();
2548 for (name, was_exported) in &prior_export {
2549 if !*was_exported {
2550 scope.unexport(name);
2551 }
2552 }
2553 }
2554
2555 match setup_err {
2556 Some(e) => Err(e),
2557 None => flow,
2558 }
2559 }
2560 Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
2561 }
2562 }.instrument(span))
2563 }
2564
2565 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(background = pipeline.background, command_count = pipeline.commands.len()))]
2567 async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
2568 if pipeline.commands.is_empty() {
2569 return Ok(ExecResult::success(""));
2570 }
2571
2572 if pipeline.background {
2574 return self.execute_background(pipeline).await;
2575 }
2576
2577 let (mut ctx, has_pipe_stdin) = {
2585 let ec = self.exec_ctx.read().await;
2586 let scope = self.scope.read().await;
2587 let has_pipe_stdin = ec.pipe_stdin.is_some();
2591 (ExecContext {
2592 backend: ec.backend.clone(),
2593 scope: scope.clone(),
2594 cwd: ec.cwd.clone(),
2595 prev_cwd: ec.prev_cwd.clone(),
2596 stdin: ec.stdin.clone(),
2601 stdin_data: ec.stdin_data.clone(),
2602 stdin_data_rx: None,
2603 pipe_stdin: None,
2604 pipe_stdout: None,
2605 stderr: ec.stderr.clone(),
2606 tool_schemas: ec.tool_schemas.clone(),
2607 tools: ec.tools.clone(),
2608 job_manager: ec.job_manager.clone(),
2609 pipeline_position: PipelinePosition::Only,
2610 interactive: self.interactive,
2611 aliases: ec.aliases.clone(),
2612 ignore_config: ec.ignore_config.clone(),
2613 output_limit: ec.output_limit.clone(),
2614 allow_external_commands: self.allow_external_commands,
2615 nonce_store: ec.nonce_store.clone(),
2616 trash_backend: ec.trash_backend.clone(),
2617 #[cfg(all(unix, feature = "subprocess"))]
2618 terminal_state: ec.terminal_state.clone(),
2619 dispatcher: self.dispatcher(),
2620 cancel: {
2621 #[allow(clippy::expect_used)]
2622 let token = self.cancel_token.lock().expect("cancel_token poisoned");
2623 token.clone()
2624 },
2625 output_format: None,
2626 vfs_budget: self.vfs_budget.clone(),
2627 watchdog: ec.watchdog.clone(),
2628 #[cfg(all(feature = "localfs", feature = "overlay"))]
2629 overlay_handle: self.overlay_handle.clone(),
2630 }, has_pipe_stdin)
2631 }; if ctx.stdin.is_some() || ctx.stdin_data.is_some() || has_pipe_stdin {
2639 let mut ec = self.exec_ctx.write().await;
2640 ctx.pipe_stdin = ec.pipe_stdin.take();
2641 ec.stdin = None;
2642 ec.stdin_data = None;
2643 }
2644
2645 let mut result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
2646
2647 if ctx.output_limit.is_enabled() {
2649 let _ = crate::output_limit::spill_if_needed(&mut result, &ctx.output_limit).await;
2650 }
2651
2652 if result.did_spill {
2655 result.original_code = Some(result.code);
2656 result.code = 3;
2657 }
2658
2659 {
2661 let mut ec = self.exec_ctx.write().await;
2662 ec.cwd = ctx.cwd.clone();
2663 ec.prev_cwd = ctx.prev_cwd.clone();
2664 ec.aliases = ctx.aliases.clone();
2665 ec.ignore_config = ctx.ignore_config.clone();
2666 ec.output_limit = ctx.output_limit.clone();
2667 }
2668 {
2669 let mut scope = self.scope.write().await;
2670 *scope = ctx.scope.clone();
2671 }
2672
2673 Ok(result)
2674 }
2675
2676 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
2684 async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
2685 use tokio::sync::oneshot;
2686
2687 let command_str = self.format_pipeline(pipeline);
2689
2690 let stdout = Arc::new(BoundedStream::default_size());
2692 let stderr = Arc::new(BoundedStream::default_size());
2693
2694 let (tx, rx) = oneshot::channel();
2696
2697 let job_id = self.jobs.register_with_streams(
2699 command_str.clone(),
2700 rx,
2701 stdout.clone(),
2702 stderr.clone(),
2703 ).await;
2704
2705 let cancel = tokio_util::sync::CancellationToken::new();
2716 self.jobs.set_cancel_token(job_id, cancel.clone()).await;
2717 let fork = self.fork_for_background(cancel, job_id).await;
2718 let runner = self.runner.clone();
2719 let commands = pipeline.commands.clone();
2720
2721 let mut bg_ctx = {
2725 let ec = fork.exec_ctx.read().await;
2726 ec.child_for_pipeline()
2727 };
2728 bg_ctx.scope = fork.scope.read().await.clone();
2729 bg_ctx.dispatcher = fork.dispatcher();
2733
2734 tokio::spawn(crate::telemetry::bind_current_context(async move {
2737 let result = runner.run(&commands, &mut bg_ctx, fork.as_ref()).await;
2740
2741 let text = result.text_out();
2743 if !text.is_empty() {
2744 stdout.write(text.as_bytes()).await;
2745 }
2746 if !result.err.is_empty() {
2747 stderr.write(result.err.as_bytes()).await;
2748 }
2749
2750 stdout.close().await;
2752 stderr.close().await;
2753
2754 let _ = tx.send(result);
2756 }));
2757
2758 Ok(ExecResult::success(format!("[{}]", job_id)))
2759 }
2760
2761 fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
2763 pipeline.commands
2764 .iter()
2765 .map(|cmd| {
2766 let mut parts = vec![cmd.name.clone()];
2767 for arg in &cmd.args {
2768 match arg {
2769 Arg::Positional(expr) => {
2770 parts.push(self.format_expr(expr));
2771 }
2772 Arg::Named { key, value } => {
2773 parts.push(format!("--{}={}", key, self.format_expr(value)));
2774 }
2775 Arg::WordAssign { key, value } => {
2776 parts.push(format!("{}={}", key, self.format_expr(value)));
2777 }
2778 Arg::ShortFlag(name) => {
2779 parts.push(format!("-{}", name));
2780 }
2781 Arg::LongFlag(name) => {
2782 parts.push(format!("--{}", name));
2783 }
2784 Arg::DoubleDash => {
2785 parts.push("--".to_string());
2786 }
2787 }
2788 }
2789 parts.join(" ")
2790 })
2791 .collect::<Vec<_>>()
2792 .join(" | ")
2793 }
2794
2795 fn format_expr(&self, expr: &Expr) -> String {
2797 match expr {
2798 Expr::Literal(Value::String(s)) => {
2799 if s.contains(' ') || s.contains('"') {
2800 format!("'{}'", s.replace('\'', "\\'"))
2801 } else {
2802 s.clone()
2803 }
2804 }
2805 Expr::Literal(Value::Int(i)) => i.to_string(),
2806 Expr::Literal(Value::Float(f)) => f.to_string(),
2807 Expr::Literal(Value::Bool(b)) => b.to_string(),
2808 Expr::Literal(Value::Null) => "null".to_string(),
2809 Expr::VarRef(path) => {
2810 let name = path.segments.iter()
2811 .map(|seg| match seg {
2812 crate::ast::VarSegment::Field(f) => f.clone(),
2813 })
2814 .collect::<Vec<_>>()
2815 .join(".");
2816 format!("${{{}}}", name)
2817 }
2818 Expr::Interpolated(_) => "\"...\"".to_string(),
2819 Expr::HereDocBody { .. } => "<<heredoc".to_string(),
2820 _ => "...".to_string(),
2821 }
2822 }
2823
2824 async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
2826 self.execute_command_depth(name, args, 0).await
2827 }
2828
2829 #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
2830 async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
2831 match name {
2833 "true" => return Ok(ExecResult::success("")),
2834 "false" => return Ok(ExecResult::failure(1, "")),
2835 "source" | "." => return self.execute_source(args).await,
2836 _ => {}
2837 }
2838
2839 if alias_depth < 10 {
2841 let alias_value = {
2842 let ctx = self.exec_ctx.read().await;
2843 ctx.aliases.get(name).cloned()
2844 };
2845 if let Some(alias_val) = alias_value {
2846 let parts: Vec<&str> = alias_val.split_whitespace().collect();
2848 if let Some((alias_cmd, alias_args)) = parts.split_first() {
2849 let mut new_args: Vec<Arg> = alias_args
2850 .iter()
2851 .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
2852 .collect();
2853 new_args.extend_from_slice(args);
2854 return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
2855 }
2856 }
2857 }
2858
2859 if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
2861 return match self.tools.get(builtin_name) {
2862 Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
2863 None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
2864 };
2865 }
2866
2867 {
2869 let user_tools = self.user_tools.read().await;
2870 if let Some(tool_def) = user_tools.get(name) {
2871 let tool_def = tool_def.clone();
2872 drop(user_tools);
2873 return self.execute_user_tool(tool_def, args).await;
2874 }
2875 }
2876
2877 let tool = match self.tools.get(name) {
2879 Some(t) => t,
2880 None => {
2881 if let Some(result) = self.try_execute_script(name, args).await? {
2883 return Ok(result);
2884 }
2885 if let Some(result) = self.try_execute_external(name, args).await? {
2887 return Ok(result);
2888 }
2889
2890 let backend = self.exec_ctx.read().await.backend.clone();
2895 let tool_schema = backend.get_tool(name).await.ok().flatten().map(|t| {
2896 let mut s = t.schema;
2897 if s.subcommands.is_empty() {
2903 s.map_positionals = true;
2904 }
2905 s
2906 });
2907 let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
2908 let mut ctx = self.exec_ctx.write().await;
2909 {
2910 let scope = self.scope.read().await;
2911 ctx.scope = scope.clone();
2912 }
2913 let backend = ctx.backend.clone();
2914 match backend.call_tool(name, tool_args, &mut *ctx).await {
2915 Ok(tool_result) => {
2916 let mut scope = self.scope.write().await;
2917 *scope = ctx.scope.clone();
2918 let mut exec = ExecResult::from_output(
2919 tool_result.code as i64, tool_result.stdout, tool_result.stderr,
2920 );
2921 exec.set_output(tool_result.output);
2922 return Ok(exec);
2923 }
2924 Err(BackendError::ToolNotFound(_)) => {
2925 }
2927 Err(e) => {
2928 tracing::debug!("backend error for {name}: {e}");
2931 }
2932 }
2933
2934 return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
2935 }
2936 };
2937
2938 let schema = tool.schema();
2940 let tool_args = self.build_args_async(args, Some(&schema)).await?;
2941
2942 let schema_claims = |flag: &str| -> bool {
2944 let bare = flag.trim_start_matches('-');
2945 schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
2946 };
2947 let wants_help =
2948 (tool_args.flags.contains("help") && !schema_claims("help"))
2949 || (tool_args.flags.contains("h") && !schema_claims("-h"));
2950 if wants_help {
2951 let help_topic = crate::help::HelpTopic::Tool(name.to_string());
2952 let ctx = self.exec_ctx.read().await;
2953 let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
2954 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
2955 }
2956
2957 let mut ctx = {
2963 let ec = self.exec_ctx.write().await;
2964 let scope = self.scope.read().await;
2965 ExecContext {
2966 backend: ec.backend.clone(),
2967 scope: scope.clone(),
2968 cwd: ec.cwd.clone(),
2969 prev_cwd: ec.prev_cwd.clone(),
2970 stdin: ec.stdin.clone(),
2971 stdin_data: ec.stdin_data.clone(),
2972 stdin_data_rx: None,
2973 pipe_stdin: None, pipe_stdout: None,
2975 stderr: ec.stderr.clone(),
2976 tool_schemas: ec.tool_schemas.clone(),
2977 tools: ec.tools.clone(),
2978 job_manager: ec.job_manager.clone(),
2979 pipeline_position: ec.pipeline_position,
2980 interactive: self.interactive,
2981 aliases: ec.aliases.clone(),
2982 ignore_config: ec.ignore_config.clone(),
2983 output_limit: ec.output_limit.clone(),
2984 allow_external_commands: self.allow_external_commands,
2985 nonce_store: ec.nonce_store.clone(),
2986 trash_backend: ec.trash_backend.clone(),
2987 #[cfg(all(unix, feature = "subprocess"))]
2988 terminal_state: ec.terminal_state.clone(),
2989 dispatcher: self.dispatcher(),
2990 cancel: ec.cancel.clone(),
2996 output_format: None,
2997 vfs_budget: self.vfs_budget.clone(),
2998 watchdog: ec.watchdog.clone(),
2999 #[cfg(all(feature = "localfs", feature = "overlay"))]
3000 overlay_handle: self.overlay_handle.clone(),
3001 }
3002 }; {
3008 let mut ec = self.exec_ctx.write().await;
3009 ctx.stdin = ec.stdin.take();
3010 ctx.stdin_data = ec.stdin_data.take();
3011 ctx.stdin_data_rx = ec.stdin_data_rx.take();
3012 ctx.pipe_stdin = ec.pipe_stdin.take();
3013 ctx.pipe_stdout = ec.pipe_stdout.take();
3014 }
3015
3016 GlobalFlags::apply_from_args(&tool_args, &mut ctx);
3021
3022 let result = tool.execute(tool_args, &mut ctx).await;
3023
3024 {
3031 let mut scope = self.scope.write().await;
3032 *scope = ctx.scope.clone();
3033 }
3034 {
3035 let mut ec = self.exec_ctx.write().await;
3036 ec.cwd = ctx.cwd;
3037 ec.prev_cwd = ctx.prev_cwd;
3038 ec.aliases = ctx.aliases;
3039 ec.output_limit = ctx.output_limit.clone();
3044 ec.pipe_stdin = ctx.pipe_stdin.take();
3045 ec.pipe_stdout = ctx.pipe_stdout.take();
3046 }
3047
3048 let result = finalize_output(result, ctx.output_format, schema.owns_output);
3053
3054 Ok(result)
3055 }
3056
3057 async fn scope_home(&self) -> Option<String> {
3062 match self.scope.read().await.get("HOME") {
3063 Some(Value::String(s)) => Some(s.clone()),
3064 _ => None,
3065 }
3066 }
3067
3068 #[allow(clippy::too_many_arguments)]
3089 async fn consume_flag_positionals(
3090 &self,
3091 args: &[Arg],
3092 flag_name: &str,
3093 canonical: &str,
3094 consumes: usize,
3095 repeatable: bool,
3096 positional_indices: &[usize],
3097 consumed: &mut std::collections::HashSet<usize>,
3098 current_idx: usize,
3099 tool_args: &mut ToolArgs,
3100 ) -> Result<()> {
3101 let home = self.scope_home().await;
3102 let mut collected: Vec<Value> = Vec::with_capacity(consumes.max(1));
3103 for _ in 0..consumes.max(1) {
3104 let allow_word_assign = consumes <= 1;
3110 let next_pos = positional_indices
3111 .iter()
3112 .find(|idx| {
3113 **idx > current_idx
3114 && !consumed.contains(idx)
3115 && (allow_word_assign || matches!(args[**idx], Arg::Positional(_)))
3116 })
3117 .copied();
3118 match next_pos {
3119 Some(pos_idx) => match &args[pos_idx] {
3120 Arg::Positional(expr) => {
3121 let value = self.eval_expr_async(expr).await?;
3122 let value = apply_tilde_expansion(value, home.as_deref());
3123 collected.push(value);
3124 consumed.insert(pos_idx);
3125 }
3126 Arg::WordAssign { key, value } => {
3129 let val = self.eval_expr_async(value).await?;
3130 let val = apply_tilde_expansion(val, home.as_deref());
3131 let val_str = crate::interpreter::value_to_string(&val);
3132 collected.push(Value::String(format!("{key}={val_str}")));
3133 consumed.insert(pos_idx);
3134 }
3135 _ => {}
3136 },
3137 None => {
3138 if consumes <= 1 && collected.is_empty() {
3139 tool_args.flags.insert(flag_name.to_string());
3143 return Ok(());
3144 }
3145 anyhow::bail!(
3146 "--{flag_name} requires {consumes} argument{}, got {}",
3147 if consumes == 1 { "" } else { "s" },
3148 collected.len()
3149 );
3150 }
3151 }
3152 }
3153
3154 if consumes <= 1 {
3155 if let Some(v) = collected.pop() {
3156 if repeatable {
3157 push_repeatable_value(tool_args, flag_name, canonical, v)?;
3158 } else {
3159 tool_args.named.insert(canonical.to_string(), v);
3160 }
3161 }
3162 return Ok(());
3163 }
3164
3165 let occ: Vec<serde_json::Value> = collected
3167 .into_iter()
3168 .map(|v| crate::interpreter::value_to_json(&v))
3169 .collect();
3170 let entry = tool_args
3171 .named
3172 .entry(canonical.to_string())
3173 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
3174 if let Value::Json(serde_json::Value::Array(outer)) = entry {
3175 outer.push(serde_json::Value::Array(occ));
3176 } else {
3177 anyhow::bail!(
3178 "--{flag_name}: named[{canonical}] already holds a non-array value"
3179 );
3180 }
3181 Ok(())
3182 }
3183
3184 async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
3188 let mut tool_args = ToolArgs::new();
3189 let home = self.scope_home().await;
3190 let leaf = match schema {
3196 Some(s) => Some(select_leaf(s, args)?),
3197 None => None,
3198 };
3199 let mut param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
3206 if let Some(l) = leaf {
3207 param_lookup.extend(schema_param_lookup(l));
3208 }
3209 let accepts_word_assign = schema
3212 .map(|s| crate::tools::accepts_word_assign(s.name.as_str()))
3213 .unwrap_or(false);
3214
3215 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
3217 let mut past_double_dash = false;
3218
3219 let positional_indices: Vec<usize> = args.iter().enumerate()
3227 .filter_map(|(i, a)| {
3228 let consumable = matches!(a, Arg::Positional(_))
3229 || (!accepts_word_assign && matches!(a, Arg::WordAssign { .. }));
3230 consumable.then_some(i)
3231 })
3232 .collect();
3233
3234 let mut i = 0;
3235 while i < args.len() {
3236 match &args[i] {
3237 Arg::DoubleDash => {
3238 past_double_dash = true;
3239 }
3240 Arg::Positional(expr) => {
3241 if !consumed.contains(&i) {
3242 if let Expr::GlobPattern(pattern) = expr {
3244 let glob_enabled = {
3245 let scope = self.scope.read().await;
3246 scope.glob_enabled()
3247 };
3248 if glob_enabled {
3249 let (paths, cwd) = {
3250 let ctx = self.exec_ctx.read().await;
3251 let paths = ctx.expand_glob(pattern).await
3252 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3253 let cwd = ctx.resolve_path(".");
3254 (paths, cwd)
3255 };
3256 if paths.is_empty() {
3257 return Err(anyhow::anyhow!("no matches: {}", pattern));
3258 }
3259 for path in paths {
3260 let display = if !pattern.starts_with('/') {
3261 path.strip_prefix(&cwd)
3262 .unwrap_or(&path)
3263 .to_string_lossy().into_owned()
3264 } else {
3265 path.to_string_lossy().into_owned()
3266 };
3267 tool_args.positional.push(Value::String(display));
3268 }
3269 i += 1;
3270 continue;
3271 }
3272 }
3273 let value = self.eval_expr_async(expr).await?;
3274 let value = apply_tilde_expansion(value, home.as_deref());
3275 tool_args.positional.push(value);
3276 }
3277 }
3278 Arg::Named { key, value } => {
3279 let val = self.eval_expr_async(value).await?;
3280 let val = apply_tilde_expansion(val, home.as_deref());
3281 if let Some(&(canonical, _, _, true)) = param_lookup.get(key.as_str()) {
3287 push_repeatable_value(&mut tool_args, key, canonical, val)?;
3288 } else {
3289 tool_args.named.insert(key.clone(), val);
3290 }
3291 }
3292 Arg::WordAssign { key, value } => {
3293 if consumed.contains(&i) {
3296 i += 1;
3297 continue;
3298 }
3299 let val = self.eval_expr_async(value).await?;
3300 let val = apply_tilde_expansion(val, home.as_deref());
3301 if accepts_word_assign {
3302 tool_args.named.insert(key.clone(), val);
3303 } else {
3304 let val_str = crate::interpreter::value_to_string(&val);
3307 tool_args.positional.push(Value::String(format!("{key}={val_str}")));
3308 }
3309 }
3310 Arg::ShortFlag(name) => {
3311 if past_double_dash {
3312 tool_args.positional.push(Value::String(format!("-{name}")));
3313 } else if name.len() == 1 {
3314 let flag_name = name.as_str();
3315 let lookup = param_lookup.get(flag_name);
3316 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3317
3318 if is_bool {
3319 tool_args.flags.insert(flag_name.to_string());
3320 } else {
3321 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(flag_name);
3323 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3324 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3325 self.consume_flag_positionals(
3326 args,
3327 name,
3328 canonical,
3329 consumes,
3330 repeatable,
3331 &positional_indices,
3332 &mut consumed,
3333 i,
3334 &mut tool_args,
3335 )
3336 .await?;
3337 }
3338 } else if let Some(&(canonical, typ, consumes, repeatable)) = param_lookup.get(name.as_str()) {
3339 if is_bool_type(typ) {
3341 tool_args.flags.insert(canonical.to_string());
3342 } else {
3343 self.consume_flag_positionals(
3344 args,
3345 name,
3346 canonical,
3347 consumes,
3348 repeatable,
3349 &positional_indices,
3350 &mut consumed,
3351 i,
3352 &mut tool_args,
3353 )
3354 .await?;
3355 }
3356 } else if let Some(&(canonical, _, consumes, repeatable)) = param_lookup
3357 .get(&name[..1])
3358 .filter(|(_, typ, ..)| !is_bool_type(typ))
3359 {
3360 bind_glued_short_value(
3367 &mut tool_args,
3368 &name[..1],
3369 canonical,
3370 consumes,
3371 repeatable,
3372 name[1..].to_string(),
3373 )?;
3374 } else {
3375 let bytes = name.as_bytes();
3389 let mut p = 0;
3390 while p < bytes.len() {
3391 let key = &name[p..p + 1];
3392 match param_lookup.get(key) {
3393 Some(&(canonical, typ, consumes, repeatable))
3394 if !is_bool_type(typ) =>
3395 {
3396 let glued = name[p + 1..].to_string();
3397 if glued.is_empty() {
3398 self.consume_flag_positionals(
3402 args,
3403 key,
3404 canonical,
3405 consumes,
3406 repeatable,
3407 &positional_indices,
3408 &mut consumed,
3409 i,
3410 &mut tool_args,
3411 )
3412 .await?;
3413 } else {
3414 bind_glued_short_value(
3415 &mut tool_args,
3416 key,
3417 canonical,
3418 consumes,
3419 repeatable,
3420 glued,
3421 )?;
3422 }
3423 break;
3424 }
3425 _ => {
3426 tool_args.flags.insert(key.to_string());
3427 p += 1;
3428 }
3429 }
3430 }
3431 }
3432 }
3433 Arg::LongFlag(name) => {
3434 if past_double_dash {
3435 tool_args.positional.push(Value::String(format!("--{name}")));
3436 } else {
3437 let lookup = param_lookup.get(name.as_str());
3438 let ambiguous_value = (lookup.is_none()
3447 && leaf.is_some_and(|s| s.map_positionals)
3448 && !consumed.contains(&(i + 1)))
3449 .then(|| match args.get(i + 1) {
3450 Some(Arg::Positional(Expr::Literal(Value::String(s)))) => {
3453 Some(s.clone())
3454 }
3455 Some(Arg::Positional(_)) => Some("VALUE".to_string()),
3456 _ => None,
3457 })
3458 .flatten();
3459 if let Some(val) = ambiguous_value {
3460 let tool = leaf.map(|s| s.name.as_str()).unwrap_or("command");
3461 anyhow::bail!(
3462 "{tool}: --{name} is not a declared flag, so the \
3463 space-separated value would be silently dropped. \
3464 Use --{name}={val}, or have {tool} declare --{name} \
3465 in its schema."
3466 );
3467 }
3468 let is_bool = lookup.map(|(_, typ, ..)| is_bool_type(typ)).unwrap_or(true);
3469
3470 if is_bool {
3471 tool_args.flags.insert(name.clone());
3472 } else {
3473 let canonical = lookup.map(|(n, ..)| *n).unwrap_or(name.as_str());
3474 let consumes = lookup.map(|(_, _, c, _)| *c).unwrap_or(1);
3475 let repeatable = lookup.map(|(_, _, _, r)| *r).unwrap_or(false);
3476 self.consume_flag_positionals(
3477 args,
3478 name,
3479 canonical,
3480 consumes,
3481 repeatable,
3482 &positional_indices,
3483 &mut consumed,
3484 i,
3485 &mut tool_args,
3486 )
3487 .await?;
3488 }
3489 }
3490 }
3491 }
3492 i += 1;
3493 }
3494
3495 if let Some(schema) = leaf.filter(|s| s.map_positionals) {
3502 let pre_dash_count = if past_double_dash {
3503 let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
3504 positional_indices.iter()
3505 .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
3506 .count()
3507 } else {
3508 tool_args.positional.len()
3509 };
3510
3511 let mut remaining = Vec::new();
3512 let mut positional_iter = tool_args.positional.drain(..).enumerate();
3513
3514 for param in &schema.params {
3515 if tool_args.named.contains_key(¶m.name) || tool_args.flags.contains(¶m.name) {
3516 continue;
3517 }
3518 if is_bool_type(¶m.param_type) {
3519 continue;
3520 }
3521 loop {
3522 match positional_iter.next() {
3523 Some((idx, val)) if idx < pre_dash_count => {
3524 tool_args.named.insert(param.name.clone(), val);
3525 break;
3526 }
3527 Some((_, val)) => {
3528 remaining.push(val);
3529 }
3530 None => break,
3531 }
3532 }
3533 }
3534
3535 remaining.extend(positional_iter.map(|(_, v)| v));
3536 tool_args.positional = remaining;
3537 }
3538
3539 Ok(tool_args)
3540 }
3541
3542 #[cfg(feature = "subprocess")]
3552 async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
3553 let mut argv = Vec::new();
3554 let home = self.scope_home().await;
3555 for arg in args {
3556 match arg {
3557 Arg::Positional(expr) => {
3558 if let Expr::GlobPattern(pattern) = expr {
3560 let glob_enabled = {
3561 let scope = self.scope.read().await;
3562 scope.glob_enabled()
3563 };
3564 if glob_enabled {
3565 let (paths, cwd) = {
3566 let ctx = self.exec_ctx.read().await;
3567 let paths = ctx.expand_glob(pattern).await
3568 .map_err(|e| anyhow::anyhow!("glob: {}", e))?;
3569 let cwd = ctx.resolve_path(".");
3570 (paths, cwd)
3571 };
3572 if paths.is_empty() {
3573 return Err(anyhow::anyhow!("no matches: {}", pattern));
3574 }
3575 for path in paths {
3576 let display = if !pattern.starts_with('/') {
3577 path.strip_prefix(&cwd)
3578 .unwrap_or(&path)
3579 .to_string_lossy().into_owned()
3580 } else {
3581 path.to_string_lossy().into_owned()
3582 };
3583 argv.push(display);
3584 }
3585 continue;
3586 }
3587 }
3588 let value = self.eval_expr_async(expr).await?;
3589 let value = apply_tilde_expansion(value, home.as_deref());
3590 argv.push(value_to_string(&value));
3591 }
3592 Arg::Named { key, value } => {
3593 let val = self.eval_expr_async(value).await?;
3594 let val = apply_tilde_expansion(val, home.as_deref());
3595 argv.push(format!("--{}={}", key, value_to_string(&val)));
3596 }
3597 Arg::WordAssign { key, value } => {
3598 let val = self.eval_expr_async(value).await?;
3599 let val = apply_tilde_expansion(val, home.as_deref());
3600 argv.push(format!("{}={}", key, value_to_string(&val)));
3601 }
3602 Arg::ShortFlag(name) => {
3603 argv.push(format!("-{}", name));
3605 }
3606 Arg::LongFlag(name) => {
3607 argv.push(format!("--{}", name));
3609 }
3610 Arg::DoubleDash => {
3611 argv.push("--".to_string());
3613 }
3614 }
3615 }
3616 Ok(argv)
3617 }
3618
3619 fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
3624 Box::pin(async move {
3625 match expr {
3626 Expr::Literal(value) => Ok(value.clone()),
3627 Expr::VarRef(path) => {
3628 let scope = self.scope.read().await;
3629 scope.resolve_path(path)
3630 .ok_or_else(|| anyhow::anyhow!("undefined variable"))
3631 }
3632 Expr::Interpolated(parts) => {
3633 let mut result = String::new();
3634 for part in parts {
3635 result.push_str(&self.eval_string_part_async(part).await?);
3636 }
3637 Ok(Value::String(result))
3638 }
3639 Expr::HereDocBody { parts, strip_tabs } => {
3640 let mut asm = crate::interpreter::HeredocAssembler::new(*strip_tabs);
3644 for sp in parts {
3645 match &sp.part {
3646 StringPart::Literal(s) => asm.push_literal(s),
3647 other => {
3648 asm.push_interpolated(&self.eval_string_part_async(other).await?)
3649 }
3650 }
3651 }
3652 Ok(Value::String(asm.into_string()))
3653 }
3654 Expr::BinaryOp { left, op, right } => match op {
3655 BinaryOp::And => {
3656 let left_val = self.eval_expr_async(left).await?;
3657 if !is_truthy(&left_val) {
3658 return Ok(left_val);
3659 }
3660 self.eval_expr_async(right).await
3661 }
3662 BinaryOp::Or => {
3663 let left_val = self.eval_expr_async(left).await?;
3664 if is_truthy(&left_val) {
3665 return Ok(left_val);
3666 }
3667 self.eval_expr_async(right).await
3668 }
3669 },
3670 Expr::CommandSubst(stmts) => {
3671 let saved_scope = { self.scope.read().await.clone() };
3674 let saved_cwd = {
3675 let ec = self.exec_ctx.read().await;
3676 (ec.cwd.clone(), ec.prev_cwd.clone())
3677 };
3678
3679 let run_result = self.execute_block_capturing(stmts).await;
3681
3682 {
3684 let mut scope = self.scope.write().await;
3685 *scope = saved_scope;
3686 if let Ok(ref r) = run_result {
3687 scope.set_last_result(r.clone());
3688 }
3689 }
3690 {
3691 let mut ec = self.exec_ctx.write().await;
3692 ec.cwd = saved_cwd.0;
3693 ec.prev_cwd = saved_cwd.1;
3694 }
3695
3696 let result = run_result?;
3698
3699 if let Some(bytes) = result.out_bytes() {
3702 Ok(Value::Bytes(bytes.to_vec()))
3703 } else if let Some(data) = &result.data {
3705 Ok(data.clone())
3706 } else if let Some(output) = result.output() {
3707 if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
3709 let items: Vec<serde_json::Value> = output.root.iter()
3710 .map(|n| serde_json::Value::String(n.display_name().to_string()))
3711 .collect();
3712 Ok(Value::Json(serde_json::Value::Array(items)))
3713 } else {
3714 Ok(Value::String(
3721 result.text_out().trim_end_matches('\n').to_string(),
3722 ))
3723 }
3724 } else {
3725 Ok(Value::String(
3727 result.text_out().trim_end_matches('\n').to_string(),
3728 ))
3729 }
3730 }
3731 Expr::Test(test_expr) => {
3732 Ok(Value::Bool(self.eval_test_async(test_expr).await?))
3733 }
3734 Expr::Positional(n) => {
3735 let scope = self.scope.read().await;
3736 match scope.get_positional(*n) {
3737 Some(s) => Ok(Value::String(s.to_string())),
3738 None => Ok(Value::String(String::new())),
3739 }
3740 }
3741 Expr::AllArgs => {
3742 let scope = self.scope.read().await;
3743 Ok(Value::String(scope.all_args().join(" ")))
3744 }
3745 Expr::ArgCount => {
3746 let scope = self.scope.read().await;
3747 Ok(Value::Int(scope.arg_count() as i64))
3748 }
3749 Expr::VarLength(name) => {
3750 let scope = self.scope.read().await;
3751 match scope.get(name) {
3752 Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
3753 None => Ok(Value::Int(0)),
3754 }
3755 }
3756 Expr::VarWithDefault { name, default } => {
3757 let scope = self.scope.read().await;
3758 let use_default = match scope.get(name) {
3759 Some(value) => value_to_string(value).is_empty(),
3760 None => true,
3761 };
3762 drop(scope); if use_default {
3764 self.eval_string_parts_async(default).await.map(Value::String)
3766 } else {
3767 let scope = self.scope.read().await;
3768 scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
3769 }
3770 }
3771 Expr::Arithmetic(expr_str) => {
3772 let scope = self.scope.read().await;
3773 crate::arithmetic::eval_arithmetic(expr_str, &scope)
3774 .map(Value::Int)
3775 .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
3776 }
3777 Expr::Command(cmd) => {
3778 let result = self.execute_command(&cmd.name, &cmd.args).await?;
3780 Ok(Value::Bool(result.code == 0))
3781 }
3782 Expr::LastExitCode => {
3783 let scope = self.scope.read().await;
3784 Ok(Value::Int(scope.last_result().code))
3785 }
3786 Expr::CurrentPid => {
3787 let scope = self.scope.read().await;
3788 Ok(Value::Int(scope.pid() as i64))
3789 }
3790 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
3791 }
3792 })
3793 }
3794
3795 fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
3797 Box::pin(async move {
3798 let mut result = String::new();
3799 for part in parts {
3800 result.push_str(&self.eval_string_part_async(part).await?);
3801 }
3802 Ok(result)
3803 })
3804 }
3805
3806 fn eval_test_async<'a>(&'a self, test_expr: &'a TestExpr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
3810 Box::pin(async move {
3811 match test_expr {
3812 TestExpr::FileTest { op, path } => {
3813 let path_value = self.eval_expr_async(path).await?;
3814 let home = self.scope_home().await;
3818 let path_value = apply_tilde_expansion(path_value, home.as_deref());
3819 let path_str = value_to_string(&path_value);
3820 let backend = self.exec_ctx.read().await.backend.clone();
3821 let entry = backend.stat(std::path::Path::new(&path_str)).await.ok();
3822 Ok(match op {
3823 FileTestOp::Exists => entry.is_some(),
3824 FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
3825 FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
3826 FileTestOp::Readable => entry.is_some(),
3827 FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
3828 e.permissions.is_none_or(|p| p & 0o222 != 0)
3829 }),
3830 FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
3831 e.permissions.is_some_and(|p| p & 0o111 != 0)
3832 }),
3833 })
3834 }
3835 TestExpr::StringTest { op, value } => {
3836 let val = self.eval_expr_async(value).await?;
3837 let s = value_to_string(&val);
3838 Ok(match op {
3839 crate::ast::StringTestOp::IsEmpty => s.is_empty(),
3840 crate::ast::StringTestOp::IsNonEmpty => !s.is_empty(),
3841 })
3842 }
3843 TestExpr::Comparison { left, op, right } => {
3844 let left_val = self.eval_expr_async(left).await?;
3846 let right_val = self.eval_expr_async(right).await?;
3847 let resolved = TestExpr::Comparison {
3848 left: Box::new(Expr::Literal(left_val)),
3849 op: *op,
3850 right: Box::new(Expr::Literal(right_val)),
3851 };
3852 let expr = Expr::Test(Box::new(resolved));
3853 let mut scope = self.scope.write().await;
3854 let value = eval_expr(&expr, &mut scope)
3855 .map_err(|e| anyhow::anyhow!("{}", e))?;
3856 Ok(value_to_bool(&value))
3857 }
3858 TestExpr::And { left, right } => {
3859 if !self.eval_test_async(left).await? {
3860 Ok(false)
3861 } else {
3862 self.eval_test_async(right).await
3863 }
3864 }
3865 TestExpr::Or { left, right } => {
3866 if self.eval_test_async(left).await? {
3867 Ok(true)
3868 } else {
3869 self.eval_test_async(right).await
3870 }
3871 }
3872 TestExpr::Not { expr } => {
3873 Ok(!self.eval_test_async(expr).await?)
3874 }
3875 }
3876 })
3877 }
3878
3879 fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
3880 Box::pin(async move {
3881 match part {
3882 StringPart::Literal(s) => Ok(s.clone()),
3883 StringPart::Var(path) => {
3884 let scope = self.scope.read().await;
3885 match scope.resolve_path(path) {
3886 Some(value) => Ok(value_to_string(&value)),
3887 None => Ok(String::new()), }
3889 }
3890 StringPart::VarWithDefault { name, default } => {
3891 let scope = self.scope.read().await;
3892 let use_default = match scope.get(name) {
3893 Some(value) => value_to_string(value).is_empty(),
3894 None => true,
3895 };
3896 drop(scope); if use_default {
3898 self.eval_string_parts_async(default).await
3900 } else {
3901 let scope = self.scope.read().await;
3902 Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
3903 }
3904 }
3905 StringPart::VarLength(name) => {
3906 let scope = self.scope.read().await;
3907 match scope.get(name) {
3908 Some(value) => Ok(value_to_string(value).len().to_string()),
3909 None => Ok("0".to_string()),
3910 }
3911 }
3912 StringPart::Positional(n) => {
3913 let scope = self.scope.read().await;
3914 match scope.get_positional(*n) {
3915 Some(s) => Ok(s.to_string()),
3916 None => Ok(String::new()),
3917 }
3918 }
3919 StringPart::AllArgs => {
3920 let scope = self.scope.read().await;
3921 Ok(scope.all_args().join(" "))
3922 }
3923 StringPart::ArgCount => {
3924 let scope = self.scope.read().await;
3925 Ok(scope.arg_count().to_string())
3926 }
3927 StringPart::Arithmetic(expr) => {
3928 let scope = self.scope.read().await;
3929 match crate::arithmetic::eval_arithmetic(expr, &scope) {
3930 Ok(value) => Ok(value.to_string()),
3931 Err(_) => Ok(String::new()),
3932 }
3933 }
3934 StringPart::CommandSubst(stmts) => {
3935 let saved_scope = { self.scope.read().await.clone() };
3938 let saved_cwd = {
3939 let ec = self.exec_ctx.read().await;
3940 (ec.cwd.clone(), ec.prev_cwd.clone())
3941 };
3942
3943 let run_result = self.execute_block_capturing(stmts).await;
3945
3946 {
3948 let mut scope = self.scope.write().await;
3949 *scope = saved_scope;
3950 if let Ok(ref r) = run_result {
3951 scope.set_last_result(r.clone());
3952 }
3953 }
3954 {
3955 let mut ec = self.exec_ctx.write().await;
3956 ec.cwd = saved_cwd.0;
3957 ec.prev_cwd = saved_cwd.1;
3958 }
3959
3960 let result = run_result?;
3962
3963 match result.try_text_out() {
3966 Ok(s) => Ok(s.trim_end_matches('\n').to_string()),
3967 Err(e) => anyhow::bail!(
3968 "command substitution in a string produced binary data ({e}) — \
3969 pipe through base64/xxd"
3970 ),
3971 }
3972 }
3973 StringPart::LastExitCode => {
3974 let scope = self.scope.read().await;
3975 Ok(scope.last_result().code.to_string())
3976 }
3977 StringPart::CurrentPid => {
3978 let scope = self.scope.read().await;
3979 Ok(scope.pid().to_string())
3980 }
3981 }
3982 })
3983 }
3984
3985 async fn update_last_result(&self, result: &ExecResult) {
3987 let mut scope = self.scope.write().await;
3988 scope.set_last_result(result.clone());
3989 }
3990
3991 async fn drain_stderr_into(&self, result: &mut ExecResult) {
3997 let drained = {
3998 let mut receiver = self.stderr_receiver.lock().await;
3999 receiver.drain_lossy()
4000 };
4001 if !drained.is_empty() {
4002 if !result.err.is_empty() && !result.err.ends_with('\n') {
4003 result.err.push('\n');
4004 }
4005 result.err.push_str(&drained);
4006 }
4007 }
4008
4009 async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
4015 let tool_args = self.build_args_async(args, None).await?;
4017
4018 {
4020 let mut scope = self.scope.write().await;
4021 scope.push_frame();
4022 }
4023
4024 let saved_positional = {
4026 let mut scope = self.scope.write().await;
4027 let saved = scope.save_positional();
4028
4029 let positional_args: Vec<String> = tool_args.positional
4031 .iter()
4032 .map(value_to_string)
4033 .collect();
4034 scope.set_positional(&def.name, positional_args);
4035
4036 saved
4037 };
4038
4039 let mut accumulated_out: Vec<u8> = Vec::new();
4044 let mut accumulated_err = String::new();
4045 let mut last_code = 0i64;
4046 let mut last_data: Option<Value> = None;
4047
4048 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
4049 match r.out_bytes() {
4050 Some(b) => buf.extend_from_slice(b),
4051 None => buf.extend_from_slice(r.text_out().as_bytes()),
4052 }
4053 }
4054
4055 let mut exec_error: Option<anyhow::Error> = None;
4057 let mut exit_code: Option<i64> = None;
4058
4059 for stmt in &def.body {
4060 match self.execute_stmt_flow(stmt).await {
4061 Ok(flow) => {
4062 let drained = {
4064 let mut receiver = self.stderr_receiver.lock().await;
4065 receiver.drain_lossy()
4066 };
4067 if !drained.is_empty() {
4068 accumulated_err.push_str(&drained);
4069 }
4070
4071 match flow {
4072 ControlFlow::Normal(r) => {
4073 push_out(&mut accumulated_out, &r);
4074 accumulated_err.push_str(&r.err);
4075 last_code = r.code;
4076 last_data = r.data;
4077 }
4078 ControlFlow::Return { value } => {
4079 push_out(&mut accumulated_out, &value);
4080 accumulated_err.push_str(&value.err);
4081 last_code = value.code;
4082 last_data = value.data;
4083 break;
4084 }
4085 ControlFlow::Exit { code } => {
4086 exit_code = Some(code);
4087 break;
4088 }
4089 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
4090 push_out(&mut accumulated_out, &r);
4091 accumulated_err.push_str(&r.err);
4092 last_code = r.code;
4093 last_data = r.data;
4094 }
4095 }
4096 }
4097 Err(e) => {
4098 exec_error = Some(e);
4099 break;
4100 }
4101 }
4102 }
4103
4104 {
4106 let mut scope = self.scope.write().await;
4107 scope.pop_frame();
4108 scope.set_positional(saved_positional.0, saved_positional.1);
4109 }
4110
4111 if let Some(e) = exec_error {
4113 return Err(e);
4114 }
4115 let code = exit_code.unwrap_or(last_code);
4116 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(code);
4117 result.err = accumulated_err;
4118 result.data = last_data;
4119 Ok(result)
4120 }
4121
4122 async fn execute_block_capturing(&self, stmts: &[Stmt]) -> Result<ExecResult> {
4130 let mut accumulated_out: Vec<u8> = Vec::new();
4134 let mut accumulated_err = String::new();
4135 let mut last_code = 0i64;
4136 let mut last_data: Option<Value> = None;
4137
4138 fn push_out(buf: &mut Vec<u8>, r: &ExecResult) {
4140 match r.out_bytes() {
4141 Some(b) => buf.extend_from_slice(b),
4142 None => buf.extend_from_slice(r.text_out().as_bytes()),
4143 }
4144 }
4145
4146 for stmt in stmts {
4147 let flow = self.execute_stmt_flow(stmt).await?;
4148
4149 let drained = {
4152 let mut receiver = self.stderr_receiver.lock().await;
4153 receiver.drain_lossy()
4154 };
4155 if !drained.is_empty() {
4156 accumulated_err.push_str(&drained);
4157 }
4158
4159 match flow {
4160 ControlFlow::Normal(r)
4161 | ControlFlow::Break { result: r, .. }
4162 | ControlFlow::Continue { result: r, .. } => {
4163 push_out(&mut accumulated_out, &r);
4164 accumulated_err.push_str(&r.err);
4165 last_code = r.code;
4166 last_data = r.data;
4167 }
4168 ControlFlow::Return { value } => {
4169 push_out(&mut accumulated_out, &value);
4170 accumulated_err.push_str(&value.err);
4171 last_code = value.code;
4172 last_data = value.data;
4173 break;
4174 }
4175 ControlFlow::Exit { code } => {
4176 last_code = code;
4177 break;
4178 }
4179 }
4180 }
4181
4182 let mut result = ExecResult::success_text_or_bytes(accumulated_out).with_code(last_code);
4183 result.err = accumulated_err;
4184 result.data = last_data;
4185 Ok(result)
4186 }
4187
4188 async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
4193 let tool_args = self.build_args_async(args, None).await?;
4195 let path = match tool_args.positional.first() {
4196 Some(Value::String(s)) => s.clone(),
4197 Some(v) => value_to_string(v),
4198 None => {
4199 return Ok(ExecResult::failure(1, "source: missing filename"));
4200 }
4201 };
4202
4203 let full_path = {
4205 let ctx = self.exec_ctx.read().await;
4206 if path.starts_with('/') {
4207 std::path::PathBuf::from(&path)
4208 } else {
4209 ctx.cwd.join(&path)
4210 }
4211 };
4212
4213 let content = {
4215 let ctx = self.exec_ctx.read().await;
4216 match ctx.backend.read(&full_path, None).await {
4217 Ok(bytes) => {
4218 String::from_utf8(bytes).map_err(|e| {
4219 anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
4220 })?
4221 }
4222 Err(e) => {
4223 return Ok(ExecResult::failure(
4224 1,
4225 format!("source: {}: {}", path, e),
4226 ));
4227 }
4228 }
4229 };
4230
4231 let program = match crate::parser::parse(&content) {
4233 Ok(p) => p,
4234 Err(errors) => {
4235 let msg = errors
4236 .iter()
4237 .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
4238 .collect::<Vec<_>>()
4239 .join("\n");
4240 return Ok(ExecResult::failure(1, format!("source: {}", msg)));
4241 }
4242 };
4243
4244 let mut result = ExecResult::success("");
4246 for stmt in program.statements {
4247 if matches!(stmt, crate::ast::Stmt::Empty) {
4248 continue;
4249 }
4250
4251 match self.execute_stmt_flow(&stmt).await {
4252 Ok(flow) => {
4253 self.drain_stderr_into(&mut result).await;
4254 match flow {
4255 ControlFlow::Normal(r) => {
4256 result = r.clone();
4257 self.update_last_result(&r).await;
4258 }
4259 ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
4260 return Err(anyhow::anyhow!(
4261 "source: {}: unexpected break/continue outside loop",
4262 path
4263 ));
4264 }
4265 ControlFlow::Return { value } => {
4266 return Ok(value);
4267 }
4268 ControlFlow::Exit { code } => {
4269 result.code = code;
4270 return Ok(result);
4271 }
4272 }
4273 }
4274 Err(e) => {
4275 return Err(e.context(format!("source: {}", path)));
4276 }
4277 }
4278 }
4279
4280 Ok(result)
4281 }
4282
4283 async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4288 let path_value = {
4290 let scope = self.scope.read().await;
4291 scope
4292 .get("PATH")
4293 .map(value_to_string)
4294 .unwrap_or_else(|| "/bin".to_string())
4295 };
4296
4297 for dir in path_value.split(':') {
4299 if dir.is_empty() {
4300 continue;
4301 }
4302
4303 let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
4305
4306 let exists = {
4308 let ctx = self.exec_ctx.read().await;
4309 ctx.backend.exists(&script_path).await
4310 };
4311
4312 if !exists {
4313 continue;
4314 }
4315
4316 let content = {
4318 let ctx = self.exec_ctx.read().await;
4319 match ctx.backend.read(&script_path, None).await {
4320 Ok(bytes) => match String::from_utf8(bytes) {
4321 Ok(s) => s,
4322 Err(e) => {
4323 return Ok(Some(ExecResult::failure(
4324 1,
4325 format!("{}: invalid UTF-8: {}", script_path.display(), e),
4326 )));
4327 }
4328 },
4329 Err(e) => {
4330 return Ok(Some(ExecResult::failure(
4331 1,
4332 format!("{}: {}", script_path.display(), e),
4333 )));
4334 }
4335 }
4336 };
4337
4338 let program = match crate::parser::parse(&content) {
4340 Ok(p) => p,
4341 Err(errors) => {
4342 let msg = errors
4343 .iter()
4344 .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
4345 .collect::<Vec<_>>()
4346 .join("\n");
4347 return Ok(Some(ExecResult::failure(1, msg)));
4348 }
4349 };
4350
4351 let tool_args = self.build_args_async(args, None).await?;
4353
4354 let mut isolated_scope = Scope::new();
4356
4357 let positional_args: Vec<String> = tool_args.positional
4359 .iter()
4360 .map(value_to_string)
4361 .collect();
4362 isolated_scope.set_positional(name, positional_args);
4363
4364 let original_scope = {
4366 let mut scope = self.scope.write().await;
4367 std::mem::replace(&mut *scope, isolated_scope)
4368 };
4369
4370 let mut result = ExecResult::success("");
4372 let mut exec_error: Option<anyhow::Error> = None;
4373 let mut exit_code: Option<i64> = None;
4374
4375 for stmt in program.statements {
4376 if matches!(stmt, crate::ast::Stmt::Empty) {
4377 continue;
4378 }
4379
4380 match self.execute_stmt_flow(&stmt).await {
4381 Ok(flow) => {
4382 match flow {
4383 ControlFlow::Normal(r) => result = r,
4384 ControlFlow::Return { value } => {
4385 result = value;
4386 break;
4387 }
4388 ControlFlow::Exit { code } => {
4389 exit_code = Some(code);
4390 break;
4391 }
4392 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
4393 result = r;
4394 }
4395 }
4396 }
4397 Err(e) => {
4398 exec_error = Some(e);
4399 break;
4400 }
4401 }
4402 }
4403
4404 {
4406 let mut scope = self.scope.write().await;
4407 *scope = original_scope;
4408 }
4409
4410 if let Some(e) = exec_error {
4412 return Err(e.context(format!("script: {}", script_path.display())));
4413 }
4414 if let Some(code) = exit_code {
4415 result.code = code;
4416 return Ok(Some(result));
4417 }
4418
4419 return Ok(Some(result));
4420 }
4421
4422 Ok(None)
4424 }
4425
4426 #[cfg(not(feature = "subprocess"))]
4440 async fn try_execute_external(&self, _name: &str, _args: &[Arg]) -> Result<Option<ExecResult>> {
4441 Ok(None)
4442 }
4443
4444 #[cfg(feature = "subprocess")]
4446 #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
4447 async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
4448 let cancel = {
4454 let ec = self.exec_ctx.read().await;
4455 ec.cancel.clone()
4456 };
4457 let kill_grace = self.kill_grace;
4458 if !self.allow_external_commands {
4459 return Ok(None);
4460 }
4461
4462 let real_cwd = {
4467 let ctx = self.exec_ctx.read().await;
4468 match ctx.backend.resolve_real_path(&ctx.cwd) {
4469 Some(p) => p,
4470 None => return Ok(None),
4471 }
4472 };
4473
4474 let executable = if name.contains('/') {
4475 let resolved = if std::path::Path::new(name).is_absolute() {
4477 std::path::PathBuf::from(name)
4478 } else {
4479 real_cwd.join(name)
4480 };
4481 if !resolved.exists() {
4482 return Ok(Some(ExecResult::failure(
4483 127,
4484 format!("{}: No such file or directory", name),
4485 )));
4486 }
4487 if !resolved.is_file() {
4488 return Ok(Some(ExecResult::failure(
4489 126,
4490 format!("{}: Is a directory", name),
4491 )));
4492 }
4493 #[cfg(unix)]
4494 {
4495 use std::os::unix::fs::PermissionsExt;
4496 let mode = std::fs::metadata(&resolved)
4497 .map(|m| m.permissions().mode())
4498 .unwrap_or(0);
4499 if mode & 0o111 == 0 {
4500 return Ok(Some(ExecResult::failure(
4501 126,
4502 format!("{}: Permission denied", name),
4503 )));
4504 }
4505 }
4506 resolved.to_string_lossy().into_owned()
4507 } else {
4508 let path_var = {
4512 let scope = self.scope.read().await;
4513 scope.get("PATH").map(value_to_string).unwrap_or_default()
4514 };
4515
4516 match resolve_in_path(name, &path_var) {
4518 Some(path) => path,
4519 None => return Ok(None), }
4521 };
4522
4523 tracing::debug!(executable = %executable, "resolved external command");
4524
4525 let argv = self.build_args_flat(args).await?;
4527
4528 let (pipe_stdin, stdin_string) = {
4537 let mut ctx = self.exec_ctx.write().await;
4538 (ctx.pipe_stdin.take(), ctx.take_stdin())
4539 };
4540 let has_stdin = pipe_stdin.is_some() || stdin_string.is_some();
4541
4542 use tokio::process::Command;
4544
4545 let mut cmd = Command::new(&executable);
4546 cmd.args(&argv);
4547 cmd.current_dir(&real_cwd);
4548
4549 cmd.env_clear();
4553 {
4554 let scope = self.scope.read().await;
4555 for (var_name, value) in scope.exported_vars() {
4556 cmd.env(var_name, value_to_string(&value));
4557 }
4558 }
4559
4560 cmd.stdin(if has_stdin {
4562 std::process::Stdio::piped()
4563 } else if self.interactive {
4564 std::process::Stdio::inherit()
4565 } else {
4566 std::process::Stdio::null()
4567 });
4568
4569 let pipeline_position = {
4573 let ctx = self.exec_ctx.read().await;
4574 ctx.pipeline_position
4575 };
4576 let inherit_output = self.interactive
4577 && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
4578
4579 if inherit_output {
4580 cmd.stdout(std::process::Stdio::inherit());
4581 cmd.stderr(std::process::Stdio::inherit());
4582 } else {
4583 cmd.stdout(std::process::Stdio::piped());
4584 cmd.stderr(std::process::Stdio::piped());
4585 }
4586
4587 #[cfg(unix)]
4593 {
4594 let restore_jc_signals = self.terminal_state.is_some() && inherit_output;
4595 #[allow(unsafe_code)]
4597 unsafe {
4598 cmd.pre_exec(move || {
4599 nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
4601 .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
4602 if restore_jc_signals {
4603 use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
4604 let mut sa: nix::libc::sigaction = std::mem::zeroed();
4605 sa.sa_sigaction = SIG_DFL;
4606 if sigaction(SIGTSTP, &sa, std::ptr::null_mut()) != 0 {
4607 return Err(std::io::Error::last_os_error());
4608 }
4609 if sigaction(SIGTTOU, &sa, std::ptr::null_mut()) != 0 {
4610 return Err(std::io::Error::last_os_error());
4611 }
4612 if sigaction(SIGTTIN, &sa, std::ptr::null_mut()) != 0 {
4613 return Err(std::io::Error::last_os_error());
4614 }
4615 if sigaction(SIGINT, &sa, std::ptr::null_mut()) != 0 {
4616 return Err(std::io::Error::last_os_error());
4617 }
4618 }
4619 Ok(())
4620 });
4621 }
4622 }
4623
4624 let in_jc_inherit_path = inherit_output && self.terminal_state.is_some();
4631 if !in_jc_inherit_path {
4632 cmd.kill_on_drop(true);
4633 }
4634
4635 let mut child = match cmd.spawn() {
4640 Ok(child) => child,
4641 Err(e) => {
4642 return Ok(Some(ExecResult::failure(
4643 127,
4644 format!("{}: {}", name, e),
4645 )));
4646 }
4647 };
4648 let kill_target = crate::pidfd::KillTarget::from_child(&child);
4649
4650 if let Some(job_id) = self.bg_job_id
4655 && let Some(pid) = child.id()
4656 {
4657 self.jobs.add_pgid(job_id, pid).await;
4658 }
4659
4660 let stdin_task: Option<tokio::task::JoinHandle<()>> = if let Some(mut pipe_in) = pipe_stdin {
4667 child.stdin.take().map(|mut child_stdin| {
4668 tokio::spawn(async move {
4669 use tokio::io::{AsyncReadExt, AsyncWriteExt};
4670 let mut buf = [0u8; 8192];
4671 loop {
4672 match pipe_in.read(&mut buf).await {
4673 Ok(0) => break, Ok(n) => {
4675 if child_stdin.write_all(&buf[..n]).await.is_err() {
4676 break; }
4678 }
4679 Err(_) => break,
4680 }
4681 }
4682 })
4684 })
4685 } else if let Some(data) = stdin_string {
4686 child.stdin.take().map(|mut child_stdin| {
4694 tokio::spawn(async move {
4695 use tokio::io::AsyncWriteExt;
4696 let _ = child_stdin.write_all(data.as_bytes()).await;
4697 })
4698 })
4699 } else {
4700 None
4701 };
4702
4703 struct AbortStdinCopyOnDrop(Option<tokio::task::JoinHandle<()>>);
4711 impl Drop for AbortStdinCopyOnDrop {
4712 fn drop(&mut self) {
4713 if let Some(t) = self.0.take() {
4714 t.abort();
4715 }
4716 }
4717 }
4718 let _stdin_copy_guard = AbortStdinCopyOnDrop(stdin_task);
4719
4720 if inherit_output {
4721 #[cfg(unix)]
4723 if let Some(ref term) = self.terminal_state {
4724 let child_id = child.id().unwrap_or(0);
4725 let pid = nix::unistd::Pid::from_raw(child_id as i32);
4726 let pgid = pid; if let Err(e) = term.give_terminal_to(pgid) {
4730 tracing::warn!("failed to give terminal to child: {}", e);
4731 }
4732
4733 let term_clone = term.clone();
4734 let cmd_name = name.to_string();
4735 let cmd_display = format!("{} {}", name, argv.join(" "));
4736 let jobs = self.jobs.clone();
4737
4738 let wait_complete = std::sync::Arc::new(
4752 std::sync::atomic::AtomicBool::new(false)
4753 );
4754 let cancel_watcher = {
4755 let cancel = cancel.clone();
4756 let wc = wait_complete.clone();
4757 let target = kill_target.as_ref().map(|t| {
4765 crate::pidfd::KillTarget::from_pid(t.pid())
4777 });
4778 tokio::spawn(async move {
4779 cancel.cancelled().await;
4780 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
4781 use nix::sys::signal::Signal;
4782 if let Some(t) = &target {
4783 t.signal(Signal::SIGTERM);
4784 t.signal_pg(Signal::SIGTERM);
4785 } else {
4786 let _ = nix::sys::signal::kill(pid, Signal::SIGTERM);
4787 let _ = nix::sys::signal::killpg(pid, Signal::SIGTERM);
4788 }
4789 if kill_grace > Duration::ZERO {
4790 tokio::time::sleep(kill_grace).await;
4791 if wc.load(std::sync::atomic::Ordering::SeqCst) { return; }
4792 }
4793 if let Some(t) = &target {
4794 t.signal(Signal::SIGKILL);
4795 t.signal_pg(Signal::SIGKILL);
4796 } else {
4797 let _ = nix::sys::signal::kill(pid, Signal::SIGKILL);
4798 let _ = nix::sys::signal::killpg(pid, Signal::SIGKILL);
4799 }
4800 })
4801 };
4802 struct AbortOnDrop(tokio::task::JoinHandle<()>);
4803 impl Drop for AbortOnDrop {
4804 fn drop(&mut self) {
4805 self.0.abort();
4806 }
4807 }
4808 let _watcher_guard = AbortOnDrop(cancel_watcher);
4809
4810 let wait_complete_setter = wait_complete.clone();
4811 let code = tokio::task::block_in_place(move || {
4812 let result = term_clone.wait_for_foreground(pid);
4813 wait_complete_setter.store(true, std::sync::atomic::Ordering::SeqCst);
4815
4816 if let Err(e) = term_clone.reclaim_terminal() {
4818 tracing::warn!("failed to reclaim terminal: {}", e);
4819 }
4820
4821 match result {
4822 crate::terminal::WaitResult::Exited(code) => code as i64,
4823 crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
4824 crate::terminal::WaitResult::Stopped(_sig) => {
4825 let rt = tokio::runtime::Handle::current();
4827 let job_id = rt.block_on(jobs.register_stopped(
4828 cmd_display,
4829 child_id,
4830 child_id, ));
4832 eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
4833 148 }
4835 }
4836 });
4837
4838 return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
4839 }
4840
4841 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
4843 Ok(s) => s,
4844 Err(e) => {
4845 return Ok(Some(ExecResult::failure(
4846 1,
4847 format!("{}: failed to wait: {}", name, e),
4848 )));
4849 }
4850 };
4851
4852 let code = status.code().unwrap_or_else(|| {
4853 #[cfg(unix)]
4854 {
4855 use std::os::unix::process::ExitStatusExt;
4856 128 + status.signal().unwrap_or(0)
4857 }
4858 #[cfg(not(unix))]
4859 {
4860 -1
4861 }
4862 }) as i64;
4863
4864 Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
4866 } else {
4867 let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
4869 let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
4870
4871 let stdout_pipe = child.stdout.take();
4872 let stderr_pipe = child.stderr.take();
4873
4874 let stdout_clone = stdout_stream.clone();
4875 let stderr_clone = stderr_stream.clone();
4876
4877 let stdout_task = stdout_pipe.map(|pipe| {
4878 tokio::spawn(async move {
4879 drain_to_stream(pipe, stdout_clone).await;
4880 })
4881 });
4882
4883 let stderr_task = stderr_pipe.map(|pipe| {
4884 tokio::spawn(async move {
4885 drain_to_stream(pipe, stderr_clone).await;
4886 })
4887 });
4888
4889 let cancelled_before_wait = cancel.is_cancelled();
4890 let status = match wait_or_kill(&mut child, kill_target.as_ref(), &cancel, kill_grace).await {
4891 Ok(s) => s,
4892 Err(e) => {
4893 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
4895 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
4896 return Ok(Some(ExecResult::failure(
4897 1,
4898 format!("{}: failed to wait: {}", name, e),
4899 )));
4900 }
4901 };
4902
4903 if cancelled_before_wait || cancel.is_cancelled() {
4907 if let Some(task) = stdout_task { task.abort(); let _ = task.await; }
4908 if let Some(task) = stderr_task { task.abort(); let _ = task.await; }
4909 } else {
4910 if let Some(task) = stdout_task {
4911 let _ = task.await;
4913 }
4914 if let Some(task) = stderr_task {
4915 let _ = task.await;
4916 }
4917 }
4918
4919 let code = status.code().unwrap_or_else(|| {
4920 #[cfg(unix)]
4921 {
4922 use std::os::unix::process::ExitStatusExt;
4923 128 + status.signal().unwrap_or(0)
4924 }
4925 #[cfg(not(unix))]
4926 {
4927 -1
4928 }
4929 }) as i64;
4930
4931 let stdout = stdout_stream.read().await;
4935 let stderr = stderr_stream.read_string().await;
4936 let mut result = ExecResult::success_text_or_bytes(stdout).with_code(code);
4937 result.err = stderr;
4938 Ok(Some(result))
4939 }
4940 }
4941
4942 pub async fn get_var(&self, name: &str) -> Option<Value> {
4946 let scope = self.scope.read().await;
4947 scope.get(name).cloned()
4948 }
4949
4950 #[cfg(test)]
4952 pub async fn error_exit_enabled(&self) -> bool {
4953 let scope = self.scope.read().await;
4954 scope.error_exit_enabled()
4955 }
4956
4957 pub async fn set_var(&self, name: &str, value: Value) {
4959 let mut scope = self.scope.write().await;
4960 scope.set(name.to_string(), value);
4961 }
4962
4963 pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
4965 let mut scope = self.scope.write().await;
4966 scope.set_positional(script_name, args);
4967 }
4968
4969 pub async fn list_vars(&self) -> Vec<(String, Value)> {
4971 let scope = self.scope.read().await;
4972 scope.all()
4973 }
4974
4975 pub async fn exported_vars(&self) -> Vec<(String, Value)> {
4978 let scope = self.scope.read().await;
4979 scope.exported_vars()
4980 }
4981
4982 pub async fn cwd(&self) -> PathBuf {
4986 self.exec_ctx.read().await.cwd.clone()
4987 }
4988
4989 pub async fn set_cwd(&self, path: PathBuf) {
4991 let mut ctx = self.exec_ctx.write().await;
4992 ctx.set_cwd(path);
4993 }
4994
4995 pub async fn try_set_cwd(&self, path: PathBuf) -> bool {
5001 let backend = self.exec_ctx.read().await.backend.clone();
5004 let is_dir = matches!(backend.stat(&path).await, Ok(entry) if entry.is_dir());
5005 if is_dir {
5006 self.exec_ctx.write().await.set_cwd(path);
5007 }
5008 is_dir
5009 }
5010
5011 pub async fn last_result(&self) -> ExecResult {
5015 let scope = self.scope.read().await;
5016 scope.last_result().clone()
5017 }
5018
5019 pub async fn has_function(&self, name: &str) -> bool {
5023 self.user_tools.read().await.contains_key(name)
5024 }
5025
5026 pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
5028 self.tools.schemas()
5029 }
5030
5031 pub fn jobs(&self) -> Arc<JobManager> {
5035 self.jobs.clone()
5036 }
5037
5038 pub fn vfs(&self) -> Arc<VfsRouter> {
5042 self.vfs.clone()
5043 }
5044
5045 pub async fn reset(&self) -> Result<()> {
5052 {
5053 let mut scope = self.scope.write().await;
5054 *scope = Scope::new();
5055 }
5056 {
5057 let mut ctx = self.exec_ctx.write().await;
5058 ctx.cwd = PathBuf::from("/");
5059 }
5060 Ok(())
5061 }
5062
5063 pub async fn shutdown(self) -> Result<()> {
5065 self.jobs.wait_all().await;
5067 Ok(())
5068 }
5069
5070 async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
5081 if let Some(d) = self.dispatcher() {
5086 ctx.dispatcher = Some(d);
5087 }
5088
5089 {
5091 let mut scope = self.scope.write().await;
5092 *scope = ctx.scope.clone();
5093 }
5094 {
5095 let mut ec = self.exec_ctx.write().await;
5096 ec.cwd = ctx.cwd.clone();
5097 ec.prev_cwd = ctx.prev_cwd.clone();
5098 ec.stdin = ctx.stdin.take();
5099 ec.stdin_data = ctx.stdin_data.take();
5100 ec.stdin_data_rx = ctx.stdin_data_rx.take();
5105 ec.pipe_stdin = ctx.pipe_stdin.take();
5111 ec.pipe_stdout = ctx.pipe_stdout.take();
5112 if let Some(stderr) = ctx.stderr.clone() {
5113 ec.stderr = Some(stderr);
5114 }
5115 ec.aliases = ctx.aliases.clone();
5116 ec.ignore_config = ctx.ignore_config.clone();
5117 ec.output_limit = ctx.output_limit.clone();
5118 ec.pipeline_position = ctx.pipeline_position;
5119 ec.cancel = ctx.cancel.clone();
5124 ec.watchdog = ctx.watchdog.clone();
5128 }
5129
5130 let result = self.execute_command(&cmd.name, &cmd.args).await?;
5132
5133 {
5135 let scope = self.scope.read().await;
5136 ctx.scope = scope.clone();
5137 }
5138 {
5139 let mut ec = self.exec_ctx.write().await;
5140 ctx.cwd = ec.cwd.clone();
5141 ctx.prev_cwd = ec.prev_cwd.clone();
5142 ctx.aliases = ec.aliases.clone();
5143 ctx.ignore_config = ec.ignore_config.clone();
5144 ctx.output_limit = ec.output_limit.clone();
5145 ctx.pipe_stdin = ec.pipe_stdin.take();
5150 ctx.pipe_stdout = ec.pipe_stdout.take();
5151 }
5152
5153 Ok(result)
5154 }
5155}
5156
5157#[async_trait]
5158impl CommandDispatcher for Kernel {
5159 async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
5165 self.dispatch_command(cmd, ctx).await
5166 }
5167
5168 async fn eval_expr(&self, expr: &Expr, _ctx: &ExecContext) -> Result<Value> {
5175 self.eval_expr_async(expr).await
5176 }
5177
5178 async fn fork(&self) -> Arc<dyn CommandDispatcher> {
5184 let fork: Arc<Kernel> = Kernel::fork(self).await;
5185 fork
5186 }
5187
5188 async fn fork_attached(&self) -> Arc<dyn CommandDispatcher> {
5190 let fork: Arc<Kernel> = Kernel::fork_attached(self).await;
5191 fork
5192 }
5193}
5194
5195fn finalize_output(
5203 result: ExecResult,
5204 format: Option<crate::interpreter::OutputFormat>,
5205 owns_output: bool,
5206) -> ExecResult {
5207 match format {
5208 Some(_) if owns_output => result,
5209 Some(format) => apply_output_format(result, format),
5210 None => result,
5211 }
5212}
5213
5214fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
5223 accumulated.materialize();
5227 match new.out_bytes() {
5228 Some(new_bytes) => {
5232 let mut combined: Vec<u8> = match accumulated.out_bytes() {
5233 Some(b) => b.to_vec(),
5234 None => accumulated.text_out().into_owned().into_bytes(),
5235 };
5236 combined.extend_from_slice(new_bytes);
5237 accumulated.set_out_bytes(combined);
5238 }
5239 None => accumulated.push_out(&new.text_out()),
5240 }
5241 accumulated.err.push_str(&new.err);
5242 accumulated.code = new.code;
5243 accumulated.data = new.data.clone();
5244 accumulated.did_spill = new.did_spill;
5245 accumulated.original_code = new.original_code;
5246 accumulated.content_type = new.content_type.clone();
5247 accumulated.baggage.clone_from(&new.baggage);
5248}
5249
5250fn fold_loop_output_into_flow(loop_output: ExecResult, flow: &mut ControlFlow) {
5256 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5257 let mut merged = loop_output;
5258 accumulate_result(&mut merged, result);
5259 *result = merged;
5260 }
5261}
5262
5263fn accumulate_flow_output(accumulated: &mut ExecResult, flow: &ControlFlow) {
5267 if let ControlFlow::Break { result, .. } | ControlFlow::Continue { result, .. } = flow {
5268 accumulate_result(accumulated, result);
5269 }
5270}
5271
5272fn is_truthy(value: &Value) -> bool {
5274 match value {
5275 Value::Null => false,
5276 Value::Bool(b) => *b,
5277 Value::Int(i) => *i != 0,
5278 Value::Float(f) => *f != 0.0,
5279 Value::String(s) => !s.is_empty(),
5280 Value::Json(json) => match json {
5281 serde_json::Value::Null => false,
5282 serde_json::Value::Array(arr) => !arr.is_empty(),
5283 serde_json::Value::Object(obj) => !obj.is_empty(),
5284 serde_json::Value::Bool(b) => *b,
5285 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
5286 serde_json::Value::String(s) => !s.is_empty(),
5287 },
5288 Value::Bytes(b) => !b.is_empty(), }
5290}
5291
5292fn apply_tilde_expansion(value: Value, home: Option<&str>) -> Value {
5298 match value {
5299 Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s, home)),
5300 _ => value,
5301 }
5302}
5303
5304pub(crate) fn argv_to_args(argv: &[Value]) -> Vec<Arg> {
5327 argv.iter().map(classify_argv_token).collect()
5328}
5329
5330fn classify_argv_token(token: &Value) -> Arg {
5331 let Value::String(s) = token else {
5332 return Arg::Positional(Expr::Literal(token.clone()));
5333 };
5334
5335 if s == "--" {
5336 return Arg::DoubleDash;
5337 }
5338
5339 if let Some(rest) = s.strip_prefix("--") {
5344 if rest.starts_with(|c: char| c.is_ascii_alphabetic()) {
5345 return match rest.split_once('=') {
5346 Some((key, val)) => Arg::Named {
5347 key: key.to_string(),
5348 value: Expr::Literal(Value::String(val.to_string())),
5349 },
5350 None => Arg::LongFlag(rest.to_string()),
5351 };
5352 }
5353 } else if let Some(rest) = s.strip_prefix('-') {
5354 if is_short_flag_body(rest) {
5360 return Arg::ShortFlag(rest.to_string());
5361 }
5362 }
5363
5364 if let Some((key, val)) = s.split_once('=') {
5365 if is_shell_identifier(key) {
5366 return Arg::WordAssign {
5367 key: key.to_string(),
5368 value: Expr::Literal(Value::String(val.to_string())),
5369 };
5370 }
5371 }
5372
5373 Arg::Positional(Expr::Literal(Value::String(s.clone())))
5374}
5375
5376fn is_short_flag_body(s: &str) -> bool {
5381 s.starts_with(|c: char| c.is_ascii_alphabetic()) && !s.contains('=')
5382}
5383
5384fn is_shell_identifier(s: &str) -> bool {
5386 let mut chars = s.chars();
5387 match chars.next() {
5388 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
5389 _ => return false,
5390 }
5391 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
5392}
5393
5394pub(crate) fn push_repeatable_value(
5403 tool_args: &mut ToolArgs,
5404 flag_name: &str,
5405 canonical: &str,
5406 v: Value,
5407) -> anyhow::Result<()> {
5408 let occ = crate::interpreter::value_to_json(&v);
5409 let entry = tool_args
5410 .named
5411 .entry(canonical.to_string())
5412 .or_insert_with(|| Value::Json(serde_json::Value::Array(Vec::new())));
5413 if let Value::Json(serde_json::Value::Array(items)) = entry {
5414 items.push(occ);
5415 Ok(())
5416 } else {
5417 anyhow::bail!("--{flag_name}: named[{canonical}] already holds a non-array value")
5418 }
5419}
5420
5421pub(crate) fn bind_glued_short_value(
5428 tool_args: &mut ToolArgs,
5429 flag_name: &str,
5430 canonical: &str,
5431 consumes: usize,
5432 repeatable: bool,
5433 value: String,
5434) -> anyhow::Result<()> {
5435 if consumes > 1 {
5436 anyhow::bail!(
5437 "-{flag_name} takes {consumes} arguments; use the separated form, not a glued value"
5438 );
5439 }
5440 if repeatable {
5441 push_repeatable_value(tool_args, flag_name, canonical, Value::String(value))
5442 } else {
5443 tool_args
5444 .named
5445 .insert(canonical.to_string(), Value::String(value));
5446 Ok(())
5447 }
5448}
5449
5450#[cfg(all(unix, feature = "subprocess"))]
5456pub(crate) async fn wait_or_kill(
5457 child: &mut tokio::process::Child,
5458 target: Option<&crate::pidfd::KillTarget>,
5459 cancel: &tokio_util::sync::CancellationToken,
5460 grace: Duration,
5461) -> std::io::Result<std::process::ExitStatus> {
5462 tokio::select! {
5463 biased;
5464 status = child.wait() => status,
5465 _ = cancel.cancelled() => kill_with_grace(child, target, grace).await,
5466 }
5467}
5468
5469#[cfg(all(not(unix), feature = "subprocess"))]
5470pub(crate) async fn wait_or_kill(
5471 child: &mut tokio::process::Child,
5472 _target: Option<&()>,
5473 cancel: &tokio_util::sync::CancellationToken,
5474 _grace: Duration,
5475) -> std::io::Result<std::process::ExitStatus> {
5476 tokio::select! {
5477 biased;
5478 status = child.wait() => status,
5479 _ = cancel.cancelled() => {
5480 let _ = child.start_kill();
5481 child.wait().await
5482 }
5483 }
5484}
5485
5486#[cfg(all(unix, feature = "subprocess"))]
5492pub(crate) async fn kill_with_grace(
5493 child: &mut tokio::process::Child,
5494 target: Option<&crate::pidfd::KillTarget>,
5495 grace: Duration,
5496) -> std::io::Result<std::process::ExitStatus> {
5497 use nix::sys::signal::Signal;
5498
5499 if let Some(t) = target {
5500 t.signal(Signal::SIGTERM);
5501 t.signal_pg(Signal::SIGTERM);
5502 if grace > Duration::ZERO
5503 && let Ok(status) = tokio::time::timeout(grace, child.wait()).await
5504 {
5505 return status;
5506 }
5507 t.signal(Signal::SIGKILL);
5508 t.signal_pg(Signal::SIGKILL);
5509 }
5510 child.wait().await
5511}
5512
5513#[cfg(test)]
5514#[allow(clippy::unwrap_used, clippy::expect_used)]
5515mod argv_classify_tests {
5516 use super::*;
5517
5518 fn canonical(arg: &Arg) -> Option<(&'static str, String, String)> {
5544 let lit = |e: &Expr| match e {
5546 Expr::Literal(Value::String(s)) => Some(s.clone()),
5547 _ => None,
5548 };
5549 Some(match arg {
5550 Arg::DoubleDash => ("dash", String::new(), String::new()),
5551 Arg::ShortFlag(s) => ("short", s.clone(), String::new()),
5552 Arg::LongFlag(s) => ("long", s.clone(), String::new()),
5553 Arg::Positional(e) => ("pos", String::new(), lit(e)?),
5554 Arg::Named { key, value } => ("named", key.clone(), lit(value)?),
5555 Arg::WordAssign { key, value } => ("pos", String::new(), format!("{key}={}", lit(value)?)),
5556 })
5557 }
5558
5559 fn classify(token: &str) -> Arg {
5561 classify_argv_token(&Value::String(token.to_string()))
5562 }
5563
5564 #[test]
5565 fn classifies_each_word_class() {
5566 assert_eq!(classify("--"), Arg::DoubleDash);
5567 assert_eq!(classify("-l"), Arg::ShortFlag("l".into()));
5568 assert_eq!(classify("-la"), Arg::ShortFlag("la".into()));
5569 assert_eq!(classify("--force"), Arg::LongFlag("force".into()));
5570 assert_eq!(
5571 classify("--key=value"),
5572 Arg::Named { key: "key".into(), value: Expr::Literal(Value::String("value".into())) }
5573 );
5574 assert_eq!(
5575 classify("NAME=val"),
5576 Arg::WordAssign { key: "NAME".into(), value: Expr::Literal(Value::String("val".into())) }
5577 );
5578 assert_eq!(classify("-A1"), Arg::ShortFlag("A1".into()));
5580 assert_eq!(classify("--type2"), Arg::LongFlag("type2".into()));
5581 assert_eq!(classify("-1"), Arg::Positional(Expr::Literal(Value::String("-1".into()))));
5583 assert_eq!(classify("00"), Arg::Positional(Expr::Literal(Value::String("00".into()))));
5587 assert_eq!(classify("1.50"), Arg::Positional(Expr::Literal(Value::String("1.50".into()))));
5588 assert_eq!(classify("-"), Arg::Positional(Expr::Literal(Value::String("-".into()))));
5590 assert_eq!(classify("1=2"), Arg::Positional(Expr::Literal(Value::String("1=2".into()))));
5592 assert_eq!(classify("plain"), Arg::Positional(Expr::Literal(Value::String("plain".into()))));
5593 }
5594
5595 #[test]
5596 fn typed_values_pass_through_as_literal_positionals() {
5597 let bytes = Value::Bytes(vec![0u8, 159, 146, 150]); assert_eq!(
5602 classify_argv_token(&bytes),
5603 Arg::Positional(Expr::Literal(bytes.clone()))
5604 );
5605 let json = Value::Json(serde_json::json!({"a": 1, "b": [2, 3]}));
5606 assert_eq!(
5607 classify_argv_token(&json),
5608 Arg::Positional(Expr::Literal(json.clone()))
5609 );
5610 assert_eq!(
5613 classify_argv_token(&Value::Int(-9)),
5614 Arg::Positional(Expr::Literal(Value::Int(-9)))
5615 );
5616 }
5617
5618 #[test]
5619 fn double_dash_only_matches_exactly() {
5620 assert_eq!(classify("--"), Arg::DoubleDash);
5623 assert_eq!(classify("--x"), Arg::LongFlag("x".into()));
5624 assert_eq!(classify("---"), Arg::Positional(Expr::Literal(Value::String("---".into()))));
5625 }
5626
5627 #[test]
5628 fn malformed_flag_words_fall_back_to_literal_positionals() {
5629 let pos = |t: &str| Arg::Positional(Expr::Literal(Value::String(t.into())));
5634 assert_eq!(classify("-k=v"), pos("-k=v"));
5637 assert_eq!(classify("-="), pos("-="));
5638 assert_eq!(classify("--=v"), pos("--=v"));
5640 assert_eq!(classify("--1"), pos("--1"));
5642 assert_eq!(classify("-"), pos("-"));
5644 assert_eq!(classify("-9"), pos("-9"));
5645 }
5646
5647 proptest::proptest! {
5648 #[test]
5654 fn classifier_matches_parser_on_clean_tokens(
5655 token in "[a-zA-Z_=./@:+-]{1,8}"
5662 ) {
5663 let parsed = match parse(&format!("cmd {token}")) {
5664 Ok(p) => p,
5665 Err(_) => return Ok(()), };
5667 let [Stmt::Command(cmd)] = parsed.statements.as_slice() else {
5668 return Ok(());
5669 };
5670 let [arg] = cmd.args.as_slice() else { return Ok(()); };
5672
5673 let (Some(theirs), Some(ours)) = (canonical(arg), canonical(&classify(&token))) else {
5674 return Ok(()); };
5676 proptest::prop_assert_eq!(
5677 ours, theirs,
5678 "classifier diverged from parser on token {:?}", token
5679 );
5680 }
5681 }
5682}
5683
5684#[cfg(all(test, feature = "subprocess"))]
5685#[allow(clippy::expect_used)]
5686mod tests {
5687 use super::*;
5688
5689 #[tokio::test]
5690 async fn test_kernel_transient() {
5691 let kernel = Kernel::transient().expect("failed to create kernel");
5692 assert_eq!(kernel.name(), "transient");
5693 }
5694
5695 #[tokio::test]
5696 async fn test_kernel_execute_echo() {
5697 let kernel = Kernel::transient().expect("failed to create kernel");
5698 let result = kernel.execute("echo hello").await.expect("execution failed");
5699 assert!(result.ok());
5700 assert_eq!(result.text_out().trim(), "hello");
5701 }
5702
5703 #[tokio::test]
5704 async fn test_multiple_statements_accumulate_output() {
5705 let kernel = Kernel::transient().expect("failed to create kernel");
5706 let result = kernel
5707 .execute("echo one\necho two\necho three")
5708 .await
5709 .expect("execution failed");
5710 assert!(result.ok());
5711 assert!(result.text_out().contains("one"), "missing 'one': {}", result.text_out());
5713 assert!(result.text_out().contains("two"), "missing 'two': {}", result.text_out());
5714 assert!(result.text_out().contains("three"), "missing 'three': {}", result.text_out());
5715 }
5716
5717 #[tokio::test]
5718 async fn test_and_chain_accumulates_output() {
5719 let kernel = Kernel::transient().expect("failed to create kernel");
5720 let result = kernel
5721 .execute("echo first && echo second")
5722 .await
5723 .expect("execution failed");
5724 assert!(result.ok());
5725 assert!(result.text_out().contains("first"), "missing 'first': {}", result.text_out());
5726 assert!(result.text_out().contains("second"), "missing 'second': {}", result.text_out());
5727 }
5728
5729 #[tokio::test]
5730 async fn test_for_loop_accumulates_output() {
5731 let kernel = Kernel::transient().expect("failed to create kernel");
5732 let result = kernel
5733 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
5734 .await
5735 .expect("execution failed");
5736 assert!(result.ok());
5737 assert!(result.text_out().contains("item: a"), "missing 'item: a': {}", result.text_out());
5738 assert!(result.text_out().contains("item: b"), "missing 'item: b': {}", result.text_out());
5739 assert!(result.text_out().contains("item: c"), "missing 'item: c': {}", result.text_out());
5740 }
5741
5742 #[tokio::test]
5743 async fn test_while_loop_accumulates_output() {
5744 let kernel = Kernel::transient().expect("failed to create kernel");
5745 let result = kernel
5746 .execute(r#"
5747 N=3
5748 while [[ ${N} -gt 0 ]]; do
5749 echo "N=${N}"
5750 N=$((N - 1))
5751 done
5752 "#)
5753 .await
5754 .expect("execution failed");
5755 assert!(result.ok());
5756 assert!(result.text_out().contains("N=3"), "missing 'N=3': {}", result.text_out());
5757 assert!(result.text_out().contains("N=2"), "missing 'N=2': {}", result.text_out());
5758 assert!(result.text_out().contains("N=1"), "missing 'N=1': {}", result.text_out());
5759 }
5760
5761 #[tokio::test]
5762 async fn test_kernel_set_var() {
5763 let kernel = Kernel::transient().expect("failed to create kernel");
5764
5765 kernel.execute("X=42").await.expect("set failed");
5766
5767 let value = kernel.get_var("X").await;
5768 assert_eq!(value, Some(Value::Int(42)));
5769 }
5770
5771 #[tokio::test]
5772 async fn test_kernel_var_expansion() {
5773 let kernel = Kernel::transient().expect("failed to create kernel");
5774
5775 kernel.execute("NAME=\"world\"").await.expect("set failed");
5776 let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
5777
5778 assert!(result.ok());
5779 assert_eq!(result.text_out().trim(), "hello world");
5780 }
5781
5782 #[tokio::test]
5783 async fn test_kernel_last_result() {
5784 let kernel = Kernel::transient().expect("failed to create kernel");
5785
5786 kernel.execute("echo test").await.expect("echo failed");
5787
5788 let last = kernel.last_result().await;
5789 assert!(last.ok());
5790 assert_eq!(last.text_out().trim(), "test");
5791 }
5792
5793 #[tokio::test]
5794 async fn test_kernel_tool_not_found() {
5795 let kernel = Kernel::transient().expect("failed to create kernel");
5796
5797 let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
5798 assert!(!result.ok());
5799 assert_eq!(result.code, 127);
5800 assert!(result.err.contains("command not found"));
5801 }
5802
5803 #[tokio::test]
5804 async fn test_external_command_true() {
5805 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
5807
5808 let result = kernel.execute("true").await.expect("execution failed");
5810 assert!(result.ok(), "true should succeed: {:?}", result);
5812 }
5813
5814 #[tokio::test]
5815 async fn test_external_command_basic() {
5816 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
5818
5819 let path_var = std::env::var("PATH").unwrap_or_default();
5824 eprintln!("System PATH: {}", path_var);
5825
5826 kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
5828
5829 let result = kernel.execute("uname").await.expect("execution failed");
5832 eprintln!("uname result: {:?}", result);
5833 assert!(result.ok() || result.code == 127, "uname: {:?}", result);
5835 }
5836
5837 #[tokio::test]
5838 async fn test_kernel_reset() {
5839 let kernel = Kernel::transient().expect("failed to create kernel");
5840
5841 kernel.execute("X=1").await.expect("set failed");
5842 assert!(kernel.get_var("X").await.is_some());
5843
5844 kernel.reset().await.expect("reset failed");
5845 assert!(kernel.get_var("X").await.is_none());
5846 }
5847
5848 #[tokio::test]
5849 async fn test_kernel_cwd() {
5850 let kernel = Kernel::transient().expect("failed to create kernel");
5851
5852 let cwd = kernel.cwd().await;
5854 let home = std::env::var("HOME")
5855 .map(PathBuf::from)
5856 .unwrap_or_else(|_| PathBuf::from("/"));
5857 assert_eq!(cwd, home);
5858
5859 kernel.set_cwd(PathBuf::from("/tmp")).await;
5860 assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
5861 }
5862
5863 #[tokio::test]
5864 async fn test_kernel_list_vars() {
5865 let kernel = Kernel::transient().expect("failed to create kernel");
5866
5867 kernel.execute("A=1").await.ok();
5868 kernel.execute("B=2").await.ok();
5869
5870 let vars = kernel.list_vars().await;
5871 assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
5872 assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
5873 }
5874
5875 #[tokio::test]
5876 async fn test_is_truthy() {
5877 assert!(!is_truthy(&Value::Null));
5878 assert!(!is_truthy(&Value::Bool(false)));
5879 assert!(is_truthy(&Value::Bool(true)));
5880 assert!(!is_truthy(&Value::Int(0)));
5881 assert!(is_truthy(&Value::Int(1)));
5882 assert!(!is_truthy(&Value::String("".into())));
5883 assert!(is_truthy(&Value::String("x".into())));
5884 }
5885
5886 #[tokio::test]
5887 async fn test_jq_in_pipeline() {
5888 let kernel = Kernel::transient().expect("failed to create kernel");
5889 let result = kernel
5891 .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
5892 .await
5893 .expect("execution failed");
5894 assert!(result.ok(), "jq pipeline failed: {}", result.err);
5895 assert_eq!(result.text_out().trim(), "Alice");
5896 }
5897
5898 #[tokio::test]
5899 async fn test_user_defined_tool() {
5900 let kernel = Kernel::transient().expect("failed to create kernel");
5901
5902 kernel
5904 .execute(r#"greet() { echo "Hello, $1!" }"#)
5905 .await
5906 .expect("function definition failed");
5907
5908 let result = kernel
5910 .execute(r#"greet "World""#)
5911 .await
5912 .expect("function call failed");
5913
5914 assert!(result.ok(), "greet failed: {}", result.err);
5915 assert_eq!(result.text_out().trim(), "Hello, World!");
5916 }
5917
5918 #[tokio::test]
5919 async fn test_user_tool_positional_args() {
5920 let kernel = Kernel::transient().expect("failed to create kernel");
5921
5922 kernel
5924 .execute(r#"greet() { echo "Hi $1" }"#)
5925 .await
5926 .expect("function definition failed");
5927
5928 let result = kernel
5930 .execute(r#"greet "Amy""#)
5931 .await
5932 .expect("function call failed");
5933
5934 assert!(result.ok(), "greet failed: {}", result.err);
5935 assert_eq!(result.text_out().trim(), "Hi Amy");
5936 }
5937
5938 #[tokio::test]
5939 async fn test_function_shared_scope() {
5940 let kernel = Kernel::transient().expect("failed to create kernel");
5941
5942 kernel
5944 .execute(r#"SECRET="hidden""#)
5945 .await
5946 .expect("set failed");
5947
5948 kernel
5950 .execute(r#"access_parent() {
5951 echo "${SECRET}"
5952 SECRET="modified"
5953 }"#)
5954 .await
5955 .expect("function definition failed");
5956
5957 let result = kernel.execute("access_parent").await.expect("function call failed");
5959
5960 assert!(
5962 result.text_out().contains("hidden"),
5963 "Function should access parent scope, got: {}",
5964 result.text_out()
5965 );
5966
5967 let secret = kernel.get_var("SECRET").await;
5969 assert_eq!(
5970 secret,
5971 Some(Value::String("modified".into())),
5972 "Function should modify parent scope"
5973 );
5974 }
5975
5976 #[tokio::test]
5977 #[ignore = "exec replaces the test binary via CommandExt::exec, hangs libtest; cannot be run under cargo test"]
5978 async fn test_exec_builtin() {
5979 let kernel = Kernel::transient().expect("failed to create kernel");
5980 let result = kernel
5982 .execute(r#"exec command="/bin/echo" argv="hello world""#)
5983 .await
5984 .expect("exec failed");
5985
5986 assert!(result.ok(), "exec failed: {}", result.err);
5987 assert_eq!(result.text_out().trim(), "hello world");
5988 }
5989
5990 #[tokio::test]
5991 async fn test_while_false_never_runs() {
5992 let kernel = Kernel::transient().expect("failed to create kernel");
5993
5994 let result = kernel
5996 .execute(r#"
5997 while false; do
5998 echo "should not run"
5999 done
6000 "#)
6001 .await
6002 .expect("while false failed");
6003
6004 assert!(result.ok());
6005 assert!(result.text_out().is_empty(), "while false should not execute body: {}", result.text_out());
6006 }
6007
6008 #[tokio::test]
6009 async fn test_while_string_comparison() {
6010 let kernel = Kernel::transient().expect("failed to create kernel");
6011
6012 kernel.execute(r#"FLAG="go""#).await.expect("set failed");
6014
6015 let result = kernel
6018 .execute(r#"
6019 while [[ ${FLAG} == "go" ]]; do
6020 FLAG="stop"
6021 echo "running"
6022 done
6023 "#)
6024 .await
6025 .expect("while with string cmp failed");
6026
6027 assert!(result.ok());
6028 assert!(result.text_out().contains("running"), "should have run once: {}", result.text_out());
6029
6030 let flag = kernel.get_var("FLAG").await;
6032 assert_eq!(flag, Some(Value::String("stop".into())));
6033 }
6034
6035 #[tokio::test]
6036 async fn test_while_numeric_comparison() {
6037 let kernel = Kernel::transient().expect("failed to create kernel");
6038
6039 kernel.execute("N=5").await.expect("set failed");
6041
6042 let result = kernel
6044 .execute(r#"
6045 while [[ ${N} -gt 3 ]]; do
6046 N=3
6047 echo "N was greater"
6048 done
6049 "#)
6050 .await
6051 .expect("while with > failed");
6052
6053 assert!(result.ok());
6054 assert!(result.text_out().contains("N was greater"), "should have run once: {}", result.text_out());
6055 }
6056
6057 #[tokio::test]
6058 async fn test_break_in_while_loop() {
6059 let kernel = Kernel::transient().expect("failed to create kernel");
6060
6061 let result = kernel
6062 .execute(r#"
6063 I=0
6064 while true; do
6065 I=1
6066 echo "before break"
6067 break
6068 echo "after break"
6069 done
6070 "#)
6071 .await
6072 .expect("while with break failed");
6073
6074 assert!(result.ok());
6075 assert!(result.text_out().contains("before break"), "should see before break: {}", result.text_out());
6076 assert!(!result.text_out().contains("after break"), "should not see after break: {}", result.text_out());
6077
6078 let i = kernel.get_var("I").await;
6080 assert_eq!(i, Some(Value::Int(1)));
6081 }
6082
6083 #[tokio::test]
6084 async fn test_continue_in_while_loop() {
6085 let kernel = Kernel::transient().expect("failed to create kernel");
6086
6087 let result = kernel
6092 .execute(r#"
6093 STATE="start"
6094 AFTER_CONTINUE="no"
6095 while [[ ${STATE} != "done" ]]; do
6096 if [[ ${STATE} == "start" ]]; then
6097 STATE="middle"
6098 continue
6099 AFTER_CONTINUE="yes"
6100 fi
6101 if [[ ${STATE} == "middle" ]]; then
6102 STATE="done"
6103 fi
6104 done
6105 "#)
6106 .await
6107 .expect("while with continue failed");
6108
6109 assert!(result.ok());
6110
6111 let state = kernel.get_var("STATE").await;
6113 assert_eq!(state, Some(Value::String("done".into())));
6114
6115 let after = kernel.get_var("AFTER_CONTINUE").await;
6117 assert_eq!(after, Some(Value::String("no".into())));
6118 }
6119
6120 #[tokio::test]
6121 async fn test_break_with_level() {
6122 let kernel = Kernel::transient().expect("failed to create kernel");
6123
6124 let result = kernel
6129 .execute(r#"
6130 OUTER=0
6131 while true; do
6132 OUTER=1
6133 for X in "1 2"; do
6134 break 2
6135 done
6136 OUTER=2
6137 done
6138 "#)
6139 .await
6140 .expect("nested break failed");
6141
6142 assert!(result.ok());
6143
6144 let outer = kernel.get_var("OUTER").await;
6146 assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
6147 }
6148
6149 #[tokio::test]
6150 async fn test_return_from_tool() {
6151 let kernel = Kernel::transient().expect("failed to create kernel");
6152
6153 kernel
6155 .execute(r#"early_return() {
6156 if [[ $1 == 1 ]]; then
6157 return 42
6158 fi
6159 echo "not returned"
6160 }"#)
6161 .await
6162 .expect("function definition failed");
6163
6164 let result = kernel
6167 .execute("early_return 1")
6168 .await
6169 .expect("function call failed");
6170
6171 assert_eq!(result.code, 42);
6173 assert!(result.text_out().is_empty());
6175 }
6176
6177 #[tokio::test]
6178 async fn test_return_without_value() {
6179 let kernel = Kernel::transient().expect("failed to create kernel");
6180
6181 kernel
6183 .execute(r#"early_exit() {
6184 if [[ $1 == "stop" ]]; then
6185 return
6186 fi
6187 echo "continued"
6188 }"#)
6189 .await
6190 .expect("function definition failed");
6191
6192 let result = kernel
6194 .execute(r#"early_exit "stop""#)
6195 .await
6196 .expect("function call failed");
6197
6198 assert!(result.ok());
6199 assert!(result.text_out().is_empty() || result.text_out().trim().is_empty());
6200 }
6201
6202 #[tokio::test]
6203 async fn test_exit_stops_execution() {
6204 let kernel = Kernel::transient().expect("failed to create kernel");
6205
6206 kernel
6208 .execute(r#"
6209 BEFORE="yes"
6210 exit 0
6211 AFTER="yes"
6212 "#)
6213 .await
6214 .expect("execution failed");
6215
6216 let before = kernel.get_var("BEFORE").await;
6218 assert_eq!(before, Some(Value::String("yes".into())));
6219
6220 let after = kernel.get_var("AFTER").await;
6221 assert!(after.is_none(), "AFTER should not be set after exit");
6222 }
6223
6224 #[tokio::test]
6225 async fn test_exit_with_code() {
6226 let kernel = Kernel::transient().expect("failed to create kernel");
6227
6228 let result = kernel
6230 .execute("exit 42")
6231 .await
6232 .expect("exit failed");
6233
6234 assert_eq!(result.code, 42);
6235 assert!(result.text_out().is_empty(), "exit should not produce stdout");
6236 }
6237
6238 #[tokio::test]
6239 async fn test_set_e_stops_on_failure() {
6240 let kernel = Kernel::transient().expect("failed to create kernel");
6241
6242 kernel.execute("set -e").await.expect("set -e failed");
6244
6245 kernel
6247 .execute(r#"
6248 STEP1="done"
6249 false
6250 STEP2="done"
6251 "#)
6252 .await
6253 .expect("execution failed");
6254
6255 let step1 = kernel.get_var("STEP1").await;
6257 assert_eq!(step1, Some(Value::String("done".into())));
6258
6259 let step2 = kernel.get_var("STEP2").await;
6260 assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
6261 }
6262
6263 #[tokio::test]
6264 async fn test_set_plus_e_disables_error_exit() {
6265 let kernel = Kernel::transient().expect("failed to create kernel");
6266
6267 kernel.execute("set -e").await.expect("set -e failed");
6269 kernel.execute("set +e").await.expect("set +e failed");
6270
6271 kernel
6273 .execute(r#"
6274 STEP1="done"
6275 false
6276 STEP2="done"
6277 "#)
6278 .await
6279 .expect("execution failed");
6280
6281 let step1 = kernel.get_var("STEP1").await;
6283 assert_eq!(step1, Some(Value::String("done".into())));
6284
6285 let step2 = kernel.get_var("STEP2").await;
6286 assert_eq!(step2, Some(Value::String("done".into())));
6287 }
6288
6289 #[tokio::test]
6290 async fn test_set_ignores_unknown_options() {
6291 let kernel = Kernel::transient().expect("failed to create kernel");
6292
6293 let result = kernel
6295 .execute("set -e -u -o pipefail")
6296 .await
6297 .expect("set with unknown options failed");
6298
6299 assert!(result.ok(), "set should succeed with unknown options");
6300
6301 kernel
6303 .execute(r#"
6304 BEFORE="yes"
6305 false
6306 AFTER="yes"
6307 "#)
6308 .await
6309 .ok();
6310
6311 let after = kernel.get_var("AFTER").await;
6312 assert!(after.is_none(), "-e should be enabled despite unknown options");
6313 }
6314
6315 #[tokio::test]
6316 async fn test_set_no_args_shows_settings() {
6317 let kernel = Kernel::transient().expect("failed to create kernel");
6318
6319 kernel.execute("set -e").await.expect("set -e failed");
6321
6322 let result = kernel.execute("set").await.expect("set failed");
6324
6325 assert!(result.ok());
6326 assert!(result.text_out().contains("set -e"), "should show -e is enabled: {}", result.text_out());
6327 }
6328
6329 #[tokio::test]
6330 async fn test_set_e_in_pipeline() {
6331 let kernel = Kernel::transient().expect("failed to create kernel");
6332
6333 kernel.execute("set -e").await.expect("set -e failed");
6334
6335 kernel
6337 .execute(r#"
6338 BEFORE="yes"
6339 false | cat
6340 AFTER="yes"
6341 "#)
6342 .await
6343 .ok();
6344
6345 let before = kernel.get_var("BEFORE").await;
6346 assert_eq!(before, Some(Value::String("yes".into())));
6347
6348 }
6353
6354 #[tokio::test]
6355 async fn test_set_e_with_and_chain() {
6356 let kernel = Kernel::transient().expect("failed to create kernel");
6357
6358 kernel.execute("set -e").await.expect("set -e failed");
6359
6360 kernel
6363 .execute(r#"
6364 RESULT="initial"
6365 false && RESULT="chained"
6366 RESULT="continued"
6367 "#)
6368 .await
6369 .ok();
6370
6371 let result = kernel.get_var("RESULT").await;
6374 assert!(result.is_some(), "RESULT should be set");
6377 }
6378
6379 #[tokio::test]
6380 async fn test_set_e_exits_in_for_loop() {
6381 let kernel = Kernel::transient().expect("failed to create kernel");
6382
6383 kernel.execute("set -e").await.expect("set -e failed");
6384
6385 kernel
6386 .execute(r#"
6387 REACHED="no"
6388 for x in 1 2 3; do
6389 false
6390 REACHED="yes"
6391 done
6392 "#)
6393 .await
6394 .ok();
6395
6396 let reached = kernel.get_var("REACHED").await;
6398 assert_eq!(reached, Some(Value::String("no".into())),
6399 "set -e should exit on failure in for loop body");
6400 }
6401
6402 #[tokio::test]
6403 async fn test_for_loop_continues_without_set_e() {
6404 let kernel = Kernel::transient().expect("failed to create kernel");
6405
6406 kernel
6408 .execute(r#"
6409 COUNT=0
6410 for x in 1 2 3; do
6411 false
6412 COUNT=$((COUNT + 1))
6413 done
6414 "#)
6415 .await
6416 .ok();
6417
6418 let count = kernel.get_var("COUNT").await;
6419 let count_val = match &count {
6421 Some(Value::Int(n)) => *n,
6422 Some(Value::String(s)) => s.parse().unwrap_or(-1),
6423 _ => -1,
6424 };
6425 assert_eq!(count_val, 3,
6426 "without set -e, loop should complete all iterations (got {:?})", count);
6427 }
6428
6429 #[tokio::test]
6434 async fn test_source_sets_variables() {
6435 let kernel = Kernel::transient().expect("failed to create kernel");
6436
6437 kernel
6439 .execute(r#"write "/test.kai" 'FOO="bar"'"#)
6440 .await
6441 .expect("write failed");
6442
6443 let result = kernel
6445 .execute(r#"source "/test.kai""#)
6446 .await
6447 .expect("source failed");
6448
6449 assert!(result.ok(), "source should succeed");
6450
6451 let foo = kernel.get_var("FOO").await;
6453 assert_eq!(foo, Some(Value::String("bar".into())));
6454 }
6455
6456 #[tokio::test]
6457 async fn test_source_with_dot_alias() {
6458 let kernel = Kernel::transient().expect("failed to create kernel");
6459
6460 kernel
6462 .execute(r#"write "/vars.kai" 'X=42'"#)
6463 .await
6464 .expect("write failed");
6465
6466 let result = kernel
6468 .execute(r#". "/vars.kai""#)
6469 .await
6470 .expect(". failed");
6471
6472 assert!(result.ok(), ". should succeed");
6473
6474 let x = kernel.get_var("X").await;
6476 assert_eq!(x, Some(Value::Int(42)));
6477 }
6478
6479 #[tokio::test]
6480 async fn test_source_not_found() {
6481 let kernel = Kernel::transient().expect("failed to create kernel");
6482
6483 let result = kernel
6485 .execute(r#"source "/nonexistent.kai""#)
6486 .await
6487 .expect("source should not fail with error");
6488
6489 assert!(!result.ok(), "source of non-existent file should fail");
6490 assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
6491 }
6492
6493 #[tokio::test]
6494 async fn test_source_missing_filename() {
6495 let kernel = Kernel::transient().expect("failed to create kernel");
6496
6497 let result = kernel
6499 .execute("source")
6500 .await
6501 .expect("source should not fail with error");
6502
6503 assert!(!result.ok(), "source without filename should fail");
6504 assert!(result.err.contains("missing filename"), "error should mention missing filename");
6505 }
6506
6507 #[tokio::test]
6508 async fn test_source_executes_multiple_statements() {
6509 let kernel = Kernel::transient().expect("failed to create kernel");
6510
6511 kernel
6513 .execute(r#"write "/multi.kai" 'A=1
6514B=2
6515C=3'"#)
6516 .await
6517 .expect("write failed");
6518
6519 kernel
6521 .execute(r#"source "/multi.kai""#)
6522 .await
6523 .expect("source failed");
6524
6525 assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
6527 assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
6528 assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
6529 }
6530
6531 #[tokio::test]
6532 async fn test_source_can_define_functions() {
6533 let kernel = Kernel::transient().expect("failed to create kernel");
6534
6535 kernel
6537 .execute(r#"write "/functions.kai" 'greet() {
6538 echo "Hello, $1!"
6539}'"#)
6540 .await
6541 .expect("write failed");
6542
6543 kernel
6545 .execute(r#"source "/functions.kai""#)
6546 .await
6547 .expect("source failed");
6548
6549 let result = kernel
6551 .execute(r#"greet "World""#)
6552 .await
6553 .expect("greet failed");
6554
6555 assert!(result.ok());
6556 assert!(result.text_out().contains("Hello, World!"));
6557 }
6558
6559 #[tokio::test]
6560 async fn test_source_inherits_error_exit() {
6561 let kernel = Kernel::transient().expect("failed to create kernel");
6562
6563 kernel.execute("set -e").await.expect("set -e failed");
6565
6566 kernel
6568 .execute(r#"write "/fail.kai" 'BEFORE="yes"
6569false
6570AFTER="yes"'"#)
6571 .await
6572 .expect("write failed");
6573
6574 kernel
6576 .execute(r#"source "/fail.kai""#)
6577 .await
6578 .ok();
6579
6580 let before = kernel.get_var("BEFORE").await;
6582 assert_eq!(before, Some(Value::String("yes".into())));
6583
6584 }
6587
6588 #[tokio::test]
6593 async fn test_set_e_and_chain_left_fails() {
6594 let kernel = Kernel::transient().expect("failed to create kernel");
6596 kernel.execute("set -e").await.expect("set -e failed");
6597
6598 kernel
6599 .execute("false && echo hi; REACHED=1")
6600 .await
6601 .expect("execution failed");
6602
6603 let reached = kernel.get_var("REACHED").await;
6604 assert_eq!(
6605 reached,
6606 Some(Value::Int(1)),
6607 "set -e should not trigger on left side of &&"
6608 );
6609 }
6610
6611 #[tokio::test]
6612 async fn test_set_e_and_chain_right_fails() {
6613 let kernel = Kernel::transient().expect("failed to create kernel");
6615 kernel.execute("set -e").await.expect("set -e failed");
6616
6617 kernel
6618 .execute("true && false; REACHED=1")
6619 .await
6620 .expect("execution failed");
6621
6622 let reached = kernel.get_var("REACHED").await;
6623 assert!(
6624 reached.is_none(),
6625 "set -e should trigger when right side of && fails"
6626 );
6627 }
6628
6629 #[tokio::test]
6630 async fn test_set_e_or_chain_recovers() {
6631 let kernel = Kernel::transient().expect("failed to create kernel");
6633 kernel.execute("set -e").await.expect("set -e failed");
6634
6635 kernel
6636 .execute("false || echo recovered; REACHED=1")
6637 .await
6638 .expect("execution failed");
6639
6640 let reached = kernel.get_var("REACHED").await;
6641 assert_eq!(
6642 reached,
6643 Some(Value::Int(1)),
6644 "set -e should not trigger when || recovers the failure"
6645 );
6646 }
6647
6648 #[tokio::test]
6649 async fn test_set_e_or_chain_both_fail() {
6650 let kernel = Kernel::transient().expect("failed to create kernel");
6652 kernel.execute("set -e").await.expect("set -e failed");
6653
6654 kernel
6655 .execute("false || false; REACHED=1")
6656 .await
6657 .expect("execution failed");
6658
6659 let reached = kernel.get_var("REACHED").await;
6660 assert!(
6661 reached.is_none(),
6662 "set -e should trigger when || chain ultimately fails"
6663 );
6664 }
6665
6666 fn schedule_cancel(kernel: &Arc<Kernel>, delay: std::time::Duration) {
6673 let k = Arc::clone(kernel);
6674 std::thread::spawn(move || {
6675 std::thread::sleep(delay);
6676 k.cancel();
6677 });
6678 }
6679
6680 #[tokio::test]
6681 async fn test_cancel_interrupts_for_loop() {
6682 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6683
6684 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
6686
6687 let result = kernel
6688 .execute("for i in $(seq 1 100000); do X=$i; done")
6689 .await
6690 .expect("execute failed");
6691
6692 assert_eq!(result.code, 130, "cancelled execution should exit with code 130");
6693
6694 let x = kernel.get_var("X").await;
6696 if let Some(Value::Int(n)) = x {
6697 assert!(n < 100000, "loop should have been interrupted before finishing, got X={n}");
6698 }
6699 }
6700
6701 #[tokio::test]
6702 async fn test_cancel_interrupts_while_loop() {
6703 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6704 kernel.execute("COUNT=0").await.expect("init failed");
6705
6706 schedule_cancel(&kernel, std::time::Duration::from_millis(10));
6707
6708 let result = kernel
6709 .execute("while true; do COUNT=$((COUNT + 1)); done")
6710 .await
6711 .expect("execute failed");
6712
6713 assert_eq!(result.code, 130);
6714
6715 let count = kernel.get_var("COUNT").await;
6716 if let Some(Value::Int(n)) = count {
6717 assert!(n > 0, "loop should have run at least once");
6718 }
6719 }
6720
6721 #[tokio::test]
6722 async fn test_reset_after_cancel() {
6723 let kernel = Kernel::transient().expect("failed to create kernel");
6725 kernel.cancel(); let result = kernel.execute("echo hello").await.expect("execute failed");
6728 assert!(result.ok(), "execute after cancel should succeed");
6729 assert_eq!(result.text_out().trim(), "hello");
6730 }
6731
6732 #[tokio::test]
6733 async fn test_cancel_interrupts_statement_sequence() {
6734 let kernel = Arc::new(Kernel::transient().expect("failed to create kernel"));
6735
6736 schedule_cancel(&kernel, std::time::Duration::from_millis(50));
6738
6739 let result = kernel
6740 .execute("STEP=1; sleep 5; STEP=2; sleep 5; STEP=3")
6741 .await
6742 .expect("execute failed");
6743
6744 assert_eq!(result.code, 130);
6745
6746 let step = kernel.get_var("STEP").await;
6748 assert_eq!(step, Some(Value::Int(1)), "cancel should stop before STEP=2");
6749 }
6750
6751 #[tokio::test]
6756 async fn test_case_simple_match() {
6757 let kernel = Kernel::transient().expect("failed to create kernel");
6758
6759 let result = kernel
6760 .execute(r#"
6761 case "hello" in
6762 hello) echo "matched hello" ;;
6763 world) echo "matched world" ;;
6764 esac
6765 "#)
6766 .await
6767 .expect("case failed");
6768
6769 assert!(result.ok());
6770 assert_eq!(result.text_out().trim(), "matched hello");
6771 }
6772
6773 #[tokio::test]
6774 async fn test_case_wildcard_match() {
6775 let kernel = Kernel::transient().expect("failed to create kernel");
6776
6777 let result = kernel
6778 .execute(r#"
6779 case "main.rs" in
6780 *.py) echo "Python" ;;
6781 *.rs) echo "Rust" ;;
6782 *) echo "Unknown" ;;
6783 esac
6784 "#)
6785 .await
6786 .expect("case failed");
6787
6788 assert!(result.ok());
6789 assert_eq!(result.text_out().trim(), "Rust");
6790 }
6791
6792 #[tokio::test]
6793 async fn test_case_default_match() {
6794 let kernel = Kernel::transient().expect("failed to create kernel");
6795
6796 let result = kernel
6797 .execute(r#"
6798 case "unknown.xyz" in
6799 *.py) echo "Python" ;;
6800 *.rs) echo "Rust" ;;
6801 *) echo "Default" ;;
6802 esac
6803 "#)
6804 .await
6805 .expect("case failed");
6806
6807 assert!(result.ok());
6808 assert_eq!(result.text_out().trim(), "Default");
6809 }
6810
6811 #[tokio::test]
6812 async fn test_case_no_match() {
6813 let kernel = Kernel::transient().expect("failed to create kernel");
6814
6815 let result = kernel
6817 .execute(r#"
6818 case "nope" in
6819 "yes") echo "yes" ;;
6820 "no") echo "no" ;;
6821 esac
6822 "#)
6823 .await
6824 .expect("case failed");
6825
6826 assert!(result.ok());
6827 assert!(result.text_out().is_empty(), "no match should produce empty output");
6828 }
6829
6830 #[tokio::test]
6831 async fn test_case_with_variable() {
6832 let kernel = Kernel::transient().expect("failed to create kernel");
6833
6834 kernel.execute(r#"LANG="rust""#).await.expect("set failed");
6835
6836 let result = kernel
6837 .execute(r#"
6838 case ${LANG} in
6839 python) echo "snake" ;;
6840 rust) echo "crab" ;;
6841 go) echo "gopher" ;;
6842 esac
6843 "#)
6844 .await
6845 .expect("case failed");
6846
6847 assert!(result.ok());
6848 assert_eq!(result.text_out().trim(), "crab");
6849 }
6850
6851 #[tokio::test]
6852 async fn test_case_multiple_patterns() {
6853 let kernel = Kernel::transient().expect("failed to create kernel");
6854
6855 let result = kernel
6856 .execute(r#"
6857 case "yes" in
6858 "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
6859 "n"|"no"|"N"|"NO") echo "negative" ;;
6860 esac
6861 "#)
6862 .await
6863 .expect("case failed");
6864
6865 assert!(result.ok());
6866 assert_eq!(result.text_out().trim(), "affirmative");
6867 }
6868
6869 #[tokio::test]
6870 async fn test_case_glob_question_mark() {
6871 let kernel = Kernel::transient().expect("failed to create kernel");
6872
6873 let result = kernel
6874 .execute(r#"
6875 case "test1" in
6876 test?) echo "matched test?" ;;
6877 *) echo "default" ;;
6878 esac
6879 "#)
6880 .await
6881 .expect("case failed");
6882
6883 assert!(result.ok());
6884 assert_eq!(result.text_out().trim(), "matched test?");
6885 }
6886
6887 #[tokio::test]
6888 async fn test_case_char_class() {
6889 let kernel = Kernel::transient().expect("failed to create kernel");
6890
6891 let result = kernel
6892 .execute(r#"
6893 case "Yes" in
6894 [Yy]*) echo "yes-like" ;;
6895 [Nn]*) echo "no-like" ;;
6896 esac
6897 "#)
6898 .await
6899 .expect("case failed");
6900
6901 assert!(result.ok());
6902 assert_eq!(result.text_out().trim(), "yes-like");
6903 }
6904
6905 #[tokio::test]
6910 async fn test_cat_from_pipeline() {
6911 let kernel = Kernel::transient().expect("failed to create kernel");
6912
6913 let result = kernel
6914 .execute(r#"echo "piped text" | cat"#)
6915 .await
6916 .expect("cat pipeline failed");
6917
6918 assert!(result.ok(), "cat failed: {}", result.err);
6919 assert_eq!(result.text_out().trim(), "piped text");
6920 }
6921
6922 #[tokio::test]
6923 async fn test_cat_from_pipeline_multiline() {
6924 let kernel = Kernel::transient().expect("failed to create kernel");
6925
6926 let result = kernel
6927 .execute(r#"echo "line1\nline2" | cat -n"#)
6928 .await
6929 .expect("cat pipeline failed");
6930
6931 assert!(result.ok(), "cat failed: {}", result.err);
6932 assert!(result.text_out().contains("1\t"), "output: {}", result.text_out());
6933 }
6934
6935 #[tokio::test]
6940 async fn test_heredoc_basic() {
6941 let kernel = Kernel::transient().expect("failed to create kernel");
6942
6943 let result = kernel
6944 .execute("cat <<EOF\nhello\nEOF")
6945 .await
6946 .expect("heredoc failed");
6947
6948 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
6949 assert_eq!(result.text_out().trim(), "hello");
6950 }
6951
6952 #[tokio::test]
6953 async fn test_arithmetic_in_string() {
6954 let kernel = Kernel::transient().expect("failed to create kernel");
6955
6956 let result = kernel
6957 .execute(r#"echo "result: $((1 + 2))""#)
6958 .await
6959 .expect("arithmetic in string failed");
6960
6961 assert!(result.ok(), "echo failed: {}", result.err);
6962 assert_eq!(result.text_out().trim(), "result: 3");
6963 }
6964
6965 #[tokio::test]
6966 async fn test_heredoc_multiline() {
6967 let kernel = Kernel::transient().expect("failed to create kernel");
6968
6969 let result = kernel
6970 .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
6971 .await
6972 .expect("heredoc failed");
6973
6974 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
6975 assert!(result.text_out().contains("line1"), "output: {}", result.text_out());
6976 assert!(result.text_out().contains("line2"), "output: {}", result.text_out());
6977 assert!(result.text_out().contains("line3"), "output: {}", result.text_out());
6978 }
6979
6980 #[tokio::test]
6981 async fn test_heredoc_variable_expansion() {
6982 let kernel = Kernel::transient().expect("failed to create kernel");
6984
6985 kernel.execute("GREETING=hello").await.expect("set var");
6986
6987 let result = kernel
6988 .execute("cat <<EOF\n$GREETING world\nEOF")
6989 .await
6990 .expect("heredoc expansion failed");
6991
6992 assert!(result.ok(), "heredoc expansion failed: {}", result.err);
6993 assert_eq!(result.text_out().trim(), "hello world");
6994 }
6995
6996 #[tokio::test]
6997 async fn test_heredoc_quoted_no_expansion() {
6998 let kernel = Kernel::transient().expect("failed to create kernel");
7000
7001 kernel.execute("GREETING=hello").await.expect("set var");
7002
7003 let result = kernel
7004 .execute("cat <<'EOF'\n$GREETING world\nEOF")
7005 .await
7006 .expect("quoted heredoc failed");
7007
7008 assert!(result.ok(), "quoted heredoc failed: {}", result.err);
7009 assert_eq!(result.text_out().trim(), "$GREETING world");
7010 }
7011
7012 #[tokio::test]
7013 async fn test_heredoc_default_value_expansion() {
7014 let kernel = Kernel::transient().expect("failed to create kernel");
7016
7017 let result = kernel
7018 .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
7019 .await
7020 .expect("heredoc default expansion failed");
7021
7022 assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
7023 assert_eq!(result.text_out().trim(), "fallback");
7024 }
7025
7026 #[tokio::test]
7031 async fn test_read_from_pipeline() {
7032 let kernel = Kernel::transient().expect("failed to create kernel");
7033
7034 let result = kernel
7036 .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
7037 .await
7038 .expect("read pipeline failed");
7039
7040 assert!(result.ok(), "read failed: {}", result.err);
7041 assert!(result.text_out().contains("Hello, Alice"), "output: {}", result.text_out());
7042 }
7043
7044 #[tokio::test]
7045 async fn test_read_multiple_vars_from_pipeline() {
7046 let kernel = Kernel::transient().expect("failed to create kernel");
7047
7048 let result = kernel
7049 .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
7050 .await
7051 .expect("read pipeline failed");
7052
7053 assert!(result.ok(), "read failed: {}", result.err);
7054 assert!(result.text_out().contains("John is 42"), "output: {}", result.text_out());
7055 }
7056
7057 #[tokio::test]
7062 async fn test_posix_function_with_positional_params() {
7063 let kernel = Kernel::transient().expect("failed to create kernel");
7064
7065 kernel
7067 .execute(r#"greet() { echo "Hello, $1!" }"#)
7068 .await
7069 .expect("function definition failed");
7070
7071 let result = kernel
7073 .execute(r#"greet "Amy""#)
7074 .await
7075 .expect("function call failed");
7076
7077 assert!(result.ok(), "greet failed: {}", result.err);
7078 assert_eq!(result.text_out().trim(), "Hello, Amy!");
7079 }
7080
7081 #[tokio::test]
7082 async fn test_posix_function_multiple_args() {
7083 let kernel = Kernel::transient().expect("failed to create kernel");
7084
7085 kernel
7087 .execute(r#"add_greeting() { echo "$1 $2!" }"#)
7088 .await
7089 .expect("function definition failed");
7090
7091 let result = kernel
7093 .execute(r#"add_greeting "Hello" "World""#)
7094 .await
7095 .expect("function call failed");
7096
7097 assert!(result.ok(), "function failed: {}", result.err);
7098 assert_eq!(result.text_out().trim(), "Hello World!");
7099 }
7100
7101 #[tokio::test]
7102 async fn test_bash_function_with_positional_params() {
7103 let kernel = Kernel::transient().expect("failed to create kernel");
7104
7105 kernel
7107 .execute(r#"function greet { echo "Hi $1" }"#)
7108 .await
7109 .expect("function definition failed");
7110
7111 let result = kernel
7113 .execute(r#"greet "Bob""#)
7114 .await
7115 .expect("function call failed");
7116
7117 assert!(result.ok(), "greet failed: {}", result.err);
7118 assert_eq!(result.text_out().trim(), "Hi Bob");
7119 }
7120
7121 #[tokio::test]
7122 async fn test_shell_function_with_all_args() {
7123 let kernel = Kernel::transient().expect("failed to create kernel");
7124
7125 kernel
7127 .execute(r#"echo_all() { echo "args: $@" }"#)
7128 .await
7129 .expect("function definition failed");
7130
7131 let result = kernel
7133 .execute(r#"echo_all "a" "b" "c""#)
7134 .await
7135 .expect("function call failed");
7136
7137 assert!(result.ok(), "function failed: {}", result.err);
7138 assert_eq!(result.text_out().trim(), "args: a b c");
7139 }
7140
7141 #[tokio::test]
7142 async fn test_shell_function_with_arg_count() {
7143 let kernel = Kernel::transient().expect("failed to create kernel");
7144
7145 kernel
7147 .execute(r#"count_args() { echo "count: $#" }"#)
7148 .await
7149 .expect("function definition failed");
7150
7151 let result = kernel
7153 .execute(r#"count_args "x" "y" "z""#)
7154 .await
7155 .expect("function call failed");
7156
7157 assert!(result.ok(), "function failed: {}", result.err);
7158 assert_eq!(result.text_out().trim(), "count: 3");
7159 }
7160
7161 #[tokio::test]
7162 async fn test_shell_function_shared_scope() {
7163 let kernel = Kernel::transient().expect("failed to create kernel");
7164
7165 kernel
7167 .execute(r#"PARENT_VAR="visible""#)
7168 .await
7169 .expect("set failed");
7170
7171 kernel
7173 .execute(r#"modify_parent() {
7174 echo "saw: ${PARENT_VAR}"
7175 PARENT_VAR="changed by function"
7176 }"#)
7177 .await
7178 .expect("function definition failed");
7179
7180 let result = kernel.execute("modify_parent").await.expect("function failed");
7182
7183 assert!(
7184 result.text_out().contains("visible"),
7185 "Shell function should access parent scope, got: {}",
7186 result.text_out()
7187 );
7188
7189 let var = kernel.get_var("PARENT_VAR").await;
7191 assert_eq!(
7192 var,
7193 Some(Value::String("changed by function".into())),
7194 "Shell function should modify parent scope"
7195 );
7196 }
7197
7198 #[tokio::test]
7203 async fn test_script_execution_from_path() {
7204 let kernel = Kernel::transient().expect("failed to create kernel");
7205
7206 kernel.execute(r#"mkdir "/bin""#).await.ok();
7208 kernel
7209 .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
7210 .await
7211 .expect("write script failed");
7212
7213 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
7215
7216 let result = kernel
7218 .execute("hello")
7219 .await
7220 .expect("script execution failed");
7221
7222 assert!(result.ok(), "script failed: {}", result.err);
7223 assert_eq!(result.text_out().trim(), "Hello from script!");
7224 }
7225
7226 #[tokio::test]
7227 async fn test_script_with_args() {
7228 let kernel = Kernel::transient().expect("failed to create kernel");
7229
7230 kernel.execute(r#"mkdir "/bin""#).await.ok();
7232 kernel
7233 .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
7234 .await
7235 .expect("write script failed");
7236
7237 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
7239
7240 let result = kernel
7242 .execute(r#"greet "World""#)
7243 .await
7244 .expect("script execution failed");
7245
7246 assert!(result.ok(), "script failed: {}", result.err);
7247 assert_eq!(result.text_out().trim(), "Hello, World!");
7248 }
7249
7250 #[tokio::test]
7251 async fn test_script_not_found() {
7252 let kernel = Kernel::transient().expect("failed to create kernel");
7253
7254 kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
7256
7257 let result = kernel
7259 .execute("noscript")
7260 .await
7261 .expect("execution failed");
7262
7263 assert!(!result.ok(), "should fail with command not found");
7264 assert_eq!(result.code, 127);
7265 assert!(result.err.contains("command not found"));
7266 }
7267
7268 #[tokio::test]
7269 async fn test_script_path_search_order() {
7270 let kernel = Kernel::transient().expect("failed to create kernel");
7271
7272 kernel.execute(r#"mkdir "/first""#).await.ok();
7275 kernel.execute(r#"mkdir "/second""#).await.ok();
7276 kernel
7277 .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
7278 .await
7279 .expect("write failed");
7280 kernel
7281 .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
7282 .await
7283 .expect("write failed");
7284
7285 kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
7287
7288 let result = kernel
7290 .execute("myscript")
7291 .await
7292 .expect("script execution failed");
7293
7294 assert!(result.ok(), "script failed: {}", result.err);
7295 assert_eq!(result.text_out().trim(), "from first");
7296 }
7297
7298 #[tokio::test]
7303 async fn test_last_exit_code_success() {
7304 let kernel = Kernel::transient().expect("failed to create kernel");
7305
7306 let result = kernel.execute("true; echo $?").await.expect("execution failed");
7308 assert!(result.text_out().contains("0"), "expected 0, got: {}", result.text_out());
7309 }
7310
7311 #[tokio::test]
7312 async fn test_last_exit_code_failure() {
7313 let kernel = Kernel::transient().expect("failed to create kernel");
7314
7315 let result = kernel.execute("false; echo $?").await.expect("execution failed");
7317 assert!(result.text_out().contains("1"), "expected 1, got: {}", result.text_out());
7318 }
7319
7320 #[tokio::test]
7321 async fn test_current_pid() {
7322 let kernel = Kernel::transient().expect("failed to create kernel");
7323
7324 let result = kernel.execute("echo $$").await.expect("execution failed");
7325 let pid: u32 = result.text_out().trim().parse().expect("PID should be a number");
7327 assert!(pid > 0, "PID should be positive");
7328 }
7329
7330 #[tokio::test]
7331 async fn test_unset_variable_expands_to_empty() {
7332 let kernel = Kernel::transient().expect("failed to create kernel");
7333
7334 let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
7336 assert_eq!(result.text_out().trim(), "prefix::suffix");
7337 }
7338
7339 #[tokio::test]
7340 async fn test_eq_ne_operators() {
7341 let kernel = Kernel::transient().expect("failed to create kernel");
7342
7343 let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
7345 assert_eq!(result.text_out().trim(), "eq works");
7346
7347 let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
7349 assert_eq!(result.text_out().trim(), "ne works");
7350
7351 let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
7353 assert_eq!(result.text_out().trim(), "correct");
7354 }
7355
7356 #[tokio::test]
7357 async fn test_escaped_dollar_in_string() {
7358 let kernel = Kernel::transient().expect("failed to create kernel");
7359
7360 let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
7362 assert_eq!(result.text_out().trim(), "$100");
7363 }
7364
7365 #[tokio::test]
7366 async fn test_special_vars_in_interpolation() {
7367 let kernel = Kernel::transient().expect("failed to create kernel");
7368
7369 let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
7371 assert_eq!(result.text_out().trim(), "exit: 0");
7372
7373 let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
7375 assert!(result.text_out().starts_with("pid: "), "unexpected output: {}", result.text_out());
7376 let text = result.text_out();
7377 let pid_part = text.trim().strip_prefix("pid: ").unwrap();
7378 let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
7379 }
7380
7381 #[tokio::test]
7386 async fn test_command_subst_assignment() {
7387 let kernel = Kernel::transient().expect("failed to create kernel");
7388
7389 let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
7391 assert_eq!(result.text_out().trim(), "hello");
7392 }
7393
7394 #[tokio::test]
7395 async fn test_command_subst_with_args() {
7396 let kernel = Kernel::transient().expect("failed to create kernel");
7397
7398 let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
7400 assert_eq!(result.text_out().trim(), "a b c");
7401 }
7402
7403 #[tokio::test]
7404 async fn test_command_subst_nested_vars() {
7405 let kernel = Kernel::transient().expect("failed to create kernel");
7406
7407 let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
7409 assert_eq!(result.text_out().trim(), "hello world");
7410 }
7411
7412 #[tokio::test]
7413 async fn test_background_job_basic() {
7414 use std::time::Duration;
7415
7416 let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
7417
7418 let result = kernel.execute("echo hello &").await.expect("execution failed");
7420 assert!(result.ok(), "background command should succeed: {}", result.err);
7421 assert!(result.text_out().contains("[1]"), "should return job ID: {}", result.text_out());
7422
7423 tokio::time::sleep(Duration::from_millis(100)).await;
7425
7426 let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
7428 assert!(status.ok(), "status should succeed: {}", status.err);
7429 assert!(
7430 status.text_out().contains("done:") || status.text_out().contains("running"),
7431 "should have valid status: {}",
7432 status.text_out()
7433 );
7434
7435 let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
7437 assert!(stdout.ok());
7438 assert!(stdout.text_out().contains("hello"));
7439 }
7440
7441 #[tokio::test]
7442 async fn test_heredoc_piped_to_command() {
7443 let kernel = Kernel::transient().expect("kernel");
7445 let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
7446 assert!(result.ok(), "heredoc | cat failed: {}", result.err);
7447 assert_eq!(result.text_out().trim(), "hello world");
7448 }
7449
7450 fn transient_with_tempdir() -> (Kernel, tempfile::TempDir, String) {
7458 let kernel = Kernel::transient().expect("kernel");
7459 let tmp = tempfile::tempdir().expect("tempdir");
7460 let dir = tmp.path().display().to_string();
7461 (kernel, tmp, dir)
7462 }
7463
7464 #[tokio::test]
7465 async fn test_for_loop_glob_iterates() {
7466 let (kernel, _tmp, dir) = transient_with_tempdir();
7468 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7469 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
7470 let result = kernel.execute(&format!(r#"
7471 N=0
7472 for F in $(glob "{dir}/*.txt"); do
7473 N=$((N + 1))
7474 done
7475 echo $N
7476 "#)).await.unwrap();
7477 assert!(result.ok(), "for glob failed: {}", result.err);
7478 assert_eq!(result.text_out().trim(), "2", "Should iterate 2 files, got: {}", result.text_out());
7479 }
7480
7481 #[tokio::test]
7482 async fn test_bare_glob_expansion_echo() {
7483 let (kernel, _tmp, dir) = transient_with_tempdir();
7484 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7485 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
7486 kernel.execute(&format!("echo c > {dir}/c.rs")).await.unwrap();
7487 kernel.execute(&format!("cd {dir}")).await.unwrap();
7488 let result = kernel.execute("echo *.txt").await.unwrap();
7489 assert!(result.ok(), "echo *.txt failed: {}", result.err);
7490 let out = result.text_out();
7491 let out = out.trim();
7492 assert!(out.contains("a.txt"), "missing a.txt in: {}", out);
7494 assert!(out.contains("b.txt"), "missing b.txt in: {}", out);
7495 assert!(!out.contains("c.rs"), "should not contain c.rs in: {}", out);
7496 }
7497
7498 #[tokio::test]
7499 async fn test_bare_glob_no_matches_errors() {
7500 let (kernel, _tmp, dir) = transient_with_tempdir();
7501 kernel.execute(&format!("cd {dir}")).await.unwrap();
7502 let result = kernel.execute("echo *.nonexistent").await;
7503 match &result {
7504 Ok(exec) => {
7505 assert!(!exec.ok(), "expected failure, got success: out={}, err={}", exec.text_out(), exec.err);
7507 assert!(exec.err.contains("no matches"), "error should say no matches: {}", exec.err);
7508 }
7509 Err(e) => {
7510 assert!(e.to_string().contains("no matches"), "error should say no matches: {}", e);
7511 }
7512 }
7513 }
7514
7515 #[tokio::test]
7516 async fn test_bare_glob_disabled_with_set() {
7517 let (kernel, _tmp, dir) = transient_with_tempdir();
7518 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7519 kernel.execute(&format!("cd {dir}")).await.unwrap();
7520 kernel.execute("set +o glob").await.unwrap();
7522 let result = kernel.execute("echo *.txt").await.unwrap();
7523 assert!(result.ok(), "echo should succeed: {}", result.err);
7525 assert_eq!(result.text_out().trim(), "*.txt", "should be literal: {}", result.text_out());
7526 }
7527
7528 #[tokio::test]
7529 async fn test_bare_glob_quoted_not_expanded() {
7530 let (kernel, _tmp, dir) = transient_with_tempdir();
7531 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7532 kernel.execute(&format!("cd {dir}")).await.unwrap();
7533 let result = kernel.execute("echo \"*.txt\"").await.unwrap();
7535 assert!(result.ok(), "echo should succeed: {}", result.err);
7536 assert_eq!(result.text_out().trim(), "*.txt", "quoted should be literal: {}", result.text_out());
7537 }
7538
7539 #[tokio::test]
7540 async fn test_bare_glob_for_loop() {
7541 let (kernel, _tmp, dir) = transient_with_tempdir();
7542 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
7543 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
7544 kernel.execute(&format!("cd {dir}")).await.unwrap();
7545 let result = kernel.execute(r#"
7546 N=0
7547 for f in *.txt; do
7548 N=$((N + 1))
7549 done
7550 echo $N
7551 "#).await.unwrap();
7552 assert!(result.ok(), "for loop failed: {}", result.err);
7553 assert_eq!(result.text_out().trim(), "2", "should iterate 2 files: {}", result.text_out());
7554 }
7555
7556 #[tokio::test]
7557 async fn test_glob_in_assignment_is_literal() {
7558 let kernel = Kernel::transient().expect("kernel");
7559 let result = kernel.execute("X=*.txt; echo $X").await.unwrap();
7560 assert!(result.ok());
7561 assert_eq!(result.text_out().trim(), "*.txt", "glob in assignment should be literal");
7562 }
7563
7564 #[tokio::test]
7565 async fn test_glob_in_test_expr_is_literal() {
7566 let kernel = Kernel::transient().expect("kernel");
7567 let result = kernel.execute(r#"
7568 if [[ *.txt == "*.txt" ]]; then
7569 echo "match"
7570 else
7571 echo "no"
7572 fi
7573 "#).await.unwrap();
7574 assert!(result.ok());
7575 assert_eq!(result.text_out().trim(), "match", "glob in test expr should be literal");
7576 }
7577
7578 #[tokio::test]
7579 async fn test_command_subst_echo_not_iterable() {
7580 let kernel = Kernel::transient().expect("kernel");
7582 let result = kernel.execute(r#"
7583 N=0
7584 for X in $(echo "a b c"); do N=$((N + 1)); done
7585 echo $N
7586 "#).await.unwrap();
7587 assert!(result.ok());
7588 assert_eq!(result.text_out().trim(), "1", "echo should be one item: {}", result.text_out());
7589 }
7590
7591 #[test]
7594 fn test_accumulate_preserves_own_newlines() {
7595 let mut acc = ExecResult::success("line1\n");
7598 let new = ExecResult::success("line2\n");
7599 accumulate_result(&mut acc, &new);
7600 assert_eq!(&*acc.text_out(), "line1\nline2\n");
7601 assert!(!acc.text_out().contains("\n\n"), "should not have double newlines: {:?}", acc.text_out());
7602 }
7603
7604 #[test]
7605 fn test_accumulate_inserts_no_separator() {
7606 let mut acc = ExecResult::success("line1");
7609 let new = ExecResult::success("line2");
7610 accumulate_result(&mut acc, &new);
7611 assert_eq!(&*acc.text_out(), "line1line2");
7612 }
7613
7614 #[test]
7615 fn test_accumulate_empty_into_nonempty() {
7616 let mut acc = ExecResult::success("");
7617 let new = ExecResult::success("hello\n");
7618 accumulate_result(&mut acc, &new);
7619 assert_eq!(&*acc.text_out(), "hello\n");
7620 }
7621
7622 #[test]
7623 fn test_accumulate_nonempty_into_empty() {
7624 let mut acc = ExecResult::success("hello\n");
7625 let new = ExecResult::success("");
7626 accumulate_result(&mut acc, &new);
7627 assert_eq!(&*acc.text_out(), "hello\n");
7628 }
7629
7630 #[test]
7631 fn test_accumulate_stderr_no_double_newlines() {
7632 let mut acc = ExecResult::failure(1, "err1\n");
7633 let new = ExecResult::failure(1, "err2\n");
7634 accumulate_result(&mut acc, &new);
7635 assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
7636 }
7637
7638 #[tokio::test]
7639 async fn test_multiple_echo_no_blank_lines() {
7640 let kernel = Kernel::transient().expect("kernel");
7641 let result = kernel
7642 .execute("echo one\necho two\necho three")
7643 .await
7644 .expect("execution failed");
7645 assert!(result.ok());
7646 assert_eq!(&*result.text_out(), "one\ntwo\nthree\n");
7647 }
7648
7649 #[tokio::test]
7650 async fn test_for_loop_no_blank_lines() {
7651 let kernel = Kernel::transient().expect("kernel");
7652 let result = kernel
7653 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
7654 .await
7655 .expect("execution failed");
7656 assert!(result.ok());
7657 assert_eq!(&*result.text_out(), "item: a\nitem: b\nitem: c\n");
7658 }
7659
7660 #[tokio::test]
7661 async fn test_for_command_subst_no_blank_lines() {
7662 let kernel = Kernel::transient().expect("kernel");
7663 let result = kernel
7664 .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
7665 .await
7666 .expect("execution failed");
7667 assert!(result.ok());
7668 assert_eq!(&*result.text_out(), "n=1\nn=2\nn=3\n");
7669 }
7670
7671 fn multi_consume_schema() -> crate::tools::ToolSchema {
7679 use crate::tools::{ParamSchema, ToolSchema};
7680 ToolSchema::new("test", "multi-consume smoke")
7681 .param(
7682 ParamSchema::optional("pair", "array", Value::Null, "name+value pair")
7683 .consumes(2),
7684 )
7685 }
7686
7687 fn pos(s: &str) -> Arg {
7688 Arg::Positional(Expr::Literal(Value::String(s.to_string())))
7689 }
7690
7691 #[tokio::test]
7692 async fn build_args_multi_consume_single_occurrence() {
7693 let kernel = Kernel::transient().expect("kernel");
7694 let schema = multi_consume_schema();
7695 let args = vec![
7697 Arg::LongFlag("pair".into()),
7698 pos("NAME"),
7699 pos("VALUE"),
7700 pos("filter"),
7701 ];
7702 let built = kernel
7703 .build_args_async(&args, Some(&schema))
7704 .await
7705 .expect("build_args should succeed");
7706
7707 let pair = built.named.get("pair").expect("named[pair] missing");
7710 match pair {
7711 Value::Json(serde_json::Value::Array(occurrences)) => {
7712 assert_eq!(occurrences.len(), 1, "expected one occurrence");
7713 match &occurrences[0] {
7714 serde_json::Value::Array(values) => {
7715 assert_eq!(values.len(), 2, "pair must have 2 values");
7716 assert_eq!(values[0], serde_json::Value::String("NAME".into()));
7717 assert_eq!(values[1], serde_json::Value::String("VALUE".into()));
7718 }
7719 other => panic!("expected inner array, got {other:?}"),
7720 }
7721 }
7722 other => panic!("expected Json(Array(...)) for named[pair], got {other:?}"),
7723 }
7724
7725 assert_eq!(built.positional.len(), 1);
7727 assert_eq!(built.positional[0], Value::String("filter".into()));
7728 }
7729 #[tokio::test]
7730 async fn build_args_multi_consume_two_occurrences_accumulate() {
7731 let kernel = Kernel::transient().expect("kernel");
7732 let schema = multi_consume_schema();
7733 let args = vec![
7735 Arg::LongFlag("pair".into()),
7736 pos("A"),
7737 pos("1"),
7738 Arg::LongFlag("pair".into()),
7739 pos("B"),
7740 pos("2"),
7741 pos("filter"),
7742 ];
7743 let built = kernel
7744 .build_args_async(&args, Some(&schema))
7745 .await
7746 .expect("build_args should succeed");
7747
7748 let pair = built.named.get("pair").expect("named[pair] missing");
7749 match pair {
7750 Value::Json(serde_json::Value::Array(occurrences)) => {
7751 assert_eq!(occurrences.len(), 2, "expected two occurrences");
7752 match &occurrences[0] {
7754 serde_json::Value::Array(values) => {
7755 assert_eq!(values[0], serde_json::Value::String("A".into()));
7756 assert_eq!(values[1], serde_json::Value::String("1".into()));
7757 }
7758 other => panic!("expected inner array, got {other:?}"),
7759 }
7760 match &occurrences[1] {
7761 serde_json::Value::Array(values) => {
7762 assert_eq!(values[0], serde_json::Value::String("B".into()));
7763 assert_eq!(values[1], serde_json::Value::String("2".into()));
7764 }
7765 other => panic!("expected inner array, got {other:?}"),
7766 }
7767 }
7768 other => panic!("expected Json(Array(...)), got {other:?}"),
7769 }
7770 }
7771
7772 use crate::tools::{ParamSchema, ToolSchema};
7780
7781 fn kj_like_schema() -> ToolSchema {
7784 ToolSchema::new("kj", "incomplete backend schema")
7785 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
7786 .with_positional_mapping()
7787 }
7788
7789 #[tokio::test]
7790 async fn build_args_undeclared_space_flag_errors_under_map_positionals() {
7791 let kernel = Kernel::transient().expect("kernel");
7792 let schema = kj_like_schema();
7793 let args = vec![
7795 pos("context"),
7796 pos("create"),
7797 pos("exp"),
7798 Arg::LongFlag("type".into()),
7799 pos("explorer"),
7800 ];
7801 let err = kernel
7802 .build_args_async(&args, Some(&schema))
7803 .await
7804 .expect_err("undeclared --type with a space value must fail loud");
7805 let msg = err.to_string();
7806 assert!(msg.contains("--type"), "message should name the flag: {msg}");
7807 assert!(msg.contains("--type=explorer"), "message should suggest the = form: {msg}");
7808 assert!(msg.contains("kj"), "message should name the tool: {msg}");
7809 }
7810
7811 #[tokio::test]
7812 async fn build_args_declared_space_flag_still_binds() {
7813 let kernel = Kernel::transient().expect("kernel");
7814 let schema = ToolSchema::new("kj", "complete schema")
7816 .param(ParamSchema::optional("name", "string", Value::Null, "context name"))
7817 .param(ParamSchema::optional("type", "string", Value::Null, "role type"))
7818 .with_positional_mapping();
7819 let args = vec![
7820 pos("exp"),
7821 Arg::LongFlag("type".into()),
7822 pos("explorer"),
7823 ];
7824 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7825 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7826 }
7827
7828 #[tokio::test]
7829 async fn build_args_equals_form_binds_for_undeclared_flag() {
7830 let kernel = Kernel::transient().expect("kernel");
7831 let schema = kj_like_schema();
7832 let args = vec![
7834 pos("exp"),
7835 Arg::Named { key: "type".into(), value: Expr::Literal(Value::String("explorer".into())) },
7836 ];
7837 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7838 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7839 }
7840
7841 #[tokio::test]
7842 async fn build_args_undeclared_bool_flag_at_end_is_ok() {
7843 let kernel = Kernel::transient().expect("kernel");
7844 let schema = kj_like_schema();
7845 let args = vec![pos("exp"), Arg::LongFlag("force".into())];
7847 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7848 assert!(built.flags.contains("force"));
7849 }
7850
7851 #[tokio::test]
7852 async fn build_args_undeclared_flag_before_another_flag_is_ok() {
7853 let kernel = Kernel::transient().expect("kernel");
7854 let schema = kj_like_schema();
7855 let args = vec![
7857 Arg::LongFlag("verbose".into()),
7858 Arg::Named { key: "name".into(), value: Expr::Literal(Value::String("x".into())) },
7859 ];
7860 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7861 assert!(built.flags.contains("verbose"));
7862 }
7863
7864 #[tokio::test]
7865 async fn build_args_undeclared_space_flag_ok_for_builtin_schema() {
7866 let kernel = Kernel::transient().expect("kernel");
7867 let schema = ToolSchema::new("frobnicate", "builtin-style")
7870 .param(ParamSchema::optional("name", "string", Value::Null, "name"));
7871 let args = vec![Arg::LongFlag("frob".into()), pos("value")];
7872 let built = kernel.build_args_async(&args, Some(&schema)).await.unwrap();
7873 assert!(built.flags.contains("frob"));
7874 }
7875
7876 fn kj_tree_schema() -> ToolSchema {
7886 ToolSchema::new("kj", "subcommand tool").subcommand(
7887 ToolSchema::new("context", "context ops")
7888 .with_command_aliases(["ctx"])
7889 .subcommand(
7890 ToolSchema::new("create", "create context")
7891 .param(ParamSchema::new("type", "string").with_aliases(["t"]))
7892 .param(ParamSchema::new("force", "bool")),
7893 ),
7894 )
7895 }
7896
7897 #[tokio::test]
7898 async fn build_args_binds_deep_leaf_value_flag_space_form() {
7899 let kernel = Kernel::transient().expect("kernel");
7900 let schema = kj_tree_schema();
7901 let args = vec![
7903 pos("context"),
7904 pos("create"),
7905 Arg::LongFlag("type".into()),
7906 pos("explorer"),
7907 ];
7908 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7909 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7911 let positionals: Vec<&str> = built
7913 .positional
7914 .iter()
7915 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
7916 .collect();
7917 assert_eq!(positionals, vec!["context", "create"]);
7918 }
7919
7920 #[tokio::test]
7921 async fn build_args_leaf_bool_flag_does_not_swallow_positional() {
7922 let kernel = Kernel::transient().expect("kernel");
7923 let schema = kj_tree_schema();
7924 let args = vec![
7927 pos("context"),
7928 pos("create"),
7929 Arg::LongFlag("force".into()),
7930 pos("somearg"),
7931 ];
7932 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7933 assert!(built.flags.contains("force"), "force should be a bare flag");
7934 let positionals: Vec<&str> = built
7935 .positional
7936 .iter()
7937 .filter_map(|v| if let Value::String(s) = v { Some(s.as_str()) } else { None })
7938 .collect();
7939 assert_eq!(positionals, vec!["context", "create", "somearg"]);
7940 }
7941
7942 #[tokio::test]
7943 async fn build_args_alias_routed_leaf_binds_value_flag() {
7944 let kernel = Kernel::transient().expect("kernel");
7945 let schema = kj_tree_schema();
7946 let args = vec![
7948 pos("ctx"),
7949 pos("create"),
7950 Arg::ShortFlag("t".into()),
7951 pos("explorer"),
7952 ];
7953 let built = kernel.build_args_async(&args, Some(&schema)).await.expect("build_args");
7954 assert_eq!(built.named.get("type"), Some(&Value::String("explorer".into())));
7955 }
7956
7957 #[tokio::test]
7958 async fn build_args_computed_subcommand_selector_fails_loud() {
7959 let kernel = Kernel::transient().expect("kernel");
7960 let schema = kj_tree_schema();
7961 let args = vec![Arg::Positional(Expr::CommandSubst(vec![Stmt::Command(
7963 crate::ast::Command { name: "echo".into(), args: vec![], redirects: vec![] },
7964 )]))];
7965 let err = kernel
7966 .build_args_async(&args, Some(&schema))
7967 .await
7968 .expect_err("computed subcommand selector must error");
7969 assert!(
7970 err.to_string().contains("subcommand name is required"),
7971 "got: {err}"
7972 );
7973 }
7974
7975 #[test]
7978 fn finalize_output_renders_when_kernel_owns_it() {
7979 use crate::interpreter::{OutputData, OutputFormat};
7980 let r = ExecResult::with_output(OutputData::text("RAW"));
7981 let out = finalize_output(r, Some(OutputFormat::Json), false);
7982 assert_ne!(out.text_out(), "RAW", "kernel should reformat to JSON");
7984 }
7985
7986 #[test]
7987 fn finalize_output_skips_when_tool_owns_output() {
7988 use crate::interpreter::{OutputData, OutputFormat};
7989 let r = ExecResult::with_output(OutputData::text("RAW"));
7990 let out = finalize_output(r, Some(OutputFormat::Json), true);
7991 assert_eq!(out.text_out(), "RAW", "owned output must be left as-is");
7993 }
7994
7995 #[test]
7996 fn finalize_output_no_format_is_noop() {
7997 use crate::interpreter::OutputData;
7998 let r = ExecResult::with_output(OutputData::text("RAW"));
7999 let out = finalize_output(r, None, false);
8000 assert_eq!(out.text_out(), "RAW");
8001 }
8002
8003 #[tokio::test]
8006 async fn test_initial_vars_set_and_exported() {
8007 let config = KernelConfig::transient()
8008 .with_var("INIT_FOO", Value::String("bar".into()));
8009 let kernel = Kernel::new(config).expect("failed to create kernel");
8010
8011 assert_eq!(
8012 kernel.get_var("INIT_FOO").await,
8013 Some(Value::String("bar".into()))
8014 );
8015 assert!(
8016 kernel.scope.read().await.is_exported("INIT_FOO"),
8017 "initial_vars entries must be marked exported"
8018 );
8019 }
8020
8021 #[tokio::test]
8022 async fn test_execute_with_vars_overlay_visible() {
8023 let kernel = Kernel::transient().expect("failed to create kernel");
8024 let mut overlay = HashMap::new();
8025 overlay.insert("OVERLAY_X".to_string(), Value::String("yes".into()));
8026
8027 let result = kernel
8028 .execute_with_options(r#"echo "${OVERLAY_X}""#, ExecuteOptions::new().with_vars(overlay))
8029 .await
8030 .expect("execute failed");
8031
8032 assert!(result.ok());
8033 assert_eq!(result.text_out().trim(), "yes");
8034 }
8035
8036 #[tokio::test]
8037 async fn test_execute_with_vars_overlay_cleanup() {
8038 let kernel = Kernel::transient().expect("failed to create kernel");
8039 let mut overlay = HashMap::new();
8040 overlay.insert("EPHEMERAL".to_string(), Value::String("transient".into()));
8041
8042 kernel
8043 .execute_with_options("echo ignored", ExecuteOptions::new().with_vars(overlay))
8044 .await
8045 .expect("execute failed");
8046
8047 assert_eq!(kernel.get_var("EPHEMERAL").await, None);
8048 assert!(
8049 !kernel.scope.read().await.is_exported("EPHEMERAL"),
8050 "overlay-only export must be cleared on return"
8051 );
8052 }
8053
8054 #[tokio::test]
8055 async fn test_execute_with_vars_does_not_clobber_existing_export() {
8056 let kernel = Kernel::transient().expect("failed to create kernel");
8057 kernel
8058 .execute("export OUTER=outer")
8059 .await
8060 .expect("export failed");
8061
8062 let mut overlay = HashMap::new();
8063 overlay.insert("OUTER".to_string(), Value::String("inner".into()));
8064 let result = kernel
8065 .execute_with_options(r#"echo "${OUTER}""#, ExecuteOptions::new().with_vars(overlay))
8066 .await
8067 .expect("execute failed");
8068 assert_eq!(result.text_out().trim(), "inner");
8069
8070 assert_eq!(
8071 kernel.get_var("OUTER").await,
8072 Some(Value::String("outer".into())),
8073 "outer value must reappear after pop"
8074 );
8075 assert!(
8076 kernel.scope.read().await.is_exported("OUTER"),
8077 "outer export must survive overlay"
8078 );
8079 }
8080
8081 #[tokio::test]
8082 async fn test_execute_with_vars_inner_assignment_is_local() {
8083 let kernel = Kernel::transient().expect("failed to create kernel");
8084 let mut overlay = HashMap::new();
8085 overlay.insert("LOCAL_FOO".to_string(), Value::String("from-overlay".into()));
8086
8087 let result = kernel
8092 .execute_with_options(
8093 r#"LOCAL_FOO="reassigned"; echo "${LOCAL_FOO}""#,
8094 ExecuteOptions::new().with_vars(overlay),
8095 )
8096 .await
8097 .expect("execute failed");
8098 assert!(result.ok());
8099
8100 assert_eq!(kernel.get_var("LOCAL_FOO").await, None);
8103 }
8104
8105 #[tokio::test]
8106 async fn test_external_command_sees_exported_var() {
8107 let kernel = Kernel::transient().expect("failed to create kernel");
8108 let path = std::env::var("PATH").unwrap_or_default();
8112 let result = kernel
8113 .execute(&format!(
8114 "PATH=\"{path}\"; export EXT_FOO=bar; printenv EXT_FOO"
8115 ))
8116 .await
8117 .expect("execute failed");
8118
8119 assert!(result.ok(), "printenv should succeed: stderr={}", result.err);
8120 assert_eq!(result.text_out().trim(), "bar");
8121 }
8122
8123 #[tokio::test]
8124 async fn test_external_command_does_not_see_unexported_var() {
8125 let kernel = Kernel::transient().expect("failed to create kernel");
8126
8127 let result = kernel
8130 .execute("EXT_BAR=hidden; printenv EXT_BAR")
8131 .await
8132 .expect("execute failed");
8133
8134 assert!(!result.ok(), "printenv should fail when var is unexported");
8135 assert!(
8136 result.text_out().trim().is_empty(),
8137 "no stdout when var is missing, got: {}",
8138 result.text_out()
8139 );
8140 }
8141
8142 #[tokio::test]
8143 async fn test_external_command_does_not_see_os_env() {
8144 assert!(
8150 std::env::var_os("PATH").is_some(),
8151 "test precondition: cargo should set PATH"
8152 );
8153
8154 let kernel = Kernel::transient().expect("failed to create kernel");
8155 let result = kernel
8156 .execute("printenv PATH")
8157 .await
8158 .expect("execute failed");
8159
8160 assert!(
8161 !result.ok(),
8162 "printenv PATH must fail in hermetic kernel, got stdout={:?}",
8163 result.text_out()
8164 );
8165 assert!(
8166 result.text_out().trim().is_empty(),
8167 "no PATH in subprocess env, got stdout={:?}",
8168 result.text_out()
8169 );
8170 }
8171
8172 #[tokio::test]
8173 async fn test_execute_with_vars_overlay_reaches_subprocess() {
8174 let kernel = Kernel::transient().expect("failed to create kernel");
8175 let mut overlay = HashMap::new();
8176 overlay.insert("SUB_FOO".to_string(), Value::String("subproc".into()));
8177 overlay.insert(
8179 "PATH".to_string(),
8180 Value::String(std::env::var("PATH").unwrap_or_default()),
8181 );
8182
8183 let result = kernel
8184 .execute_with_options("printenv SUB_FOO", ExecuteOptions::new().with_vars(overlay))
8185 .await
8186 .expect("execute failed");
8187
8188 assert!(
8189 result.ok(),
8190 "printenv should succeed: code={} stdout={:?} stderr={:?}",
8191 result.code,
8192 result.text_out(),
8193 result.err
8194 );
8195 assert_eq!(result.text_out().trim(), "subproc");
8196 }
8197}