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