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