diff --git a/crates/webcodex-runner/src/main_tests.rs b/crates/webcodex-runner/src/main_tests.rs index fe20cd837..fb862ae73 100644 --- a/crates/webcodex-runner/src/main_tests.rs +++ b/crates/webcodex-runner/src/main_tests.rs @@ -1,3 +1,4 @@ +use crate::webcodex_runner::shell::ShellExecutionContext; #[test] fn check_config_is_a_separate_non_runtime_action() { assert_eq!( @@ -78,7 +79,7 @@ use crate::webcodex_runner::transport::registration::*; use crate::webcodex_runner::config::validate_shell_config; use crate::webcodex_runner::job_manager::job_manager_tests::{shell_job_request, ws_sink}; use crate::webcodex_runner::projects::{project_root_fingerprint, RunnerProjectFile}; -use crate::webcodex_runner::run_shell_with_profiles; + use crate::webcodex_runner::{ handle_prepare_managed_worktree, handle_project_lifecycle_op, handle_project_op, handle_resolve_or_register_project, @@ -415,18 +416,18 @@ fn run_profile_shell( command: &str, ) -> CommandResult { let cwd = cwd.to_string_lossy().to_string(); - run_shell_with_profiles( - 1, + (ShellExecutionContext { + generation: 1, policy, shell, project_registry_dir, cache, - Some(&cwd), - command, - None, - 10, - None, - ) + cwd: Some(&cwd), + timeout_secs: 10, + stop_requested: None, + }) + .run_shell(command, None, false, None) + .result } fn line_edit_json(result: CommandResult) -> serde_json::Value { diff --git a/crates/webcodex-runner/src/main_tests/profile_process_lifecycle.rs b/crates/webcodex-runner/src/main_tests/profile_process_lifecycle.rs index e37e7eec8..427b22cff 100644 --- a/crates/webcodex-runner/src/main_tests/profile_process_lifecycle.rs +++ b/crates/webcodex-runner/src/main_tests/profile_process_lifecycle.rs @@ -1,4 +1,5 @@ use super::*; +use crate::webcodex_runner::shell::ShellExecutionContext; #[cfg(unix)] #[test] @@ -33,18 +34,18 @@ fn shell_job_profile_prepare_stop_reaps_whole_tree() { written }); - let result = run_shell_with_profiles( - 1, - &policy, - &shell, - tmp.path(), - &cache, - Some(&cwd), - "true", - None, - 10, - Some(stop_requested.as_ref()), - ); + let result = (ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &shell, + project_registry_dir: tmp.path(), + cache: &cache, + cwd: Some(&cwd), + timeout_secs: 10, + stop_requested: Some(stop_requested.as_ref()), + }) + .run_shell("true", None, false, None) + .result; assert!(stopper.join().expect("stopper thread panicked")); assert!( @@ -90,18 +91,18 @@ fn shell_profile_prepare_timeout_cleans_up_whole_tree() { )], ); let start = Instant::now(); - let result = run_shell_with_profiles( - 1, - &unrestricted_test_policy(), - &shell, - tmp.path(), - &PreparedShellProfileCache::default(), - Some(tmp.path().to_string_lossy().as_ref()), - "true", - None, - 10, - None, - ); + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_test_policy(), + shell: &shell, + project_registry_dir: tmp.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(tmp.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_shell("true", None, false, None) + .result; assert!( wait_until_file(&markers.parent, Duration::from_secs(15)), "prepare tree markers were never written: {result:?}" @@ -158,18 +159,18 @@ fn shell_profile_prepare_stop_cleans_up_whole_tree() { written }); - let result = run_shell_with_profiles( - 1, - &unrestricted_test_policy(), - &shell, - tmp.path(), - &PreparedShellProfileCache::default(), - Some(tmp.path().to_string_lossy().as_ref()), - "true", - None, - 10, - Some(stop_requested.as_ref()), - ); + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_test_policy(), + shell: &shell, + project_registry_dir: tmp.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(tmp.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: Some(stop_requested.as_ref()), + }) + .run_shell("true", None, false, None) + .result; assert!(stopper.join().expect("stopper thread panicked")); assert!( diff --git a/crates/webcodex-runner/src/main_tests/shell_job_execution.rs b/crates/webcodex-runner/src/main_tests/shell_job_execution.rs index 915c2d13a..20c95ac9a 100644 --- a/crates/webcodex-runner/src/main_tests/shell_job_execution.rs +++ b/crates/webcodex-runner/src/main_tests/shell_job_execution.rs @@ -285,18 +285,18 @@ fn runner_real_process_shell_job_timeout_profile_reaps_descendant_process_group( // Exercise the production request path directly rather than the // test-only `run_shell` wrapper. - let result = run_shell_with_profiles( - 1, - &policy, - &shell, - tmp.path(), - &cache, - Some(&cwd), - &long_lived_descendant_command(&pid_file), - None, - 1, - None, - ); + let result = (ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &shell, + project_registry_dir: tmp.path(), + cache: &cache, + cwd: Some(&cwd), + timeout_secs: 1, + stop_requested: None, + }) + .run_shell(&long_lived_descendant_command(&pid_file), None, false, None) + .result; assert_eq!(cache.len(), 1, "prepared profile path was not used"); assert_eq!(result.exit_code, Some(-1), "{result:?}"); diff --git a/crates/webcodex-runner/src/main_tests/shell_profiles.rs b/crates/webcodex-runner/src/main_tests/shell_profiles.rs index d04828b2d..634f5e8b1 100644 --- a/crates/webcodex-runner/src/main_tests/shell_profiles.rs +++ b/crates/webcodex-runner/src/main_tests/shell_profiles.rs @@ -1,4 +1,5 @@ use super::*; +use crate::webcodex_runner::shell::ShellExecutionContext; #[test] fn prepared_profile_env_is_available_to_run_shell() { @@ -295,50 +296,50 @@ fn prepared_profile_init_script_runs_once_per_project_profile_generation() { } assert_eq!(std::fs::read_to_string(&counter).unwrap().trim(), "1"); let cwd = tmp.path().to_string_lossy().to_string(); - let result = run_shell_with_profiles( - 2, - &unrestricted_test_policy(), - &shell, - tmp.path(), - &cache, - Some(&cwd), - &shell_env_var("WEBCODEX_TEST_PROFILE"), - None, - 10, - None, - ); + let result = (ShellExecutionContext { + generation: 2, + policy: &unrestricted_test_policy(), + shell: &shell, + project_registry_dir: tmp.path(), + cache: &cache, + cwd: Some(&cwd), + timeout_secs: 10, + stop_requested: None, + }) + .run_shell(&shell_env_var("WEBCODEX_TEST_PROFILE"), None, false, None) + .result; assert_eq!(result.stdout.as_deref(), Some("counted")); assert_eq!(std::fs::read_to_string(&counter).unwrap().trim(), "2"); // A late request that still holds generation 1 may prepare its own // snapshot, but it must not evict the already-cached active generation. - let stale = run_shell_with_profiles( - 1, - &unrestricted_test_policy(), - &shell, - tmp.path(), - &cache, - Some(&cwd), - &shell_env_var("WEBCODEX_TEST_PROFILE"), - None, - 10, - None, - ); + let stale = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_test_policy(), + shell: &shell, + project_registry_dir: tmp.path(), + cache: &cache, + cwd: Some(&cwd), + timeout_secs: 10, + stop_requested: None, + }) + .run_shell(&shell_env_var("WEBCODEX_TEST_PROFILE"), None, false, None) + .result; assert_eq!(stale.stdout.as_deref(), Some("counted")); assert_eq!(std::fs::read_to_string(&counter).unwrap().trim(), "3"); - let current = run_shell_with_profiles( - 2, - &unrestricted_test_policy(), - &shell, - tmp.path(), - &cache, - Some(&cwd), - &shell_env_var("WEBCODEX_TEST_PROFILE"), - None, - 10, - None, - ); + let current = (ShellExecutionContext { + generation: 2, + policy: &unrestricted_test_policy(), + shell: &shell, + project_registry_dir: tmp.path(), + cache: &cache, + cwd: Some(&cwd), + timeout_secs: 10, + stop_requested: None, + }) + .run_shell(&shell_env_var("WEBCODEX_TEST_PROFILE"), None, false, None) + .result; assert_eq!(current.stdout.as_deref(), Some("counted")); assert_eq!(std::fs::read_to_string(counter).unwrap().trim(), "3"); assert_eq!(cache.len(), 1); @@ -406,18 +407,18 @@ fn prepared_profile_prepare_reaps_background_pipe_holder() { let worker_cwd = cwd.clone(); let (result_tx, result_rx) = std::sync::mpsc::channel(); let worker = std::thread::spawn(move || { - let result = run_shell_with_profiles( - 1, - &worker_policy, - &worker_shell, - &worker_project_registry_dir, - &worker_cache, - Some(&worker_cwd), - &shell_env_var("WEBCODEX_TEST_PROFILE"), - None, - 10, - None, - ); + let result = (ShellExecutionContext { + generation: 1, + policy: &worker_policy, + shell: &worker_shell, + project_registry_dir: &worker_project_registry_dir, + cache: &worker_cache, + cwd: Some(&worker_cwd), + timeout_secs: 10, + stop_requested: None, + }) + .run_shell(&shell_env_var("WEBCODEX_TEST_PROFILE"), None, false, None) + .result; let _ = result_tx.send(result); }); diff --git a/crates/webcodex-runner/src/webcodex_runner/dispatch.rs b/crates/webcodex-runner/src/webcodex_runner/dispatch.rs index 30ebaf72f..85ee70400 100644 --- a/crates/webcodex-runner/src/webcodex_runner/dispatch.rs +++ b/crates/webcodex-runner/src/webcodex_runner/dispatch.rs @@ -7,15 +7,13 @@ use super::{ handle_browser_operation, handle_computer_operation, handle_prepare_managed_worktree_operation, handle_project_lifecycle_operation, handle_project_operation, handle_resolve_or_register_project_operation, handle_runner_instruction_request, - handle_runner_skill_request, run_internal_posix_script_with_profiles_and_execution_state, - run_internal_search_script_with_profiles_and_execution_state, - run_process_with_profiles_and_execution_state, run_script_with_profiles_and_execution_state, - run_shell_with_profiles_and_execution_state, - run_skill_resource_with_profiles_and_execution_state, run_ssh_shell_with_execution_state, - CommandResult, HotRunnerConfig, PersistentShellManager, ReloadableRunnerConfig, RunnerSink, + handle_runner_skill_request, run_ssh_shell_with_execution_state, CommandResult, + HotRunnerConfig, PersistentShellManager, ReloadableRunnerConfig, RunnerSink, ShellCommandResult, SubmitResultError, }; use crate::webcodex_runner::file_dispatch::handle_file_operation_with_artifact_store; +use crate::webcodex_runner::runner_skills::run_skill_resource; +use crate::webcodex_runner::shell::ShellExecutionContext; use std::path::Path; use std::sync::atomic::Ordering; use webcodex_core::runner_operation::{ @@ -108,6 +106,16 @@ fn run_native_shell_or_internal_search( project_registry_dir: &Path, operation: &RunnerShellOperation, ) -> ShellCommandResult { + let context = ShellExecutionContext { + generation: config.generation, + policy: &config.policy, + shell: &config.shell, + project_registry_dir, + cache: jobs.prepared_profiles(), + cwd: operation.cwd.as_deref(), + timeout_secs: operation.timeout_secs, + stop_requested: Some(runtime.shutdown_flag()), + }; if operation.command.lines().next() == Some(EXTERNAL_SEARCH_REQUEST_PREFIX) { let Some(script) = internal_search_script(&operation.command) else { return ShellCommandResult::not_started(invalid_command( @@ -115,44 +123,16 @@ fn run_native_shell_or_internal_search( )); }; #[cfg(windows)] - if let Some(result) = super::run_windows_native_search_with_profiles( - config.generation, - &config.policy, - &config.shell, - project_registry_dir, - jobs.prepared_profiles(), - operation.cwd.as_deref(), - operation.stdin.as_deref(), - operation.timeout_secs, - Some(runtime.shutdown_flag()), - ) { + if let Some(result) = context.run_windows_native_search(operation.stdin.as_deref()) { return result; } - return run_internal_search_script_with_profiles_and_execution_state( - config.generation, - &config.policy, - &config.shell, - project_registry_dir, - jobs.prepared_profiles(), - operation.cwd.as_deref(), - script, - operation.timeout_secs, - Some(runtime.shutdown_flag()), - ); + return context.run_internal_search_script(script); } - run_shell_with_profiles_and_execution_state( - config.generation, - &config.policy, - &config.shell, - project_registry_dir, - jobs.prepared_profiles(), - operation.cwd.as_deref(), + context.run_shell( &operation.command, operation.shell, operation.login, operation.stdin.as_deref(), - operation.timeout_secs, - Some(runtime.shutdown_flag()), ) } @@ -498,19 +478,21 @@ pub(crate) fn dispatch_request_with_outcome( .map(|_| true) } RunnerOperation::RunSkillResource(operation) => { - let result = run_skill_resource_with_profiles_and_execution_state( - config.generation, + let result = run_skill_resource( + &ShellExecutionContext { + generation: config.generation, + policy, + shell, + project_registry_dir, + cache: jobs.prepared_profiles(), + cwd: operation.cwd.as_deref(), + timeout_secs: operation.timeout_secs, + stop_requested: Some(runtime.shutdown_flag()), + }, &config.skills, runtime.client_id(), runtime.server_url(), - policy, - shell, - project_registry_dir, - jobs.prepared_profiles(), - operation.cwd.as_deref(), &operation.request, - operation.timeout_secs, - Some(runtime.shutdown_flag()), None, ); sink.submit_shell_result_with_metadata(request_id, result, config, runtime) @@ -522,50 +504,54 @@ pub(crate) fn dispatch_request_with_outcome( .map(|_| true) } RunnerOperation::RunProcess(operation) => { - let result = run_process_with_profiles_and_execution_state( - config.generation, + let result = (ShellExecutionContext { + generation: config.generation, policy, shell, project_registry_dir, - jobs.prepared_profiles(), - operation.cwd.as_deref(), + cache: jobs.prepared_profiles(), + cwd: operation.cwd.as_deref(), + timeout_secs: operation.timeout_secs, + stop_requested: Some(runtime.shutdown_flag()), + }) + .run_process( &operation.process.executable, &operation.process.args, operation.stdin.as_deref(), - operation.timeout_secs, - Some(runtime.shutdown_flag()), + &[], + None, + None, ); sink.submit_shell_result_with_metadata(request_id, result, config, runtime) .map(|_| true) } RunnerOperation::RunScript(operation) => { - let result = run_script_with_profiles_and_execution_state( - config.generation, + let result = (ShellExecutionContext { + generation: config.generation, policy, shell, project_registry_dir, - jobs.prepared_profiles(), - operation.cwd.as_deref(), - &operation.script, - operation.stdin.as_deref(), - operation.timeout_secs, - Some(runtime.shutdown_flag()), - ); + cache: jobs.prepared_profiles(), + cwd: operation.cwd.as_deref(), + timeout_secs: operation.timeout_secs, + stop_requested: Some(runtime.shutdown_flag()), + }) + .run_script(&operation.script, operation.stdin.as_deref(), None); sink.submit_shell_result_with_metadata(request_id, result, config, runtime) .map(|_| true) } RunnerOperation::RunInternalPosixScript(operation) => { - let result = run_internal_posix_script_with_profiles_and_execution_state( - config.generation, + let result = (ShellExecutionContext { + generation: config.generation, policy, shell, project_registry_dir, - jobs.prepared_profiles(), - operation.cwd.as_deref(), - &operation.script.script, - operation.timeout_secs, - Some(runtime.shutdown_flag()), - ); + cache: jobs.prepared_profiles(), + cwd: operation.cwd.as_deref(), + timeout_secs: operation.timeout_secs, + stop_requested: Some(runtime.shutdown_flag()), + }) + .run_internal_posix_script(&operation.script.script); sink.submit_shell_result_with_metadata(request_id, result, config, runtime) .map(|_| true) } diff --git a/crates/webcodex-runner/src/webcodex_runner/job_manager.rs b/crates/webcodex-runner/src/webcodex_runner/job_manager.rs index 216e64624..6550fa047 100644 --- a/crates/webcodex-runner/src/webcodex_runner/job_manager.rs +++ b/crates/webcodex-runner/src/webcodex_runner/job_manager.rs @@ -16,14 +16,11 @@ use super::execution_io::{ validation_module_available, wait_failure_error, wait_managed_tree_exit, OutputChunk, }; use super::output_text::OutputTextSource; -use super::runner_skills::run_skill_resource_with_profiles_and_execution_state; + use super::shell::{ configured_explicit_shell_command, configured_prepared_shell_job_command, configured_shell_job_command, configured_validation_job_command, cwd_allowed, - prepare_detached_process_launch, resolve_prepared_shell_profile, - run_process_with_profiles_and_execution_state_with_internal_env_and_start_hook, - run_process_with_profiles_and_execution_state_with_start_hook, - run_script_with_profiles_and_execution_state_with_start_hook, PreparedShellProfileCache, + resolve_prepared_shell_profile, PreparedShellProfileCache, }; use super::shutdown::{lock_unpoison, ActivityTracker}; use super::ssh::{is_transport_failure, SshConnectionPool}; diff --git a/crates/webcodex-runner/src/webcodex_runner/job_manager/detached.rs b/crates/webcodex-runner/src/webcodex_runner/job_manager/detached.rs index 935155b4f..ace6df85d 100644 --- a/crates/webcodex-runner/src/webcodex_runner/job_manager/detached.rs +++ b/crates/webcodex-runner/src/webcodex_runner/job_manager/detached.rs @@ -3,6 +3,7 @@ use super::lifecycle::{runner_job_is_active, runner_job_is_terminal}; use super::retained::job_update_from_snapshot; use super::*; +use crate::webcodex_runner::shell::ShellExecutionContext; use webcodex_core::workflow_session_contract::ExecutionPurpose; pub(super) fn validate_detached_recovery_context( context: &ShellJobContext, @@ -340,18 +341,18 @@ impl JobManager { let RunnerJobOperation::StartDetachedProcess(request) = &operation else { unreachable!("detached Job starter received non-detached operation"); }; - let prepared = match prepare_detached_process_launch( + let prepared = match (ShellExecutionContext { generation, - &policy, - &shell, - &project_registry_dir, - &manager.prepared_profiles, - request.cwd.as_deref(), - &request.process.executable, - &request.process.args, - request.timeout_secs, - Some(stop_requested.as_ref()), - ) { + policy: &policy, + shell: &shell, + project_registry_dir: &project_registry_dir, + cache: &manager.prepared_profiles, + cwd: request.cwd.as_deref(), + timeout_secs: request.timeout_secs, + stop_requested: Some(stop_requested.as_ref()), + }) + .prepare_detached_process_launch(&request.process.executable, &request.process.args) + { Ok(prepared) => prepared, Err(error) => { manager.fail_job(&operation, error, None); diff --git a/crates/webcodex-runner/src/webcodex_runner/job_manager/interactive.rs b/crates/webcodex-runner/src/webcodex_runner/job_manager/interactive.rs index 5c42ee04a..c16565b12 100644 --- a/crates/webcodex-runner/src/webcodex_runner/job_manager/interactive.rs +++ b/crates/webcodex-runner/src/webcodex_runner/job_manager/interactive.rs @@ -3,6 +3,7 @@ use super::shell_worker::PreparedShellJob; use super::*; +use crate::webcodex_runner::shell::ShellExecutionContext; impl JobManager { pub(super) fn start_interactive_process(&self, start: PendingJobStart) { let RunnerJobOperation::StartInteractiveProcess(request) = &start.operation else { @@ -12,18 +13,17 @@ impl JobManager { if request.stdin.is_some() { return Err("interactive_process_requires_job_write_input".into()); } - let launch = prepare_detached_process_launch( - start.generation, - &start.policy, - &start.shell, - &start.project_registry_dir, - &self.prepared_profiles, - request.cwd.as_deref(), - &request.process.executable, - &request.process.args, - request.timeout_secs, - Some(self.shutting_down.as_ref()), - )?; + let launch = (ShellExecutionContext { + generation: start.generation, + policy: &start.policy, + shell: &start.shell, + project_registry_dir: &start.project_registry_dir, + cache: &self.prepared_profiles, + cwd: request.cwd.as_deref(), + timeout_secs: request.timeout_secs, + stop_requested: Some(self.shutting_down.as_ref()), + }) + .prepare_detached_process_launch(&request.process.executable, &request.process.args)?; let mut command = super::super::shell::structured_process_command( std::ffi::OsStr::new(&launch.process.executable), &launch.process.args, diff --git a/crates/webcodex-runner/src/webcodex_runner/job_manager/process.rs b/crates/webcodex-runner/src/webcodex_runner/job_manager/process.rs index af5136f6b..2e3dee846 100644 --- a/crates/webcodex-runner/src/webcodex_runner/job_manager/process.rs +++ b/crates/webcodex-runner/src/webcodex_runner/job_manager/process.rs @@ -2,6 +2,8 @@ use super::lifecycle::process_running_activity; use super::*; +use crate::webcodex_runner::runner_skills::run_skill_resource; +use crate::webcodex_runner::shell::ShellExecutionContext; impl JobManager { pub(super) fn start_structured_job(&self, start: PendingJobStart) { let PendingJobStart { @@ -71,6 +73,16 @@ impl JobManager { }; let build_started = Instant::now(); let mut build_artifacts = None; + let context = |cwd, timeout_secs| ShellExecutionContext { + generation, + policy: &policy, + shell: &shell, + project_registry_dir: &project_registry_dir, + cache: &manager.prepared_profiles, + cwd, + timeout_secs, + stop_requested: Some(stop_requested.as_ref()), + }; let mut result = match &operation { RunnerJobOperation::StartBuild(request) if request.provenance.backend == "rust" @@ -92,16 +104,8 @@ impl JobManager { RunnerJobOperation::StartBuild(request) if request.provenance.backend == "python" => { - crate::webcodex_runner::shell::run_python_build_with_start_hook( - generation, - &policy, - &shell, - &project_registry_dir, - &manager.prepared_profiles, - request.cwd.as_deref(), + context(request.cwd.as_deref(), request.timeout_secs).run_python_build( &request.process.args, - request.timeout_secs, - Some(stop_requested.as_ref()), Some(&on_started), Some(&|| { crate::webcodex_runner::project_build::fence( @@ -125,18 +129,10 @@ impl JobManager { } else { &[] }; - run_process_with_profiles_and_execution_state_with_internal_env_and_start_hook( - generation, - &policy, - &shell, - &project_registry_dir, - &manager.prepared_profiles, - request.cwd.as_deref(), + context(request.cwd.as_deref(), request.timeout_secs).run_process( &request.process.executable, &request.process.args, None, - request.timeout_secs, - Some(stop_requested.as_ref()), env_overrides, Some(&on_started), Some(&|| { @@ -149,53 +145,30 @@ impl JobManager { ) } RunnerJobOperation::StartProcess(request) => { - run_process_with_profiles_and_execution_state_with_start_hook( - generation, - &policy, - &shell, - &project_registry_dir, - &manager.prepared_profiles, - request.cwd.as_deref(), + context(request.cwd.as_deref(), request.timeout_secs).run_process( &request.process.executable, &request.process.args, request.stdin.as_deref(), - request.timeout_secs, - Some(stop_requested.as_ref()), + &[], Some(&on_started), + None, ) } RunnerJobOperation::StartScript(request) => { - run_script_with_profiles_and_execution_state_with_start_hook( - generation, - &policy, - &shell, - &project_registry_dir, - &manager.prepared_profiles, - request.cwd.as_deref(), + context(request.cwd.as_deref(), request.timeout_secs).run_script( &request.script, request.stdin.as_deref(), - request.timeout_secs, - Some(stop_requested.as_ref()), - Some(&on_started), - ) - } - RunnerJobOperation::StartSkillResource(request) => { - run_skill_resource_with_profiles_and_execution_state( - generation, - &skills, - &client_id, - &server_url, - &policy, - &shell, - &project_registry_dir, - &manager.prepared_profiles, - request.cwd.as_deref(), - &request.request, - request.timeout_secs, - Some(stop_requested.as_ref()), Some(&on_started), ) } + RunnerJobOperation::StartSkillResource(request) => run_skill_resource( + &context(request.cwd.as_deref(), request.timeout_secs), + &skills, + &client_id, + &server_url, + &request.request, + Some(&on_started), + ), _ => unreachable!("structured Job starter received non structured operation"), }; if let RunnerJobOperation::StartBuild(request) = &operation { diff --git a/crates/webcodex-runner/src/webcodex_runner/mod.rs b/crates/webcodex-runner/src/webcodex_runner/mod.rs index a6e7ec542..62a111953 100644 --- a/crates/webcodex-runner/src/webcodex_runner/mod.rs +++ b/crates/webcodex-runner/src/webcodex_runner/mod.rs @@ -59,14 +59,14 @@ pub(crate) use artifacts::is_artifact_request_kind; pub(crate) use browser::handle_browser_operation; pub(crate) use computer::handle_computer_operation; #[cfg(test)] -pub(crate) use config::SshConfig; -#[cfg(test)] pub(crate) use config::{ default_quic_alpn, default_quic_connect_timeout_secs, default_quic_keepalive_interval_secs, default_websocket_connect_timeout_secs, QuicClientConfig, ShellProfileConfig, CLIENT_PROFILE_ERROR, DEFAULT_MAX_CONCURRENT_JOBS, }; -pub(crate) use config::{HotRunnerConfig, ReloadableRunnerConfig, RunnerPolicy, ShellConfig}; +pub(crate) use config::{HotRunnerConfig, ReloadableRunnerConfig, RunnerPolicy}; +#[cfg(test)] +pub(crate) use config::{ShellConfig, SshConfig}; #[cfg(test)] pub(super) use dispatch::dispatch_request; #[cfg(test)] @@ -94,21 +94,10 @@ pub(crate) use projects::{ parse_runner_project_toml, runner_project_summary, validate_project_path_policy, }; pub(crate) use runner_instructions::handle_runner_instruction_request; -pub(crate) use runner_skills::{ - handle_runner_skill_request, run_skill_resource_with_profiles_and_execution_state, -}; -#[cfg(windows)] -pub(crate) use shell::run_windows_native_search_with_profiles; +pub(crate) use runner_skills::handle_runner_skill_request; +pub(crate) use shell::explicit_shell_available; #[cfg(test)] -pub(crate) use shell::{ - cwd_allowed, run_shell, run_shell_with_profiles, PreparedShellProfileCache, -}; -pub(crate) use shell::{ - explicit_shell_available, run_internal_posix_script_with_profiles_and_execution_state, - run_internal_search_script_with_profiles_and_execution_state, - run_process_with_profiles_and_execution_state, run_script_with_profiles_and_execution_state, - run_shell_with_profiles_and_execution_state, -}; +pub(crate) use shell::{cwd_allowed, run_shell, PreparedShellProfileCache}; pub(crate) use ssh::run_ssh_shell_with_execution_state; pub(crate) use string_match::contains_any; #[cfg(all(test, unix))] diff --git a/crates/webcodex-runner/src/webcodex_runner/project_build/rust.rs b/crates/webcodex-runner/src/webcodex_runner/project_build/rust.rs index 68bc277dd..28b7738ef 100644 --- a/crates/webcodex-runner/src/webcodex_runner/project_build/rust.rs +++ b/crates/webcodex-runner/src/webcodex_runner/project_build/rust.rs @@ -10,6 +10,7 @@ use std::time::{Duration, Instant}; use webcodex_core::project_build::{ProjectBuildArtifact, ProjectBuildScope, ProjectBuildTargets}; use webcodex_core::runner_protocol::ShellCommandExecutionState; +use crate::webcodex_runner::shell::ShellExecutionContext; struct SelectedPackages { ids: BTreeSet, } @@ -267,6 +268,16 @@ pub(crate) fn run_rust_build( let cwd = build.cwd.as_deref().expect("admitted build cwd"); let deadline = started + Duration::from_secs(build.timeout_secs.min(policy.max_timeout_secs).max(1)); + let context = ShellExecutionContext { + generation, + policy, + shell, + project_registry_dir: registry, + cache, + cwd: Some(cwd), + timeout_secs: policy.max_timeout_secs, + stop_requested: Some(stop), + }; let fence = || super::fence(policy, registry, operation); let mut packages = None; let mut root = None; @@ -290,9 +301,7 @@ pub(crate) fn run_rust_build( "--offline", ] .map(str::to_owned); - let mut metadata = crate::webcodex_runner::shell::run_rust_build_phase( - generation, policy, shell, registry, cache, cwd, &args, deadline, stop, None, &fence, - ); + let mut metadata = context.run_rust_build_phase(&args, deadline, None, &fence); if metadata.execution_state != ShellCommandExecutionState::Completed || metadata.result.exit_code != Some(0) || metadata.result.error.is_some() @@ -327,19 +336,8 @@ pub(crate) fn run_rust_build( } } } - let mut result = crate::webcodex_runner::shell::run_rust_build_phase( - generation, - policy, - shell, - registry, - cache, - cwd, - &build.process.args, - deadline, - stop, - Some(on_started), - &fence, - ); + let mut result = + context.run_rust_build_phase(&build.process.args, deadline, Some(on_started), &fence); if result.execution_state != ShellCommandExecutionState::Completed || result.result.exit_code != Some(0) || result.result.error.is_some() diff --git a/crates/webcodex-runner/src/webcodex_runner/project_format.rs b/crates/webcodex-runner/src/webcodex_runner/project_format.rs index ecf57ba17..05abe14bf 100644 --- a/crates/webcodex-runner/src/webcodex_runner/project_format.rs +++ b/crates/webcodex-runner/src/webcodex_runner/project_format.rs @@ -5,6 +5,7 @@ use super::config::RunnerPolicy; use super::file_access::{directory_identity, file_identity, file_link_count, open_regular_file}; use super::projects::find_project_shell_context_by_id; use super::shell::cwd_allowed; +use crate::webcodex_runner::shell::ShellExecutionContext; use sha2::{Digest, Sha256}; use std::fs; use std::io::Read; @@ -550,18 +551,20 @@ pub(crate) fn format_candidates_with_deadline( args.push("--config-path".into()); args.push(config_path.into()); } - let result = super::shell::run_format_candidate( + let result = (ShellExecutionContext { generation, policy, shell, - registry, + project_registry_dir: registry, cache, - Some(cwd), + cwd: Some(cwd), + timeout_secs: remaining_secs, + stop_requested, + }) + .run_format_candidate( exe, &args, Some(source), - remaining_secs, - stop_requested, &env_override_refs, &env_removals, Some(deadline), diff --git a/crates/webcodex-runner/src/webcodex_runner/project_format_tests.rs b/crates/webcodex-runner/src/webcodex_runner/project_format_tests.rs index 41933d452..7972925a4 100644 --- a/crates/webcodex-runner/src/webcodex_runner/project_format_tests.rs +++ b/crates/webcodex-runner/src/webcodex_runner/project_format_tests.rs @@ -259,7 +259,7 @@ fn project_format_bounds_total_bytes_and_rejects_invalid_utf8() { #[ignore = "requires installed rustfmt; exercises the owned native process boundary"] fn runner_real_process_project_format_rustfmt_stdin_leaves_sources_untouched() { use super::config::ShellConfig; - use super::shell::{run_process_with_profiles_and_execution_state, PreparedShellProfileCache}; + use super::shell::{PreparedShellProfileCache, ShellExecutionContext}; let fixture = Fixture::new(); fs::write( fixture.root.join("src/main.rs"), @@ -269,17 +269,22 @@ fn runner_real_process_project_format_rustfmt_stdin_leaves_sources_untouched() { fs::write(fixture.root.join("child.rs"), "fn child( ){ }\n").unwrap(); let planned = fixture.plan(); let command = planned.plan.profile.process("src/main.rs").unwrap(); - let result = run_process_with_profiles_and_execution_state( - 1, - &fixture.policy, - &ShellConfig::default(), - &fixture.registry, - &PreparedShellProfileCache::default(), - planned.cwd.to_str(), + let result = (ShellExecutionContext { + generation: 1, + policy: &fixture.policy, + shell: &ShellConfig::default(), + project_registry_dir: &fixture.registry, + cache: &PreparedShellProfileCache::default(), + cwd: planned.cwd.to_str(), + timeout_secs: 10, + stop_requested: None, + }) + .run_process( &command.executable, &command.args, Some(&planned.sources[0]), - 10, + &[], + None, None, ); assert_eq!( diff --git a/crates/webcodex-runner/src/webcodex_runner/runner_skills.rs b/crates/webcodex-runner/src/webcodex_runner/runner_skills.rs index e8c9014ec..b3058ff24 100644 --- a/crates/webcodex-runner/src/webcodex_runner/runner_skills.rs +++ b/crates/webcodex-runner/src/webcodex_runner/runner_skills.rs @@ -1,16 +1,11 @@ use super::config::SkillsConfig; use super::configured_skills; use super::output::{CommandResult, ShellCommandResult}; -#[cfg(windows)] -use super::shell::prepare_detached_process_launch; -use super::shell::{ - run_process_with_profiles_and_execution_state_with_start_hook, PreparedShellProfileCache, -}; use super::skill_store::SkillStore; -use super::{RunnerPolicy, ShellConfig}; +use super::RunnerPolicy; +use crate::webcodex_runner::shell::ShellExecutionContext; use std::collections::BTreeSet; use std::path::{Path, PathBuf}; -use std::sync::atomic::AtomicBool; use std::time::Instant; use webcodex_core::runner_protocol::{ PROCESS_ARGV_MAX_BYTES, PROCESS_ARG_MAX_BYTES, PROCESS_ARG_MAX_COUNT, @@ -153,20 +148,12 @@ fn windows_skill_interpreter_path_available(path: &Path) -> bool { !configured_skills::metadata_is_link_like(&metadata) } -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_skill_resource_with_profiles_and_execution_state( - generation: u64, +pub(crate) fn run_skill_resource( + context: &ShellExecutionContext<'_>, skills: &SkillsConfig, client_id: &str, server_url: &str, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, request: &RunnerSkillExecutionRequest, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, on_started: Option<&dyn Fn()>, ) -> ShellCommandResult { let prepared = match prepare_skill_resource_execution(skills, client_id, server_url, request) { @@ -206,18 +193,7 @@ pub(crate) fn run_skill_resource_with_profiles_and_execution_state( for (index, (executable, args)) in candidates.into_iter().enumerate() { #[cfg(windows)] let executable = { - let resolved = match prepare_detached_process_launch( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - &executable, - &args, - timeout_secs, - stop_requested, - ) { + let resolved = match context.prepare_detached_process_launch(&executable, &args) { Ok(launch) => launch.process.executable, Err(error) if index != last @@ -249,19 +225,13 @@ pub(crate) fn run_skill_resource_with_profiles_and_execution_state( } resolved }; - let result = run_process_with_profiles_and_execution_state_with_start_hook( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, + let result = context.run_process( &executable, &args, Some(&prepared.script), - timeout_secs, - stop_requested, + &[], on_started, + None, ); if index != last && interpreter_unavailable(&result) { continue; diff --git a/crates/webcodex-runner/src/webcodex_runner/shell.rs b/crates/webcodex-runner/src/webcodex_runner/shell.rs index effd068a0..3ac92c465 100644 --- a/crates/webcodex-runner/src/webcodex_runner/shell.rs +++ b/crates/webcodex-runner/src/webcodex_runner/shell.rs @@ -74,6 +74,21 @@ pub(crate) struct PreparedShellProfileCache { profiles: Arc>>>, } +/// Borrowed configuration and execution bounds for one Runner invocation. +/// Constructed from the admitted request and its exact config generation; it +/// neither owns a child process nor grants permission to start one. +#[derive(Clone, Copy)] +pub(crate) struct ShellExecutionContext<'a> { + pub(crate) generation: u64, + pub(crate) policy: &'a RunnerPolicy, + pub(crate) shell: &'a ShellConfig, + pub(crate) project_registry_dir: &'a Path, + pub(crate) cache: &'a PreparedShellProfileCache, + pub(crate) cwd: Option<&'a str>, + pub(crate) timeout_secs: u64, + pub(crate) stop_requested: Option<&'a AtomicBool>, +} + #[derive(Debug, Clone)] pub(crate) struct PreparedDetachedProcessLaunch { pub(crate) process: ShellProcessArgv, @@ -116,8 +131,7 @@ mod internal; mod output; mod preparation; mod process; -pub(crate) use process::run_python_build_with_start_hook; -pub(crate) use process::run_rust_build_phase; + mod process_tree; mod profiles; mod run; @@ -150,20 +164,12 @@ pub(crate) use environment::{base_shell_env, env_keys_equal, is_sensitive_env_ke use execution::execute_configured_command; #[cfg(test)] use execution::spawned_output_failure; -pub(crate) use internal::{ - run_internal_posix_script_with_profiles_and_execution_state, - run_internal_search_script_with_profiles_and_execution_state, -}; + #[cfg(test)] use output::IncrementalUtf8Validator; use output::{read_bounded_pipe_tail, BoundedPipeTail}; use preparation::{run_prepare_command, stderr_tail}; -pub(crate) use process::{ - prepare_detached_process_launch, run_format_candidate, - run_process_with_profiles_and_execution_state, - run_process_with_profiles_and_execution_state_with_internal_env_and_start_hook, - run_process_with_profiles_and_execution_state_with_start_hook, -}; + use process_tree::{ terminate_child_process_tree, terminate_child_process_tree_until, terminate_child_without_output, wait_child_until, with_cleanup_error, @@ -175,9 +181,9 @@ pub(crate) use profiles::{ }; #[cfg(test)] use run::run_shell_impl; -pub(crate) use run::run_shell_with_profiles_and_execution_state; + #[cfg(test)] -pub(crate) use run::{run_shell, run_shell_with_profiles}; +pub(crate) use run::run_shell; use scripts::configured_script_interpreter; #[cfg(test)] use scripts::{ @@ -185,18 +191,12 @@ use scripts::{ fixed_script_prefix_args, parse_node_version, typescript_node_prefix_args, typescript_node_probe_error, NodeVersion, ScriptRuntimePlan, }; -pub(crate) use scripts::{ - run_script_with_profiles_and_execution_state, - run_script_with_profiles_and_execution_state_with_start_hook, -}; + #[cfg(windows)] #[cfg(test)] use search::native_search_spec; #[cfg(windows)] use search::resolve_windows_internal_posix_interpreter; -#[cfg(windows)] -pub(crate) use search::run_windows_native_search_with_profiles; - #[cfg(test)] #[path = "shell_tests.rs"] mod runner_lifecycle_tests; diff --git a/crates/webcodex-runner/src/webcodex_runner/shell/internal.rs b/crates/webcodex-runner/src/webcodex_runner/shell/internal.rs index d4fcc831b..51cffc112 100644 --- a/crates/webcodex-runner/src/webcodex_runner/shell/internal.rs +++ b/crates/webcodex-runner/src/webcodex_runner/shell/internal.rs @@ -2,167 +2,103 @@ use super::*; -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_internal_search_script_with_profiles_and_execution_state( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - script: &str, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> ShellCommandResult { - run_internal_posix_script_impl( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - script, - timeout_secs, - stop_requested, - true, - ) -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_internal_posix_script_with_profiles_and_execution_state( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - script: &str, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> ShellCommandResult { - run_internal_posix_script_impl( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - script, - timeout_secs, - stop_requested, - false, - ) -} - -#[allow(clippy::too_many_arguments)] -pub(super) fn run_internal_posix_script_impl( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - script: &str, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, - search_compat: bool, -) -> ShellCommandResult { - #[cfg(not(windows))] - let _ = search_compat; - #[cfg(not(windows))] - { - let payload = ShellScriptPayload { - language: ShellScriptLanguage::Sh, - script: script.to_string(), - args: Vec::new(), - }; - return run_script_with_profiles_and_execution_state( +impl ShellExecutionContext<'_> { + pub(crate) fn run_internal_search_script(&self, script: &str) -> ShellCommandResult { + self.run_internal_posix_script_impl(script, true) + } + pub(crate) fn run_internal_posix_script(&self, script: &str) -> ShellCommandResult { + self.run_internal_posix_script_impl(script, false) + } + pub(super) fn run_internal_posix_script_impl( + &self, + script: &str, + search_compat: bool, + ) -> ShellCommandResult { + #[cfg(windows)] + let ShellExecutionContext { generation, policy, shell, project_registry_dir, cache, cwd, - &payload, - None, timeout_secs, stop_requested, - ); - } + .. + } = *self; - #[cfg(windows)] - { - // WebCodex-generated POSIX programs never inherit the configured shell - // parser. Native Windows Bash launchers map the process cwd; `bash -s` - // consumes the generated bytes directly from stdin without asking Bash - // to interpret a Windows temporary-script path. - if !policy.allow_raw_shell { - let subject = if search_compat { - "internal search script" - } else { - "internal POSIX script" - }; - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(0), - error: Some(format!( - "{subject} execution is disabled by local Runner policy" - )), - }); - } - if script.is_empty() { - let subject = if search_compat { - "internal search script" - } else { - "internal POSIX script" + #[cfg(not(windows))] + let _ = search_compat; + #[cfg(not(windows))] + { + let payload = ShellScriptPayload { + language: ShellScriptLanguage::Sh, + script: script.to_string(), + args: Vec::new(), }; - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(0), - error: Some(format!("{subject} is empty; command was not started")), - }); + return self.run_script(&payload, None, None); } - let cwd_path = cwd - .map(PathBuf::from) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); - if let Err(error) = cwd_allowed(policy, &cwd_path) { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(0), - error: Some(error), - }); - } - let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); - let start = Instant::now(); - let profile = match resolve_prepared_shell_profile( - generation, - shell, - project_registry_dir, - &cwd_path, - cwd.is_some(), - cache, - stop_requested, - ) { - Ok(profile) => profile, - Err(error) => { + + #[cfg(windows)] + { + // WebCodex-generated POSIX programs never inherit the configured shell + // parser. Native Windows Bash launchers map the process cwd; `bash -s` + // consumes the generated bytes directly from stdin without asking Bash + // to interpret a Windows temporary-script path. + if !policy.allow_raw_shell { + let subject = if search_compat { + "internal search script" + } else { + "internal POSIX script" + }; + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(0), + error: Some(format!( + "{subject} execution is disabled by local Runner policy" + )), + }); + } + if script.is_empty() { + let subject = if search_compat { + "internal search script" + } else { + "internal POSIX script" + }; + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(0), + error: Some(format!("{subject} is empty; command was not started")), + }); + } + let cwd_path = cwd + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); + if let Err(error) = cwd_allowed(policy, &cwd_path) { return ShellCommandResult::not_started(CommandResult { exit_code: None, stdout: None, stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), + duration_ms: Some(0), error: Some(error), - }) + }); } - }; - let interpreter = - match resolve_windows_internal_posix_interpreter(shell, profile.as_deref()) { - Ok(interpreter) => interpreter, + let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); + let start = Instant::now(); + let profile = match resolve_prepared_shell_profile( + generation, + shell, + project_registry_dir, + &cwd_path, + cwd.is_some(), + cache, + stop_requested, + ) { + Ok(profile) => profile, Err(error) => { return ShellCommandResult::not_started(CommandResult { exit_code: None, @@ -173,50 +109,64 @@ pub(super) fn run_internal_posix_script_impl( }) } }; - let mut command = Command::new(interpreter); - command.arg("-s"); - match profile.as_deref() { - Some(profile) => apply_env_snapshot(&mut command, &profile.env_snapshot), - None => { - if let Err(error) = apply_shell_environment(&mut command, shell) { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), - error: Some(error), - }); + let interpreter = + match resolve_windows_internal_posix_interpreter(shell, profile.as_deref()) { + Ok(interpreter) => interpreter, + Err(error) => { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }) + } + }; + let mut command = Command::new(interpreter); + command.arg("-s"); + match profile.as_deref() { + Some(profile) => apply_env_snapshot(&mut command, &profile.env_snapshot), + None => { + if let Err(error) = apply_shell_environment(&mut command, shell) { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }); + } } } - } - let script = if search_compat { - // WSL/Git-Bash interop may expose a host ripgrep binary as `rg.exe` - // rather than `rg`. Keep the existing search script and its rg-first - // semantics unchanged by providing a process-local function only when - // no native `rg` command is already visible. Relative project paths are - // preserved across the Windows/Bash cwd mapping. - format!( + let script = if search_compat { + // WSL/Git-Bash interop may expose a host ripgrep binary as `rg.exe` + // rather than `rg`. Keep the existing search script and its rg-first + // semantics unchanged by providing a process-local function only when + // no native `rg` command is already visible. Relative project paths are + // preserved across the Windows/Bash cwd mapping. + format!( "if ! command -v rg >/dev/null 2>&1 && command -v rg.exe >/dev/null 2>&1; then rg() {{ command rg.exe --path-separator / \"$@\"; }}; fi\n{script}" ) - } else { - script.to_string() - }; - let spawn_error = if search_compat { - "failed to spawn internal search interpreter" - } else { - "failed to spawn internal POSIX interpreter" - }; + } else { + script.to_string() + }; + let spawn_error = if search_compat { + "failed to spawn internal search interpreter" + } else { + "failed to spawn internal POSIX interpreter" + }; - execute_configured_command( - policy, - command, - &cwd_path, - Some(&script), - timeout_secs, - stop_requested, - start, - spawn_error, - None, - ) + execute_configured_command( + policy, + command, + &cwd_path, + Some(&script), + timeout_secs, + stop_requested, + start, + spawn_error, + None, + ) + } } } diff --git a/crates/webcodex-runner/src/webcodex_runner/shell/process.rs b/crates/webcodex-runner/src/webcodex_runner/shell/process.rs index ed04a1c56..d6e6f2fe3 100644 --- a/crates/webcodex-runner/src/webcodex_runner/shell/process.rs +++ b/crates/webcodex-runner/src/webcodex_runner/shell/process.rs @@ -2,414 +2,327 @@ use super::*; -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_process_with_profiles_and_execution_state( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - executable: &str, - args: &[String], - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> ShellCommandResult { - run_process_with_profiles_and_execution_state_with_start_hook( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - executable, - args, - stdin, - timeout_secs, - stop_requested, - None, - ) -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn prepare_detached_process_launch( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - executable: &str, - args: &[String], - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> Result { - // Detached structured execution has the same local policy boundary as the - // ordinary native-argv path. This helper performs preparation only; it never - // spawns the requested payload. - if !policy.allow_raw_shell { - return Err("structured process execution is disabled by local Runner policy".to_string()); - } - let cwd_path = cwd - .map(PathBuf::from) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); - cwd_allowed(policy, &cwd_path)?; - let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); - let profile = resolve_prepared_shell_profile( - generation, - shell, - project_registry_dir, - &cwd_path, - cwd.is_some(), - cache, - stop_requested, - )?; - let resolved_program = - resolve_process_program(shell, profile.as_deref(), executable, Some(&cwd_path))?; - // Validate the same batch argv contract before a detached Job is accepted. - let _ = structured_process_command(&resolved_program, args, Some(&cwd_path))?; - let resolved_program = resolved_program.into_string().map_err(|_| { - "structured process executable resolved to a non-UTF-8 native path".to_string() - })?; - let cwd = cwd_path - .to_str() - .ok_or_else(|| "structured process cwd resolved to a non-UTF-8 native path".to_string())? - .to_string(); - let env = match profile.as_deref() { - Some(profile) => profile.env_snapshot.clone(), - None => base_shell_env(shell, &ShellProfileConfig::default())?, - }; - let mut env = env.into_iter().collect::>(); - env.sort_by(|left, right| left.0.cmp(&right.0)); - Ok(PreparedDetachedProcessLaunch { - process: ShellProcessArgv { - executable: resolved_program, - args: args.to_vec(), - }, - cwd, - env, - timeout_secs, - }) -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_process_with_profiles_and_execution_state_with_start_hook( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - executable: &str, - args: &[String], - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, - on_started: Option<&dyn Fn()>, -) -> ShellCommandResult { - run_process_with_profiles_and_execution_state_with_internal_env_and_start_hook( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - executable, - args, - stdin, - timeout_secs, - stop_requested, - &[], - on_started, - None, - ) -} - -/// Apply Runner-owned semantic environment overrides after shell/profile -/// preparation and before native spawn. These values are internal invariants, -/// never caller/model-provided environment input. -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_process_with_profiles_and_execution_state_with_internal_env_and_start_hook( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - executable: &str, - args: &[String], - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, - env_overrides: &[(&str, &str)], - on_started: Option<&dyn Fn()>, - on_prepared: Option<&dyn Fn() -> Result<(), String>>, -) -> ShellCommandResult { - run_process_with_output( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - executable, - args, - stdin, - timeout_secs, - stop_requested, - env_overrides, - &[], - on_started, - on_prepared, - false, - None, - false, - ) -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_format_candidate( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - executable: &str, - args: &[String], - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, - env_overrides: &[(&str, &str)], - env_removals: &[&str], +// Internal output and launch modes are selected only by the semantic entry points below. +struct ProcessOutputOptions<'a> { + env_overrides: &'a [(&'a str, &'a str)], + env_removals: &'a [&'a str], + on_started: Option<&'a dyn Fn()>, + on_prepared: Option<&'a dyn Fn() -> Result<(), String>>, + exact_utf8: bool, deadline: Option, -) -> ShellCommandResult { - run_process_with_output( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - executable, - args, - stdin, - timeout_secs, - stop_requested, - env_overrides, - env_removals, - None, - None, - true, - deadline, - false, - ) + python_build: bool, } -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_python_build_with_start_hook( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - args: &[String], - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, - on_started: Option<&dyn Fn()>, - on_prepared: Option<&dyn Fn() -> Result<(), String>>, -) -> ShellCommandResult { - let deadline = - Instant::now() + Duration::from_secs(timeout_secs.min(policy.max_timeout_secs).max(1)); - run_process_with_output( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - "python", - args, - None, - timeout_secs, - stop_requested, - &[], - &[], - on_started, - on_prepared, - false, - Some(deadline), - true, - ) -} +impl ShellExecutionContext<'_> { + pub(crate) fn prepare_detached_process_launch( + &self, + executable: &str, + args: &[String], + ) -> Result { + let ShellExecutionContext { + generation, + policy, + shell, + project_registry_dir, + cache, + cwd, + timeout_secs, + stop_requested, + .. + } = *self; -#[allow(clippy::too_many_arguments)] -fn run_process_with_output( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - executable: &str, - args: &[String], - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, - env_overrides: &[(&str, &str)], - env_removals: &[&str], - on_started: Option<&dyn Fn()>, - on_prepared: Option<&dyn Fn() -> Result<(), String>>, - exact_utf8: bool, - deadline: Option, - python_build: bool, -) -> ShellCommandResult { - // Structured execution intentionally receives the same policy treatment - // as run_shell. Absence of shell syntax is not a permission bypass. - if !policy.allow_raw_shell { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(0), - error: Some("structured process execution is disabled by local Runner policy".into()), - }); + // Detached structured execution has the same local policy boundary as the + // ordinary native-argv path. This helper performs preparation only; it never + // spawns the requested payload. + if !policy.allow_raw_shell { + return Err( + "structured process execution is disabled by local Runner policy".to_string(), + ); + } + let cwd_path = cwd + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); + cwd_allowed(policy, &cwd_path)?; + let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); + let profile = resolve_prepared_shell_profile( + generation, + shell, + project_registry_dir, + &cwd_path, + cwd.is_some(), + cache, + stop_requested, + )?; + let resolved_program = + resolve_process_program(shell, profile.as_deref(), executable, Some(&cwd_path))?; + // Validate the same batch argv contract before a detached Job is accepted. + let _ = structured_process_command(&resolved_program, args, Some(&cwd_path))?; + let resolved_program = resolved_program.into_string().map_err(|_| { + "structured process executable resolved to a non-UTF-8 native path".to_string() + })?; + let cwd = cwd_path + .to_str() + .ok_or_else(|| { + "structured process cwd resolved to a non-UTF-8 native path".to_string() + })? + .to_string(); + let env = match profile.as_deref() { + Some(profile) => profile.env_snapshot.clone(), + None => base_shell_env(shell, &ShellProfileConfig::default())?, + }; + let mut env = env.into_iter().collect::>(); + env.sort_by(|left, right| left.0.cmp(&right.0)); + Ok(PreparedDetachedProcessLaunch { + process: ShellProcessArgv { + executable: resolved_program, + args: args.to_vec(), + }, + cwd, + env, + timeout_secs, + }) } - let cwd_path = cwd - .map(PathBuf::from) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); - if let Err(error) = cwd_allowed(policy, &cwd_path) { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(0), - error: Some(error), - }); + /// Apply Runner-owned semantic environment overrides after shell/profile + /// preparation and before native spawn. These values are internal invariants, + /// never caller/model-provided environment input. + pub(crate) fn run_process( + &self, + executable: &str, + args: &[String], + stdin: Option<&str>, + env_overrides: &[(&str, &str)], + on_started: Option<&dyn Fn()>, + on_prepared: Option<&dyn Fn() -> Result<(), String>>, + ) -> ShellCommandResult { + self.run_process_with_output( + executable, + args, + stdin, + ProcessOutputOptions { + env_overrides, + env_removals: &[], + on_started, + on_prepared, + exact_utf8: false, + deadline: None, + python_build: false, + }, + ) } - let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); - let start = Instant::now(); - let profile = match resolve_prepared_shell_profile_with_deadline( - generation, - shell, - project_registry_dir, - &cwd_path, - cwd.is_some(), - cache, - stop_requested, - deadline, - ) { - Ok(profile) => profile, - Err(error) => { + pub(crate) fn run_format_candidate( + &self, + executable: &str, + args: &[String], + stdin: Option<&str>, + env_overrides: &[(&str, &str)], + env_removals: &[&str], + deadline: Option, + ) -> ShellCommandResult { + self.run_process_with_output( + executable, + args, + stdin, + ProcessOutputOptions { + env_overrides, + env_removals, + on_started: None, + on_prepared: None, + exact_utf8: true, + deadline, + python_build: false, + }, + ) + } + pub(crate) fn run_python_build( + &self, + args: &[String], + on_started: Option<&dyn Fn()>, + on_prepared: Option<&dyn Fn() -> Result<(), String>>, + ) -> ShellCommandResult { + let ShellExecutionContext { + policy, + timeout_secs, + .. + } = *self; + + let deadline = + Instant::now() + Duration::from_secs(timeout_secs.min(policy.max_timeout_secs).max(1)); + self.run_process_with_output( + "python", + args, + None, + ProcessOutputOptions { + env_overrides: &[], + env_removals: &[], + on_started, + on_prepared, + exact_utf8: false, + deadline: Some(deadline), + python_build: true, + }, + ) + } + fn run_process_with_output( + &self, + executable: &str, + args: &[String], + stdin: Option<&str>, + options: ProcessOutputOptions<'_>, + ) -> ShellCommandResult { + let ProcessOutputOptions { + env_overrides, + env_removals, + on_started, + on_prepared, + exact_utf8, + deadline, + python_build, + } = options; + let ShellExecutionContext { + generation, + policy, + shell, + project_registry_dir, + cache, + cwd, + timeout_secs, + stop_requested, + .. + } = *self; + + // Structured execution intentionally receives the same policy treatment + // as run_shell. Absence of shell syntax is not a permission bypass. + if !policy.allow_raw_shell { return ShellCommandResult::not_started(CommandResult { exit_code: None, stdout: None, stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), - error: Some(error), - }) + duration_ms: Some(0), + error: Some( + "structured process execution is disabled by local Runner policy".into(), + ), + }); } - }; - let command = if python_build { - super::commands::configured_python_build_job_command( - shell, - profile.as_deref(), - args, - &cwd_path, - stop_requested, - deadline.expect("Python build deadline"), - ) - } else { - configured_process_command(shell, profile.as_deref(), executable, args, Some(&cwd_path)) - }; - let mut cmd = match command { - Ok(cmd) => cmd, - Err(error) => { + let cwd_path = cwd + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); + if let Err(error) = cwd_allowed(policy, &cwd_path) { return ShellCommandResult::not_started(CommandResult { exit_code: None, stdout: None, stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), + duration_ms: Some(0), error: Some(error), - }) + }); } - }; - for key in env_removals { - cmd.env_remove(key); - } - for (key, value) in env_overrides { - cmd.env(key, value); - } - if python_build && deadline.is_some_and(|deadline| Instant::now() >= deadline) { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), - error: Some("build_timeout".into()), - }); - } - if let Some(on_prepared) = on_prepared { - if let Err(error) = on_prepared() { + let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); + let start = Instant::now(); + let profile = match resolve_prepared_shell_profile_with_deadline( + generation, + shell, + project_registry_dir, + &cwd_path, + cwd.is_some(), + cache, + stop_requested, + deadline, + ) { + Ok(profile) => profile, + Err(error) => { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }) + } + }; + let command = if python_build { + super::commands::configured_python_build_job_command( + shell, + profile.as_deref(), + args, + &cwd_path, + stop_requested, + deadline.expect("Python build deadline"), + ) + } else { + configured_process_command(shell, profile.as_deref(), executable, args, Some(&cwd_path)) + }; + let mut cmd = match command { + Ok(cmd) => cmd, + Err(error) => { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }) + } + }; + for key in env_removals { + cmd.env_remove(key); + } + for (key, value) in env_overrides { + cmd.env(key, value); + } + if python_build && deadline.is_some_and(|deadline| Instant::now() >= deadline) { return ShellCommandResult::not_started(CommandResult { exit_code: None, stdout: None, stderr: None, duration_ms: Some(start.elapsed().as_millis() as u64), - error: Some(error), + error: Some("build_timeout".into()), }); } + if let Some(on_prepared) = on_prepared { + if let Err(error) = on_prepared() { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }); + } + } + super::execution::execute_configured_command_with_output( + policy, + cmd, + &cwd_path, + stdin, + timeout_secs, + stop_requested, + start, + "failed to spawn structured process", + on_started, + exact_utf8, + deadline, + ) + } + /// Private multi-phase build path; ordinary process requests keep their policy. + pub(crate) fn run_rust_build_phase( + &self, + args: &[String], + deadline: Instant, + on_started: Option<&dyn Fn()>, + fence: &dyn Fn() -> Result<(), String>, + ) -> ShellCommandResult { + self.run_process_with_output( + "cargo", + args, + None, + ProcessOutputOptions { + env_overrides: &[("RUSTUP_AUTO_INSTALL", "0")], + env_removals: &[], + on_started, + on_prepared: Some(fence), + exact_utf8: false, + deadline: Some(deadline), + python_build: false, + }, + ) } - super::execution::execute_configured_command_with_output( - policy, - cmd, - &cwd_path, - stdin, - timeout_secs, - stop_requested, - start, - "failed to spawn structured process", - on_started, - exact_utf8, - deadline, - ) -} - -/// Private multi-phase build path; ordinary process requests keep their policy. -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_rust_build_phase( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - registry: &Path, - cache: &PreparedShellProfileCache, - cwd: &str, - args: &[String], - deadline: Instant, - stop: &AtomicBool, - on_started: Option<&dyn Fn()>, - fence: &dyn Fn() -> Result<(), String>, -) -> ShellCommandResult { - run_process_with_output( - generation, - policy, - shell, - registry, - cache, - Some(cwd), - "cargo", - args, - None, - policy.max_timeout_secs, - Some(stop), - &[("RUSTUP_AUTO_INSTALL", "0")], - &[], - on_started, - Some(fence), - false, - Some(deadline), - false, - ) } diff --git a/crates/webcodex-runner/src/webcodex_runner/shell/run.rs b/crates/webcodex-runner/src/webcodex_runner/shell/run.rs index 29b8edb9b..e3361c4e6 100644 --- a/crates/webcodex-runner/src/webcodex_runner/shell/run.rs +++ b/crates/webcodex-runner/src/webcodex_runner/shell/run.rs @@ -3,7 +3,7 @@ use super::*; // Test-only wrapper for callers that do not need prepared shell profiles; the -// production request path uses `run_shell_with_profiles` directly. +// production request path uses `ShellExecutionContext::run_shell`. #[cfg(test)] pub(crate) fn run_shell( policy: &RunnerPolicy, @@ -29,64 +29,39 @@ pub(crate) fn run_shell( .result } -#[cfg(test)] -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_shell_with_profiles( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - command: &str, - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> CommandResult { - run_shell_with_profiles_and_execution_state( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - command, - None, - false, - stdin, - timeout_secs, - stop_requested, - ) - .result -} +impl ShellExecutionContext<'_> { + pub(crate) fn run_shell( + &self, + command: &str, + explicit_shell: Option, + login: bool, + stdin: Option<&str>, + ) -> ShellCommandResult { + let ShellExecutionContext { + generation, + policy, + shell, + project_registry_dir, + cache, + cwd, + timeout_secs, + stop_requested, + .. + } = *self; -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_shell_with_profiles_and_execution_state( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - command: &str, - explicit_shell: Option, - login: bool, - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> ShellCommandResult { - run_shell_impl( - policy, - shell, - Some((generation, project_registry_dir, cache)), - cwd, - command, - explicit_shell, - login, - stdin, - timeout_secs, - stop_requested, - ) + run_shell_impl( + policy, + shell, + Some((generation, project_registry_dir, cache)), + cwd, + command, + explicit_shell, + login, + stdin, + timeout_secs, + stop_requested, + ) + } } pub(super) fn run_shell_impl( diff --git a/crates/webcodex-runner/src/webcodex_runner/shell/scripts.rs b/crates/webcodex-runner/src/webcodex_runner/shell/scripts.rs index ab93a6ea3..40c2d243a 100644 --- a/crates/webcodex-runner/src/webcodex_runner/shell/scripts.rs +++ b/crates/webcodex-runner/src/webcodex_runner/shell/scripts.rs @@ -329,158 +329,139 @@ pub(super) fn redact_temporary_script_path(result: &mut ShellCommandResult, path } } -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_script_with_profiles_and_execution_state( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - payload: &ShellScriptPayload, - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> ShellCommandResult { - run_script_with_profiles_and_execution_state_with_start_hook( - generation, - policy, - shell, - project_registry_dir, - cache, - cwd, - payload, - stdin, - timeout_secs, - stop_requested, - None, - ) -} +impl ShellExecutionContext<'_> { + pub(crate) fn run_script( + &self, + payload: &ShellScriptPayload, + stdin: Option<&str>, + on_started: Option<&dyn Fn()>, + ) -> ShellCommandResult { + let ShellExecutionContext { + generation, + policy, + shell, + project_registry_dir, + cache, + cwd, + timeout_secs, + stop_requested, + .. + } = *self; -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_script_with_profiles_and_execution_state_with_start_hook( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - payload: &ShellScriptPayload, - stdin: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, - on_started: Option<&dyn Fn()>, -) -> ShellCommandResult { - // Typed script execution is consequential and receives the same Runner - // policy treatment as raw shell and structured native processes. - if !policy.allow_raw_shell { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(0), - error: Some("structured script execution is disabled by local Runner policy".into()), - }); - } - let cwd_path = cwd - .map(PathBuf::from) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); - if let Err(error) = cwd_allowed(policy, &cwd_path) { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(0), - error: Some(error), - }); - } - let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); - let start = Instant::now(); - let profile = match resolve_prepared_shell_profile( - generation, - shell, - project_registry_dir, - &cwd_path, - cwd.is_some(), - cache, - stop_requested, - ) { - Ok(profile) => profile, - Err(error) => { + // Typed script execution is consequential and receives the same Runner + // policy treatment as raw shell and structured native processes. + if !policy.allow_raw_shell { return ShellCommandResult::not_started(CommandResult { exit_code: None, stdout: None, stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), - error: Some(error), - }) + duration_ms: Some(0), + error: Some( + "structured script execution is disabled by local Runner policy".into(), + ), + }); } - }; - // Resolve the semantic runtime plan before creating the payload file. Missing - // interpreters and unsupported TypeScript Node versions are therefore - // definite pre-start rejections with no user-script side effect. TypeScript - // performs only a bounded Runner-owned `node --version` capability probe; - // the user's script body is still spawned exactly once. - let runtime_plan = match configured_script_runtime_plan( - shell, - profile.as_deref(), - payload.language, - &cwd_path, - stop_requested, - ) { - Ok(plan) => plan, - Err(error) => { + let cwd_path = cwd + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); + if let Err(error) = cwd_allowed(policy, &cwd_path) { return ShellCommandResult::not_started(CommandResult { exit_code: None, stdout: None, stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), + duration_ms: Some(0), error: Some(error), - }) + }); } - }; - let (temporary_path, original_path, absolute_path) = match create_temporary_script(payload) { - Ok(temporary) => temporary, - Err(error) => { + let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); + let start = Instant::now(); + let profile = match resolve_prepared_shell_profile( + generation, + shell, + project_registry_dir, + &cwd_path, + cwd.is_some(), + cache, + stop_requested, + ) { + Ok(profile) => profile, + Err(error) => { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }) + } + }; + // Resolve the semantic runtime plan before creating the payload file. Missing + // interpreters and unsupported TypeScript Node versions are therefore + // definite pre-start rejections with no user-script side effect. TypeScript + // performs only a bounded Runner-owned `node --version` capability probe; + // the user's script body is still spawned exactly once. + let runtime_plan = match configured_script_runtime_plan( + shell, + profile.as_deref(), + payload.language, + &cwd_path, + stop_requested, + ) { + Ok(plan) => plan, + Err(error) => { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }) + } + }; + let (temporary_path, original_path, absolute_path) = match create_temporary_script(payload) + { + Ok(temporary) => temporary, + Err(error) => { + return ShellCommandResult::not_started(CommandResult { + exit_code: None, + stdout: None, + stderr: None, + duration_ms: Some(start.elapsed().as_millis() as u64), + error: Some(error), + }) + } + }; + let mut command = build_script_command(&runtime_plan, &absolute_path, &payload.args); + if let Err(error) = apply_script_environment(&mut command, shell, profile.as_deref()) { return ShellCommandResult::not_started(CommandResult { exit_code: None, stdout: None, stderr: None, duration_ms: Some(start.elapsed().as_millis() as u64), error: Some(error), - }) + }); } - }; - let mut command = build_script_command(&runtime_plan, &absolute_path, &payload.args); - if let Err(error) = apply_script_environment(&mut command, shell, profile.as_deref()) { - return ShellCommandResult::not_started(CommandResult { - exit_code: None, - stdout: None, - stderr: None, - duration_ms: Some(start.elapsed().as_millis() as u64), - error: Some(error), - }); - } - let mut result = execute_configured_command( - policy, - command, - &cwd_path, - stdin, - timeout_secs, - stop_requested, - start, - "failed to spawn script interpreter", - on_started, - ); - redact_temporary_script_path(&mut result, &[original_path.as_path(), &absolute_path]); - if let Err(error) = temporary_path.close() { - // Cleanup is infrastructure after the child result is already known. - // Never rewrite completed/timed_out/outcome_unknown lifecycle truth. - tracing::warn!( - language = payload.language.as_str(), - error_kind = ?error.kind(), - "failed to remove Runner-owned temporary script file" + let mut result = execute_configured_command( + policy, + command, + &cwd_path, + stdin, + timeout_secs, + stop_requested, + start, + "failed to spawn script interpreter", + on_started, ); + redact_temporary_script_path(&mut result, &[original_path.as_path(), &absolute_path]); + if let Err(error) = temporary_path.close() { + // Cleanup is infrastructure after the child result is already known. + // Never rewrite completed/timed_out/outcome_unknown lifecycle truth. + tracing::warn!( + language = payload.language.as_str(), + error_kind = ?error.kind(), + "failed to remove Runner-owned temporary script file" + ); + } + result } - result } diff --git a/crates/webcodex-runner/src/webcodex_runner/shell/search.rs b/crates/webcodex-runner/src/webcodex_runner/shell/search.rs index 22079098f..e48af34d0 100644 --- a/crates/webcodex-runner/src/webcodex_runner/shell/search.rs +++ b/crates/webcodex-runner/src/webcodex_runner/shell/search.rs @@ -160,99 +160,105 @@ pub(super) fn native_search_spec(payload: &str) -> Option { } #[cfg(windows)] -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_windows_native_search_with_profiles( - generation: u64, - policy: &RunnerPolicy, - shell: &ShellConfig, - project_registry_dir: &Path, - cache: &PreparedShellProfileCache, - cwd: Option<&str>, - payload: Option<&str>, - timeout_secs: u64, - stop_requested: Option<&AtomicBool>, -) -> Option { - use webcodex_core::project_search::PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET; - const MARKER: &str = - "{\"webcodex_search\":{\"backend\":\"rg\",\"feature_unavailable\":false}}\n"; - // Native execution has the same policy gate as generated internal POSIX - // programs. Small transport budgets keep the existing fallback behavior. - if !policy.allow_raw_shell - || policy.max_output_bytes < MARKER.len() + PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET + 4 - { - return None; - } - let spec = native_search_spec(payload?)?; - let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); - let cwd_path = cwd - .map(PathBuf::from) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); - if cwd_allowed(policy, &cwd_path).is_err() { - return None; - } - let canonical_cwd = canonicalize_existing(&cwd_path).ok()?; - let canonical_target = canonicalize_existing(&cwd_path.join(&spec.path)).ok()?; - if !(canonical_target.is_file() || canonical_target.is_dir()) - || !webcodex_runner_config::paths::path_is_within(&canonical_target, &canonical_cwd) - { - return None; - } +impl ShellExecutionContext<'_> { + pub(crate) fn run_windows_native_search( + &self, + payload: Option<&str>, + ) -> Option { + let ShellExecutionContext { + generation, + policy, + shell, + project_registry_dir, + cache, + cwd, + timeout_secs, + stop_requested, + .. + } = *self; - let profile = resolve_prepared_shell_profile( - generation, - shell, - project_registry_dir, - &cwd_path, - cwd.is_some(), - cache, - stop_requested, - ) - .ok()?; - let path = configured_process_path(shell, profile.as_deref()).ok()?; - let resolved = crate::webcodex_runner::util::resolve_program_in_path("rg", &path) - .or_else(|| crate::webcodex_runner::util::resolve_program_in_path("rg.exe", &path))?; - let crate::webcodex_runner::util::ResolvedProgram::Native(program) = resolved else { - return None; - }; + use webcodex_core::project_search::PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET; + const MARKER: &str = + "{\"webcodex_search\":{\"backend\":\"rg\",\"feature_unavailable\":false}}\n"; + // Native execution has the same policy gate as generated internal POSIX + // programs. Small transport budgets keep the existing fallback behavior. + if !policy.allow_raw_shell + || policy.max_output_bytes < MARKER.len() + PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET + 4 + { + return None; + } + let spec = native_search_spec(payload?)?; + let timeout_secs = timeout_secs.min(policy.max_timeout_secs).max(1); + let cwd_path = cwd + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); + if cwd_allowed(policy, &cwd_path).is_err() { + return None; + } + let canonical_cwd = canonicalize_existing(&cwd_path).ok()?; + let canonical_target = canonicalize_existing(&cwd_path.join(&spec.path)).ok()?; + if !(canonical_target.is_file() || canonical_target.is_dir()) + || !webcodex_runner_config::paths::path_is_within(&canonical_target, &canonical_cwd) + { + return None; + } - let mut command = Command::new(program); - command.args(&spec.args); - match profile.as_deref() { - Some(profile) => apply_env_snapshot(&mut command, &profile.env_snapshot), - None => { - if apply_shell_environment(&mut command, shell).is_err() { - return None; + let profile = resolve_prepared_shell_profile( + generation, + shell, + project_registry_dir, + &cwd_path, + cwd.is_some(), + cache, + stop_requested, + ) + .ok()?; + let path = configured_process_path(shell, profile.as_deref()).ok()?; + let resolved = crate::webcodex_runner::util::resolve_program_in_path("rg", &path) + .or_else(|| crate::webcodex_runner::util::resolve_program_in_path("rg.exe", &path))?; + let crate::webcodex_runner::util::ResolvedProgram::Native(program) = resolved else { + return None; + }; + + let mut command = Command::new(program); + command.args(&spec.args); + match profile.as_deref() { + Some(profile) => apply_env_snapshot(&mut command, &profile.env_snapshot), + None => { + if apply_shell_environment(&mut command, shell).is_err() { + return None; + } } } - } - let start = Instant::now(); - let mut result = super::execution::execute_configured_command_with_prefix( - policy, - command, - &cwd_path, - None, - timeout_secs, - stop_requested, - start, - "failed to spawn native ripgrep search", - None, - Some(super::drains::StdoutPrefixLimit { - lines: spec.line_limit, - bytes: PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET + 1, - }), - ); - if result.execution_state == ShellCommandExecutionState::NotStarted { - return None; - } - if result.stdout_truncated { - return None; - } - if result.execution_state == ShellCommandExecutionState::Completed { - let stdout = result.result.stdout.take().unwrap_or_default(); - if MARKER.len().saturating_add(stdout.len()) > policy.max_output_bytes { + let start = Instant::now(); + let mut result = super::execution::execute_configured_command_with_prefix( + policy, + command, + &cwd_path, + None, + timeout_secs, + stop_requested, + start, + "failed to spawn native ripgrep search", + None, + Some(super::drains::StdoutPrefixLimit { + lines: spec.line_limit, + bytes: PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET + 1, + }), + ); + if result.execution_state == ShellCommandExecutionState::NotStarted { + return None; + } + if result.stdout_truncated { return None; } - result.result.stdout = Some(format!("{MARKER}{stdout}")); + if result.execution_state == ShellCommandExecutionState::Completed { + let stdout = result.result.stdout.take().unwrap_or_default(); + if MARKER.len().saturating_add(stdout.len()) > policy.max_output_bytes { + return None; + } + result.result.stdout = Some(format!("{MARKER}{stdout}")); + } + Some(result) } - Some(result) } diff --git a/crates/webcodex-runner/src/webcodex_runner/shell_tests.rs b/crates/webcodex-runner/src/webcodex_runner/shell_tests.rs index 4b692900a..a37eab143 100644 --- a/crates/webcodex-runner/src/webcodex_runner/shell_tests.rs +++ b/crates/webcodex-runner/src/webcodex_runner/shell_tests.rs @@ -70,19 +70,17 @@ fn run_direct_process( timeout_secs: u64, ) -> ShellCommandResult { let project_registry_dir = tempfile::tempdir().unwrap(); - run_process_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &ShellConfig::default(), - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.to_string_lossy().as_ref()), - &executable.to_string_lossy(), - args, - stdin, + (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &ShellConfig::default(), + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.to_string_lossy().as_ref()), timeout_secs, - None, - ) + stop_requested: None, + }) + .run_process(&executable.to_string_lossy(), args, stdin, &[], None, None) } fn run_direct_script( @@ -94,20 +92,23 @@ fn run_direct_script( timeout_secs: u64, ) -> ShellCommandResult { let project_registry_dir = tempfile::tempdir().unwrap(); - run_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &ShellConfig::default(), - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.to_string_lossy().as_ref()), + (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &ShellConfig::default(), + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.to_string_lossy().as_ref()), + timeout_secs, + stop_requested: None, + }) + .run_script( &ShellScriptPayload { language, script, args, }, stdin, - timeout_secs, None, ) } @@ -119,6 +120,74 @@ fn unrestricted_policy() -> RunnerPolicy { } } +#[test] +fn process_preparation_fence_controls_effects_and_start_notification() { + let tmp = tempfile::tempdir().unwrap(); + let policy = unrestricted_policy(); + let shell = ShellConfig::default(); + let cache = PreparedShellProfileCache::default(); + let context = ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &shell, + project_registry_dir: tmp.path(), + cache: &cache, + cwd: tmp.path().to_str(), + timeout_secs: 5, + stop_requested: None, + }; + for admitted in [false, true] { + let marker = tmp.path().join(format!("effects-{admitted}")); + let events = Mutex::new(Vec::new()); + let on_prepared = || { + assert!(!marker.exists(), "payload ran before the preparation fence"); + events.lock().unwrap().push("prepared"); + if admitted { + Ok(()) + } else { + Err("fixture_stale_build".into()) + } + }; + let on_started = || events.lock().unwrap().push("started"); + let result = context.run_process( + process_argv_helper().to_str().unwrap(), + &["fixture-argument".into()], + None, + &[ + ( + "WEBCODEX_TEST_CAPTURE_GO_MODULE_ENV", + marker.to_str().unwrap(), + ), + ("GOWORK", "off"), + ("GO111MODULE", "on"), + ], + Some(&on_started), + Some(&on_prepared), + ); + if admitted { + assert_eq!( + result.execution_state, + ShellCommandExecutionState::Completed + ); + assert_eq!(result.result.exit_code, Some(0)); + assert!(result.result.error.is_none(), "{result:?}"); + assert_eq!(*events.lock().unwrap(), ["prepared", "started"]); + assert_eq!( + std::fs::read_to_string(&marker).unwrap(), + "GOWORK=off\nGO111MODULE=on\nARGV=fixture-argument\n" + ); + } else { + assert_eq!( + result.execution_state, + ShellCommandExecutionState::NotStarted + ); + assert_eq!(result.result.error.as_deref(), Some("fixture_stale_build")); + assert_eq!(*events.lock().unwrap(), ["prepared"]); + assert!(!marker.exists()); + } + } +} + #[cfg(windows)] fn compile_internal_posix_test_executable(path: &Path) { let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -275,17 +344,17 @@ fn runner_real_process_native_search_preserves_modes_and_fallback() { }) }; let run = |payload: &serde_json::Value| { - run_windows_native_search_with_profiles( - 1, - &policy, - &shell, - root.path(), - &cache, - Some(project.to_str().unwrap()), - Some(&payload.to_string()), - 10, - None, - ) + (ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &shell, + project_registry_dir: root.path(), + cache: &cache, + cwd: Some(project.to_str().unwrap()), + timeout_secs: 10, + stop_requested: None, + }) + .run_windows_native_search(Some(&payload.to_string())) }; for (mode, expected) in [ ( @@ -443,17 +512,17 @@ fn runner_real_process_native_search_directory_globs_and_bounds() { shell: &ShellConfig, payload: &serde_json::Value, stopped: Option<&AtomicBool>| { - run_windows_native_search_with_profiles( - 1, + (ShellExecutionContext { + generation: 1, policy, shell, - root.path(), - &cache, - Some(project.to_str().unwrap()), - Some(&payload.to_string()), - 10, - stopped, - ) + project_registry_dir: root.path(), + cache: &cache, + cwd: Some(project.to_str().unwrap()), + timeout_secs: 10, + stop_requested: stopped, + }) + .run_windows_native_search(Some(&payload.to_string())) }; let mut payload = serde_json::json!({"pattern": "needle\\.literal", "path": ".", "limit": 20, "context_before": 0, "context_after": 0, "include_globs": ["**/*.rs"], "exclude_globs": ["**/skip.rs"], "result_mode": "matches"}); for mode in ["matches", "files_with_matches", "count"] { @@ -481,21 +550,21 @@ fn runner_real_process_native_search_directory_globs_and_bounds() { .map(|arg| format!("'{}'", arg.replace('\'', "'\\''"))) .collect::>() .join(" "); - let fallback = run_internal_search_script_with_profiles_and_execution_state( - 1, - &policy, - &shell, - root.path(), - &cache, - Some(project.to_str().unwrap()), - &format!( - "set -o pipefail\nrg {argv} | head -n {} | head -c {}", - spec.line_limit, - PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET + 1 - ), - 10, - None, - ); + let fallback = (ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &shell, + project_registry_dir: root.path(), + cache: &cache, + cwd: Some(project.to_str().unwrap()), + timeout_secs: 10, + stop_requested: None, + }) + .run_internal_search_script(&format!( + "set -o pipefail\nrg {argv} | head -n {} | head -c {}", + spec.line_limit, + PROJECT_TEXT_SEARCH_OUTPUT_BYTE_BUDGET + 1 + )); assert_eq!( fallback.execution_state, ShellCommandExecutionState::Completed, @@ -778,17 +847,17 @@ fn internal_posix_runtime_uses_git_bash_stdin_with_powershell_configured() { .insert("PATH".to_string(), git_cmd.to_string_lossy().into_owned()); let script = "n=0\nwhile [ \"$n\" -lt 1 ]; do n=$((n + 1)); done\nprintf 'internal-posix-ok\\n'\n"; - let result = run_internal_posix_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &shell, - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), - script, - 10, - None, - ); + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &shell, + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_internal_posix_script(script); assert_eq!( result.execution_state, @@ -820,17 +889,17 @@ fn internal_posix_runtime_ignores_configured_shell_on_posix_hosts() { ..Default::default() }; let script = "printf 'internal-posix-ok\\n'\n"; - let result = run_internal_posix_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &shell, - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), - script, - 10, - None, - ); + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &shell, + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_internal_posix_script(script); assert_eq!( result.execution_state, @@ -1310,17 +1379,22 @@ fn structured_process_continuously_drains_large_stdout_and_stderr() { let project_registry_dir = tempfile::tempdir().unwrap(); let mut policy = unrestricted_policy(); policy.max_output_bytes = 16 * 1024; - let result = run_process_with_profiles_and_execution_state( - 1, - &policy, - &ShellConfig::default(), - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let result = (ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &ShellConfig::default(), + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_process( &process_argv_helper().to_string_lossy(), &["chatty".to_string(), "512".to_string()], None, - 10, + &[], + None, None, ); @@ -1374,20 +1448,23 @@ fn structured_script_continuously_drains_large_stdout_and_stderr() { "out='{stdout_payload}'\nerr='{stderr_payload}'\ni=0\nwhile [ \"$i\" -lt 512 ]; do\n printf '%s' \"$out\"\n printf '%s' \"$err\" >&2\n i=$((i + 1))\ndone\n" ), ); - let result = run_script_with_profiles_and_execution_state( - 1, - &policy, - &ShellConfig::default(), - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let result = (ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &ShellConfig::default(), + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 30, + stop_requested: None, + }) + .run_script( &ShellScriptPayload { language, script, args: Vec::new(), }, None, - 30, None, ); @@ -1691,20 +1768,23 @@ fn missing_script_interpreter_is_prestart_and_does_not_run_script() { ..Default::default() }; shell.env.insert("PATH".to_string(), String::new()); - let result = run_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &shell, - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &shell, + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_script( &ShellScriptPayload { language: ShellScriptLanguage::Bash, script: "touch marker".to_string(), args: Vec::new(), }, None, - 10, None, ); assert_eq!( @@ -1731,13 +1811,17 @@ fn missing_javascript_interpreter_is_prestart_and_does_not_run_script() { ..Default::default() }; shell.env.insert("PATH".to_string(), String::new()); - let result = run_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &shell, - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &shell, + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_script( &ShellScriptPayload { language: ShellScriptLanguage::Javascript, script: "import { writeFileSync } from 'node:fs'; writeFileSync('marker', 'ran');" @@ -1745,7 +1829,6 @@ fn missing_javascript_interpreter_is_prestart_and_does_not_run_script() { args: Vec::new(), }, None, - 10, None, ); assert_eq!( @@ -1769,20 +1852,23 @@ fn missing_typescript_interpreter_is_prestart_and_does_not_run_script() { ..Default::default() }; shell.env.insert("PATH".to_string(), String::new()); - let result = run_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &shell, - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &shell, + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_script( &ShellScriptPayload { language: ShellScriptLanguage::Typescript, script: "const marker: string = 'ran';\nvoid marker;".to_string(), args: Vec::new(), }, None, - 10, None, ); assert_eq!( @@ -1917,20 +2003,23 @@ fn arbitrary_configured_shell_is_not_treated_as_a_script_language() { ..Default::default() }; shell.env.insert("PATH".to_string(), String::new()); - let result = run_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &shell, - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let result = (ShellExecutionContext { + generation: 1, + policy: &unrestricted_policy(), + shell: &shell, + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_script( &ShellScriptPayload { language: ShellScriptLanguage::Bash, script: "true".to_string(), args: Vec::new(), }, None, - 10, None, ); @@ -2497,22 +2586,25 @@ fn powershell_runtime_executes_from_file_when_available() { { return; } - let result = run_script_with_profiles_and_execution_state( - 1, - &unrestricted_policy(), - &ShellConfig::default(), - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let result = (ShellExecutionContext { +generation: 1, +policy: &unrestricted_policy(), +shell: &ShellConfig::default(), +project_registry_dir: project_registry_dir.path(), +cache: &PreparedShellProfileCache::default(), +cwd: Some(cwd.path().to_string_lossy().as_ref()), +timeout_secs: // This smoke validates external PowerShell file execution, not timeout + // behavior. Allow cold pwsh startup under a loaded CI runner. + 30, +stop_requested: None, +}) + .run_script( &ShellScriptPayload { language: ShellScriptLanguage::Powershell, script: "param([string]$Value)\nWrite-Output $Value".to_string(), args: vec!["two words".to_string()], }, None, - // This smoke validates external PowerShell file execution, not timeout - // behavior. Allow cold pwsh startup under a loaded CI runner. - 30, None, ); assert_eq!( @@ -2594,13 +2686,17 @@ fn phase_f_windows_native_oem_stdout_stderr_use_active_oem_page() { let project_registry_dir = tempfile::tempdir().unwrap(); let mut policy = unrestricted_policy(); policy.max_output_bytes = 64; - let bounded = run_process_with_profiles_and_execution_state( - 1, - &policy, - &ShellConfig::default(), - project_registry_dir.path(), - &PreparedShellProfileCache::default(), - Some(cwd.path().to_string_lossy().as_ref()), + let bounded = (ShellExecutionContext { + generation: 1, + policy: &policy, + shell: &ShellConfig::default(), + project_registry_dir: project_registry_dir.path(), + cache: &PreparedShellProfileCache::default(), + cwd: Some(cwd.path().to_string_lossy().as_ref()), + timeout_secs: 10, + stop_requested: None, + }) + .run_process( &process_argv_helper().to_string_lossy(), &[ "windows-oem-output".to_string(), @@ -2608,7 +2704,8 @@ fn phase_f_windows_native_oem_stdout_stderr_use_active_oem_page() { "1000".to_string(), ], None, - 10, + &[], + None, None, ); for output in [bounded.result.stdout, bounded.result.stderr] {