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::{Duration, Instant};
9
10use harn_parser::DiagnosticSeverity;
11use harn_vm::event_log::EventLog;
12use serde::Serialize;
13
14use crate::commands::time::{self, PhaseRecord, RunTiming};
15use crate::package;
16use crate::skill_loader::{
17 canonicalize_cli_dirs, emit_loader_warnings, install_skills_global, load_skills,
18 SkillLoaderInputs,
19};
20
21mod chunk_loading;
22pub(crate) mod environment;
23mod eval_source;
24mod explain_cost;
25pub mod harnpack;
26mod interrupts;
27pub mod json_events;
28mod lifecycle;
29mod llm_mock;
30mod manifest_runtime;
31pub(crate) mod sandbox;
32
33pub(crate) use self::chunk_loading::{
34 compile_or_load_chunk_for_run, compile_or_load_chunk_with_timing, LoadedChunk,
35};
36use self::chunk_loading::{parse_source_for_run, typecheck_with_imports};
37pub(crate) use self::environment::{EnvironmentPolicyArg, EnvironmentPolicyConfig};
38use self::eval_source::create_eval_temp_file;
39pub(crate) use self::eval_source::prepare_eval_temp_file;
40#[cfg(test)]
41use self::eval_source::{eval_source_for_code, split_eval_header};
42use self::harnpack::{HarnpackError, HarnpackRunOptions, PreparedHarnpack};
43use self::interrupts::{
44 install_signal_shutdown_handler, start_run_deadline_watchdog, RunDeadlineGuard,
45};
46use self::json_events::NdjsonEmitter;
47pub use self::lifecycle::RunProfileOptions;
48use self::lifecycle::{RunExecution, TerminalRun};
49pub use self::llm_mock::*;
50pub(crate) use self::manifest_runtime::connect_mcp_servers;
51#[cfg(test)]
52use self::sandbox::default_run_capability_policy;
53pub use self::sandbox::RunSandboxOptions;
54use self::sandbox::{
55 default_run_workspace_root, install_run_sandbox_scope, run_sandbox_attestation,
56};
57
58#[derive(Clone, Default)]
60pub struct RunJsonOptions {
61 pub quiet: bool,
64}
65
66#[derive(Clone, Debug)]
68pub struct RunSummaryOptions {
69 pub sink: RunJsonSink,
70}
71
72#[derive(Clone, Debug)]
73pub struct RunPhaseOptions {
74 pub sink: RunJsonSink,
75}
76
77#[derive(Clone, Debug)]
78pub struct RunRusageOptions {
79 pub sink: RunJsonSink,
80}
81
82#[derive(Clone, Debug, Default)]
83pub struct RunAuxOptions {
84 pub summary: Option<RunSummaryOptions>,
85 pub phase: Option<RunPhaseOptions>,
86 pub rusage: Option<RunRusageOptions>,
87}
88
89#[derive(Clone, Debug, Default)]
90pub struct RunControlOptions {
91 pub timeout: Option<Duration>,
92}
93
94#[derive(Clone, Debug)]
95pub struct RunJsonSink {
96 pub target: RunJsonSinkTarget,
97 pub fd_flag: &'static str,
98}
99
100#[derive(Clone, Debug)]
101pub enum RunJsonSinkTarget {
102 Stderr,
106 File(PathBuf),
107 Fd(i32),
108}
109
110#[derive(Serialize)]
111struct RunSummary<'a> {
112 schema_version: u32,
113 event: &'static str,
114 wall_time_ms: u64,
115 exit_code: i32,
116 llm: RunSummaryLlm,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 profile: Option<&'a harn_vm::profile::RunProfile>,
119}
120
121#[derive(Serialize)]
122struct RunSummaryLlm {
123 call_count: i64,
124 input_tokens: i64,
125 output_tokens: i64,
126 time_ms: i64,
127 cost_usd: f64,
128}
129
130pub const RUN_SUMMARY_SCHEMA_VERSION: u32 = 1;
131pub const RUN_PHASE_SCHEMA_VERSION: u32 = 2;
132pub const RUN_RUSAGE_SCHEMA_VERSION: u32 = 1;
133
134#[derive(Serialize)]
135struct RunPhaseEvent {
136 schema_version: u32,
137 event: &'static str,
138 phases: Vec<PhaseRecord>,
139}
140
141#[derive(Serialize)]
142struct RunRusageEvent {
143 schema_version: u32,
144 event: &'static str,
145 cpu_ms: u64,
146}
147
148pub(crate) fn run_summary_options_from_args(
149 args: &crate::cli::RunArgs,
150) -> Option<RunSummaryOptions> {
151 args.emit_summary_json.then(|| RunSummaryOptions {
152 sink: build_run_json_sink(args.summary_file.clone(), args.summary_fd, "--summary-fd"),
153 })
154}
155
156pub(crate) fn run_aux_options_from_args(args: &crate::cli::RunArgs) -> RunAuxOptions {
157 RunAuxOptions {
158 summary: run_summary_options_from_args(args),
159 phase: run_phase_options_from_args(args),
160 rusage: run_rusage_options_from_args(args),
161 }
162}
163
164pub(crate) fn run_control_options_from_args(args: &crate::cli::RunArgs) -> RunControlOptions {
165 RunControlOptions {
166 timeout: args.timeout,
167 }
168}
169
170pub(crate) fn run_phase_options_from_args(args: &crate::cli::RunArgs) -> Option<RunPhaseOptions> {
171 args.emit_phase_json.then(|| RunPhaseOptions {
172 sink: build_run_json_sink(args.phase_file.clone(), args.phase_fd, "--phase-fd"),
173 })
174}
175
176pub(crate) fn run_rusage_options_from_args(args: &crate::cli::RunArgs) -> Option<RunRusageOptions> {
177 args.emit_rusage_json.then(|| RunRusageOptions {
178 sink: build_run_json_sink(args.rusage_file.clone(), args.rusage_fd, "--rusage-fd"),
179 })
180}
181
182fn build_run_json_sink(
183 file: Option<PathBuf>,
184 fd: Option<i32>,
185 fd_flag: &'static str,
186) -> RunJsonSink {
187 RunJsonSink {
188 target: if let Some(path) = file {
189 RunJsonSinkTarget::File(path)
190 } else if let Some(fd) = fd {
191 RunJsonSinkTarget::Fd(fd)
192 } else {
193 RunJsonSinkTarget::Stderr
194 },
195 fd_flag,
196 }
197}
198
199pub(crate) enum RunFileMcpServeMode {
200 Stdio,
201 Http(Box<RunFileMcpServeHttp>),
202}
203
204pub(crate) struct RunFileMcpServeHttp {
205 pub options: harn_serve::McpHttpServeOptions,
206 pub auth_policy: harn_serve::AuthPolicy,
207}
208
209const CORE_BUILTINS: &[&str] = &[
211 "println",
212 "print",
213 "log",
214 "type_of",
215 "to_string",
216 "to_int",
217 "to_float",
218 "len",
219 "assert",
220 "assert_eq",
221 "assert_ne",
222 "json_parse",
223 "json_stringify",
224 "runtime_context",
225 "task_current",
226 "runtime_context_values",
227 "runtime_context_get",
228 "runtime_context_set",
229 "runtime_context_clear",
230];
231
232pub(crate) fn build_denied_builtins(
237 deny_csv: Option<&str>,
238 allow_csv: Option<&str>,
239) -> HashSet<String> {
240 if let Some(csv) = deny_csv {
241 csv.split(',')
242 .map(|s| s.trim().to_string())
243 .filter(|s| !s.is_empty())
244 .collect()
245 } else if let Some(csv) = allow_csv {
246 let allowed: HashSet<String> = csv
249 .split(',')
250 .map(|s| s.trim().to_string())
251 .filter(|s| !s.is_empty())
252 .collect();
253 let core: HashSet<&str> = CORE_BUILTINS.iter().copied().collect();
254
255 let mut tmp = harn_vm::Vm::new();
257 harn_vm::register_vm_stdlib(&mut tmp);
258 harn_vm::register_store_builtins(&mut tmp, std::path::Path::new("."));
259 harn_vm::register_metadata_builtins(&mut tmp, std::path::Path::new("."));
260
261 tmp.builtin_names()
262 .into_iter()
263 .filter(|name| !allowed.contains(name) && !core.contains(name.as_str()))
264 .collect()
265 } else {
266 HashSet::new()
267 }
268}
269
270#[derive(Clone, Debug, Default, PartialEq, Eq)]
271pub struct RunAttestationOptions {
272 pub receipt_out: Option<PathBuf>,
273 pub agent_id: Option<String>,
274}
275
276#[derive(Clone)]
277pub struct RunInterruptTokens {
278 pub cancel_token: Arc<AtomicBool>,
279 pub signal_token: Arc<Mutex<Option<String>>>,
280}
281
282struct ExecuteRunInputs<'a> {
283 path: &'a str,
284 trace: bool,
285 denied_builtins: HashSet<String>,
286 script_argv: Vec<String>,
287 skill_dirs_raw: Vec<String>,
288 llm_mock_mode: CliLlmMockMode,
289 attestation: Option<RunAttestationOptions>,
290 profile: RunProfileOptions,
291 sandbox: RunSandboxOptions,
292 interrupt_tokens: Option<RunInterruptTokens>,
293 json: Option<JsonRunSession>,
294 aux: RunAuxOptions,
295 timing: Option<&'a mut RunTiming>,
296 harnpack: HarnpackRunOptions,
297}
298
299#[derive(Clone, Debug, Default)]
303pub struct RunOutcome {
304 pub stdout: String,
305 pub stderr: String,
306 pub exit_code: i32,
307}
308
309pub(crate) async fn run_file(
310 path: &str,
311 trace: bool,
312 denied_builtins: HashSet<String>,
313 script_argv: Vec<String>,
314 llm_mock_mode: CliLlmMockMode,
315 attestation: Option<RunAttestationOptions>,
316 profile: RunProfileOptions,
317) {
318 let exit_code = run_file_with_skill_dirs(
319 path,
320 trace,
321 denied_builtins,
322 script_argv,
323 Vec::new(),
324 llm_mock_mode,
325 attestation,
326 profile,
327 RunSandboxOptions::default(),
328 None,
329 RunAuxOptions::default(),
330 RunControlOptions::default(),
331 HarnpackRunOptions::default(),
332 )
333 .await;
334 if exit_code != 0 {
335 process::exit(exit_code);
336 }
337}
338
339pub(crate) fn run_explain_cost_file_with_skill_dirs(path: &str) -> i32 {
340 let outcome = execute_explain_cost(path);
341 if !outcome.stderr.is_empty() {
342 io::stderr().write_all(outcome.stderr.as_bytes()).ok();
343 }
344 if !outcome.stdout.is_empty() {
345 io::stdout().write_all(outcome.stdout.as_bytes()).ok();
346 }
347 outcome.exit_code
348}
349
350#[allow(clippy::too_many_arguments)]
351pub(crate) async fn run_file_with_skill_dirs(
352 path: &str,
353 trace: bool,
354 denied_builtins: HashSet<String>,
355 script_argv: Vec<String>,
356 skill_dirs_raw: Vec<String>,
357 llm_mock_mode: CliLlmMockMode,
358 attestation: Option<RunAttestationOptions>,
359 profile: RunProfileOptions,
360 sandbox: RunSandboxOptions,
361 json: Option<RunJsonOptions>,
362 aux: RunAuxOptions,
363 control: RunControlOptions,
364 harnpack: HarnpackRunOptions,
365) -> i32 {
366 let interrupt_tokens = install_signal_shutdown_handler();
368 let deadline_guard = control
369 .timeout
370 .map(|timeout| start_run_deadline_watchdog(timeout, interrupt_tokens.clone()));
371
372 let _stdout_passthrough = StdoutPassthroughGuard::enable();
373 let json_session = json.map(|options| {
374 JsonRunSession::new(options, Box::new(io::stdout()) as Box<dyn io::Write + Send>)
375 });
376 let outcome = execute_run_inner(ExecuteRunInputs {
377 path,
378 trace,
379 denied_builtins,
380 script_argv,
381 skill_dirs_raw,
382 llm_mock_mode,
383 attestation,
384 profile,
385 sandbox,
386 interrupt_tokens: Some(interrupt_tokens.clone()),
387 json: json_session,
388 aux,
389 timing: None,
390 harnpack,
391 })
392 .await;
393 if let Some(guard) = &deadline_guard {
394 guard.finish();
395 }
396
397 if !outcome.stderr.is_empty() {
400 io::stderr().write_all(outcome.stderr.as_bytes()).ok();
401 }
402 if !outcome.stdout.is_empty() {
403 io::stdout().write_all(outcome.stdout.as_bytes()).ok();
404 }
405
406 let mut exit_code = outcome.exit_code;
407 if deadline_guard
408 .as_ref()
409 .is_some_and(RunDeadlineGuard::timed_out)
410 || (exit_code != 0 && interrupt_tokens.cancel_token.load(Ordering::SeqCst))
411 {
412 exit_code = 124;
413 }
414 exit_code
415}
416
417#[allow(clippy::too_many_arguments)]
418pub(crate) async fn run_resume_with_skill_dirs(
419 target: &str,
420 trace: bool,
421 denied_builtins: HashSet<String>,
422 resume_argv: Vec<String>,
423 skill_dirs_raw: Vec<String>,
424 llm_mock_mode: CliLlmMockMode,
425 attestation: Option<RunAttestationOptions>,
426 profile: RunProfileOptions,
427 sandbox: RunSandboxOptions,
428 json: Option<RunJsonOptions>,
429 aux: RunAuxOptions,
430 control: RunControlOptions,
431) -> i32 {
432 let source = r#"import { resume_agent, wait_agent } from "std/agent/workers"
433
434pipeline main(harness: Harness) {
435 const input = if len(argv) > 1 {
436 argv[1]
437 } else {
438 nil
439 }
440 const handle = resume_agent(harness.agent, argv[0], input, true)
441 return wait_agent(harness.agent, handle)
442}
443"#;
444 let tmp = match create_eval_temp_file() {
445 Ok(tmp) => tmp,
446 Err(error) => {
447 eprintln!("error: {error}");
448 return 1;
449 }
450 };
451 let tmp_path = tmp.path().to_path_buf();
452 if let Err(error) = fs::write(&tmp_path, source) {
453 eprintln!("error: failed to write temp file for --resume: {error}");
454 return 1;
455 }
456 let mut argv = Vec::with_capacity(resume_argv.len() + 1);
457 argv.push(target.to_string());
458 argv.extend(resume_argv);
459 let tmp_str = tmp_path.to_string_lossy().into_owned();
460 run_file_with_skill_dirs(
461 &tmp_str,
462 trace,
463 denied_builtins,
464 argv,
465 skill_dirs_raw,
466 llm_mock_mode,
467 attestation,
468 profile,
469 sandbox,
470 json,
471 aux,
472 control,
473 HarnpackRunOptions::default(),
474 )
475 .await
476}
477
478pub fn execute_explain_cost(path: &str) -> RunOutcome {
479 let stdout = String::new();
480 let mut stderr = String::new();
481
482 let source = match fs::read_to_string(path) {
483 Ok(source) => source,
484 Err(error) => {
485 stderr.push_str(&format!("Error reading {path}: {error}\n"));
486 return RunOutcome {
487 stdout,
488 stderr,
489 exit_code: 1,
490 };
491 }
492 };
493 let program = match parse_source_for_run(path, &source, &mut stderr) {
494 Some(program) => program,
495 None => {
496 return RunOutcome {
497 stdout,
498 stderr,
499 exit_code: 1,
500 };
501 }
502 };
503
504 let mut had_type_error = false;
505 let type_diagnostics = match typecheck_with_imports(&program, Path::new(path), &source) {
506 Ok(diagnostics) => diagnostics,
507 Err(error) => {
508 stderr.push_str(&format!("error: {error}\n"));
509 return RunOutcome {
510 stdout,
511 stderr,
512 exit_code: 1,
513 };
514 }
515 };
516 for diag in &type_diagnostics {
517 let rendered = harn_parser::diagnostic::render_type_diagnostic(&source, path, diag);
518 if matches!(diag.severity, DiagnosticSeverity::Error) {
519 had_type_error = true;
520 }
521 stderr.push_str(&rendered);
522 }
523 if had_type_error {
524 return RunOutcome {
525 stdout,
526 stderr,
527 exit_code: 1,
528 };
529 }
530
531 let extensions = package::load_runtime_extensions(Path::new(path));
532 package::install_runtime_extensions(&extensions);
533 RunOutcome {
534 stdout: explain_cost::render_explain_cost(path, &program),
535 stderr,
536 exit_code: 0,
537 }
538}
539
540pub(crate) struct StdoutPassthroughGuard {
541 previous: bool,
542}
543
544impl StdoutPassthroughGuard {
545 pub(crate) fn enable() -> Self {
546 Self {
547 previous: harn_vm::set_stdout_passthrough(true),
548 }
549 }
550}
551
552impl Drop for StdoutPassthroughGuard {
553 fn drop(&mut self) {
554 harn_vm::set_stdout_passthrough(self.previous);
555 }
556}
557
558pub async fn execute_run(
572 path: &str,
573 trace: bool,
574 denied_builtins: HashSet<String>,
575 script_argv: Vec<String>,
576 skill_dirs_raw: Vec<String>,
577 llm_mock_mode: CliLlmMockMode,
578 attestation: Option<RunAttestationOptions>,
579 profile: RunProfileOptions,
580) -> RunOutcome {
581 crate::ensure_builtin_signatures_installed();
582 execute_run_with_harnpack_and_sandbox_options(
583 path,
584 trace,
585 denied_builtins,
586 script_argv,
587 skill_dirs_raw,
588 llm_mock_mode,
589 attestation,
590 profile,
591 RunSandboxOptions::default(),
592 HarnpackRunOptions::default(),
593 )
594 .await
595}
596
597#[allow(clippy::too_many_arguments)]
601pub async fn execute_run_with_sandbox_options(
602 path: &str,
603 trace: bool,
604 denied_builtins: HashSet<String>,
605 script_argv: Vec<String>,
606 skill_dirs_raw: Vec<String>,
607 llm_mock_mode: CliLlmMockMode,
608 attestation: Option<RunAttestationOptions>,
609 profile: RunProfileOptions,
610 sandbox: RunSandboxOptions,
611) -> RunOutcome {
612 execute_run_with_harnpack_and_sandbox_options(
613 path,
614 trace,
615 denied_builtins,
616 script_argv,
617 skill_dirs_raw,
618 llm_mock_mode,
619 attestation,
620 profile,
621 sandbox,
622 HarnpackRunOptions::default(),
623 )
624 .await
625}
626
627#[allow(clippy::too_many_arguments)]
632pub async fn execute_run_with_harnpack_options(
633 path: &str,
634 trace: bool,
635 denied_builtins: HashSet<String>,
636 script_argv: Vec<String>,
637 skill_dirs_raw: Vec<String>,
638 llm_mock_mode: CliLlmMockMode,
639 attestation: Option<RunAttestationOptions>,
640 profile: RunProfileOptions,
641 harnpack: HarnpackRunOptions,
642) -> RunOutcome {
643 execute_run_with_harnpack_and_sandbox_options(
644 path,
645 trace,
646 denied_builtins,
647 script_argv,
648 skill_dirs_raw,
649 llm_mock_mode,
650 attestation,
651 profile,
652 RunSandboxOptions::default(),
653 harnpack,
654 )
655 .await
656}
657
658#[allow(clippy::too_many_arguments)]
659async fn execute_run_with_harnpack_and_sandbox_options(
660 path: &str,
661 trace: bool,
662 denied_builtins: HashSet<String>,
663 script_argv: Vec<String>,
664 skill_dirs_raw: Vec<String>,
665 llm_mock_mode: CliLlmMockMode,
666 attestation: Option<RunAttestationOptions>,
667 profile: RunProfileOptions,
668 sandbox: RunSandboxOptions,
669 harnpack: HarnpackRunOptions,
670) -> RunOutcome {
671 execute_run_inner(ExecuteRunInputs {
672 path,
673 trace,
674 denied_builtins,
675 script_argv,
676 skill_dirs_raw,
677 llm_mock_mode,
678 attestation,
679 profile,
680 sandbox,
681 interrupt_tokens: None,
682 json: None,
683 aux: RunAuxOptions::default(),
684 timing: None,
685 harnpack,
686 })
687 .await
688}
689
690#[allow(clippy::too_many_arguments)]
696pub async fn execute_run_json(
697 path: &str,
698 trace: bool,
699 denied_builtins: HashSet<String>,
700 script_argv: Vec<String>,
701 skill_dirs_raw: Vec<String>,
702 llm_mock_mode: CliLlmMockMode,
703 attestation: Option<RunAttestationOptions>,
704 profile: RunProfileOptions,
705 out: Box<dyn io::Write + Send>,
706 options: RunJsonOptions,
707) -> RunOutcome {
708 execute_run_inner(ExecuteRunInputs {
709 path,
710 trace,
711 denied_builtins,
712 script_argv,
713 skill_dirs_raw,
714 llm_mock_mode,
715 attestation,
716 profile,
717 sandbox: RunSandboxOptions::default(),
718 interrupt_tokens: None,
719 json: Some(JsonRunSession::new(options, out)),
720 aux: RunAuxOptions::default(),
721 timing: None,
722 harnpack: HarnpackRunOptions::default(),
723 })
724 .await
725}
726
727pub(crate) async fn execute_run_with_timing(
731 path: &str,
732 script_argv: Vec<String>,
733 timing: Option<&mut RunTiming>,
734 sandbox: RunSandboxOptions,
735) -> RunOutcome {
736 execute_run_inner(ExecuteRunInputs {
737 path,
738 trace: false,
739 denied_builtins: HashSet::new(),
740 script_argv,
741 skill_dirs_raw: Vec::new(),
742 llm_mock_mode: CliLlmMockMode::Off,
743 attestation: None,
744 profile: RunProfileOptions::default(),
745 sandbox,
746 interrupt_tokens: None,
747 json: None,
748 aux: RunAuxOptions::default(),
749 timing,
750 harnpack: HarnpackRunOptions::default(),
751 })
752 .await
753}
754
755fn entry_source_dir(path: &str) -> std::path::PathBuf {
769 match std::path::Path::new(path).parent() {
770 Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
771 _ => std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
772 }
773}
774
775#[allow(clippy::needless_option_as_deref)]
778async fn execute_run_inner(inputs: ExecuteRunInputs<'_>) -> RunOutcome {
779 let mut inputs = inputs;
780 let json_session = inputs.json.take();
781 let Some(json_session) = json_session else {
782 return execute_run_inner_scoped(inputs, None).await;
783 };
784 let sink = json_session.sink();
785 harn_vm::run_events::scope(sink, execute_run_inner_scoped(inputs, Some(json_session))).await
786}
787
788async fn execute_run_inner_scoped(
789 inputs: ExecuteRunInputs<'_>,
790 json_session: Option<JsonRunSession>,
791) -> RunOutcome {
792 let ExecuteRunInputs {
793 path,
794 trace,
795 denied_builtins,
796 script_argv,
797 skill_dirs_raw,
798 llm_mock_mode,
799 attestation,
800 profile,
801 sandbox,
802 interrupt_tokens,
803 json: _,
804 aux,
805 timing,
806 harnpack,
807 } = inputs;
808 let RunAuxOptions {
809 summary,
810 phase,
811 rusage,
812 } = aux;
813 let run_started = Instant::now();
814 let cpu_started_ms = rusage.as_ref().map(|_| time::cpu_ms());
815 let mut owned_timing = if timing.is_none() && (phase.is_some() || rusage.is_some()) {
816 Some(RunTiming::default())
817 } else {
818 None
819 };
820 let mut timing = timing.or(owned_timing.as_mut());
821
822 let mut stderr = String::new();
823 let mut stdout = String::new();
824
825 let owned_run_path: String;
830 let resolved_path: &str = if harnpack::looks_like_harnpack(Path::new(path)) {
831 let outcome = match harnpack::prepare_harnpack(Path::new(path), &harnpack, &mut stderr) {
832 Ok(prepared) => prepared,
833 Err(err) => {
834 return finalize_harnpack_error(
835 stderr,
836 json_session,
837 summary.as_ref(),
838 phase.as_ref(),
839 rusage.as_ref(),
840 run_started,
841 err,
842 );
843 }
844 };
845 harn_vm::run_events::emit(harn_vm::run_events::RunEvent::PackRun {
846 bundle_hash: outcome.bundle_hash.clone(),
847 signature_verified: outcome.signature_verified,
848 key_id: outcome.key_id.clone(),
849 cache_hit: outcome.cache_hit,
850 dry_run_verify: harnpack.dry_run_verify,
851 });
852 if harnpack.dry_run_verify {
853 return finalize_harnpack_dry_run(
854 stderr,
855 json_session,
856 summary.as_ref(),
857 phase.as_ref(),
858 rusage.as_ref(),
859 run_started,
860 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
861 &outcome,
862 );
863 }
864 owned_run_path = outcome.entrypoint_path.to_string_lossy().into_owned();
865 owned_run_path.as_str()
866 } else {
867 path
868 };
869
870 let Some(LoadedChunk {
871 source,
872 chunk,
873 link_table,
874 }) = compile_or_load_chunk_with_timing(resolved_path, &mut stderr, timing.as_deref_mut())
875 else {
876 let message = stderr.clone();
877 return finalize_run_error(
878 stdout,
879 stderr,
880 json_session,
881 summary.as_ref(),
882 phase.as_ref(),
883 rusage.as_ref(),
884 run_started,
885 None,
886 timing.as_deref(),
887 0,
888 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
889 "compile_error",
890 message,
891 );
892 };
893 let path = resolved_path;
894
895 let setup_start = Instant::now();
896 if trace || summary.is_some() {
897 harn_vm::llm::enable_tracing();
898 }
899 if profile.is_enabled() || phase.is_some() {
900 harn_vm::tracing::set_tracing_enabled(true);
901 }
902 let _builtin_profile_guard = profile.is_enabled().then(harn_vm::builtin_profile::enable);
908 if let Err(error) = install_cli_llm_mock_mode(&llm_mock_mode) {
909 stderr.push_str(&format!("error: {error}\n"));
910 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
911 return finalize_run_error(
912 stdout,
913 stderr,
914 json_session,
915 summary.as_ref(),
916 phase.as_ref(),
917 rusage.as_ref(),
918 run_started,
919 None,
920 timing.as_deref(),
921 0,
922 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
923 "llm_mock_install",
924 error,
925 );
926 }
927
928 let mut vm = harn_vm::Vm::new();
929 vm.set_graph_link_table(link_table);
930 if let Some(timing) = timing.as_deref_mut() {
931 timing.module_phases = Some(vm.enable_module_phase_timing());
932 }
933 if let Some(interrupt_tokens) = interrupt_tokens {
934 vm.install_interrupt_signal_token(interrupt_tokens.signal_token);
935 vm.install_cancel_token(interrupt_tokens.cancel_token);
936 }
937 harn_vm::register_vm_stdlib(&mut vm);
938 crate::install_default_hostlib(&mut vm);
939 let source_parent = std::path::Path::new(path)
940 .parent()
941 .unwrap_or(std::path::Path::new("."));
942 let project_root = harn_vm::stdlib::process::find_project_root(source_parent);
944 let store_base = project_root.as_deref().unwrap_or(source_parent);
945 let sandbox_root = sandbox
946 .workspace_root
947 .clone()
948 .unwrap_or_else(|| default_run_workspace_root(project_root.as_deref(), source_parent));
949 let _sandbox_scope = install_run_sandbox_scope(&sandbox, &sandbox_root, &mut stderr);
950
951 let (_environment_scope, environment_policy, grant_receipts) =
958 match environment::launch_scope(&sandbox.environment, &mut stderr) {
959 Ok(launched) => launched,
960 Err(error) => {
961 stderr.push_str(&format!("error: {error}\n"));
962 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
963 let code = error.code();
964 return finalize_run_error(
965 stdout,
966 stderr,
967 json_session,
968 summary.as_ref(),
969 phase.as_ref(),
970 rusage.as_ref(),
971 run_started,
972 None,
973 timing.as_deref(),
974 0,
975 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
976 code,
977 error.to_string(),
978 );
979 }
980 };
981
982 let attestation_started_at_ms = now_ms();
983 let attestation_log = if attestation.is_some() {
984 Some(harn_vm::event_log::install_memory_for_current_thread(256))
985 } else {
986 None
987 };
988 if let Some(log) = attestation_log.as_ref() {
989 append_run_provenance_event(
990 log,
991 "started",
992 serde_json::json!({
993 "pipeline": path,
994 "argv": &script_argv,
995 "project_root": store_base.display().to_string(),
996 "sandbox": run_sandbox_attestation(&sandbox),
997 "environment_policy": environment_policy.as_str(),
998 "environment_grants": &grant_receipts,
1000 }),
1001 )
1002 .await;
1003 }
1004 harn_vm::register_store_builtins(&mut vm, store_base);
1005 harn_vm::register_metadata_builtins(&mut vm, store_base);
1006 let pipeline_name = std::path::Path::new(path)
1007 .file_stem()
1008 .and_then(|s| s.to_str())
1009 .unwrap_or("default");
1010 harn_vm::register_checkpoint_builtins(&mut vm, store_base, pipeline_name);
1011 vm.set_source_info(path, &source);
1012 let lazy_manifest_handlers = !denied_builtins.is_empty();
1013 if lazy_manifest_handlers {
1014 vm.set_denied_builtins(denied_builtins);
1015 }
1016 if let Some(ref root) = project_root {
1017 vm.set_project_root(root);
1018 }
1019
1020 vm.set_source_dir(&entry_source_dir(path));
1025
1026 let cli_dirs = canonicalize_cli_dirs(&skill_dirs_raw, None);
1029 let loaded = load_skills(&SkillLoaderInputs {
1030 cli_dirs,
1031 source_path: Some(std::path::PathBuf::from(path)),
1032 });
1033 emit_loader_warnings(&loaded.loader_warnings);
1034 install_skills_global(&mut vm, &loaded);
1035
1036 let argv_values: Vec<harn_vm::VmValue> = script_argv
1039 .iter()
1040 .map(|s| harn_vm::VmValue::String(arcstr::ArcStr::from(s.as_str())))
1041 .collect();
1042 vm.set_global(
1043 "argv",
1044 harn_vm::VmValue::List(std::sync::Arc::new(argv_values)),
1045 );
1046
1047 let runtime_harness =
1051 match crate::default_harness_for_manifest_or_base_dir(Path::new(path), store_base) {
1052 Ok(harness) => harness,
1053 Err(error) => {
1054 stderr.push_str(&format!(
1055 "error: failed to configure harness secret provider: {error}\n"
1056 ));
1057 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
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 None,
1067 timing.as_deref(),
1068 0,
1069 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1070 "harness_secret_provider",
1071 error,
1072 );
1073 }
1074 };
1075 vm.set_harness(runtime_harness);
1076
1077 if let Err(error) =
1080 manifest_runtime::install_manifest_runtime(Path::new(path), &mut vm, lazy_manifest_handlers)
1081 .await
1082 {
1083 stderr.push_str(&format!(
1084 "error: failed to install {}: {error}\n",
1085 error.label()
1086 ));
1087 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
1088 return finalize_run_error(
1089 stdout,
1090 stderr,
1091 json_session,
1092 summary.as_ref(),
1093 phase.as_ref(),
1094 rusage.as_ref(),
1095 run_started,
1096 None,
1097 timing.as_deref(),
1098 0,
1099 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1100 error.phase(),
1101 error.to_string(),
1102 );
1103 }
1104
1105 let local = tokio::task::LocalSet::new();
1106 time::record_run_setup_elapsed(timing.as_deref_mut(), setup_start);
1107 let main_start = Instant::now();
1108 vm.set_source_dir(&entry_source_dir(path));
1116 let execution = local
1117 .run_until(async {
1118 match vm.execute(&chunk).await {
1119 Ok(value) => RunExecution::Terminal(TerminalRun::Returned(value)),
1120 Err(error) => match error.process_exit_code() {
1121 Some(code) => RunExecution::Terminal(TerminalRun::ProcessExited(code)),
1122 None => RunExecution::Failed(vm.format_runtime_error(&error)),
1123 },
1124 }
1125 })
1126 .await;
1127 let output = vm.output();
1128 if let Some(t) = timing.as_deref_mut() {
1129 t.run_main = main_start.elapsed();
1130 }
1131 if let Err(error) = persist_cli_llm_mock_recording(&llm_mock_mode) {
1132 stderr.push_str(&format!("error: {error}\n"));
1133 let profile_rollup = if profile.is_enabled() {
1134 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1135 } else {
1136 None
1137 };
1138 return finalize_run_error(
1139 stdout,
1140 stderr,
1141 json_session,
1142 summary.as_ref(),
1143 phase.as_ref(),
1144 rusage.as_ref(),
1145 run_started,
1146 profile_rollup.as_ref(),
1147 timing.as_deref(),
1148 harn_vm::tracing::peek_spans().len() as u64,
1149 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1150 "llm_mock_record",
1151 error,
1152 );
1153 }
1154
1155 let buffered_stderr = harn_vm::take_stderr_buffer();
1157 stderr.push_str(&buffered_stderr);
1158
1159 let exit_code = match &execution {
1160 RunExecution::Terminal(terminal) => terminal.exit_code(),
1161 RunExecution::Failed(_) => 1,
1162 };
1163
1164 if let (Some(options), Some(log)) = (attestation.as_ref(), attestation_log.as_ref()) {
1165 if let Err(error) = emit_run_attestation(
1166 log,
1167 path,
1168 store_base,
1169 attestation_started_at_ms,
1170 exit_code,
1171 options,
1172 &mut stderr,
1173 )
1174 .await
1175 {
1176 stderr.push_str(&format!(
1177 "error: failed to emit provenance receipt: {error}\n"
1178 ));
1179 let profile_rollup = if profile.is_enabled() {
1180 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1181 } else {
1182 None
1183 };
1184 return finalize_run_error(
1185 stdout,
1186 stderr,
1187 json_session,
1188 summary.as_ref(),
1189 phase.as_ref(),
1190 rusage.as_ref(),
1191 run_started,
1192 profile_rollup.as_ref(),
1193 timing.as_deref(),
1194 harn_vm::tracing::peek_spans().len() as u64,
1195 cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start)),
1196 "attestation",
1197 error,
1198 );
1199 }
1200 harn_vm::event_log::reset_active_event_log();
1201 }
1202
1203 match execution {
1204 RunExecution::Terminal(terminal) => {
1205 stdout.push_str(output);
1206 let main_events = harn_vm::tracing::peek_spans().len() as u64;
1207 let cpu_ms_total = cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start));
1208 let profile_rollup = if profile.is_enabled() {
1209 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1210 } else {
1211 None
1212 };
1213 let summary_llm = summary.as_ref().map(|_| run_summary_llm_snapshot());
1214 if trace {
1215 stderr.push_str(&render_trace_summary());
1216 }
1217 if let Some(profile_rollup) = profile_rollup.as_ref() {
1218 if let Err(error) =
1219 render_and_persist_profile_rollup(&profile, profile_rollup, &mut stderr)
1220 {
1221 stderr.push_str(&format!("warning: failed to write profile: {error}\n"));
1222 }
1223 }
1224 if let Some(diagnostic) = terminal.nonzero_return_diagnostic() {
1225 stderr.push_str(&diagnostic);
1226 }
1227 let aux_emission = emit_run_aux_for_exit(
1228 summary.as_ref(),
1229 phase.as_ref(),
1230 rusage.as_ref(),
1231 run_started,
1232 exit_code,
1233 profile_rollup.as_ref(),
1234 summary_llm,
1235 timing.as_deref(),
1236 main_events,
1237 cpu_ms_total,
1238 json_session.is_some(),
1239 &mut stderr,
1240 );
1241 if let Some(session) = json_session {
1242 if let Some(error) = aux_emission.error {
1243 let mut outcome = session.finalize_error(
1244 "run_aux",
1245 format!("failed to emit auxiliary run JSON: {error}"),
1246 1,
1247 );
1248 outcome.stderr = aux_emission.stderr;
1249 return outcome;
1250 }
1251 let value = terminal.json_value();
1252 let mut outcome = session.finalize_result(value, aux_emission.exit_code);
1253 outcome.stderr = aux_emission.stderr;
1254 return outcome;
1255 }
1256 RunOutcome {
1257 stdout,
1258 stderr,
1259 exit_code: aux_emission.exit_code,
1260 }
1261 }
1262 RunExecution::Failed(rendered_error) => {
1263 stderr.push_str(&rendered_error);
1264 let main_events = harn_vm::tracing::peek_spans().len() as u64;
1265 let cpu_ms_total = cpu_started_ms.map(|start| time::cpu_ms().saturating_sub(start));
1266 let profile_rollup = if profile.is_enabled() {
1267 Some(harn_vm::profile::build(&harn_vm::tracing::peek_spans()))
1268 } else {
1269 None
1270 };
1271 if let Some(profile_rollup) = profile_rollup.as_ref() {
1272 if let Err(error) =
1273 render_and_persist_profile_rollup(&profile, profile_rollup, &mut stderr)
1274 {
1275 stderr.push_str(&format!("warning: failed to write profile: {error}\n"));
1276 }
1277 }
1278 let aux_emission = emit_run_aux_for_exit(
1279 summary.as_ref(),
1280 phase.as_ref(),
1281 rusage.as_ref(),
1282 run_started,
1283 1,
1284 profile_rollup.as_ref(),
1285 None,
1286 timing.as_deref(),
1287 main_events,
1288 cpu_ms_total,
1289 json_session.is_some(),
1290 &mut stderr,
1291 );
1292 if let Some(session) = json_session {
1293 let mut outcome =
1294 session.finalize_error("runtime", rendered_error, aux_emission.exit_code);
1295 outcome.stderr = aux_emission.stderr;
1296 return outcome;
1297 }
1298 RunOutcome {
1299 stdout,
1300 stderr,
1301 exit_code: aux_emission.exit_code,
1302 }
1303 }
1304 }
1305}
1306
1307fn render_and_persist_profile_rollup(
1308 options: &RunProfileOptions,
1309 profile: &harn_vm::profile::RunProfile,
1310 stderr: &mut String,
1311) -> Result<(), String> {
1312 if options.text {
1313 stderr.push_str(&harn_vm::profile::render(profile));
1314 }
1315 if let Some(path) = options.json_path.as_ref() {
1316 if let Some(parent) = path.parent() {
1317 if !parent.as_os_str().is_empty() {
1318 fs::create_dir_all(parent)
1319 .map_err(|error| format!("create {}: {error}", parent.display()))?;
1320 }
1321 }
1322 let json = serde_json::to_string_pretty(profile)
1323 .map_err(|error| format!("serialize profile: {error}"))?;
1324 fs::write(path, json).map_err(|error| format!("write {}: {error}", path.display()))?;
1325 }
1326 Ok(())
1327}
1328
1329fn build_run_summary<'a>(
1330 started: Instant,
1331 exit_code: i32,
1332 profile: Option<&'a harn_vm::profile::RunProfile>,
1333 llm: RunSummaryLlm,
1334) -> RunSummary<'a> {
1335 RunSummary {
1336 schema_version: RUN_SUMMARY_SCHEMA_VERSION,
1337 event: "run_summary",
1338 wall_time_ms: started.elapsed().as_millis().min(u128::from(u64::MAX)) as u64,
1339 exit_code,
1340 llm,
1341 profile,
1342 }
1343}
1344
1345fn run_summary_llm_snapshot() -> RunSummaryLlm {
1346 let (input_tokens, output_tokens, time_ms, call_count) = harn_vm::llm::peek_trace_summary();
1347 let cost_usd = harn_vm::llm::peek_total_cost();
1348 RunSummaryLlm {
1349 call_count,
1350 input_tokens,
1351 output_tokens,
1352 time_ms,
1353 cost_usd: if cost_usd.is_finite() { cost_usd } else { 0.0 },
1354 }
1355}
1356
1357struct RunAuxEmission {
1358 stderr: String,
1359 exit_code: i32,
1360 error: Option<String>,
1361}
1362
1363#[allow(clippy::too_many_arguments)]
1364fn emit_run_aux_for_exit(
1365 summary: Option<&RunSummaryOptions>,
1366 phase: Option<&RunPhaseOptions>,
1367 rusage: Option<&RunRusageOptions>,
1368 started: Instant,
1369 exit_code: i32,
1370 profile: Option<&harn_vm::profile::RunProfile>,
1371 llm: Option<RunSummaryLlm>,
1372 timing: Option<&RunTiming>,
1373 main_events: u64,
1374 cpu_ms_total: Option<u64>,
1375 json_mode: bool,
1376 stderr: &mut String,
1377) -> RunAuxEmission {
1378 let mut aux_stderr = String::new();
1379 let mut final_exit_code = exit_code;
1380 let mut aux_error = None;
1381 let aux_target = if json_mode { &mut aux_stderr } else { stderr };
1382 let default_timing = RunTiming::default();
1383 let timing = timing.unwrap_or(&default_timing);
1384
1385 if let Some(options) = summary {
1386 let llm = llm.unwrap_or_else(run_summary_llm_snapshot);
1387 let summary = build_run_summary(started, exit_code, profile, llm);
1388 if let Err(error) = emit_raw_json_line(&options.sink, &summary, "run summary", aux_target) {
1389 record_aux_error(
1390 &mut final_exit_code,
1391 &mut aux_error,
1392 aux_target,
1393 "run summary",
1394 error,
1395 );
1396 }
1397 }
1398 if let Some(options) = phase {
1399 let phase_event = RunPhaseEvent {
1400 schema_version: RUN_PHASE_SCHEMA_VERSION,
1401 event: "run_phase",
1402 phases: time::build_phase_records(timing, main_events),
1403 };
1404 if let Err(error) = emit_raw_json_line(&options.sink, &phase_event, "run phase", aux_target)
1405 {
1406 record_aux_error(
1407 &mut final_exit_code,
1408 &mut aux_error,
1409 aux_target,
1410 "run phase",
1411 error,
1412 );
1413 }
1414 }
1415 if let Some(options) = rusage {
1416 let rusage_event = RunRusageEvent {
1417 schema_version: RUN_RUSAGE_SCHEMA_VERSION,
1418 event: "run_rusage",
1419 cpu_ms: cpu_ms_total.unwrap_or(0),
1420 };
1421 if let Err(error) =
1422 emit_raw_json_line(&options.sink, &rusage_event, "run rusage", aux_target)
1423 {
1424 record_aux_error(
1425 &mut final_exit_code,
1426 &mut aux_error,
1427 aux_target,
1428 "run rusage",
1429 error,
1430 );
1431 }
1432 }
1433
1434 RunAuxEmission {
1435 stderr: aux_stderr,
1436 exit_code: final_exit_code,
1437 error: aux_error,
1438 }
1439}
1440
1441fn record_aux_error(
1442 final_exit_code: &mut i32,
1443 aux_error: &mut Option<String>,
1444 stderr: &mut String,
1445 label: &str,
1446 error: String,
1447) {
1448 stderr.push_str(&format!("error: failed to emit {label}: {error}\n"));
1449 if *final_exit_code == 0 {
1450 *final_exit_code = 1;
1451 }
1452 if aux_error.is_none() {
1453 *aux_error = Some(error);
1454 }
1455}
1456
1457fn emit_raw_json_line(
1458 sink: &RunJsonSink,
1459 value: &impl Serialize,
1460 label: &str,
1461 stderr: &mut String,
1462) -> Result<(), String> {
1463 let line =
1464 serde_json::to_string(value).map_err(|error| format!("serialize {label}: {error}"))? + "\n";
1465 match &sink.target {
1466 RunJsonSinkTarget::Stderr => {
1467 stderr.push_str(&line);
1468 Ok(())
1469 }
1470 RunJsonSinkTarget::File(path) => write_raw_json_file(path, &line),
1471 RunJsonSinkTarget::Fd(fd) => write_raw_json_fd(*fd, &line, sink.fd_flag),
1472 }
1473}
1474
1475fn write_raw_json_file(path: &Path, line: &str) -> Result<(), String> {
1476 if let Some(parent) = path.parent() {
1477 if !parent.as_os_str().is_empty() {
1478 fs::create_dir_all(parent)
1479 .map_err(|error| format!("create {}: {error}", parent.display()))?;
1480 }
1481 }
1482 fs::write(path, line).map_err(|error| format!("write {}: {error}", path.display()))
1483}
1484
1485#[cfg(unix)]
1486fn write_raw_json_fd(fd: i32, line: &str, flag: &str) -> Result<(), String> {
1487 use std::fs::File;
1488 use std::os::unix::io::FromRawFd;
1489
1490 if fd < 0 {
1491 return Err(format!("invalid {flag} {fd}: must be non-negative"));
1492 }
1493 let duped = unsafe { libc::dup(fd) };
1494 if duped < 0 {
1495 return Err(format!(
1496 "duplicate {flag} {fd}: {}",
1497 io::Error::last_os_error()
1498 ));
1499 }
1500 let mut file = unsafe { File::from_raw_fd(duped) };
1501 file.write_all(line.as_bytes())
1502 .and_then(|_| file.flush())
1503 .map_err(|error| format!("write {flag} {fd}: {error}"))
1504}
1505
1506#[cfg(not(unix))]
1507fn write_raw_json_fd(_fd: i32, _line: &str, flag: &str) -> Result<(), String> {
1508 Err(format!("{flag} is only supported on Unix platforms"))
1509}
1510
1511async fn append_run_provenance_event(
1512 log: &Arc<harn_vm::event_log::AnyEventLog>,
1513 kind: &str,
1514 payload: serde_json::Value,
1515) {
1516 let Ok(topic) = harn_vm::event_log::Topic::new("run.provenance") else {
1517 return;
1518 };
1519 let _ = log
1520 .append(&topic, harn_vm::event_log::LogEvent::new(kind, payload))
1521 .await;
1522}
1523
1524async fn emit_run_attestation(
1525 log: &Arc<harn_vm::event_log::AnyEventLog>,
1526 path: &str,
1527 store_base: &Path,
1528 started_at_ms: i64,
1529 exit_code: i32,
1530 options: &RunAttestationOptions,
1531 stderr: &mut String,
1532) -> Result<(), String> {
1533 let finished_at_ms = now_ms();
1534 let status = if exit_code == 0 { "success" } else { "failure" };
1535 append_run_provenance_event(
1536 log,
1537 "finished",
1538 serde_json::json!({
1539 "pipeline": path,
1540 "status": status,
1541 "exit_code": exit_code,
1542 }),
1543 )
1544 .await;
1545 log.flush()
1546 .await
1547 .map_err(|error| format!("failed to flush attestation event log: {error}"))?;
1548 let secret_provider = harn_vm::secrets::configured_default_chain("harn.provenance")
1549 .map_err(|error| format!("failed to configure provenance secrets: {error}"))?;
1550 let (signing_key, key_id) =
1551 harn_vm::load_or_generate_agent_signing_key(&secret_provider, options.agent_id.as_deref())
1552 .await
1553 .map_err(|error| format!("failed to load provenance signing key: {error}"))?;
1554 let receipt = harn_vm::build_signed_receipt(
1555 log,
1556 harn_vm::ReceiptBuildOptions {
1557 pipeline: path.to_string(),
1558 status: status.to_string(),
1559 started_at_ms,
1560 finished_at_ms,
1561 exit_code,
1562 producer_name: "harn-cli".to_string(),
1563 producer_version: env!("CARGO_PKG_VERSION").to_string(),
1564 },
1565 &signing_key,
1566 key_id,
1567 )
1568 .await
1569 .map_err(|error| format!("failed to build provenance receipt: {error}"))?;
1570 let receipt_path = receipt_output_path(store_base, options, &receipt.receipt_id);
1571 if let Some(parent) = receipt_path.parent() {
1572 fs::create_dir_all(parent)
1573 .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
1574 }
1575 let encoded = serde_json::to_vec_pretty(&receipt)
1576 .map_err(|error| format!("failed to encode provenance receipt: {error}"))?;
1577 fs::write(&receipt_path, encoded)
1578 .map_err(|error| format!("failed to write {}: {error}", receipt_path.display()))?;
1579 stderr.push_str(&format!("provenance receipt: {}\n", receipt_path.display()));
1580 Ok(())
1581}
1582
1583fn receipt_output_path(
1584 store_base: &Path,
1585 options: &RunAttestationOptions,
1586 receipt_id: &str,
1587) -> PathBuf {
1588 if let Some(path) = options.receipt_out.as_ref() {
1589 return path.clone();
1590 }
1591 harn_vm::runtime_paths::state_root(store_base)
1592 .join("receipts")
1593 .join(format!("{receipt_id}.json"))
1594}
1595
1596fn now_ms() -> i64 {
1597 std::time::SystemTime::now()
1598 .duration_since(std::time::UNIX_EPOCH)
1599 .map(|duration| duration.as_millis() as i64)
1600 .unwrap_or(0)
1601}
1602
1603fn exit_code_from_return_value(value: &harn_vm::VmValue) -> i32 {
1610 use harn_vm::VmValue;
1611 match value {
1612 VmValue::Int(n) => (*n).clamp(0, 255) as i32,
1613 VmValue::EnumVariant(enum_variant) if enum_variant.is_variant("Result", "Err") => 1,
1614 _ => 0,
1615 }
1616}
1617
1618struct JsonRunSession {
1628 emitter: self::json_events::NdjsonEmitter,
1629}
1630
1631impl JsonRunSession {
1632 fn new(options: RunJsonOptions, out: Box<dyn io::Write + Send>) -> Self {
1633 Self {
1634 emitter: NdjsonEmitter::new(out, options.quiet),
1635 }
1636 }
1637
1638 fn sink(&self) -> Arc<dyn harn_vm::run_events::RunEventSink> {
1639 self.emitter.sink()
1640 }
1641
1642 fn finalize_result(self, value: serde_json::Value, exit_code: i32) -> RunOutcome {
1643 self.emitter.emit_result(value, exit_code);
1644 RunOutcome {
1645 stdout: String::new(),
1646 stderr: String::new(),
1647 exit_code,
1648 }
1649 }
1650
1651 fn finalize_error(
1652 self,
1653 code: impl Into<String>,
1654 message: impl Into<String>,
1655 exit_code: i32,
1656 ) -> RunOutcome {
1657 self.emitter.emit_error(code, message);
1658 RunOutcome {
1659 stdout: String::new(),
1660 stderr: String::new(),
1661 exit_code,
1662 }
1663 }
1664}
1665
1666#[allow(clippy::too_many_arguments)]
1667fn finalize_run_error(
1668 stdout: String,
1669 mut stderr: String,
1670 json_session: Option<JsonRunSession>,
1671 summary: Option<&RunSummaryOptions>,
1672 phase: Option<&RunPhaseOptions>,
1673 rusage: Option<&RunRusageOptions>,
1674 started: Instant,
1675 profile: Option<&harn_vm::profile::RunProfile>,
1676 timing: Option<&RunTiming>,
1677 main_events: u64,
1678 cpu_ms_total: Option<u64>,
1679 code: impl Into<String>,
1680 message: impl Into<String>,
1681) -> RunOutcome {
1682 let aux_emission = emit_run_aux_for_exit(
1683 summary,
1684 phase,
1685 rusage,
1686 started,
1687 1,
1688 profile,
1689 None,
1690 timing,
1691 main_events,
1692 cpu_ms_total,
1693 json_session.is_some(),
1694 &mut stderr,
1695 );
1696 if let Some(session) = json_session {
1697 let mut outcome = session.finalize_error(code, message, aux_emission.exit_code);
1698 outcome.stderr = aux_emission.stderr;
1699 return outcome;
1700 }
1701 RunOutcome {
1702 stdout,
1703 stderr,
1704 exit_code: aux_emission.exit_code,
1705 }
1706}
1707
1708fn finalize_harnpack_error(
1713 mut stderr: String,
1714 json_session: Option<JsonRunSession>,
1715 summary: Option<&RunSummaryOptions>,
1716 phase: Option<&RunPhaseOptions>,
1717 rusage: Option<&RunRusageOptions>,
1718 started: Instant,
1719 err: HarnpackError,
1720) -> RunOutcome {
1721 let code = err.code;
1722 let message = err.message;
1723 stderr.push_str(&format!("error: {message}\n"));
1724 finalize_run_error(
1725 String::new(),
1726 stderr,
1727 json_session,
1728 summary,
1729 phase,
1730 rusage,
1731 started,
1732 None,
1733 None,
1734 0,
1735 None,
1736 code,
1737 message,
1738 )
1739}
1740
1741fn finalize_harnpack_dry_run(
1746 mut stderr: String,
1747 json_session: Option<JsonRunSession>,
1748 summary_options: Option<&RunSummaryOptions>,
1749 phase_options: Option<&RunPhaseOptions>,
1750 rusage_options: Option<&RunRusageOptions>,
1751 started: Instant,
1752 cpu_ms_total: Option<u64>,
1753 prepared: &PreparedHarnpack,
1754) -> RunOutcome {
1755 let summary = format!(
1756 "[harn] harnpack verify ok: bundle_hash={}, signature_verified={}, cache_hit={}\n",
1757 prepared.bundle_hash, prepared.signature_verified, prepared.cache_hit
1758 );
1759 stderr.push_str(&summary);
1760 let aux_emission = emit_run_aux_for_exit(
1761 summary_options,
1762 phase_options,
1763 rusage_options,
1764 started,
1765 0,
1766 None,
1767 None,
1768 None,
1769 0,
1770 cpu_ms_total,
1771 json_session.is_some(),
1772 &mut stderr,
1773 );
1774 if let Some(session) = json_session {
1775 if let Some(error) = aux_emission.error {
1776 let mut outcome = session.finalize_error(
1777 "run_aux",
1778 format!("failed to emit auxiliary run JSON: {error}"),
1779 1,
1780 );
1781 outcome.stderr = aux_emission.stderr;
1782 return outcome;
1783 }
1784 let value = serde_json::json!({
1785 "bundle_hash": prepared.bundle_hash,
1786 "signature_verified": prepared.signature_verified,
1787 "key_id": prepared.key_id,
1788 "cache_hit": prepared.cache_hit,
1789 "dry_run_verify": true,
1790 });
1791 let mut outcome = session.finalize_result(value, aux_emission.exit_code);
1792 outcome.stderr = aux_emission.stderr;
1793 return outcome;
1794 }
1795 RunOutcome {
1796 stdout: String::new(),
1797 stderr,
1798 exit_code: aux_emission.exit_code,
1799 }
1800}
1801
1802fn render_return_value_error(value: &harn_vm::VmValue) -> String {
1803 let harn_vm::VmValue::EnumVariant(enum_variant) = value else {
1804 return String::new();
1805 };
1806 if !enum_variant.is_variant("Result", "Err") {
1807 return String::new();
1808 }
1809 let rendered = enum_variant
1810 .fields
1811 .first()
1812 .map(|p| p.display())
1813 .unwrap_or_default();
1814 if rendered.is_empty() {
1815 "error\n".to_string()
1816 } else if rendered.ends_with('\n') {
1817 rendered
1818 } else {
1819 format!("{rendered}\n")
1820 }
1821}
1822
1823pub(crate) fn render_trace_summary() -> String {
1824 use std::fmt::Write;
1825 let entries = harn_vm::llm::take_trace();
1826 if entries.is_empty() {
1827 return String::new();
1828 }
1829 let mut out = String::new();
1830 let _ = writeln!(out, "\n\x1b[2m─── LLM trace ───\x1b[0m");
1831 let mut total_input = 0i64;
1832 let mut total_output = 0i64;
1833 let mut total_ms = 0u64;
1834 for (i, entry) in entries.iter().enumerate() {
1835 let _ = writeln!(
1836 out,
1837 " #{}: {} | {} in + {} out tokens | {} ms",
1838 i + 1,
1839 entry.model,
1840 entry.input_tokens,
1841 entry.output_tokens,
1842 entry.duration_ms,
1843 );
1844 total_input += entry.input_tokens;
1845 total_output += entry.output_tokens;
1846 total_ms += entry.duration_ms;
1847 }
1848 let total_tokens = total_input + total_output;
1849 let cost = (total_input as f64 * 3.0 + total_output as f64 * 15.0) / 1_000_000.0;
1851 let _ = writeln!(
1852 out,
1853 " \x1b[1m{} call{}, {} tokens ({}in + {}out), {} ms, ~${:.4}\x1b[0m",
1854 entries.len(),
1855 if entries.len() == 1 { "" } else { "s" },
1856 total_tokens,
1857 total_input,
1858 total_output,
1859 total_ms,
1860 cost,
1861 );
1862 out
1863}
1864
1865pub(crate) async fn run_file_mcp_serve(
1879 path: &str,
1880 card_source: Option<&str>,
1881 mode: RunFileMcpServeMode,
1882) {
1883 let mut diagnostics = String::new();
1884 let Some(LoadedChunk {
1885 source,
1886 chunk,
1887 link_table,
1888 }) = compile_or_load_chunk_for_run(path, &mut diagnostics)
1889 else {
1890 eprint!("{diagnostics}");
1891 process::exit(1);
1892 };
1893 if !diagnostics.is_empty() {
1894 eprint!("{diagnostics}");
1895 }
1896
1897 let mut vm = harn_vm::Vm::new();
1898 vm.set_graph_link_table(link_table);
1899 harn_vm::register_vm_stdlib(&mut vm);
1900 crate::install_default_hostlib(&mut vm);
1901 let source_parent = std::path::Path::new(path)
1902 .parent()
1903 .unwrap_or(std::path::Path::new("."));
1904 let project_root = harn_vm::stdlib::process::find_project_root(source_parent);
1905 let store_base = project_root.as_deref().unwrap_or(source_parent);
1906 harn_vm::register_store_builtins(&mut vm, store_base);
1907 harn_vm::register_metadata_builtins(&mut vm, store_base);
1908 let pipeline_name = std::path::Path::new(path)
1909 .file_stem()
1910 .and_then(|s| s.to_str())
1911 .unwrap_or("default");
1912 harn_vm::register_checkpoint_builtins(&mut vm, store_base, pipeline_name);
1913 vm.set_source_info(path, &source);
1914 if let Some(ref root) = project_root {
1915 vm.set_project_root(root);
1916 }
1917 vm.set_source_dir(&entry_source_dir(path));
1921
1922 let loaded = load_skills(&SkillLoaderInputs {
1924 cli_dirs: Vec::new(),
1925 source_path: Some(std::path::PathBuf::from(path)),
1926 });
1927 emit_loader_warnings(&loaded.loader_warnings);
1928 install_skills_global(&mut vm, &loaded);
1929
1930 if let Err(error) =
1931 manifest_runtime::install_manifest_runtime(Path::new(path), &mut vm, false).await
1932 {
1933 eprintln!("error: failed to install {}: {error}", error.label());
1934 process::exit(1);
1935 }
1936
1937 vm.set_source_dir(&entry_source_dir(path));
1942 let local = tokio::task::LocalSet::new();
1943 local
1944 .run_until(async {
1945 match vm.execute(&chunk).await {
1946 Ok(_) => {}
1947 Err(error) => crate::commands::serve::exit_after_mcp_pipeline_error(&vm, &error),
1948 }
1949
1950 let output = vm.output();
1952 if !output.is_empty() {
1953 eprint!("{output}");
1954 }
1955
1956 let registry = match harn_vm::take_mcp_serve_registry() {
1957 Some(r) => r,
1958 None => {
1959 eprintln!("error: pipeline did not call mcp_serve(registry)");
1960 eprintln!("hint: call mcp_serve(tools) at the end of your pipeline");
1961 process::exit(1);
1962 }
1963 };
1964
1965 let tools = match harn_vm::tool_registry_to_mcp_tools(®istry) {
1966 Ok(t) => t,
1967 Err(e) => {
1968 eprintln!("error: {e}");
1969 process::exit(1);
1970 }
1971 };
1972
1973 let resources = harn_vm::take_mcp_serve_resources();
1974 let resource_templates = harn_vm::take_mcp_serve_resource_templates();
1975 let prompts = harn_vm::take_mcp_serve_prompts();
1976 let metadata = harn_vm::take_mcp_serve_metadata();
1977
1978 let mut server_name = std::path::Path::new(path)
1979 .file_stem()
1980 .and_then(|s| s.to_str())
1981 .unwrap_or("harn")
1982 .to_string();
1983 if let Some(name) = metadata
1984 .as_ref()
1985 .and_then(|metadata| metadata.name.as_ref())
1986 {
1987 server_name = name.clone();
1988 }
1989
1990 let mut caps = Vec::new();
1991 if !tools.is_empty() {
1992 caps.push(format!(
1993 "{} tool{}",
1994 tools.len(),
1995 if tools.len() == 1 { "" } else { "s" }
1996 ));
1997 }
1998 let total_resources = resources.len() + resource_templates.len();
1999 if total_resources > 0 {
2000 caps.push(format!(
2001 "{total_resources} resource{}",
2002 if total_resources == 1 { "" } else { "s" }
2003 ));
2004 }
2005 if !prompts.is_empty() {
2006 caps.push(format!(
2007 "{} prompt{}",
2008 prompts.len(),
2009 if prompts.len() == 1 { "" } else { "s" }
2010 ));
2011 }
2012 eprintln!(
2013 "[harn] serve mcp: serving {} as '{server_name}'",
2014 caps.join(", ")
2015 );
2016
2017 let mut server =
2018 harn_vm::McpServer::new(server_name, tools, resources, resource_templates, prompts);
2019 if let Some(metadata) = metadata {
2020 server = server.with_metadata(metadata);
2021 }
2022 if let Some(source) = card_source {
2023 match resolve_card_source(source) {
2024 Ok(card) => server = server.with_server_card(card),
2025 Err(e) => {
2026 eprintln!("error: --card: {e}");
2027 process::exit(1);
2028 }
2029 }
2030 }
2031 match mode {
2032 RunFileMcpServeMode::Stdio => {
2033 if let Err(e) = server.run(&mut vm).await {
2034 eprintln!("error: MCP server error: {e}");
2035 process::exit(1);
2036 }
2037 }
2038 RunFileMcpServeMode::Http(http) => {
2039 let RunFileMcpServeHttp {
2040 options,
2041 auth_policy,
2042 } = *http;
2043 if let Err(e) = crate::commands::serve::run_script_mcp_http_server(
2044 server,
2045 vm,
2046 options,
2047 auth_policy,
2048 )
2049 .await
2050 {
2051 eprintln!("error: MCP server error: {e}");
2052 process::exit(1);
2053 }
2054 }
2055 }
2056 })
2057 .await;
2058}
2059
2060pub(crate) fn resolve_card_source(source: &str) -> Result<serde_json::Value, String> {
2065 let trimmed = source.trim_start();
2066 if trimmed.starts_with('{') || trimmed.starts_with('[') {
2067 return serde_json::from_str(source).map_err(|e| format!("inline JSON parse error: {e}"));
2068 }
2069 let path = std::path::Path::new(source);
2070 harn_vm::load_server_card_from_path(path).map_err(|e| format!("{e}"))
2071}
2072
2073pub(crate) async fn run_watch(path: &str, denied_builtins: HashSet<String>) {
2074 use notify::{Event, EventKind, RecursiveMode, Watcher};
2075
2076 let abs_path = std::fs::canonicalize(path).unwrap_or_else(|e| {
2077 eprintln!("Error: {e}");
2078 process::exit(1);
2079 });
2080 let watch_dir = abs_path.parent().unwrap_or(Path::new("."));
2081
2082 eprintln!("\x1b[2m[watch] running {path}...\x1b[0m");
2083 run_file(
2084 path,
2085 false,
2086 denied_builtins.clone(),
2087 Vec::new(),
2088 CliLlmMockMode::Off,
2089 None,
2090 RunProfileOptions::default(),
2091 )
2092 .await;
2093
2094 let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
2095 let _watcher = {
2096 let tx = tx.clone();
2097 let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
2098 if let Ok(event) = res {
2099 if matches!(
2100 event.kind,
2101 EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
2102 ) {
2103 let has_harn = event
2104 .paths
2105 .iter()
2106 .any(|p| p.extension().is_some_and(|ext| ext == "harn"));
2107 if has_harn {
2108 let _ = tx.blocking_send(());
2109 }
2110 }
2111 }
2112 })
2113 .unwrap_or_else(|e| {
2114 eprintln!("Error setting up file watcher: {e}");
2115 process::exit(1);
2116 });
2117 watcher
2118 .watch(watch_dir, RecursiveMode::Recursive)
2119 .unwrap_or_else(|e| {
2120 eprintln!("Error watching directory: {e}");
2121 process::exit(1);
2122 });
2123 watcher };
2125
2126 eprintln!(
2127 "\x1b[2m[watch] watching {} for .harn changes (ctrl-c to stop)\x1b[0m",
2128 watch_dir.display()
2129 );
2130
2131 loop {
2132 rx.recv().await;
2133 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2135 while rx.try_recv().is_ok() {}
2136
2137 eprintln!();
2138 eprintln!("\x1b[2m[watch] change detected, re-running {path}...\x1b[0m");
2139 run_file(
2140 path,
2141 false,
2142 denied_builtins.clone(),
2143 Vec::new(),
2144 CliLlmMockMode::Off,
2145 None,
2146 RunProfileOptions::default(),
2147 )
2148 .await;
2149 }
2150}
2151
2152#[cfg(test)]
2153mod tests;