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 if crate::compiler_context::trusted_host_dispatch_for_source(std::path::Path::new(path)) {
873 if let Err(error) = vm.enable_trusted_host_dispatch() {
874 stderr.push_str(&format!(
875 "warning: failed to enable trusted host dispatch: {error}\n"
876 ));
877 }
878 }
879 let source_parent = std::path::Path::new(path)
880 .parent()
881 .unwrap_or(std::path::Path::new("."));
882 let project_root = harn_vm::stdlib::process::find_project_root(source_parent);
884 let store_base = project_root.as_deref().unwrap_or(source_parent);
885 let sandbox_root = sandbox
886 .workspace_root
887 .clone()
888 .unwrap_or_else(|| default_run_workspace_root(project_root.as_deref(), source_parent));
889 let _sandbox_scope = install_run_sandbox_scope(&sandbox, &sandbox_root, &mut stderr);
890
891 let (_environment_scope, environment_policy, grant_receipts) =
898 match environment::launch_scope(&sandbox.environment, &mut stderr) {
899 Ok(launched) => launched,
900 Err(error) => {
901 stderr.push_str(&format!("error: {error}\n"));
902 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
903 let code = error.code();
904 return finalize_run_error(
905 stdout,
906 stderr,
907 json_session,
908 summary.as_ref(),
909 phase.as_ref(),
910 rusage.as_ref(),
911 run_started,
912 None,
913 timing.as_deref(),
914 0,
915 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
916 code,
917 error.to_string(),
918 );
919 }
920 };
921
922 let attestation_started_at_ms = now_ms();
923 let attestation_log = if attestation.is_some() {
924 Some(harn_vm::event_log::install_memory_for_current_thread(256))
925 } else {
926 None
927 };
928 if let Some(log) = attestation_log.as_ref() {
929 append_run_provenance_event(
930 log,
931 "started",
932 serde_json::json!({
933 "pipeline": path,
934 "argv": &script_argv,
935 "project_root": store_base.display().to_string(),
936 "sandbox": run_sandbox_attestation(&sandbox),
937 "environment_policy": environment_policy.as_str(),
938 "environment_grants": &grant_receipts,
940 }),
941 )
942 .await;
943 }
944 harn_vm::register_store_builtins(&mut vm, store_base);
945 harn_vm::register_metadata_builtins(&mut vm, store_base);
946 let pipeline_name = std::path::Path::new(path)
947 .file_stem()
948 .and_then(|s| s.to_str())
949 .unwrap_or("default");
950 harn_vm::register_checkpoint_builtins(&mut vm, store_base, pipeline_name);
951 vm.set_source_info(path, &source);
952 let defer_manifest_handlers = defer_project_handlers || !denied_builtins.is_empty();
953 if !denied_builtins.is_empty() {
954 vm.set_denied_builtins(denied_builtins);
955 }
956 if let Some(ref root) = project_root {
957 vm.set_project_root(root);
958 }
959
960 vm.set_source_dir(&entry_source_dir(path));
965
966 let cli_dirs = canonicalize_cli_dirs(&skill_dirs_raw, None);
969 let loaded = load_skills(&SkillLoaderInputs {
970 cli_dirs,
971 source_path: Some(std::path::PathBuf::from(path)),
972 });
973 emit_loader_warnings(&loaded.loader_warnings);
974 install_skills_global(&mut vm, &loaded);
975
976 let argv_values: Vec<harn_vm::VmValue> = script_argv
979 .iter()
980 .map(|s| harn_vm::VmValue::String(arcstr::ArcStr::from(s.as_str())))
981 .collect();
982 vm.set_global(
983 "argv",
984 harn_vm::VmValue::List(std::sync::Arc::new(argv_values)),
985 );
986
987 let runtime_harness =
991 match crate::default_harness_for_manifest_or_base_dir(Path::new(path), store_base) {
992 Ok(harness) => harness,
993 Err(error) => {
994 stderr.push_str(&format!(
995 "error: failed to configure harness secret provider: {error}\n"
996 ));
997 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
998 return finalize_run_error(
999 stdout,
1000 stderr,
1001 json_session,
1002 summary.as_ref(),
1003 phase.as_ref(),
1004 rusage.as_ref(),
1005 run_started,
1006 None,
1007 timing.as_deref(),
1008 0,
1009 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1010 "harness_secret_provider",
1011 error,
1012 );
1013 }
1014 };
1015 vm.set_harness(runtime_harness);
1016
1017 let _manifest_runtime = match manifest_runtime::install_manifest_runtime(
1020 Path::new(path),
1021 store_base,
1022 &mut vm,
1023 defer_manifest_handlers,
1024 )
1025 .await
1026 {
1027 Ok(runtime) => runtime,
1028 Err(error) => {
1029 stderr.push_str(&format!(
1030 "error: failed to install {}: {error}\n",
1031 error.label()
1032 ));
1033 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
1034 return finalize_run_error(
1035 stdout,
1036 stderr,
1037 json_session,
1038 summary.as_ref(),
1039 phase.as_ref(),
1040 rusage.as_ref(),
1041 run_started,
1042 None,
1043 timing.as_deref(),
1044 0,
1045 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1046 error.phase(),
1047 error.to_string(),
1048 );
1049 }
1050 };
1051
1052 let local = tokio::task::LocalSet::new();
1053 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
1054 let main_start = Instant::now();
1055 vm.set_source_dir(&entry_source_dir(path));
1063 let execution = local
1064 .run_until(async {
1065 match vm.execute(&chunk).await {
1066 Ok(value) => RunExecution::Terminal(TerminalRun::Returned(value)),
1067 Err(error) => match error.process_exit_code() {
1068 Some(code) => RunExecution::Terminal(TerminalRun::ProcessExited(code)),
1069 None => RunExecution::Failed(vm.format_runtime_error(&error)),
1070 },
1071 }
1072 })
1073 .await;
1074 let output = vm.output();
1075 if let Some(t) = timing.as_deref_mut() {
1076 t.run_main = main_start.elapsed();
1077 }
1078 if let Err(error) = persist_cli_llm_mock_recording(&llm_mock_mode) {
1079 stderr.push_str(&format!("error: {error}\n"));
1080 let profile_rollup = if profile.is_enabled() {
1081 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1082 } else {
1083 None
1084 };
1085 return finalize_run_error(
1086 stdout,
1087 stderr,
1088 json_session,
1089 summary.as_ref(),
1090 phase.as_ref(),
1091 rusage.as_ref(),
1092 run_started,
1093 profile_rollup.as_ref(),
1094 timing.as_deref(),
1095 harn_vm::tracing::peek_spans().len() as u64,
1096 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1097 "llm_mock_record",
1098 error,
1099 );
1100 }
1101
1102 let buffered_stderr = harn_vm::take_stderr_buffer();
1104 stderr.push_str(&buffered_stderr);
1105
1106 let exit_code = match &execution {
1107 RunExecution::Terminal(terminal) => terminal.exit_code(),
1108 RunExecution::Failed(_) => 1,
1109 };
1110
1111 if let (Some(options), Some(log)) = (attestation.as_ref(), attestation_log.as_ref()) {
1112 if let Err(error) = emit_run_attestation(
1113 log,
1114 path,
1115 store_base,
1116 attestation_started_at_ms,
1117 exit_code,
1118 options,
1119 &mut stderr,
1120 )
1121 .await
1122 {
1123 stderr.push_str(&format!(
1124 "error: failed to emit provenance receipt: {error}\n"
1125 ));
1126 let profile_rollup = if profile.is_enabled() {
1127 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1128 } else {
1129 None
1130 };
1131 return finalize_run_error(
1132 stdout,
1133 stderr,
1134 json_session,
1135 summary.as_ref(),
1136 phase.as_ref(),
1137 rusage.as_ref(),
1138 run_started,
1139 profile_rollup.as_ref(),
1140 timing.as_deref(),
1141 harn_vm::tracing::peek_spans().len() as u64,
1142 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1143 "attestation",
1144 error,
1145 );
1146 }
1147 harn_vm::event_log::reset_active_event_log();
1148 }
1149
1150 match execution {
1151 RunExecution::Terminal(terminal) => {
1152 stdout.push_str(output);
1153 let main_events = harn_vm::tracing::peek_spans().len() as u64;
1154 let cpu_ms_total = cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start));
1155 let profile_rollup = if profile.is_enabled() {
1156 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1157 } else {
1158 None
1159 };
1160 let summary_llm = summary.as_ref().map(|_| run_summary_llm_snapshot());
1161 if trace {
1162 stderr.push_str(&render_trace_summary());
1163 }
1164 if let Some(profile_rollup) = profile_rollup.as_ref() {
1165 if let Err(error) =
1166 render_and_persist_profile_rollup(&profile, profile_rollup, &mut stderr)
1167 {
1168 stderr.push_str(&format!("warning: failed to write profile: {error}\n"));
1169 }
1170 }
1171 if let Some(diagnostic) = terminal.nonzero_return_diagnostic() {
1172 stderr.push_str(&diagnostic);
1173 }
1174 let aux_emission = emit_run_aux_for_exit(
1175 summary.as_ref(),
1176 phase.as_ref(),
1177 rusage.as_ref(),
1178 run_started,
1179 exit_code,
1180 profile_rollup.as_ref(),
1181 summary_llm,
1182 timing.as_deref(),
1183 main_events,
1184 cpu_ms_total,
1185 json_session.is_some(),
1186 &mut stderr,
1187 );
1188 if let Some(session) = json_session {
1189 if let Some(error) = aux_emission.error {
1190 let mut outcome = session.finalize_error(
1191 "run_aux",
1192 format!("failed to emit auxiliary run JSON: {error}"),
1193 1,
1194 );
1195 outcome.stderr = aux_emission.stderr;
1196 return outcome;
1197 }
1198 let value = terminal.json_value();
1199 let mut outcome = session.finalize_result(value, aux_emission.exit_code);
1200 outcome.stderr = aux_emission.stderr;
1201 return outcome;
1202 }
1203 RunOutcome {
1204 stdout,
1205 stderr,
1206 exit_code: aux_emission.exit_code,
1207 }
1208 }
1209 RunExecution::Failed(rendered_error) => {
1210 stderr.push_str(&rendered_error);
1211 let main_events = harn_vm::tracing::peek_spans().len() as u64;
1212 let cpu_ms_total = cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start));
1213 let profile_rollup = if profile.is_enabled() {
1214 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1215 } else {
1216 None
1217 };
1218 if let Some(profile_rollup) = profile_rollup.as_ref() {
1219 if let Err(error) =
1220 render_and_persist_profile_rollup(&profile, profile_rollup, &mut stderr)
1221 {
1222 stderr.push_str(&format!("warning: failed to write profile: {error}\n"));
1223 }
1224 }
1225 let aux_emission = emit_run_aux_for_exit(
1226 summary.as_ref(),
1227 phase.as_ref(),
1228 rusage.as_ref(),
1229 run_started,
1230 1,
1231 profile_rollup.as_ref(),
1232 None,
1233 timing.as_deref(),
1234 main_events,
1235 cpu_ms_total,
1236 json_session.is_some(),
1237 &mut stderr,
1238 );
1239 if let Some(session) = json_session {
1240 let mut outcome =
1241 session.finalize_error("runtime", rendered_error, aux_emission.exit_code);
1242 outcome.stderr = aux_emission.stderr;
1243 return outcome;
1244 }
1245 RunOutcome {
1246 stdout,
1247 stderr,
1248 exit_code: aux_emission.exit_code,
1249 }
1250 }
1251 }
1252}
1253
1254pub(crate) async fn run_watch(path: &str, denied_builtins: HashSet<String>) {
1255 use notify::{Event, EventKind, RecursiveMode, Watcher};
1256
1257 let abs_path = std::fs::canonicalize(path).unwrap_or_else(|e| {
1258 eprintln!("Error: {e}");
1259 process::exit(1);
1260 });
1261 let watch_dir = abs_path.parent().unwrap_or(Path::new("."));
1262
1263 eprintln!("\x1b[2m[watch] running {path}...\x1b[0m");
1264 run_file(
1265 path,
1266 false,
1267 denied_builtins.clone(),
1268 Vec::new(),
1269 CliLlmMockMode::Off,
1270 None,
1271 RunProfileOptions::default(),
1272 )
1273 .await;
1274
1275 let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
1276 let _watcher = {
1277 let tx = tx.clone();
1278 let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
1279 if let Ok(event) = res {
1280 if matches!(
1281 event.kind,
1282 EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
1283 ) {
1284 let has_harn = event
1285 .paths
1286 .iter()
1287 .any(|p| p.extension().is_some_and(|ext| ext == "harn"));
1288 if has_harn {
1289 let _ = tx.blocking_send(());
1290 }
1291 }
1292 }
1293 })
1294 .unwrap_or_else(|e| {
1295 eprintln!("Error setting up file watcher: {e}");
1296 process::exit(1);
1297 });
1298 watcher
1299 .watch(watch_dir, RecursiveMode::Recursive)
1300 .unwrap_or_else(|e| {
1301 eprintln!("Error watching directory: {e}");
1302 process::exit(1);
1303 });
1304 watcher };
1306
1307 eprintln!(
1308 "\x1b[2m[watch] watching {} for .harn changes (ctrl-c to stop)\x1b[0m",
1309 watch_dir.display()
1310 );
1311
1312 loop {
1313 rx.recv().await;
1314 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1316 while rx.try_recv().is_ok() {}
1317
1318 eprintln!();
1319 eprintln!("\x1b[2m[watch] change detected, re-running {path}...\x1b[0m");
1320 run_file(
1321 path,
1322 false,
1323 denied_builtins.clone(),
1324 Vec::new(),
1325 CliLlmMockMode::Off,
1326 None,
1327 RunProfileOptions::default(),
1328 )
1329 .await;
1330 }
1331}
1332
1333#[cfg(test)]
1334mod tests;