1use std::collections::HashSet;
2use std::fs;
3use std::io::{self, Write};
4use std::path::{Path, PathBuf};
5use std::process;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use std::time::Instant;
9
10use crate::commands::time::{self, RunTiming};
11use crate::package;
12use crate::skill_loader::{
13 canonicalize_cli_dirs, emit_loader_warnings, install_skills_global, load_skills,
14 SkillLoaderInputs,
15};
16use harn_parser::DiagnosticSeverity;
17
18mod chunk_loading;
19pub(crate) mod environment;
20mod eval_source;
21mod explain_cost;
22pub mod harnpack;
23mod interrupts;
24pub mod json_events;
25mod lifecycle;
26mod llm_mock;
27mod manifest_runtime;
28mod mcp_serve;
29mod outcome;
30mod reporting;
31
32use outcome::{
33 finalize_harnpack_dry_run, finalize_harnpack_error, finalize_run_error,
34 render_return_value_error, JsonRunSession,
35};
36pub(crate) mod sandbox;
37
38pub(crate) use self::chunk_loading::{
39 compile_or_load_chunk_for_run, compile_or_load_chunk_with_timing, LoadedChunk,
40};
41use self::chunk_loading::{parse_source_for_run, typecheck_with_imports};
42pub(crate) use self::environment::{EnvironmentPolicyArg, EnvironmentPolicyConfig};
43use self::eval_source::create_eval_temp_file;
44pub(crate) use self::eval_source::prepare_eval_temp_file;
45#[cfg(test)]
46use self::eval_source::{eval_source_for_code, split_eval_header};
47use self::harnpack::{HarnpackError, HarnpackRunOptions, PreparedHarnpack};
48use self::interrupts::{
49 install_signal_shutdown_handler, start_run_deadline_watchdog, RunDeadlineGuard,
50};
51use self::json_events::NdjsonEmitter;
52pub use self::lifecycle::RunProfileOptions;
53use self::lifecycle::{RunExecution, TerminalRun};
54pub use self::llm_mock::*;
55pub(crate) use self::manifest_runtime::connect_mcp_servers;
56pub(crate) use self::mcp_serve::{
57 resolve_card_source, run_file_mcp_serve, RunFileAppServe, RunFileMcpServeHttp,
58 RunFileMcpServeMode,
59};
60use self::reporting::{
61 append_run_provenance_event, emit_run_attestation, emit_run_aux_for_exit,
62 exit_code_from_return_value, now_ms, render_and_persist_profile_rollup,
63 run_summary_llm_snapshot,
64};
65pub(crate) use self::reporting::{
66 render_trace_summary, run_aux_options_from_args, run_control_options_from_args,
67};
68pub use self::reporting::{
69 RunAuxOptions, RunControlOptions, RunJsonOptions, RunJsonSink, RunJsonSinkTarget,
70 RunPhaseOptions, RunRusageOptions, RunSummaryOptions, RUN_PHASE_SCHEMA_VERSION,
71 RUN_RUSAGE_SCHEMA_VERSION, RUN_SUMMARY_SCHEMA_VERSION,
72};
73#[cfg(test)]
74use self::sandbox::default_run_capability_policy;
75pub use self::sandbox::RunSandboxOptions;
76use self::sandbox::{
77 default_run_workspace_root, install_run_sandbox_scope, run_sandbox_attestation,
78};
79
80const CORE_BUILTINS: &[&str] = &[
82 "println",
83 "print",
84 "log",
85 "type_of",
86 "to_string",
87 "to_int",
88 "to_float",
89 "len",
90 "assert",
91 "assert_eq",
92 "assert_ne",
93 "json_parse",
94 "json_stringify",
95 "runtime_context",
96 "task_current",
97 "runtime_context_values",
98 "runtime_context_get",
99 "runtime_context_set",
100 "runtime_context_clear",
101];
102
103pub(crate) fn build_denied_builtins(
108 deny_csv: Option<&str>,
109 allow_csv: Option<&str>,
110) -> HashSet<String> {
111 if let Some(csv) = deny_csv {
112 csv.split(',')
113 .map(|s| s.trim().to_string())
114 .filter(|s| !s.is_empty())
115 .collect()
116 } else if let Some(csv) = allow_csv {
117 let allowed: HashSet<String> = csv
120 .split(',')
121 .map(|s| s.trim().to_string())
122 .filter(|s| !s.is_empty())
123 .collect();
124 let core: HashSet<&str> = CORE_BUILTINS.iter().copied().collect();
125
126 let mut tmp = harn_vm::Vm::new();
128 harn_vm::register_vm_stdlib(&mut tmp);
129 harn_vm::register_store_builtins(&mut tmp, std::path::Path::new("."));
130 harn_vm::register_metadata_builtins(&mut tmp, std::path::Path::new("."));
131
132 tmp.builtin_names()
133 .into_iter()
134 .filter(|name| !allowed.contains(name) && !core.contains(name.as_str()))
135 .collect()
136 } else {
137 HashSet::new()
138 }
139}
140
141#[derive(Clone, Debug, Default, PartialEq, Eq)]
142pub struct RunAttestationOptions {
143 pub receipt_out: Option<PathBuf>,
144 pub agent_id: Option<String>,
145}
146
147#[derive(Clone)]
148pub struct RunInterruptTokens {
149 pub cancel_token: Arc<AtomicBool>,
150 pub signal_token: Arc<Mutex<Option<String>>>,
151}
152
153struct ExecuteRunInputs<'a> {
154 path: &'a str,
155 trace: bool,
156 denied_builtins: HashSet<String>,
157 script_argv: Vec<String>,
158 skill_dirs_raw: Vec<String>,
159 llm_mock_mode: CliLlmMockMode,
160 attestation: Option<RunAttestationOptions>,
161 profile: RunProfileOptions,
162 sandbox: RunSandboxOptions,
163 interrupt_tokens: Option<RunInterruptTokens>,
164 json: Option<JsonRunSession>,
165 aux: RunAuxOptions,
166 timing: Option<&'a mut RunTiming>,
167 harnpack: HarnpackRunOptions,
168 defer_project_handlers: bool,
169}
170
171#[derive(Clone, Debug, Default)]
175pub struct RunOutcome {
176 pub stdout: String,
177 pub stderr: String,
178 pub exit_code: i32,
179}
180
181pub(crate) async fn run_file(
182 path: &str,
183 trace: bool,
184 denied_builtins: HashSet<String>,
185 script_argv: Vec<String>,
186 llm_mock_mode: CliLlmMockMode,
187 attestation: Option<RunAttestationOptions>,
188 profile: RunProfileOptions,
189) {
190 let exit_code = run_file_with_skill_dirs(
191 path,
192 trace,
193 denied_builtins,
194 script_argv,
195 Vec::new(),
196 llm_mock_mode,
197 attestation,
198 profile,
199 RunSandboxOptions::default(),
200 None,
201 RunAuxOptions::default(),
202 RunControlOptions::default(),
203 HarnpackRunOptions::default(),
204 )
205 .await;
206 if exit_code != 0 {
207 process::exit(exit_code);
208 }
209}
210
211pub(crate) fn run_explain_cost_file_with_skill_dirs(path: &str) -> i32 {
212 let outcome = execute_explain_cost(path);
213 if !outcome.stderr.is_empty() {
214 io::stderr().write_all(outcome.stderr.as_bytes()).ok();
215 }
216 if !outcome.stdout.is_empty() {
217 io::stdout().write_all(outcome.stdout.as_bytes()).ok();
218 }
219 outcome.exit_code
220}
221
222#[allow(clippy::too_many_arguments)]
223pub(crate) async fn run_file_with_skill_dirs(
224 path: &str,
225 trace: bool,
226 denied_builtins: HashSet<String>,
227 script_argv: Vec<String>,
228 skill_dirs_raw: Vec<String>,
229 llm_mock_mode: CliLlmMockMode,
230 attestation: Option<RunAttestationOptions>,
231 profile: RunProfileOptions,
232 sandbox: RunSandboxOptions,
233 json: Option<RunJsonOptions>,
234 aux: RunAuxOptions,
235 control: RunControlOptions,
236 harnpack: HarnpackRunOptions,
237) -> i32 {
238 let interrupt_tokens = install_signal_shutdown_handler();
240 let deadline_guard = control
241 .timeout
242 .map(|timeout| start_run_deadline_watchdog(timeout, interrupt_tokens.clone()));
243
244 let _stdout_passthrough = StdoutPassthroughGuard::enable();
245 let json_session = json.map(|options| {
246 JsonRunSession::new(options, Box::new(io::stdout()) as Box<dyn io::Write + Send>)
247 });
248 let outcome = execute_run_inner(ExecuteRunInputs {
249 path,
250 trace,
251 denied_builtins,
252 script_argv,
253 skill_dirs_raw,
254 llm_mock_mode,
255 attestation,
256 profile,
257 sandbox,
258 interrupt_tokens: Some(interrupt_tokens.clone()),
259 json: json_session,
260 aux,
261 timing: None,
262 harnpack,
263 defer_project_handlers: control.defer_project_handlers,
264 })
265 .await;
266 if let Some(guard) = &deadline_guard {
267 guard.finish();
268 }
269
270 if !outcome.stderr.is_empty() {
273 io::stderr().write_all(outcome.stderr.as_bytes()).ok();
274 }
275 if !outcome.stdout.is_empty() {
276 io::stdout().write_all(outcome.stdout.as_bytes()).ok();
277 }
278
279 let mut exit_code = outcome.exit_code;
280 if deadline_guard
281 .as_ref()
282 .is_some_and(RunDeadlineGuard::timed_out)
283 || (exit_code != 0 && interrupt_tokens.cancel_token.load(Ordering::SeqCst))
284 {
285 exit_code = 124;
286 }
287 exit_code
288}
289
290#[allow(clippy::too_many_arguments)]
291pub(crate) async fn run_resume_with_skill_dirs(
292 target: &str,
293 trace: bool,
294 denied_builtins: HashSet<String>,
295 resume_argv: Vec<String>,
296 skill_dirs_raw: Vec<String>,
297 llm_mock_mode: CliLlmMockMode,
298 attestation: Option<RunAttestationOptions>,
299 profile: RunProfileOptions,
300 sandbox: RunSandboxOptions,
301 json: Option<RunJsonOptions>,
302 aux: RunAuxOptions,
303 control: RunControlOptions,
304) -> i32 {
305 let source = r#"import { resume_agent, wait_agent } from "std/agent/workers"
306
307pipeline main(harness: Harness) {
308 const input = if len(argv) > 1 {
309 argv[1]
310 } else {
311 nil
312 }
313 const handle = resume_agent(harness.agent, argv[0], input, true)
314 return wait_agent(harness.agent, handle)
315}
316"#;
317 let tmp = match create_eval_temp_file() {
318 Ok(tmp) => tmp,
319 Err(error) => {
320 eprintln!("error: {error}");
321 return 1;
322 }
323 };
324 let tmp_path = tmp.path().to_path_buf();
325 if let Err(error) = fs::write(&tmp_path, source) {
326 eprintln!("error: failed to write temp file for --resume: {error}");
327 return 1;
328 }
329 let mut argv = Vec::with_capacity(resume_argv.len() + 1);
330 argv.push(target.to_string());
331 argv.extend(resume_argv);
332 let tmp_str = tmp_path.to_string_lossy().into_owned();
333 run_file_with_skill_dirs(
334 &tmp_str,
335 trace,
336 denied_builtins,
337 argv,
338 skill_dirs_raw,
339 llm_mock_mode,
340 attestation,
341 profile,
342 sandbox,
343 json,
344 aux,
345 control,
346 HarnpackRunOptions::default(),
347 )
348 .await
349}
350
351pub fn execute_explain_cost(path: &str) -> RunOutcome {
352 let stdout = String::new();
353 let mut stderr = String::new();
354
355 let source = match fs::read_to_string(path) {
356 Ok(source) => source,
357 Err(error) => {
358 stderr.push_str(&format!("Error reading {path}: {error}\n"));
359 return RunOutcome {
360 stdout,
361 stderr,
362 exit_code: 1,
363 };
364 }
365 };
366 let program = match parse_source_for_run(path, &source, &mut stderr) {
367 Some(program) => program,
368 None => {
369 return RunOutcome {
370 stdout,
371 stderr,
372 exit_code: 1,
373 };
374 }
375 };
376
377 let mut had_type_error = false;
378 let type_diagnostics = match typecheck_with_imports(&program, Path::new(path), &source) {
379 Ok(diagnostics) => diagnostics,
380 Err(error) => {
381 stderr.push_str(&format!("error: {error}\n"));
382 return RunOutcome {
383 stdout,
384 stderr,
385 exit_code: 1,
386 };
387 }
388 };
389 for diag in &type_diagnostics {
390 let rendered = harn_parser::diagnostic::render_type_diagnostic(&source, path, diag);
391 if matches!(diag.severity, DiagnosticSeverity::Error) {
392 had_type_error = true;
393 }
394 stderr.push_str(&rendered);
395 }
396 if had_type_error {
397 return RunOutcome {
398 stdout,
399 stderr,
400 exit_code: 1,
401 };
402 }
403
404 let extensions = package::load_runtime_extensions(Path::new(path));
405 package::install_runtime_extensions(&extensions);
406 RunOutcome {
407 stdout: explain_cost::render_explain_cost(path, &program),
408 stderr,
409 exit_code: 0,
410 }
411}
412
413pub(crate) struct StdoutPassthroughGuard {
414 previous: bool,
415}
416
417impl StdoutPassthroughGuard {
418 pub(crate) fn enable() -> Self {
419 Self {
420 previous: harn_vm::set_stdout_passthrough(true),
421 }
422 }
423}
424
425impl Drop for StdoutPassthroughGuard {
426 fn drop(&mut self) {
427 harn_vm::set_stdout_passthrough(self.previous);
428 }
429}
430
431pub async fn execute_run(
445 path: &str,
446 trace: bool,
447 denied_builtins: HashSet<String>,
448 script_argv: Vec<String>,
449 skill_dirs_raw: Vec<String>,
450 llm_mock_mode: CliLlmMockMode,
451 attestation: Option<RunAttestationOptions>,
452 profile: RunProfileOptions,
453) -> RunOutcome {
454 crate::ensure_builtin_signatures_installed();
455 execute_run_with_harnpack_and_sandbox_options(
456 path,
457 trace,
458 denied_builtins,
459 script_argv,
460 skill_dirs_raw,
461 llm_mock_mode,
462 attestation,
463 profile,
464 RunSandboxOptions::default(),
465 HarnpackRunOptions::default(),
466 )
467 .await
468}
469
470#[allow(clippy::too_many_arguments)]
474pub async fn execute_run_with_sandbox_options(
475 path: &str,
476 trace: bool,
477 denied_builtins: HashSet<String>,
478 script_argv: Vec<String>,
479 skill_dirs_raw: Vec<String>,
480 llm_mock_mode: CliLlmMockMode,
481 attestation: Option<RunAttestationOptions>,
482 profile: RunProfileOptions,
483 sandbox: RunSandboxOptions,
484) -> RunOutcome {
485 execute_run_with_harnpack_and_sandbox_options(
486 path,
487 trace,
488 denied_builtins,
489 script_argv,
490 skill_dirs_raw,
491 llm_mock_mode,
492 attestation,
493 profile,
494 sandbox,
495 HarnpackRunOptions::default(),
496 )
497 .await
498}
499
500#[allow(clippy::too_many_arguments)]
505pub async fn execute_run_with_harnpack_options(
506 path: &str,
507 trace: bool,
508 denied_builtins: HashSet<String>,
509 script_argv: Vec<String>,
510 skill_dirs_raw: Vec<String>,
511 llm_mock_mode: CliLlmMockMode,
512 attestation: Option<RunAttestationOptions>,
513 profile: RunProfileOptions,
514 harnpack: HarnpackRunOptions,
515) -> RunOutcome {
516 execute_run_with_harnpack_and_sandbox_options(
517 path,
518 trace,
519 denied_builtins,
520 script_argv,
521 skill_dirs_raw,
522 llm_mock_mode,
523 attestation,
524 profile,
525 RunSandboxOptions::default(),
526 harnpack,
527 )
528 .await
529}
530
531#[allow(clippy::too_many_arguments)]
532async fn execute_run_with_harnpack_and_sandbox_options(
533 path: &str,
534 trace: bool,
535 denied_builtins: HashSet<String>,
536 script_argv: Vec<String>,
537 skill_dirs_raw: Vec<String>,
538 llm_mock_mode: CliLlmMockMode,
539 attestation: Option<RunAttestationOptions>,
540 profile: RunProfileOptions,
541 sandbox: RunSandboxOptions,
542 harnpack: HarnpackRunOptions,
543) -> RunOutcome {
544 execute_run_inner(ExecuteRunInputs {
545 path,
546 trace,
547 denied_builtins,
548 script_argv,
549 skill_dirs_raw,
550 llm_mock_mode,
551 attestation,
552 profile,
553 sandbox,
554 interrupt_tokens: None,
555 json: None,
556 aux: RunAuxOptions::default(),
557 timing: None,
558 harnpack,
559 defer_project_handlers: false,
560 })
561 .await
562}
563
564#[allow(clippy::too_many_arguments)]
570pub async fn execute_run_json(
571 path: &str,
572 trace: bool,
573 denied_builtins: HashSet<String>,
574 script_argv: Vec<String>,
575 skill_dirs_raw: Vec<String>,
576 llm_mock_mode: CliLlmMockMode,
577 attestation: Option<RunAttestationOptions>,
578 profile: RunProfileOptions,
579 out: Box<dyn io::Write + Send>,
580 options: RunJsonOptions,
581) -> RunOutcome {
582 execute_run_inner(ExecuteRunInputs {
583 path,
584 trace,
585 denied_builtins,
586 script_argv,
587 skill_dirs_raw,
588 llm_mock_mode,
589 attestation,
590 profile,
591 sandbox: RunSandboxOptions::default(),
592 interrupt_tokens: None,
593 json: Some(JsonRunSession::new(options, out)),
594 aux: RunAuxOptions::default(),
595 timing: None,
596 harnpack: HarnpackRunOptions::default(),
597 defer_project_handlers: false,
598 })
599 .await
600}
601
602pub(crate) async fn execute_run_with_timing(
606 path: &str,
607 script_argv: Vec<String>,
608 timing: Option<&mut RunTiming>,
609 sandbox: RunSandboxOptions,
610) -> RunOutcome {
611 execute_run_inner(ExecuteRunInputs {
612 path,
613 trace: false,
614 denied_builtins: HashSet::new(),
615 script_argv,
616 skill_dirs_raw: Vec::new(),
617 llm_mock_mode: CliLlmMockMode::Off,
618 attestation: None,
619 profile: RunProfileOptions::default(),
620 sandbox,
621 interrupt_tokens: None,
622 json: None,
623 aux: RunAuxOptions::default(),
624 timing,
625 harnpack: HarnpackRunOptions::default(),
626 defer_project_handlers: false,
627 })
628 .await
629}
630
631fn entry_source_dir(path: &str) -> std::path::PathBuf {
645 match std::path::Path::new(path).parent() {
646 Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
647 _ => std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
648 }
649}
650
651#[allow(clippy::needless_option_as_deref)]
654async fn execute_run_inner(inputs: ExecuteRunInputs<'_>) -> RunOutcome {
655 let mut inputs = inputs;
656 let json_session = inputs.json.take();
657 let Some(json_session) = json_session else {
658 return execute_run_inner_scoped(inputs, None).await;
659 };
660 let sink = json_session.sink();
661 harn_vm::run_events::scope(sink, execute_run_inner_scoped(inputs, Some(json_session))).await
662}
663
664async fn execute_run_inner_scoped(
665 inputs: ExecuteRunInputs<'_>,
666 json_session: Option<JsonRunSession>,
667) -> RunOutcome {
668 let ExecuteRunInputs {
669 path,
670 trace,
671 denied_builtins,
672 script_argv,
673 skill_dirs_raw,
674 llm_mock_mode,
675 attestation,
676 profile,
677 sandbox,
678 interrupt_tokens,
679 json: _,
680 aux,
681 timing,
682 harnpack,
683 defer_project_handlers,
684 } = inputs;
685 let RunAuxOptions {
686 summary,
687 phase,
688 rusage,
689 } = aux;
690 let run_started = Instant::now();
691 let cpu_started_ms = rusage.as_ref().map(|_| time::cpu_ms());
692 let mut owned_timing = if timing.is_none() && (phase.is_some() || rusage.is_some()) {
693 Some(RunTiming::default())
694 } else {
695 None
696 };
697 let mut timing = timing.or(owned_timing.as_mut());
698
699 let mut stderr = String::new();
700 let mut stdout = String::new();
701
702 let owned_run_path: String;
707 let mut prepared_harnpack: Option<PreparedHarnpack> = None;
708 let resolved_path: &str = if harnpack::looks_like_harnpack(Path::new(path)) {
709 let outcome = match harnpack::prepare_harnpack(Path::new(path), &harnpack, &mut stderr) {
710 Ok(prepared) => prepared,
711 Err(err) => {
712 return finalize_harnpack_error(
713 stderr,
714 json_session,
715 summary.as_ref(),
716 phase.as_ref(),
717 rusage.as_ref(),
718 run_started,
719 err,
720 );
721 }
722 };
723 harn_vm::run_events::emit(harn_vm::run_events::RunEvent::PackRun {
724 bundle_hash: outcome.bundle_hash.clone(),
725 signature_verified: outcome.signature_verified,
726 key_id: outcome.key_id.clone(),
727 cache_hit: outcome.cache_hit,
728 dry_run_verify: harnpack.dry_run_verify,
729 execution_artifact_state: outcome.execution_artifact_state.to_string(),
730 fallback_reason: outcome.fallback_reason.clone(),
731 artifact_decode_ms: outcome.artifact_decode_elapsed.as_millis() as u64,
732 });
733 if harnpack.dry_run_verify {
734 return finalize_harnpack_dry_run(
735 stderr,
736 json_session,
737 summary.as_ref(),
738 phase.as_ref(),
739 rusage.as_ref(),
740 run_started,
741 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
742 &outcome,
743 );
744 }
745 owned_run_path = outcome.entrypoint_path.to_string_lossy().into_owned();
746 prepared_harnpack = Some(outcome);
747 owned_run_path.as_str()
748 } else {
749 path
750 };
751
752 let mut linked_runtime = None;
753 let loaded = if let Some(linked) = prepared_harnpack
754 .as_mut()
755 .and_then(|prepared| prepared.linked_program.take())
756 {
757 let source = match std::fs::read_to_string(resolved_path) {
758 Ok(source) => source,
759 Err(error) => {
760 stderr.push_str(&format!("Error reading {resolved_path}: {error}\n"));
761 return finalize_run_error(
762 stdout,
763 stderr.clone(),
764 json_session,
765 summary.as_ref(),
766 phase.as_ref(),
767 rusage.as_ref(),
768 run_started,
769 None,
770 timing.as_deref(),
771 0,
772 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
773 "linked_program_source",
774 stderr,
775 );
776 }
777 };
778 let source_root = prepared_harnpack
779 .as_ref()
780 .expect("linked program came from a prepared pack")
781 .cache_dir
782 .join("sources");
783 let runtime = linked.into_runtime(&source_root);
784 let chunk = runtime.entry_chunk.clone();
785 linked_runtime = Some(runtime);
786 Some(LoadedChunk {
787 source,
788 chunk,
789 link_table: None,
790 })
791 } else {
792 compile_or_load_chunk_with_timing(resolved_path, &mut stderr, timing.as_deref_mut())
793 };
794 let Some(LoadedChunk {
795 source,
796 chunk,
797 link_table,
798 }) = loaded
799 else {
800 let message = stderr.clone();
801 return finalize_run_error(
802 stdout,
803 stderr,
804 json_session,
805 summary.as_ref(),
806 phase.as_ref(),
807 rusage.as_ref(),
808 run_started,
809 None,
810 timing.as_deref(),
811 0,
812 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
813 "compile_error",
814 message,
815 );
816 };
817 let path = resolved_path;
818
819 let setup_start = Instant::now();
820 if trace || summary.is_some() {
821 harn_vm::llm::enable_tracing();
822 }
823 if profile.is_enabled() || phase.is_some() {
824 harn_vm::tracing::set_tracing_enabled(true);
825 }
826 let _builtin_profile_guard = profile.is_enabled().then(harn_vm::builtin_profile::enable);
832 if let Err(error) = install_cli_llm_mock_mode(&llm_mock_mode) {
833 stderr.push_str(&format!("error: {error}\n"));
834 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
835 return finalize_run_error(
836 stdout,
837 stderr,
838 json_session,
839 summary.as_ref(),
840 phase.as_ref(),
841 rusage.as_ref(),
842 run_started,
843 None,
844 timing.as_deref(),
845 0,
846 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
847 "llm_mock_install",
848 error,
849 );
850 }
851
852 let mut vm = harn_vm::Vm::new();
853 vm.set_graph_link_table(link_table);
854 if let Some(runtime) = &linked_runtime {
855 vm.set_linked_program_runtime(runtime);
856 }
857 if let Some(timing) = timing.as_deref_mut() {
858 timing.module_phases = Some(vm.enable_module_phase_timing());
859 }
860 if let Some(interrupt_tokens) = interrupt_tokens {
861 vm.install_interrupt_signal_token(interrupt_tokens.signal_token);
862 vm.install_cancel_token(interrupt_tokens.cancel_token);
863 }
864 harn_vm::register_vm_stdlib(&mut vm);
865 crate::install_default_hostlib(&mut vm);
866 let source_parent = std::path::Path::new(path)
867 .parent()
868 .unwrap_or(std::path::Path::new("."));
869 let project_root = harn_vm::stdlib::process::find_project_root(source_parent);
871 let store_base = project_root.as_deref().unwrap_or(source_parent);
872 let sandbox_root = sandbox
873 .workspace_root
874 .clone()
875 .unwrap_or_else(|| default_run_workspace_root(project_root.as_deref(), source_parent));
876 let _sandbox_scope = install_run_sandbox_scope(&sandbox, &sandbox_root, &mut stderr);
877
878 let (_environment_scope, environment_policy, grant_receipts) =
885 match environment::launch_scope(&sandbox.environment, &mut stderr) {
886 Ok(launched) => launched,
887 Err(error) => {
888 stderr.push_str(&format!("error: {error}\n"));
889 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
890 let code = error.code();
891 return finalize_run_error(
892 stdout,
893 stderr,
894 json_session,
895 summary.as_ref(),
896 phase.as_ref(),
897 rusage.as_ref(),
898 run_started,
899 None,
900 timing.as_deref(),
901 0,
902 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
903 code,
904 error.to_string(),
905 );
906 }
907 };
908
909 let attestation_started_at_ms = now_ms();
910 let attestation_log = if attestation.is_some() {
911 Some(harn_vm::event_log::install_memory_for_current_thread(256))
912 } else {
913 None
914 };
915 if let Some(log) = attestation_log.as_ref() {
916 append_run_provenance_event(
917 log,
918 "started",
919 serde_json::json!({
920 "pipeline": path,
921 "argv": &script_argv,
922 "project_root": store_base.display().to_string(),
923 "sandbox": run_sandbox_attestation(&sandbox),
924 "environment_policy": environment_policy.as_str(),
925 "environment_grants": &grant_receipts,
927 }),
928 )
929 .await;
930 }
931 harn_vm::register_store_builtins(&mut vm, store_base);
932 harn_vm::register_metadata_builtins(&mut vm, store_base);
933 let pipeline_name = std::path::Path::new(path)
934 .file_stem()
935 .and_then(|s| s.to_str())
936 .unwrap_or("default");
937 harn_vm::register_checkpoint_builtins(&mut vm, store_base, pipeline_name);
938 vm.set_source_info(path, &source);
939 let defer_manifest_handlers = defer_project_handlers || !denied_builtins.is_empty();
940 if !denied_builtins.is_empty() {
941 vm.set_denied_builtins(denied_builtins);
942 }
943 if let Some(ref root) = project_root {
944 vm.set_project_root(root);
945 }
946
947 vm.set_source_dir(&entry_source_dir(path));
952
953 let cli_dirs = canonicalize_cli_dirs(&skill_dirs_raw, None);
956 let loaded = load_skills(&SkillLoaderInputs {
957 cli_dirs,
958 source_path: Some(std::path::PathBuf::from(path)),
959 });
960 emit_loader_warnings(&loaded.loader_warnings);
961 install_skills_global(&mut vm, &loaded);
962
963 let argv_values: Vec<harn_vm::VmValue> = script_argv
966 .iter()
967 .map(|s| harn_vm::VmValue::String(arcstr::ArcStr::from(s.as_str())))
968 .collect();
969 vm.set_global(
970 "argv",
971 harn_vm::VmValue::List(std::sync::Arc::new(argv_values)),
972 );
973
974 let runtime_harness =
978 match crate::default_harness_for_manifest_or_base_dir(Path::new(path), store_base) {
979 Ok(harness) => harness,
980 Err(error) => {
981 stderr.push_str(&format!(
982 "error: failed to configure harness secret provider: {error}\n"
983 ));
984 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
985 return finalize_run_error(
986 stdout,
987 stderr,
988 json_session,
989 summary.as_ref(),
990 phase.as_ref(),
991 rusage.as_ref(),
992 run_started,
993 None,
994 timing.as_deref(),
995 0,
996 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
997 "harness_secret_provider",
998 error,
999 );
1000 }
1001 };
1002 vm.set_harness(runtime_harness);
1003
1004 let _manifest_runtime = match manifest_runtime::install_manifest_runtime(
1007 Path::new(path),
1008 store_base,
1009 &mut vm,
1010 defer_manifest_handlers,
1011 )
1012 .await
1013 {
1014 Ok(runtime) => runtime,
1015 Err(error) => {
1016 stderr.push_str(&format!(
1017 "error: failed to install {}: {error}\n",
1018 error.label()
1019 ));
1020 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
1021 return finalize_run_error(
1022 stdout,
1023 stderr,
1024 json_session,
1025 summary.as_ref(),
1026 phase.as_ref(),
1027 rusage.as_ref(),
1028 run_started,
1029 None,
1030 timing.as_deref(),
1031 0,
1032 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1033 error.phase(),
1034 error.to_string(),
1035 );
1036 }
1037 };
1038
1039 let local = tokio::task::LocalSet::new();
1040 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
1041 let main_start = Instant::now();
1042 vm.set_source_dir(&entry_source_dir(path));
1050 let execution = local
1051 .run_until(async {
1052 match vm.execute(&chunk).await {
1053 Ok(value) => RunExecution::Terminal(TerminalRun::Returned(value)),
1054 Err(error) => match error.process_exit_code() {
1055 Some(code) => RunExecution::Terminal(TerminalRun::ProcessExited(code)),
1056 None => RunExecution::Failed(vm.format_runtime_error(&error)),
1057 },
1058 }
1059 })
1060 .await;
1061 let output = vm.output();
1062 if let Some(t) = timing.as_deref_mut() {
1063 t.run_main = main_start.elapsed();
1064 }
1065 if let Err(error) = persist_cli_llm_mock_recording(&llm_mock_mode) {
1066 stderr.push_str(&format!("error: {error}\n"));
1067 let profile_rollup = if profile.is_enabled() {
1068 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1069 } else {
1070 None
1071 };
1072 return finalize_run_error(
1073 stdout,
1074 stderr,
1075 json_session,
1076 summary.as_ref(),
1077 phase.as_ref(),
1078 rusage.as_ref(),
1079 run_started,
1080 profile_rollup.as_ref(),
1081 timing.as_deref(),
1082 harn_vm::tracing::peek_spans().len() as u64,
1083 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1084 "llm_mock_record",
1085 error,
1086 );
1087 }
1088
1089 let buffered_stderr = harn_vm::take_stderr_buffer();
1091 stderr.push_str(&buffered_stderr);
1092
1093 let exit_code = match &execution {
1094 RunExecution::Terminal(terminal) => terminal.exit_code(),
1095 RunExecution::Failed(_) => 1,
1096 };
1097
1098 if let (Some(options), Some(log)) = (attestation.as_ref(), attestation_log.as_ref()) {
1099 if let Err(error) = emit_run_attestation(
1100 log,
1101 path,
1102 store_base,
1103 attestation_started_at_ms,
1104 exit_code,
1105 options,
1106 &mut stderr,
1107 )
1108 .await
1109 {
1110 stderr.push_str(&format!(
1111 "error: failed to emit provenance receipt: {error}\n"
1112 ));
1113 let profile_rollup = if profile.is_enabled() {
1114 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1115 } else {
1116 None
1117 };
1118 return finalize_run_error(
1119 stdout,
1120 stderr,
1121 json_session,
1122 summary.as_ref(),
1123 phase.as_ref(),
1124 rusage.as_ref(),
1125 run_started,
1126 profile_rollup.as_ref(),
1127 timing.as_deref(),
1128 harn_vm::tracing::peek_spans().len() as u64,
1129 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1130 "attestation",
1131 error,
1132 );
1133 }
1134 harn_vm::event_log::reset_active_event_log();
1135 }
1136
1137 match execution {
1138 RunExecution::Terminal(terminal) => {
1139 stdout.push_str(output);
1140 let main_events = harn_vm::tracing::peek_spans().len() as u64;
1141 let cpu_ms_total = cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start));
1142 let profile_rollup = if profile.is_enabled() {
1143 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1144 } else {
1145 None
1146 };
1147 let summary_llm = summary.as_ref().map(|_| run_summary_llm_snapshot());
1148 if trace {
1149 stderr.push_str(&render_trace_summary());
1150 }
1151 if let Some(profile_rollup) = profile_rollup.as_ref() {
1152 if let Err(error) =
1153 render_and_persist_profile_rollup(&profile, profile_rollup, &mut stderr)
1154 {
1155 stderr.push_str(&format!("warning: failed to write profile: {error}\n"));
1156 }
1157 }
1158 if let Some(diagnostic) = terminal.nonzero_return_diagnostic() {
1159 stderr.push_str(&diagnostic);
1160 }
1161 let aux_emission = emit_run_aux_for_exit(
1162 summary.as_ref(),
1163 phase.as_ref(),
1164 rusage.as_ref(),
1165 run_started,
1166 exit_code,
1167 profile_rollup.as_ref(),
1168 summary_llm,
1169 timing.as_deref(),
1170 main_events,
1171 cpu_ms_total,
1172 json_session.is_some(),
1173 &mut stderr,
1174 );
1175 if let Some(session) = json_session {
1176 if let Some(error) = aux_emission.error {
1177 let mut outcome = session.finalize_error(
1178 "run_aux",
1179 format!("failed to emit auxiliary run JSON: {error}"),
1180 1,
1181 );
1182 outcome.stderr = aux_emission.stderr;
1183 return outcome;
1184 }
1185 let value = terminal.json_value();
1186 let mut outcome = session.finalize_result(value, aux_emission.exit_code);
1187 outcome.stderr = aux_emission.stderr;
1188 return outcome;
1189 }
1190 RunOutcome {
1191 stdout,
1192 stderr,
1193 exit_code: aux_emission.exit_code,
1194 }
1195 }
1196 RunExecution::Failed(rendered_error) => {
1197 stderr.push_str(&rendered_error);
1198 let main_events = harn_vm::tracing::peek_spans().len() as u64;
1199 let cpu_ms_total = cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start));
1200 let profile_rollup = if profile.is_enabled() {
1201 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1202 } else {
1203 None
1204 };
1205 if let Some(profile_rollup) = profile_rollup.as_ref() {
1206 if let Err(error) =
1207 render_and_persist_profile_rollup(&profile, profile_rollup, &mut stderr)
1208 {
1209 stderr.push_str(&format!("warning: failed to write profile: {error}\n"));
1210 }
1211 }
1212 let aux_emission = emit_run_aux_for_exit(
1213 summary.as_ref(),
1214 phase.as_ref(),
1215 rusage.as_ref(),
1216 run_started,
1217 1,
1218 profile_rollup.as_ref(),
1219 None,
1220 timing.as_deref(),
1221 main_events,
1222 cpu_ms_total,
1223 json_session.is_some(),
1224 &mut stderr,
1225 );
1226 if let Some(session) = json_session {
1227 let mut outcome =
1228 session.finalize_error("runtime", rendered_error, aux_emission.exit_code);
1229 outcome.stderr = aux_emission.stderr;
1230 return outcome;
1231 }
1232 RunOutcome {
1233 stdout,
1234 stderr,
1235 exit_code: aux_emission.exit_code,
1236 }
1237 }
1238 }
1239}
1240
1241pub(crate) async fn run_watch(path: &str, denied_builtins: HashSet<String>) {
1242 use notify::{Event, EventKind, RecursiveMode, Watcher};
1243
1244 let abs_path = std::fs::canonicalize(path).unwrap_or_else(|e| {
1245 eprintln!("Error: {e}");
1246 process::exit(1);
1247 });
1248 let watch_dir = abs_path.parent().unwrap_or(Path::new("."));
1249
1250 eprintln!("\x1b[2m[watch] running {path}...\x1b[0m");
1251 run_file(
1252 path,
1253 false,
1254 denied_builtins.clone(),
1255 Vec::new(),
1256 CliLlmMockMode::Off,
1257 None,
1258 RunProfileOptions::default(),
1259 )
1260 .await;
1261
1262 let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
1263 let _watcher = {
1264 let tx = tx.clone();
1265 let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
1266 if let Ok(event) = res {
1267 if matches!(
1268 event.kind,
1269 EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
1270 ) {
1271 let has_harn = event
1272 .paths
1273 .iter()
1274 .any(|p| p.extension().is_some_and(|ext| ext == "harn"));
1275 if has_harn {
1276 let _ = tx.blocking_send(());
1277 }
1278 }
1279 }
1280 })
1281 .unwrap_or_else(|e| {
1282 eprintln!("Error setting up file watcher: {e}");
1283 process::exit(1);
1284 });
1285 watcher
1286 .watch(watch_dir, RecursiveMode::Recursive)
1287 .unwrap_or_else(|e| {
1288 eprintln!("Error watching directory: {e}");
1289 process::exit(1);
1290 });
1291 watcher };
1293
1294 eprintln!(
1295 "\x1b[2m[watch] watching {} for .harn changes (ctrl-c to stop)\x1b[0m",
1296 watch_dir.display()
1297 );
1298
1299 loop {
1300 rx.recv().await;
1301 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1303 while rx.try_recv().is_ok() {}
1304
1305 eprintln!();
1306 eprintln!("\x1b[2m[watch] change detected, re-running {path}...\x1b[0m");
1307 run_file(
1308 path,
1309 false,
1310 denied_builtins.clone(),
1311 Vec::new(),
1312 CliLlmMockMode::Off,
1313 None,
1314 RunProfileOptions::default(),
1315 )
1316 .await;
1317 }
1318}
1319
1320#[cfg(test)]
1321mod tests;