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