1#![recursion_limit = "256"]
2
3pub mod acp;
4mod bootstrap;
5pub mod cli;
6mod cli_bytecode;
7pub mod commands;
8#[doc(hidden)]
9pub mod dispatch;
10pub mod env_guard;
11pub mod format;
12pub mod json_envelope;
13mod net;
14pub mod package;
15mod provider_bootstrap;
16mod runtime;
17pub mod skill_loader;
18pub mod skill_provenance;
19pub mod test_report;
20pub mod test_runner;
21pub mod test_timing;
22#[doc(hidden)]
23pub mod tests;
24mod typecheck_imports;
25pub use harn_skills::{get_embedded_skill, list_embedded_skills, EmbeddedSkill, SkillFrontmatter};
26
27use clap::{error::ErrorKind, CommandFactory, Parser as ClapParser};
28use std::path::{Path, PathBuf};
29use std::sync::{Arc, Once};
30use std::{env, fs, panic, process, thread};
31
32use cli::{
33 refresh_provider_catalog_if_requested, Cli, Command, CompletionShell, EvalCommand,
34 GuardCommand, MergeCaptainCommand, MergeCaptainMockCommand, ModelInfoArgs,
35 PackageArtifactsCommand, PackageCacheCommand, PackageCommand, PackageScaffoldCommand,
36 PgCommand, ProviderCatalogCommand, ProviderCommand, RunsCommand, SkillCommand, SkillKeyCommand,
37 SkillTrustCommand, TimeCommand, ToolCommand,
38};
39use harn_lexer::Lexer;
40use harn_modules::project_config;
41use harn_parser::{DiagnosticSeverity, Parser, TypeChecker};
42use runtime::{build_cli_runtime, cli_runtime_mode, CliRuntimeMode};
43
44pub const CLI_RUNTIME_STACK_SIZE: usize = 16 * 1024 * 1024;
45static BROKEN_PIPE_PANIC_HOOK: Once = Once::new();
46
47pub(crate) fn ensure_builtin_signatures_installed() {
61 harn_parser::install_builtin_signatures(harn_vm::stdlib::all_builtin_signatures());
62}
63
64#[cfg(feature = "hostlib")]
65pub(crate) fn install_default_hostlib(vm: &mut harn_vm::Vm) {
66 let _ = harn_hostlib::install_default(vm);
67 harn_rules_hostlib::install(vm);
71}
72
73#[cfg(not(feature = "hostlib"))]
74pub(crate) fn install_default_hostlib(_vm: &mut harn_vm::Vm) {}
75
76pub fn run() {
79 install_broken_pipe_panic_hook();
80 harn_vm::initialize_runtime_assets();
81 let raw_args = normalize_serve_args(bootstrap::args_after_pre_runtime_command());
82 harn_vm::stdlib::force_link();
87
88 ensure_builtin_signatures_installed();
89
90 let runtime_mode = cli_runtime_mode(&raw_args);
91
92 let handle = thread::Builder::new()
93 .name("harn-cli".to_string())
94 .stack_size(CLI_RUNTIME_STACK_SIZE)
95 .spawn(move || {
96 let runtime = build_cli_runtime(runtime_mode);
97 runtime.block_on(async_main(raw_args, runtime_mode));
98 if let Err(error) = harn_vm::events::shutdown_otel_sink() {
105 eprintln!("[harn] OTel exporter shutdown failed: {error}");
106 }
107 })
108 .unwrap_or_else(|error| {
109 eprintln!("failed to start CLI runtime thread: {error}");
110 process::exit(1);
111 });
112
113 if let Err(payload) = handle.join() {
114 if runtime::is_broken_pipe_panic_payload(payload.as_ref()) {
115 process::exit(0);
116 }
117 std::panic::resume_unwind(payload);
118 }
119}
120
121fn install_broken_pipe_panic_hook() {
122 BROKEN_PIPE_PANIC_HOOK.call_once(|| {
123 let previous = panic::take_hook();
124 panic::set_hook(Box::new(move |info| {
125 if runtime::is_broken_pipe_panic_payload(info.payload()) {
126 return;
127 }
128 previous(info);
129 }));
130 });
131}
132
133#[allow(clippy::large_stack_frames)] async fn async_main(raw_args: Vec<String>, runtime_mode: CliRuntimeMode) {
135 if runtime_mode.enables_tokio_io() {
142 if let Err(error) = harn_vm::events::install_otel_sink_from_env() {
143 eprintln!("[harn] OTel exporter disabled: {error}");
144 }
145 }
146
147 if raw_args.len() == 2 && raw_args[1].ends_with(".harn") {
148 provider_bootstrap::maybe_seed_ollama_for_run_file(Path::new(&raw_args[1]), false, false)
149 .await;
150 commands::run::run_file(
151 &raw_args[1],
152 false,
153 std::collections::HashSet::new(),
154 Vec::new(),
155 commands::run::CliLlmMockMode::Off,
156 None,
157 commands::run::RunProfileOptions::default(),
158 )
159 .await;
160 return;
161 }
162
163 let cli = match Cli::try_parse_from(&raw_args) {
164 Ok(cli) => cli,
165 Err(error) => {
166 if matches!(
167 error.kind(),
168 ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
169 ) {
170 error.exit();
171 }
172 error.exit();
173 }
174 };
175
176 if cli.json_schemas {
177 commands::json_schemas::run(cli.schema_command.as_deref());
178 return;
179 }
180
181 let Some(subcommand) = cli.command else {
182 let mut cmd = Cli::command();
186 cmd.print_help().ok();
187 return;
188 };
189 match subcommand {
190 Command::Version(args) => {
191 let exit = run_version(args).await;
192 if exit != 0 {
193 process::exit(exit);
194 }
195 }
196 Command::Upgrade(args) => {
197 if let Err(error) = commands::upgrade::run(args).await {
198 eprintln!("error: {error}");
199 process::exit(1);
200 }
201 }
202 Command::Dap(_) => run_dap_adapter(),
203 Command::ConformanceHelper(args) => {
204 if let Err(error) = commands::conformance_helper::run(args).await {
205 eprintln!("error: {error}");
206 process::exit(1);
207 }
208 }
209 Command::Skill(args) => match args.command {
210 SkillCommand::List(list) => commands::skills::run_list(&list),
211 SkillCommand::Get(get) => commands::skills::run_get(&get),
212 SkillCommand::Dump(dump) => commands::skills::run_dump(&dump),
213 SkillCommand::Resolved(resolved) => commands::skills::run_resolved(&resolved),
214 SkillCommand::Inspect(inspect) => commands::skills::run_inspect(&inspect),
215 SkillCommand::Match(matcher) => commands::skills::run_match(&matcher),
216 SkillCommand::Install(install) => commands::skills::run_install(&install),
217 SkillCommand::New(new_args) => commands::skills::run_new(&new_args),
218 SkillCommand::Key(key_args) => match key_args.command {
219 SkillKeyCommand::Generate(generate) => commands::skill::run_key_generate(&generate),
220 },
221 SkillCommand::Sign(sign) => commands::skill::run_sign(&sign),
222 SkillCommand::Endorse(endorse) => commands::skill::run_endorse(&endorse),
223 SkillCommand::Verify(verify) => commands::skill::run_verify(&verify),
224 SkillCommand::WhoSigned(who_signed) => {
225 commands::skill::run_who_signed(&who_signed).await;
226 }
227 SkillCommand::Trust(trust_args) => match trust_args.command {
228 SkillTrustCommand::Add(add) => commands::skill::run_trust_add(&add),
229 SkillTrustCommand::List(list) => commands::skill::run_trust_list(&list),
230 },
231 },
232 Command::Guard(args) => match args.command {
233 GuardCommand::List(list_args) => commands::guard::run_list(&list_args),
234 GuardCommand::Install(install_args) => {
235 commands::guard::run_install(&install_args).await;
236 }
237 GuardCommand::Status(status_args) => commands::guard::run_status(&status_args),
238 GuardCommand::Remove(remove_args) => commands::guard::run_remove(&remove_args),
239 },
240 Command::Run(args) => {
241 if !args.explain_cost {
242 match (args.eval.as_deref(), args.file.as_deref()) {
243 (Some(code), None) => {
244 provider_bootstrap::maybe_seed_ollama_for_inline(
245 code,
246 args.yes,
247 args.llm_mock.is_some(),
248 )
249 .await;
250 }
251 (None, Some(file)) => {
252 provider_bootstrap::maybe_seed_ollama_for_run_file(
253 Path::new(file),
254 args.yes,
255 args.llm_mock.is_some(),
256 )
257 .await;
258 }
259 _ => {}
260 }
261 }
262 let denied =
263 commands::run::build_denied_builtins(args.deny.as_deref(), args.allow.as_deref());
264 let llm_mock_mode = if let Some(path) = args.llm_mock.as_ref() {
265 commands::run::CliLlmMockMode::Replay {
266 fixture_path: PathBuf::from(path),
267 }
268 } else if let Some(path) = args.llm_mock_record.as_ref() {
269 commands::run::CliLlmMockMode::Record {
270 fixture_path: PathBuf::from(path),
271 }
272 } else {
273 commands::run::CliLlmMockMode::Off
274 };
275 let attestation = args.attest.then(|| commands::run::RunAttestationOptions {
276 receipt_out: args.receipt_out.as_ref().map(PathBuf::from),
277 agent_id: args.attest_agent.clone(),
278 });
279 let profile_options = run_profile_options(&args.profile);
280 let sandbox_options = if args.no_sandbox {
281 commands::run::RunSandboxOptions::disabled()
282 } else {
283 commands::run::RunSandboxOptions::sandboxed(args.allow_process_network)
284 .with_write_roots(args.write_root.iter().cloned())
285 .with_read_only_roots(args.read_only_root.iter().cloned())
286 };
287 let json_options = args
288 .json
289 .then_some(commands::run::RunJsonOptions { quiet: args.quiet });
290 let aux_options = commands::run::run_aux_options_from_args(&args);
291 let control_options = commands::run::run_control_options_from_args(&args);
292 let harnpack_options = commands::run::harnpack::HarnpackRunOptions {
293 allow_unsigned: args.allow_unsigned,
294 dry_run_verify: args.dry_run_verify,
295 };
296
297 if let Some(resume_target) = args.resume.as_deref() {
298 let exit_code = commands::run::run_resume_with_skill_dirs(
299 resume_target,
300 args.trace,
301 denied,
302 args.argv.clone(),
303 args.skill_dir.clone(),
304 llm_mock_mode,
305 attestation,
306 profile_options,
307 sandbox_options.clone(),
308 json_options,
309 aux_options,
310 control_options,
311 )
312 .await;
313 runtime::exit_on_error(exit_code);
314 return;
315 }
316
317 if args.as_job {
318 run_as_job(&args).await;
319 return;
320 }
321
322 match (args.eval.as_deref(), args.file.as_deref()) {
323 (Some(code), None) => {
324 if args.allow_unsigned || args.dry_run_verify {
325 command_error(
326 "`--allow-unsigned` and `--dry-run-verify` apply to `.harnpack` inputs; \
327 they cannot be combined with `-e`",
328 );
329 }
330 let (wrapped, tmp) = commands::run::prepare_eval_temp_file(code)
331 .unwrap_or_else(|e| command_error(&e));
332 let tmp_path: PathBuf = tmp.path().to_path_buf();
333 if let Err(error) = fs::write(&tmp_path, &wrapped) {
334 drop(tmp);
335 command_error(&format!("failed to write temp file for -e: {error}"));
336 }
337 let tmp_str = tmp_path.to_string_lossy().into_owned();
338 let exit_code = if args.explain_cost {
339 commands::run::run_explain_cost_file_with_skill_dirs(&tmp_str)
340 } else {
341 commands::run::run_file_with_skill_dirs(
342 &tmp_str,
343 args.trace,
344 denied,
345 args.argv.clone(),
346 args.skill_dir.clone(),
347 llm_mock_mode.clone(),
348 attestation.clone(),
349 profile_options.clone(),
350 sandbox_options.clone(),
351 json_options.clone(),
352 aux_options.clone(),
353 control_options.clone(),
354 harnpack_options.clone(),
355 )
356 .await
357 };
358 drop(tmp);
359 runtime::exit_on_error(exit_code);
360 }
361 (None, Some(file)) => {
362 let exit_code = if args.explain_cost {
363 commands::run::run_explain_cost_file_with_skill_dirs(file)
364 } else {
365 commands::run::run_file_with_skill_dirs(
366 file,
367 args.trace,
368 denied,
369 args.argv.clone(),
370 args.skill_dir.clone(),
371 llm_mock_mode,
372 attestation,
373 profile_options,
374 sandbox_options,
375 json_options,
376 aux_options,
377 control_options,
378 harnpack_options,
379 )
380 .await
381 };
382 runtime::exit_on_error(exit_code);
383 }
384 (Some(_), Some(_)) => command_error(
385 "`harn run` accepts either `-e <code>` or `<file.harn>`, not both",
386 ),
387 (None, None) => command_error(
388 "`harn run` requires `--resume <snapshot>`, `-e <code>`, or `<file.harn>`",
389 ),
390 }
391 }
392 Command::Check(args) => commands::check::run_check_command(args),
393 Command::Parse(args) => {
394 if let Err(error) = commands::parse_tokens::run_parse(&args) {
395 command_error(&error);
396 }
397 }
398 Command::Tokens(args) => {
399 if let Err(error) = commands::parse_tokens::run_tokens(&args) {
400 command_error(&error);
401 }
402 }
403 Command::Config(args) => {
404 if let Err(error) = commands::config_cmd::run(args).await {
405 command_error(&error);
406 }
407 }
408 Command::Explain(args) => {
409 let code = commands::explain::run_explain(&args).await;
410 if code != 0 {
411 process::exit(code);
412 }
413 }
414 Command::Fix(args) => {
415 if let Err(error) = commands::fix::run(&args) {
416 if error.is_partial_failure() {
417 eprintln!("error: {}", error.message());
418 process::exit(1);
419 }
420 command_error(error.message());
421 }
422 }
423 Command::Contracts(args) => {
424 commands::contracts::handle_contracts_command(args).await;
425 }
426 Command::Connect(args) => {
427 commands::connect::run_connect(*args).await;
428 }
429 Command::Lint(args) => {
430 let targets: Vec<&str> = args.targets.iter().map(String::as_str).collect();
431 let (files, prompt_files) = commands::check::collect_lint_targets(&targets);
432 if files.is_empty() && prompt_files.is_empty() {
433 if args.json {
434 print_lint_error(
435 "no_lint_targets",
436 "no .harn, .harn.txt, or .harn.prompt files found under the given target(s)",
437 );
438 }
439 command_error(
440 "no .harn, .harn.txt, or .harn.prompt files found under the given target(s)",
441 );
442 }
443 if args.json {
444 let outcome = commands::check::run_lint_json(
445 &files,
446 commands::check::LintJsonOptions {
447 strict: args.strict,
448 require_file_header: args.require_file_header,
449 require_public_api_types: args.require_public_api_types,
450 },
451 )
452 .await;
453 println!("{}", json_envelope::to_string_pretty(&outcome.envelope));
454 runtime::exit_on_error(outcome.exit_code);
455 return;
456 }
457 let mut analysis = harn_parser::analysis::AnalysisDatabase::new();
458 let module_graph =
459 commands::check::build_module_graph_and_seed_analysis(&files, &mut analysis);
460 let cross_file_imports = commands::check::collect_cross_file_imports(&module_graph);
461 let script_rule_diags = commands::check::run_project_script_rules(&files).await;
466 let script_diags_for = |file: &std::path::Path| -> &[harn_lint::LintDiagnostic] {
467 script_rule_diags
468 .get(file)
469 .map(Vec::as_slice)
470 .unwrap_or(&[])
471 };
472 if args.fix {
473 let mut should_fail = false;
474 for file in &files {
475 let mut config = package::load_check_config(Some(file));
476 let mut lint_config = commands::check::load_harn_lint_config(file);
477 lint_config.require_file_header |= args.require_file_header;
478 lint_config.require_public_api_types |= args.require_public_api_types;
479 commands::check::apply_loaded_harn_lint_config(&lint_config, &mut config);
480 let outcome = commands::check::lint_fix_file(
481 &mut analysis,
482 file,
483 &config,
484 &cross_file_imports,
485 &module_graph,
486 &lint_config,
487 );
488 should_fail |= outcome.should_fail(config.strict || args.strict);
489 }
490 for file in &prompt_files {
491 let lint_config = commands::check::load_harn_lint_config(file);
492 let config = package::load_check_config(Some(file));
493 let outcome = commands::check::lint_prompt_file_inner(
498 file,
499 lint_config.template_variant_branch_threshold,
500 &lint_config.disabled,
501 );
502 should_fail |= outcome.should_fail(config.strict || args.strict);
503 }
504 if should_fail {
510 process::exit(1);
511 }
512 } else {
513 let mut should_fail = false;
514 let mut total_findings = 0usize;
515 let mut total_fixable = 0usize;
516 for file in &files {
517 let mut config = package::load_check_config(Some(file));
518 let mut lint_config = commands::check::load_harn_lint_config(file);
519 lint_config.require_file_header |= args.require_file_header;
520 lint_config.require_public_api_types |= args.require_public_api_types;
521 commands::check::apply_loaded_harn_lint_config(&lint_config, &mut config);
522 let outcome = commands::check::lint_file_inner(
523 &mut analysis,
524 file,
525 &config,
526 &cross_file_imports,
527 &module_graph,
528 &lint_config,
529 script_diags_for(file.as_path()),
530 );
531 total_findings += outcome.findings;
532 total_fixable += outcome.fixable;
533 should_fail |= outcome.should_fail(config.strict || args.strict);
534 }
535 for file in &prompt_files {
536 let lint_config = commands::check::load_harn_lint_config(file);
537 let config = package::load_check_config(Some(file));
538 let outcome = commands::check::lint_prompt_file_inner(
539 file,
540 lint_config.template_variant_branch_threshold,
541 &lint_config.disabled,
542 );
543 total_findings += outcome.findings;
544 total_fixable += outcome.fixable;
545 should_fail |= outcome.should_fail(config.strict || args.strict);
546 }
547 if total_fixable > 0 {
550 if total_fixable == total_findings {
551 eprintln!(
552 "\nAll {total_fixable} finding(s) are auto-fixable — run `harn lint --fix` to apply them."
553 );
554 } else {
555 eprintln!(
556 "\n{total_fixable} of {total_findings} finding(s) are auto-fixable — run `harn lint --fix` to apply them."
557 );
558 }
559 }
560 if should_fail {
561 process::exit(1);
562 }
563 }
564 }
565 Command::Fmt(args) => {
566 let targets: Vec<&str> = args.targets.iter().map(String::as_str).collect();
567 let anchor = targets.first().map(Path::new).unwrap_or(Path::new("."));
570 let loaded = match project_config::load_for_path(anchor) {
571 Ok(c) => c,
572 Err(e) => {
573 eprintln!("warning: {e}");
574 project_config::HarnConfig::default()
575 }
576 };
577 let mut opts = harn_fmt::FmtOptions::default();
578 if let Some(w) = loaded.fmt.line_width {
579 opts.line_width = w;
580 }
581 if let Some(w) = loaded.fmt.separator_width {
582 opts.separator_width = w;
583 }
584 if let Some(w) = args.line_width {
585 opts.line_width = w;
586 }
587 if let Some(w) = args.separator_width {
588 opts.separator_width = w;
589 }
590 let mode = commands::check::FmtMode::from_check_flag(args.check);
591 if args.json {
592 let envelope = commands::check::fmt_targets_json(&targets, mode, &opts);
593 let failed = !envelope.ok;
594 println!("{}", json_envelope::to_string_pretty(&envelope));
595 if failed {
596 process::exit(1);
597 }
598 } else {
599 commands::check::fmt_targets(&targets, mode, &opts);
600 }
601 }
602 Command::Test(args) => commands::test::run_command(args).await,
603 Command::Init(args) => {
604 commands::init::init_project(args.name.as_deref(), args.template).await;
605 }
606 Command::New(args) => match commands::init::resolve_new_args(&args) {
607 Ok((name, template)) => commands::init::init_project(name.as_deref(), template).await,
608 Err(error) => {
609 eprintln!("error: {error}");
610 process::exit(1);
611 }
612 },
613 Command::Doctor(args) => {
614 commands::doctor::run_doctor_with_options(commands::doctor::DoctorOptions {
615 json: args.json,
616 check_providers: args.check_providers,
617 check_targets: args.check_targets,
618 })
619 .await;
620 }
621 Command::Host(args) => {
622 let exit = commands::host::run(args).await;
623 if exit != 0 {
624 process::exit(exit);
625 }
626 }
627 Command::Models(args) => commands::models::run(args).await,
628 Command::Local(args) => commands::local::run(args).await,
629 Command::Provider(args) => match args.command {
630 ProviderCommand::Capabilities(capabilities) => {
631 commands::provider_capabilities::run_or_exit(capabilities);
632 }
633 ProviderCommand::Catalog(catalog) => match catalog.command {
634 ProviderCatalogCommand::Refresh(refresh) => {
635 if let Err(error) = commands::providers::run_refresh(&refresh).await {
636 command_error(&error);
637 }
638 }
639 ProviderCatalogCommand::Validate(validate) => {
640 if let Err(error) = commands::providers::run_validate(&validate) {
641 command_error(&error);
642 }
643 }
644 ProviderCatalogCommand::Generate(generate) => {
645 if let Err(error) = commands::providers::run_generate(&generate) {
646 command_error(&error);
647 }
648 }
649 ProviderCatalogCommand::Export(export) => {
650 if let Err(error) = commands::providers::run_export(&export) {
651 command_error(&error);
652 }
653 }
654 ProviderCatalogCommand::Matrix(matrix) => {
655 if let Err(error) = commands::providers::run_matrix(&matrix) {
656 command_error(&error);
657 }
658 }
659 ProviderCatalogCommand::Support(support) => {
660 if let Err(error) = commands::provider_support::run(&support) {
661 command_error(&error);
662 }
663 }
664 ProviderCatalogCommand::Recommend(recommend) => {
665 if let Err(error) = commands::providers::run_recommend(&recommend).await {
666 command_error(&error);
667 }
668 }
669 ProviderCatalogCommand::Show(show) => {
670 refresh_provider_catalog_if_requested(&show).await;
671 let exit_code = dispatch_provider_catalog(show.available_only).await;
672 runtime::exit_on_error(exit_code);
673 }
674 },
675 ProviderCommand::Ready(ready) => {
676 run_provider_ready(
677 &ready.provider,
678 ready.model.as_deref(),
679 ready.base_url.as_deref(),
680 ready.json,
681 )
682 .await;
683 }
684 ProviderCommand::Probe(probe) => commands::provider::run_provider_probe(probe).await,
685 ProviderCommand::ToolProbe(args) => commands::provider::run_tool_probe(args).await,
686 ProviderCommand::ToolProbeAudit(args) => commands::providers::run_audit(args),
687 ProviderCommand::ToolScorecard(tool_scorecard) => {
688 commands::provider::run_provider_tool_scorecard(tool_scorecard).await;
689 }
690 ProviderCommand::CacheProbe(cache_probe) => {
691 commands::provider::run_provider_cache_probe(cache_probe).await;
692 }
693 ProviderCommand::DispatchExplain(explain) => commands::dispatch_explain::run(&explain),
694 ProviderCommand::DispatchAudit(audit) => commands::dispatch_explain::run_audit(&audit),
695 ProviderCommand::Limits(limits) => {
696 commands::provider_limits::run(&limits);
697 }
698 },
699 Command::Scan(args) => commands::scan::run(args).await,
700 Command::Codemod(args) => commands::codemod::run(args).await,
701 Command::Rule(args) => commands::rule::run(args).await,
702 Command::Try(args) => commands::try_cmd::run(args).await,
703 Command::Quickstart(args) => {
704 if let Err(error) = commands::quickstart::run_quickstart(&args).await {
705 command_error(&error);
706 }
707 }
708 Command::Demo(args) => {
709 let code = commands::demo::run(args).await;
710 if code != 0 {
711 process::exit(code);
712 }
713 }
714 Command::Serve(args) => commands::serve::run_command(args.command).await,
715 Command::Connector(args) => {
716 if let Err(error) = commands::connector::handle_connector_command(args).await {
717 eprintln!("error: {error}");
718 process::exit(1);
719 }
720 }
721 Command::Mcp(args) => commands::mcp::handle_mcp_command(&args.command).await,
722 Command::Watch(args) => {
723 let denied =
724 commands::run::build_denied_builtins(args.deny.as_deref(), args.allow.as_deref());
725 commands::run::run_watch(&args.file, denied).await;
726 }
727 Command::Dev(args) => {
728 commands::dev::run(args).await;
729 }
730 Command::Portal(args) => {
731 commands::portal::run_portal(
732 &args.dir,
733 args.manifest,
734 args.persona_state_dir,
735 &args.host,
736 args.port,
737 args.open,
738 args.allow_remote_launch,
739 )
740 .await;
741 }
742 Command::Trigger(args) => {
743 if let Err(error) = commands::trigger::handle(args).await {
744 eprintln!("error: {error}");
745 process::exit(1);
746 }
747 }
748 Command::Graph(args) => {
749 let code = commands::graph::run(args).await;
750 if code != 0 {
751 process::exit(code);
752 }
753 }
754 Command::Doc(args) => {
755 let code = commands::doc::run(&args.path, args.output.as_deref());
756 if code != 0 {
757 process::exit(code);
758 }
759 }
760 Command::Routes(args) => {
761 let code = commands::routes::run(args).await;
762 if code != 0 {
763 process::exit(code);
764 }
765 }
766 Command::Usage(args) => {
767 let code = commands::usage::run(args).await;
768 if code != 0 {
769 process::exit(code);
770 }
771 }
772 Command::Flow(args) => match commands::flow::run_flow(&args) {
773 Ok(code) => {
774 if code != 0 {
775 process::exit(code);
776 }
777 }
778 Err(error) => command_error(&error),
779 },
780 Command::Canon(args) => commands::canon::run(args).await,
781 Command::Workflow(args) => match commands::workflow::handle(args) {
782 Ok(code) => {
783 if code != 0 {
784 process::exit(code);
785 }
786 }
787 Err(error) => command_error(&error),
788 },
789 Command::Supervisor(args) => {
790 if let Err(error) = commands::supervisor::handle(args).await {
791 eprintln!("error: {error}");
792 process::exit(1);
793 }
794 }
795 Command::Trace(args) => {
796 if let Err(error) = commands::trace::handle(args).await {
797 eprintln!("error: {error}");
798 process::exit(1);
799 }
800 }
801 Command::Crystallize(args) => {
802 if let Err(error) = commands::crystallize::run(args) {
803 eprintln!("error: {error}");
804 process::exit(1);
805 }
806 }
807 Command::Trust(args) => {
808 if let Err(error) = commands::trust::handle(args).await {
809 eprintln!("error: {error}");
810 process::exit(1);
811 }
812 }
813 Command::Verify(args) => {
814 if let Err(error) = verify_provenance_receipt(&args.receipt, args.json) {
815 eprintln!("error: {error}");
816 process::exit(1);
817 }
818 }
819 Command::Completion(args) => print_completions(args.shell),
820 Command::Orchestrator(args) => {
821 if let Err(error) = commands::orchestrator::handle(args).await {
822 eprintln!("error: {error}");
823 process::exit(1);
824 }
825 }
826 Command::Playground(args) => {
827 provider_bootstrap::maybe_seed_ollama_for_playground(
828 Path::new(&args.host),
829 Path::new(&args.script),
830 args.yes,
831 args.llm.is_some(),
832 args.llm_mock.is_some(),
833 )
834 .await;
835 let llm_mock_mode = if let Some(path) = args.llm_mock.as_ref() {
836 commands::run::CliLlmMockMode::Replay {
837 fixture_path: PathBuf::from(path),
838 }
839 } else if let Some(path) = args.llm_mock_record.as_ref() {
840 commands::run::CliLlmMockMode::Record {
841 fixture_path: PathBuf::from(path),
842 }
843 } else {
844 commands::run::CliLlmMockMode::Off
845 };
846 if let Err(error) = commands::playground::run_command(args, llm_mock_mode).await {
847 eprint!("{error}");
848 process::exit(1);
849 }
850 }
851 Command::Runs(args) => match args.command {
852 RunsCommand::Inspect(inspect) => {
853 inspect_run_record(&inspect.path, inspect.compare.as_deref());
854 }
855 RunsCommand::View(view) => {
856 cli::print_runs_view(&view.path, view.session, view.json);
857 }
858 },
859 Command::Session(args) => commands::session::run(args),
860 Command::Replay(args) => {
861 let exit = commands::replay::run(args);
862 if exit != 0 {
863 process::exit(exit);
864 }
865 }
866 Command::Eval(args) => match args.command {
867 Some(EvalCommand::CodingAgent(coding_agent_args)) => {
868 let code = commands::eval_coding_agent::run(coding_agent_args).await;
869 if code != 0 {
870 process::exit(code);
871 }
872 }
873 Some(EvalCommand::Context(context_args)) => {
874 let code = commands::eval_context::run(context_args).await;
875 if code != 0 {
876 process::exit(code);
877 }
878 }
879 Some(EvalCommand::Prompt(prompt_args)) => {
880 let code = commands::eval_prompt::run(prompt_args).await;
881 if code != 0 {
882 process::exit(code);
883 }
884 }
885 Some(EvalCommand::SkillGate(skill_gate_args)) => {
886 let code = commands::eval_skill_gate::run(skill_gate_args).await;
887 if code != 0 {
888 process::exit(code);
889 }
890 }
891 Some(EvalCommand::ScopeTriage(scope_args)) => {
892 process::exit(commands::eval_scope_triage::run(scope_args).await)
893 }
894 Some(EvalCommand::ToolCalls(tool_calls_args)) => {
895 let code = commands::eval_tool_calls::run(tool_calls_args).await;
896 if code != 0 {
897 process::exit(code);
898 }
899 }
900 None => {
901 let Some(path) = args.path else {
902 eprintln!("error: `harn eval` requires a path or a subcommand (e.g. `prompt`).\nSee `harn eval --help`.");
903 process::exit(2);
904 };
905 let llm_mock_mode = if let Some(path) = args.llm_mock.as_ref() {
906 commands::run::CliLlmMockMode::Replay {
907 fixture_path: PathBuf::from(path),
908 }
909 } else if let Some(path) = args.llm_mock_record.as_ref() {
910 commands::run::CliLlmMockMode::Record {
911 fixture_path: PathBuf::from(path),
912 }
913 } else {
914 commands::run::CliLlmMockMode::Off
915 };
916 eval_run_record(
917 &path,
918 args.compare.as_deref(),
919 args.structural_experiment.as_deref(),
920 &args.argv,
921 &llm_mock_mode,
922 );
923 }
924 },
925 Command::Repl => commands::repl::run_repl().await,
926 Command::Bench(args) => commands::bench::run(args).await,
927 Command::Precompile(args) => commands::precompile::run(args).await,
928 Command::Pack(args) => commands::pack::run(args),
929 Command::TestBench(args) => commands::test_bench::run(args.command).await,
930 Command::Viz(args) => commands::viz::run_viz(&args.file, args.output.as_deref()),
931 Command::Install(args) => package::install_packages(
932 args.frozen || args.locked || args.offline,
933 args.refetch.as_deref(),
934 args.offline,
935 args.json,
936 ),
937 Command::Add(args) => package::add_package_with_registry(
938 &args.name_or_spec,
939 args.alias.as_deref(),
940 args.git.as_deref(),
941 args.tag.as_deref(),
942 args.rev.as_deref(),
943 args.branch.as_deref(),
944 args.path.as_deref(),
945 args.registry.as_deref(),
946 ),
947 Command::Update(args) => {
948 package::update_packages(args.alias.as_deref(), args.all, args.json);
949 }
950 Command::Remove(args) => package::remove_package(&args.alias),
951 Command::Lock => package::lock_packages(),
952 Command::Package(args) => match args.command {
953 PackageCommand::List(list) => package::list_packages(list.json),
954 PackageCommand::Doctor(doctor) => package::doctor_packages(doctor.json),
955 PackageCommand::Search(search) => package::search_package_registry(
956 search.query.as_deref(),
957 search.registry.as_deref(),
958 search.json,
959 ),
960 PackageCommand::Info(info) => {
961 package::show_package_registry_info(
962 &info.name,
963 info.registry.as_deref(),
964 info.json,
965 );
966 }
967 PackageCommand::Check(check) => {
968 package::check_package(check.package.as_deref(), check.json);
969 }
970 PackageCommand::Pack(pack) => package::pack_package(
971 pack.package.as_deref(),
972 pack.output.as_deref(),
973 pack.dry_run,
974 pack.json,
975 ),
976 PackageCommand::Docs(docs) => package::generate_package_docs(
977 docs.package.as_deref(),
978 docs.output.as_deref(),
979 docs.check,
980 ),
981 PackageCommand::Cache(cache) => match cache.command {
982 PackageCacheCommand::List => package::list_package_cache(),
983 PackageCacheCommand::Clean(clean) => package::clean_package_cache(clean.all),
984 PackageCacheCommand::Verify(verify) => {
985 package::verify_package_cache(verify.materialized);
986 }
987 },
988 PackageCommand::Outdated(args) => package::outdated_packages(
989 args.refresh,
990 args.remote,
991 args.registry.as_deref(),
992 args.json,
993 ),
994 PackageCommand::Audit(args) => {
995 package::audit_packages(
996 args.registry.as_deref(),
997 args.skip_materialized,
998 args.json,
999 );
1000 }
1001 PackageCommand::Artifacts(args) => match args.command {
1002 PackageArtifactsCommand::Manifest(manifest) => {
1003 package::artifacts_manifest(manifest.output.as_deref());
1004 }
1005 PackageArtifactsCommand::Check(check) => {
1006 package::artifacts_check(&check.manifest, check.json);
1007 }
1008 },
1009 PackageCommand::Scaffold(args) => match args.command {
1010 PackageScaffoldCommand::Openapi(openapi) => {
1011 if let Err(error) = commands::package_scaffold::run_openapi(&openapi).await {
1012 eprintln!("error: {error}");
1013 process::exit(1);
1014 }
1015 }
1016 },
1017 },
1018 Command::Publish(args) => package::publish_package(
1019 args.package.as_deref(),
1020 args.dry_run,
1021 &args.remote,
1022 &args.index_repo,
1023 &args.index_path,
1024 args.registry_name.as_deref(),
1025 args.skip_index_pr,
1026 args.registry.as_deref(),
1027 args.json,
1028 ),
1029 Command::MergeCaptain(args) => match args.command {
1030 MergeCaptainCommand::Run(run) => {
1031 let code = commands::merge_captain::run_driver(&run);
1032 if code != 0 {
1033 process::exit(code);
1034 }
1035 }
1036 MergeCaptainCommand::Ladder(ladder) => {
1037 let code = commands::merge_captain::run_ladder(&ladder);
1038 if code != 0 {
1039 process::exit(code);
1040 }
1041 }
1042 MergeCaptainCommand::Iterate(iterate) => {
1043 let code = commands::merge_captain::run_iterate(&iterate);
1044 if code != 0 {
1045 process::exit(code);
1046 }
1047 }
1048 MergeCaptainCommand::Audit(audit) => {
1049 let code = commands::merge_captain::run_audit(&audit);
1050 if code != 0 {
1051 process::exit(code);
1052 }
1053 }
1054 MergeCaptainCommand::Mock(mock) => {
1055 let code = match mock {
1056 MergeCaptainMockCommand::Init(args) => {
1057 commands::merge_captain_mock::run_init(&args)
1058 }
1059 MergeCaptainMockCommand::Step(args) => {
1060 commands::merge_captain_mock::run_step(&args)
1061 }
1062 MergeCaptainMockCommand::Status(args) => {
1063 commands::merge_captain_mock::run_status(&args)
1064 }
1065 MergeCaptainMockCommand::Serve(args) => {
1066 commands::merge_captain_mock::run_serve(&args).await
1067 }
1068 MergeCaptainMockCommand::Cleanup(args) => {
1069 commands::merge_captain_mock::run_cleanup(&args)
1070 }
1071 MergeCaptainMockCommand::Scenarios => {
1072 commands::merge_captain_mock::run_scenarios()
1073 }
1074 };
1075 if code != 0 {
1076 process::exit(code);
1077 }
1078 }
1079 },
1080 Command::Pg(args) => match args.command {
1081 PgCommand::Codegen(codegen) => {
1082 let code = commands::pg_codegen::run(&codegen);
1083 if code != 0 {
1084 process::exit(code);
1085 }
1086 }
1087 },
1088 Command::Persona(args) => {
1089 if let Err(error) = commands::persona_dispatch::run(args).await {
1090 eprintln!("error: {error}");
1091 process::exit(1);
1092 }
1093 }
1094 Command::Tool(args) => match args.command {
1095 ToolCommand::New(new_args) => {
1096 if let Err(error) = commands::tool::run_new(&new_args).await {
1097 eprintln!("error: {error}");
1098 process::exit(1);
1099 }
1100 }
1101 },
1102 Command::DumpHighlightKeywords(args) => {
1103 commands::dump_highlight_keywords::run(&args.output, args.check);
1104 }
1105 Command::DumpTriggerQuickref(args) => {
1106 commands::dump_trigger_quickref::run(&args.output, args.check);
1107 }
1108 Command::DumpConnectorMatrix(args) => {
1109 commands::check::connector_matrix::run_docs(&args.output, &args.sources, args.check);
1110 }
1111 Command::DumpProtocolArtifacts(args) => {
1112 commands::dump_protocol_artifacts::run(
1113 &args.output_dir,
1114 args.check,
1115 args.artifact_version.as_deref(),
1116 );
1117 }
1118 Command::ConnectorSchemaCodegen(args) => {
1119 let code = commands::connector_schema_codegen::run(&args);
1120 if code != 0 {
1121 process::exit(code);
1122 }
1123 }
1124 Command::Time(args) => match args.command {
1125 TimeCommand::Run(time_args) => commands::time::run(time_args).await,
1126 },
1127 }
1128}
1129
1130fn run_profile_options(args: &cli::ProfileArgs) -> commands::run::RunProfileOptions {
1131 commands::run::RunProfileOptions {
1132 text: args.text,
1133 json_path: args.json_path.clone(),
1134 }
1135}
1136
1137fn print_completions(shell: CompletionShell) {
1138 let mut command = Cli::command();
1139 let shell = clap_complete::Shell::from(shell);
1140 clap_complete::generate(shell, &mut command, "harn", &mut std::io::stdout());
1141}
1142
1143fn normalize_serve_args(mut raw_args: Vec<String>) -> Vec<String> {
1152 if raw_args.len() > 2 && raw_args.get(1).is_some_and(|arg| arg == "serve") {
1153 let token = raw_args.get(2).map(String::as_str).unwrap_or_default();
1154 let is_explicit = token == "-h"
1155 || token == "--help"
1156 || serve_subcommand_names().iter().any(|name| name == token);
1157 if !is_explicit {
1158 raw_args.insert(2, "a2a".to_string());
1159 }
1160 }
1161 raw_args
1162}
1163
1164fn serve_subcommand_names() -> Vec<String> {
1166 use clap::CommandFactory;
1167 Cli::command()
1168 .find_subcommand("serve")
1169 .map(|serve| {
1170 serve
1171 .get_subcommands()
1172 .map(|sub| sub.get_name().to_string())
1173 .collect()
1174 })
1175 .unwrap_or_default()
1176}
1177
1178pub(crate) const VERSION_SCHEMA_VERSION: u32 = 1;
1181
1182fn run_dap_adapter() {
1191 thread::Builder::new()
1192 .name("harn-dap".to_string())
1193 .spawn(harn_dap::run)
1194 .expect("spawn harn-dap adapter thread")
1195 .join()
1196 .expect("harn-dap adapter thread panicked");
1197}
1198
1199async fn run_version(args: cli::VersionArgs) -> i32 {
1202 let _name = env_guard::ScopedEnvVar::set("HARN_BUILD_NAME", env!("CARGO_PKG_NAME"));
1203 let _version = env_guard::ScopedEnvVar::set("HARN_BUILD_VERSION", env!("CARGO_PKG_VERSION"));
1204 let _description =
1205 env_guard::ScopedEnvVar::set("HARN_BUILD_DESCRIPTION", env!("CARGO_PKG_DESCRIPTION"));
1206 let argv = if args.json {
1207 vec!["--json".to_string()]
1208 } else {
1209 Vec::new()
1210 };
1211 dispatch::dispatch_to_embedded_script("version", argv, args.json).await
1212}
1213
1214pub(crate) async fn print_model_info(args: &ModelInfoArgs) -> bool {
1215 let resolved = harn_vm::llm_config::resolve_model_info(&args.model);
1216 let api_key_result = harn_vm::llm::resolve_api_key(&resolved.provider);
1217 let api_key_set = api_key_result.is_ok();
1218 let api_key = api_key_result.unwrap_or_default();
1219 let context_window =
1220 harn_vm::llm::fetch_provider_max_context(&resolved.provider, &resolved.id, &api_key).await;
1221 let readiness = local_provider_readiness(&resolved.provider, &resolved.id, &api_key).await;
1222 let catalog = harn_vm::llm_config::model_catalog_entry(&resolved.id);
1223 let runtime_context_window = catalog
1224 .as_ref()
1225 .and_then(|entry| entry.runtime_context_window);
1226 let capabilities = harn_vm::llm::capabilities::lookup(&resolved.provider, &resolved.id);
1227 let batch_api =
1228 harn_vm::llm_config::effective_batch_api_supported(&resolved.provider, &capabilities);
1229 let mut payload = serde_json::json!({
1230 "alias": args.model,
1231 "id": resolved.id,
1232 "provider": resolved.provider,
1233 "resolved_alias": resolved.alias,
1234 "tool_format": resolved.tool_format,
1235 "tier": resolved.tier,
1236 "api_key_set": api_key_set,
1237 "context_window": context_window,
1238 "runtime_context_window": runtime_context_window,
1239 "readiness": readiness,
1240 "catalog": catalog,
1241 "capabilities": {
1242 "native_tools": capabilities.native_tools,
1243 "defer_loading": capabilities.defer_loading,
1244 "tool_search": capabilities.tool_search,
1245 "max_tools": capabilities.max_tools,
1246 "prompt_caching": capabilities.prompt_caching,
1247 "batch_api": batch_api,
1248 "batch_wire_format": if batch_api { capabilities.batch_wire_format } else { None },
1249 "batch_input_mode": if batch_api { capabilities.batch_input_mode } else { None },
1250 "batch_discount_percent": if batch_api { capabilities.batch_discount_percent } else { None },
1251 "batch_turnaround_hours": if batch_api { capabilities.batch_turnaround_hours } else { None },
1252 "vision": capabilities.vision,
1253 "vision_supported": capabilities.vision_supported,
1254 "audio": capabilities.audio,
1255 "pdf": capabilities.pdf,
1256 "files_api_supported": capabilities.files_api_supported,
1257 "json_schema": capabilities.json_schema,
1258 "prefers_xml_scaffolding": capabilities.prefers_xml_scaffolding,
1259 "prefers_markdown_scaffolding": capabilities.prefers_markdown_scaffolding,
1260 "structured_output_mode": capabilities.structured_output_mode,
1261 "supports_assistant_prefill": capabilities.supports_assistant_prefill,
1262 "prefers_role_developer": capabilities.prefers_role_developer,
1263 "prefers_xml_tools": capabilities.prefers_xml_tools,
1264 "thinking": !capabilities.thinking_modes.is_empty(),
1265 "thinking_block_style": capabilities.thinking_block_style,
1266 "thinking_modes": capabilities.thinking_modes,
1267 "interleaved_thinking_supported": capabilities.interleaved_thinking_supported,
1268 "anthropic_beta_features": capabilities.anthropic_beta_features,
1269 "preserve_thinking": capabilities.preserve_thinking,
1270 "server_parser": capabilities.server_parser,
1271 "honors_chat_template_kwargs": capabilities.honors_chat_template_kwargs,
1272 "recommended_endpoint": capabilities.recommended_endpoint,
1273 "text_tool_wire_format_supported": capabilities.text_tool_wire_format_supported,
1274 "preferred_tool_format": capabilities.preferred_tool_format,
1275 "tool_mode_parity": capabilities.tool_mode_parity,
1276 "tool_mode_parity_notes": capabilities.tool_mode_parity_notes,
1277 },
1278 "qc_default_model": harn_vm::llm_config::qc_default_model(&resolved.provider),
1279 });
1280
1281 let should_verify = args.verify || args.warm;
1282 let mut ok = true;
1283 if should_verify {
1284 if commands::local::runtime::uses_ollama_wire_protocol(&resolved.provider) {
1285 let mut readiness = harn_vm::llm::OllamaReadinessOptions::new(resolved.id.clone());
1286 readiness.warm = args.warm;
1287 readiness.observe_loaded = true;
1288 readiness.keep_alive = args
1289 .keep_alive
1290 .as_deref()
1291 .and_then(harn_vm::llm::normalize_ollama_keep_alive);
1292 let result = harn_vm::llm::ollama_readiness(readiness).await;
1293 ok = result.valid;
1294 payload["readiness"] = serde_json::to_value(&result).unwrap_or_else(|error| {
1295 serde_json::json!({
1296 "valid": false,
1297 "status": "serialization_error",
1298 "message": format!("failed to serialize readiness result: {error}"),
1299 })
1300 });
1301 } else {
1302 ok = false;
1303 payload["readiness"] = serde_json::json!({
1304 "valid": false,
1305 "status": "unsupported_provider",
1306 "message": format!(
1307 "models info --verify is only supported for models whose local runtime declares the Ollama API protocol; resolved provider is '{}'",
1308 resolved.provider
1309 ),
1310 "provider": resolved.provider,
1311 });
1312 }
1313 }
1314
1315 println!(
1316 "{}",
1317 serde_json::to_string(&payload).unwrap_or_else(|error| {
1318 command_error(&format!("failed to serialize model info: {error}"))
1319 })
1320 );
1321 ok
1322}
1323
1324async fn local_provider_readiness(
1325 provider: &str,
1326 model: &str,
1327 api_key: &str,
1328) -> Option<serde_json::Value> {
1329 let def = harn_vm::llm_config::provider_config(provider)?;
1330 if def.auth_style != "none" || !harn_vm::llm::supports_model_readiness_probe(&def) {
1331 return None;
1332 }
1333 let readiness = harn_vm::llm::readiness::probe_provider_readiness_with_options(
1334 provider,
1335 harn_vm::llm::readiness::ProviderReadinessOptions {
1336 requested_model: Some(model),
1337 base_url_override: None,
1338 api_key_override: Some(api_key),
1339 },
1340 )
1341 .await;
1342 Some(serde_json::to_value(readiness).unwrap_or_else(|error| {
1343 serde_json::json!({
1344 "ok": false,
1345 "status": "bad_response",
1346 "message": format!("failed to serialize readiness result: {error}"),
1347 "provider": provider,
1348 })
1349 }))
1350}
1351
1352fn build_provider_catalog_payload(available_only: bool) -> serde_json::Value {
1353 let provider_names = if available_only {
1354 harn_vm::llm::available_provider_names()
1355 } else {
1356 harn_vm::llm_config::provider_names()
1357 };
1358 let providers: Vec<_> = provider_names
1359 .into_iter()
1360 .filter_map(|name| {
1361 harn_vm::llm_config::provider_config(&name).map(|def| {
1362 serde_json::json!({
1363 "name": name,
1364 "display_name": def.display_name,
1365 "icon": def.icon,
1366 "base_url": harn_vm::llm_config::resolve_base_url(&def),
1367 "base_url_env": def.base_url_env,
1368 "region_env": def.region_env,
1369 "regions": def.regions,
1370 "auth_style": def.auth_style,
1371 "auth_envs": harn_vm::llm_config::auth_env_names(&def.auth_env),
1372 "auth_available": harn_vm::llm::provider_auth_status(&name).available,
1373 "features": def.features,
1374 "cost_per_1k_in": def.cost_per_1k_in,
1375 "cost_per_1k_out": def.cost_per_1k_out,
1376 "latency_p50_ms": def.latency_p50_ms,
1377 "performance": def.performance,
1378 })
1379 })
1380 })
1381 .collect();
1382 let models: Vec<_> = harn_vm::llm_config::model_catalog_entries()
1383 .into_iter()
1384 .map(|(id, model)| {
1385 serde_json::json!({
1386 "id": id,
1387 "name": model.name,
1388 "provider": model.provider,
1389 "context_window": model.context_window,
1390 "runtime_context_window": model.runtime_context_window,
1391 "stream_timeout": model.stream_timeout,
1392 "capabilities": model.capabilities,
1393 "pricing": model.pricing,
1394 "performance": model.performance,
1395 })
1396 })
1397 .collect();
1398 let aliases: Vec<_> = harn_vm::llm_config::alias_entries()
1399 .into_iter()
1400 .map(|(name, alias)| {
1401 serde_json::json!({
1402 "name": name,
1403 "id": alias.id,
1404 "provider": alias.provider,
1405 "tool_format": alias.tool_format,
1406 "tool_calling": harn_vm::llm_config::alias_tool_calling_entry(&name),
1407 })
1408 })
1409 .collect();
1410 let routing_routes = harn_vm::provider_catalog::artifact().routing_routes;
1411 serde_json::json!({
1412 "providers": providers,
1413 "known_model_names": harn_vm::llm_config::known_model_names(),
1414 "available_providers": harn_vm::llm::available_provider_names(),
1415 "aliases": aliases,
1416 "models": models,
1417 "routing_routes": routing_routes,
1418 "qc_defaults": harn_vm::llm_config::qc_defaults(),
1419 })
1420}
1421
1422async fn dispatch_provider_catalog(available_only: bool) -> i32 {
1431 static DISPATCH_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1432 let payload = build_provider_catalog_payload(available_only);
1433 let payload_json = match serde_json::to_string(&payload) {
1434 Ok(json) => json,
1435 Err(error) => {
1436 eprintln!("error: failed to serialise provider catalog payload: {error}");
1437 return 1;
1438 }
1439 };
1440 let _guard = DISPATCH_LOCK.lock().await;
1441 let _payload_guard =
1442 crate::env_guard::ScopedEnvVar::set("HARN_PROVIDER_CATALOG_PAYLOAD_JSON", &payload_json);
1443 crate::dispatch::dispatch_to_embedded_script("providers/catalog", Vec::new(), true).await
1447}
1448
1449async fn run_provider_ready(
1450 provider: &str,
1451 model: Option<&str>,
1452 base_url: Option<&str>,
1453 json: bool,
1454) {
1455 let readiness =
1456 harn_vm::llm::readiness::probe_provider_readiness(provider, model, base_url).await;
1457 if json {
1458 match serde_json::to_string_pretty(&readiness) {
1459 Ok(payload) => println!("{payload}"),
1460 Err(error) => command_error(&format!("failed to serialize readiness result: {error}")),
1461 }
1462 } else if readiness.ok {
1463 println!("{}", readiness.message);
1464 } else {
1465 eprintln!("{}", readiness.message);
1466 }
1467 if !readiness.ok {
1468 process::exit(1);
1469 }
1470}
1471
1472pub(crate) fn command_error(message: &str) -> ! {
1473 Cli::command()
1474 .error(ErrorKind::ValueValidation, message)
1475 .exit()
1476}
1477
1478async fn run_as_job(args: &cli::RunArgs) {
1483 let Some(file) = args.file.as_deref() else {
1484 command_error("`--as-job` requires a `.harn` file path");
1485 };
1486 let Some(job) = args.job.as_deref() else {
1487 command_error("`--as-job` requires `--job <name>`");
1488 };
1489 let Some(request) = args.request.as_deref() else {
1490 command_error("`--as-job` requires `--request <path>`");
1491 };
1492
1493 let script_path = PathBuf::from(file);
1494 let job = job.to_string();
1495 let request = request.to_path_buf();
1496 let result_out = args.result_out.clone();
1497
1498 let local = tokio::task::LocalSet::new();
1504 let outcome = local
1505 .run_until(async move {
1506 harn_serve::run_job_from_files(
1507 &script_path,
1508 &job,
1509 &request,
1510 result_out.as_deref(),
1511 false,
1512 )
1513 .await
1514 })
1515 .await;
1516
1517 match outcome {
1518 Ok((outcome, rendered)) => {
1519 println!("{rendered}");
1520 if !outcome.succeeded() {
1521 process::exit(1);
1522 }
1523 }
1524 Err(error) => {
1525 eprintln!("[harn] job failed: {}", error.message());
1526 process::exit(1);
1527 }
1528 }
1529}
1530
1531fn print_check_error(code: &str, message: &str) -> ! {
1532 let envelope: json_envelope::JsonEnvelope<commands::check::CheckReport> =
1533 json_envelope::JsonEnvelope::err(commands::check::CHECK_SCHEMA_VERSION, code, message);
1534 println!("{}", json_envelope::to_string_pretty(&envelope));
1535 process::exit(1);
1536}
1537
1538fn print_lint_error(code: &str, message: &str) -> ! {
1539 let envelope: json_envelope::JsonEnvelope<commands::check::LintReport> =
1540 json_envelope::JsonEnvelope::err(commands::check::LINT_SCHEMA_VERSION, code, message);
1541 println!("{}", json_envelope::to_string_pretty(&envelope));
1542 process::exit(1);
1543}
1544
1545fn verify_provenance_receipt(path: &str, json: bool) -> Result<(), String> {
1546 let raw =
1547 fs::read_to_string(path).map_err(|error| format!("failed to read {path}: {error}"))?;
1548 let receipt: harn_vm::ProvenanceReceipt = serde_json::from_str(&raw)
1549 .map_err(|error| format!("failed to parse provenance receipt {path}: {error}"))?;
1550 let report = harn_vm::verify_receipt(&receipt);
1551 if json {
1552 println!(
1553 "{}",
1554 serde_json::to_string_pretty(&report).map_err(|error| error.to_string())?
1555 );
1556 } else if report.verified {
1557 println!(
1558 "verified receipt={} events={} receipt_hash={} event_root_hash={}",
1559 report.receipt_id.unwrap_or_else(|| "-".to_string()),
1560 report.event_count,
1561 report.receipt_hash.unwrap_or_else(|| "-".to_string()),
1562 report.event_root_hash.unwrap_or_else(|| "-".to_string())
1563 );
1564 } else {
1565 println!(
1566 "failed receipt={} events={}",
1567 report.receipt_id.unwrap_or_else(|| "-".to_string()),
1568 report.event_count
1569 );
1570 for error in &report.errors {
1571 println!(" {error}");
1572 }
1573 return Err("provenance receipt verification failed".to_string());
1574 }
1575 Ok(())
1576}
1577
1578pub(crate) fn load_run_record_or_exit(path: &Path) -> harn_vm::orchestration::RunRecord {
1579 match harn_vm::orchestration::load_run_record(path) {
1580 Ok(run) => run,
1581 Err(error) => {
1582 eprintln!("Failed to load run record: {error}");
1583 process::exit(1);
1584 }
1585 }
1586}
1587
1588fn load_eval_suite_manifest_or_exit(path: &Path) -> harn_vm::orchestration::EvalSuiteManifest {
1589 harn_vm::orchestration::load_eval_suite_manifest(path).unwrap_or_else(|error| {
1590 eprintln!("Failed to load eval manifest {}: {error}", path.display());
1591 process::exit(1);
1592 })
1593}
1594
1595fn load_eval_pack_manifest_or_exit(path: &Path) -> harn_vm::orchestration::EvalPackManifest {
1596 harn_vm::orchestration::load_eval_pack_manifest(path).unwrap_or_else(|error| {
1597 eprintln!("Failed to load eval pack {}: {error}", path.display());
1598 process::exit(1);
1599 })
1600}
1601
1602fn load_persona_eval_ladder_manifest_or_exit(
1603 path: &Path,
1604) -> harn_vm::orchestration::PersonaEvalLadderManifest {
1605 harn_vm::orchestration::load_persona_eval_ladder_manifest(path).unwrap_or_else(|error| {
1606 eprintln!(
1607 "Failed to load persona eval ladder {}: {error}",
1608 path.display()
1609 );
1610 process::exit(1);
1611 })
1612}
1613
1614fn file_looks_like_eval_manifest(path: &Path) -> bool {
1615 if path.file_name().and_then(|name| name.to_str()) == Some("harn.eval.toml") {
1616 return true;
1617 }
1618 if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
1619 let Ok(content) = fs::read_to_string(path) else {
1620 return false;
1621 };
1622 return toml::from_str::<harn_vm::orchestration::EvalPackManifest>(&content)
1623 .is_ok_and(|manifest| !manifest.cases.is_empty() || !manifest.ladders.is_empty());
1624 }
1625 let Ok(content) = fs::read_to_string(path) else {
1626 return false;
1627 };
1628 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
1629 return false;
1630 };
1631 json.get("_type").and_then(|value| value.as_str()) == Some("eval_suite_manifest")
1632 || json.get("cases").is_some()
1633}
1634
1635fn file_looks_like_eval_pack_manifest(path: &Path) -> bool {
1636 if path.file_name().and_then(|name| name.to_str()) == Some("harn.eval.toml") {
1637 return true;
1638 }
1639 if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
1640 return file_looks_like_eval_manifest(path);
1641 }
1642 let Ok(content) = fs::read_to_string(path) else {
1643 return false;
1644 };
1645 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
1646 return false;
1647 };
1648 json.get("version").is_some()
1649 && (json.get("cases").is_some() || json.get("ladders").is_some())
1650 && json.get("_type").and_then(|value| value.as_str()) != Some("eval_suite_manifest")
1651}
1652
1653fn file_looks_like_persona_eval_ladder_manifest(path: &Path) -> bool {
1654 let Ok(content) = fs::read_to_string(path) else {
1655 return false;
1656 };
1657 if path.extension().and_then(|ext| ext.to_str()) == Some("json") {
1658 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
1659 return false;
1660 };
1661 return json.get("_type").and_then(|value| value.as_str())
1662 == Some("persona_eval_ladder_manifest")
1663 || json.get("timeout_tiers").is_some()
1664 || json.get("timeout-tiers").is_some();
1665 }
1666 toml::from_str::<harn_vm::orchestration::PersonaEvalLadderManifest>(&content).is_ok_and(
1667 |manifest| {
1668 manifest
1669 .type_name
1670 .eq_ignore_ascii_case("persona_eval_ladder_manifest")
1671 || (!manifest.timeout_tiers.is_empty() && manifest.backend.path.is_some())
1672 },
1673 )
1674}
1675
1676pub(crate) fn collect_run_record_paths(path: &str) -> Vec<PathBuf> {
1677 let path = Path::new(path);
1678 if path.is_file() {
1679 return vec![path.to_path_buf()];
1680 }
1681 if path.is_dir() {
1682 let mut entries: Vec<PathBuf> = fs::read_dir(path)
1683 .unwrap_or_else(|error| {
1684 eprintln!("Failed to read run directory {}: {error}", path.display());
1685 process::exit(1);
1686 })
1687 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
1688 .filter(|entry| entry.extension().and_then(|ext| ext.to_str()) == Some("json"))
1689 .collect();
1690 entries.sort();
1691 return entries;
1692 }
1693 eprintln!("Run path does not exist: {}", path.display());
1694 process::exit(1);
1695}
1696
1697fn print_run_diff(diff: &harn_vm::orchestration::RunDiffReport) {
1698 println!(
1699 "Diff: {} -> {} [{} -> {}]",
1700 diff.left_run_id, diff.right_run_id, diff.left_status, diff.right_status
1701 );
1702 println!("Identical: {}", diff.identical);
1703 println!("Stage diffs: {}", diff.stage_diffs.len());
1704 println!("Tool diffs: {}", diff.tool_diffs.len());
1705 println!("Observability diffs: {}", diff.observability_diffs.len());
1706 println!("Transition delta: {}", diff.transition_count_delta);
1707 println!("Artifact delta: {}", diff.artifact_count_delta);
1708 println!("Checkpoint delta: {}", diff.checkpoint_count_delta);
1709 for stage in &diff.stage_diffs {
1710 println!("- {} [{}]", stage.node_id, stage.change);
1711 for detail in &stage.details {
1712 println!(" {detail}");
1713 }
1714 }
1715 for tool in &diff.tool_diffs {
1716 println!("- tool {} [{}]", tool.tool_name, tool.args_hash);
1717 println!(" left: {:?}", tool.left_result);
1718 println!(" right: {:?}", tool.right_result);
1719 }
1720 for item in &diff.observability_diffs {
1721 println!("- {} [{}]", item.label, item.section);
1722 for detail in &item.details {
1723 println!(" {detail}");
1724 }
1725 }
1726}
1727
1728fn inspect_run_record(path: &str, compare: Option<&str>) {
1729 let run = load_run_record_or_exit(Path::new(path));
1730 println!("Run: {}", run.id);
1731 println!(
1732 "Workflow: {}",
1733 run.workflow_name
1734 .clone()
1735 .unwrap_or_else(|| run.workflow_id.clone())
1736 );
1737 println!("Status: {}", run.status);
1738 println!("Task: {}", run.task);
1739 println!("Stages: {}", run.stages.len());
1740 println!("Artifacts: {}", run.artifacts.len());
1741 println!("Transitions: {}", run.transitions.len());
1742 println!("Checkpoints: {}", run.checkpoints.len());
1743 println!("HITL questions: {}", run.hitl_questions.len());
1744 if let Some(observability) = &run.observability {
1745 println!("Planner rounds: {}", observability.planner_rounds.len());
1746 println!("Research facts: {}", observability.research_fact_count);
1747 println!("Workers: {}", observability.worker_lineage.len());
1748 println!(
1749 "Action graph: {} nodes / {} edges",
1750 observability.action_graph_nodes.len(),
1751 observability.action_graph_edges.len()
1752 );
1753 println!(
1754 "Transcript pointers: {}",
1755 observability.transcript_pointers.len()
1756 );
1757 println!("Daemon events: {}", observability.daemon_events.len());
1758 }
1759 if let Some(parent_worker_id) = run
1760 .metadata
1761 .get("parent_worker_id")
1762 .and_then(|value| value.as_str())
1763 {
1764 println!("Parent worker: {parent_worker_id}");
1765 }
1766 if let Some(parent_stage_id) = run
1767 .metadata
1768 .get("parent_stage_id")
1769 .and_then(|value| value.as_str())
1770 {
1771 println!("Parent stage: {parent_stage_id}");
1772 }
1773 if run
1774 .metadata
1775 .get("delegated")
1776 .and_then(|value| value.as_bool())
1777 .unwrap_or(false)
1778 {
1779 println!("Delegated: true");
1780 }
1781 println!(
1782 "Pending nodes: {}",
1783 if run.pending_nodes.is_empty() {
1784 "-".to_string()
1785 } else {
1786 run.pending_nodes.join(", ")
1787 }
1788 );
1789 println!(
1790 "Replay fixture: {}",
1791 if run.replay_fixture.is_some() {
1792 "embedded"
1793 } else {
1794 "derived"
1795 }
1796 );
1797 for stage in &run.stages {
1798 let worker = stage.metadata.get("worker");
1799 let worker_suffix = worker
1800 .and_then(|value| value.get("name"))
1801 .and_then(|value| value.as_str())
1802 .map(|name| format!(" worker={name}"))
1803 .unwrap_or_default();
1804 println!(
1805 "- {} [{}] status={} outcome={} branch={}{}",
1806 stage.node_id,
1807 stage.kind,
1808 stage.status,
1809 stage.outcome,
1810 stage.branch.clone().unwrap_or_else(|| "-".to_string()),
1811 worker_suffix,
1812 );
1813 if let Some(worker) = worker {
1814 if let Some(worker_id) = worker.get("id").and_then(|value| value.as_str()) {
1815 println!(" worker_id: {worker_id}");
1816 }
1817 if let Some(child_run_id) = worker.get("child_run_id").and_then(|value| value.as_str())
1818 {
1819 println!(" child_run_id: {child_run_id}");
1820 }
1821 if let Some(child_run_path) = worker
1822 .get("child_run_path")
1823 .and_then(|value| value.as_str())
1824 {
1825 println!(" child_run_path: {child_run_path}");
1826 }
1827 }
1828 }
1829 if let Some(observability) = &run.observability {
1830 for round in &observability.planner_rounds {
1831 println!(
1832 "- planner {} iterations={} llm_calls={} tools={} research_facts={}",
1833 round.node_id,
1834 round.iteration_count,
1835 round.llm_call_count,
1836 round.tool_execution_count,
1837 round.research_facts.len()
1838 );
1839 }
1840 for pointer in &observability.transcript_pointers {
1841 println!(
1842 "- transcript {} [{}] available={} {}",
1843 pointer.label,
1844 pointer.kind,
1845 pointer.available,
1846 pointer
1847 .path
1848 .clone()
1849 .unwrap_or_else(|| pointer.location.clone())
1850 );
1851 }
1852 for event in &observability.daemon_events {
1853 println!(
1854 "- daemon {} [{:?}] at {}",
1855 event.name, event.kind, event.timestamp
1856 );
1857 println!(" id: {}", event.daemon_id);
1858 println!(" persist_path: {}", event.persist_path);
1859 if let Some(summary) = &event.payload_summary {
1860 println!(" payload: {summary}");
1861 }
1862 }
1863 }
1864 if let Some(compare_path) = compare {
1865 let baseline = load_run_record_or_exit(Path::new(compare_path));
1866 print_run_diff(&harn_vm::orchestration::diff_run_records(&baseline, &run));
1867 }
1868}
1869
1870fn eval_run_record(
1871 path: &str,
1872 compare: Option<&str>,
1873 structural_experiment: Option<&str>,
1874 argv: &[String],
1875 llm_mock_mode: &commands::run::CliLlmMockMode,
1876) {
1877 if let Some(experiment) = structural_experiment {
1878 let path_buf = PathBuf::from(path);
1879 if !path_buf.is_file() || path_buf.extension().and_then(|ext| ext.to_str()) != Some("harn")
1880 {
1881 eprintln!(
1882 "--structural-experiment currently requires a .harn pipeline path, got {path}"
1883 );
1884 process::exit(1);
1885 }
1886 if compare.is_some() {
1887 eprintln!("--compare cannot be combined with --structural-experiment");
1888 process::exit(1);
1889 }
1890 if matches!(llm_mock_mode, commands::run::CliLlmMockMode::Record { .. }) {
1891 eprintln!("--llm-mock-record cannot be combined with --structural-experiment");
1892 process::exit(1);
1893 }
1894 let path_buf = fs::canonicalize(&path_buf).unwrap_or_else(|error| {
1895 command_error(&format!(
1896 "failed to canonicalize structural eval pipeline {}: {error}",
1897 path_buf.display()
1898 ))
1899 });
1900 run_structural_experiment_eval(&path_buf, experiment, argv, llm_mock_mode);
1901 return;
1902 }
1903
1904 let path_buf = PathBuf::from(path);
1905 if path_buf.is_file() && file_looks_like_persona_eval_ladder_manifest(&path_buf) {
1906 if compare.is_some() {
1907 eprintln!("--compare is not supported with persona eval ladder manifests");
1908 process::exit(1);
1909 }
1910 let manifest = load_persona_eval_ladder_manifest_or_exit(&path_buf);
1911 let report =
1912 harn_vm::orchestration::run_persona_eval_ladder(&manifest).unwrap_or_else(|error| {
1913 eprintln!(
1914 "Failed to evaluate persona eval ladder {}: {error}",
1915 path_buf.display()
1916 );
1917 process::exit(1);
1918 });
1919 print_persona_ladder_report(&report);
1920 if !report.pass {
1921 process::exit(1);
1922 }
1923 return;
1924 }
1925
1926 if path_buf.is_file() && file_looks_like_eval_pack_manifest(&path_buf) {
1927 if compare.is_some() {
1928 eprintln!("--compare is not supported with eval pack manifests");
1929 process::exit(1);
1930 }
1931 let manifest = load_eval_pack_manifest_or_exit(&path_buf);
1932 let report = harn_vm::orchestration::evaluate_eval_pack_manifest_resumable(&manifest, None)
1933 .unwrap_or_else(|error| {
1934 eprintln!(
1935 "Failed to evaluate eval pack {}: {error}",
1936 path_buf.display()
1937 );
1938 process::exit(1);
1939 });
1940 print_eval_pack_report(&report);
1941 if !report.pass {
1942 process::exit(1);
1943 }
1944 return;
1945 }
1946
1947 if path_buf.is_file() && file_looks_like_eval_manifest(&path_buf) {
1948 if compare.is_some() {
1949 eprintln!("--compare is not supported with eval suite manifests");
1950 process::exit(1);
1951 }
1952 let manifest = load_eval_suite_manifest_or_exit(&path_buf);
1953 let suite = harn_vm::orchestration::evaluate_run_suite_manifest(&manifest).unwrap_or_else(
1954 |error| {
1955 eprintln!(
1956 "Failed to evaluate manifest {}: {error}",
1957 path_buf.display()
1958 );
1959 process::exit(1);
1960 },
1961 );
1962 println!(
1963 "{} {} passed, {} failed, {} total",
1964 if suite.pass { "PASS" } else { "FAIL" },
1965 suite.passed,
1966 suite.failed,
1967 suite.total
1968 );
1969 for case in &suite.cases {
1970 println!(
1971 "- {} [{}] {}",
1972 case.label.clone().unwrap_or_else(|| case.run_id.clone()),
1973 case.workflow_id,
1974 if case.pass { "PASS" } else { "FAIL" }
1975 );
1976 if let Some(path) = &case.source_path {
1977 println!(" path: {path}");
1978 }
1979 if let Some(comparison) = &case.comparison {
1980 println!(" baseline identical: {}", comparison.identical);
1981 if !comparison.identical {
1982 println!(
1983 " baseline status: {} -> {}",
1984 comparison.left_status, comparison.right_status
1985 );
1986 }
1987 }
1988 for failure in &case.failures {
1989 println!(" {failure}");
1990 }
1991 }
1992 if !suite.pass {
1993 process::exit(1);
1994 }
1995 return;
1996 }
1997
1998 let paths = collect_run_record_paths(path);
1999 if paths.len() > 1 {
2000 let mut cases = Vec::new();
2001 for path in &paths {
2002 let run = load_run_record_or_exit(path);
2003 let fixture = run
2004 .replay_fixture
2005 .clone()
2006 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(&run));
2007 cases.push((run, fixture, Some(path.display().to_string())));
2008 }
2009 let suite = harn_vm::orchestration::evaluate_run_suite(cases);
2010 println!(
2011 "{} {} passed, {} failed, {} total",
2012 if suite.pass { "PASS" } else { "FAIL" },
2013 suite.passed,
2014 suite.failed,
2015 suite.total
2016 );
2017 for case in &suite.cases {
2018 println!(
2019 "- {} [{}] {}",
2020 case.run_id,
2021 case.workflow_id,
2022 if case.pass { "PASS" } else { "FAIL" }
2023 );
2024 if let Some(path) = &case.source_path {
2025 println!(" path: {path}");
2026 }
2027 if let Some(comparison) = &case.comparison {
2028 println!(" baseline identical: {}", comparison.identical);
2029 }
2030 for failure in &case.failures {
2031 println!(" {failure}");
2032 }
2033 }
2034 if !suite.pass {
2035 process::exit(1);
2036 }
2037 return;
2038 }
2039
2040 let run = load_run_record_or_exit(&paths[0]);
2041 let fixture = run
2042 .replay_fixture
2043 .clone()
2044 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(&run));
2045 let report = harn_vm::orchestration::evaluate_run_against_fixture(&run, &fixture);
2046 println!("{}", if report.pass { "PASS" } else { "FAIL" });
2047 println!("Stages: {}", report.stage_count);
2048 if let Some(compare_path) = compare {
2049 let baseline = load_run_record_or_exit(Path::new(compare_path));
2050 print_run_diff(&harn_vm::orchestration::diff_run_records(&baseline, &run));
2051 }
2052 if !report.failures.is_empty() {
2053 for failure in &report.failures {
2054 println!("- {failure}");
2055 }
2056 }
2057 if !report.pass {
2058 process::exit(1);
2059 }
2060}
2061
2062fn print_eval_pack_report(report: &harn_vm::orchestration::EvalPackReport) {
2063 println!(
2064 "{} {} passed, {} blocking failed, {} warning, {} informational, {} total",
2065 if report.pass { "PASS" } else { "FAIL" },
2066 report.passed,
2067 report.blocking_failed,
2068 report.warning_failed,
2069 report.informational_failed,
2070 report.total
2071 );
2072 for case in &report.cases {
2073 println!(
2074 "- {} [{}] {} ({})",
2075 case.label,
2076 case.workflow_id,
2077 if case.pass { "PASS" } else { "FAIL" },
2078 case.severity
2079 );
2080 if let Some(path) = &case.source_path {
2081 println!(" path: {path}");
2082 }
2083 if let Some(comparison) = &case.comparison {
2084 println!(" baseline identical: {}", comparison.identical);
2085 if !comparison.identical {
2086 println!(
2087 " baseline status: {} -> {}",
2088 comparison.left_status, comparison.right_status
2089 );
2090 }
2091 }
2092 for failure in &case.failures {
2093 println!(" {failure}");
2094 }
2095 for warning in &case.warnings {
2096 println!(" warning: {warning}");
2097 }
2098 for item in &case.informational {
2099 println!(" info: {item}");
2100 }
2101 }
2102 for ladder in &report.ladders {
2103 println!(
2104 "- ladder {} [{}] {} ({}) first_correct={}/{}",
2105 ladder.id,
2106 ladder.persona,
2107 if ladder.pass { "PASS" } else { "FAIL" },
2108 ladder.severity,
2109 ladder.first_correct_route.as_deref().unwrap_or("<none>"),
2110 ladder.first_correct_tier.as_deref().unwrap_or("<none>")
2111 );
2112 println!(" artifacts: {}", ladder.artifact_root);
2113 for tier in &ladder.tiers {
2114 println!(
2115 " - {} [{}] {} tools={} models={} latency={}ms cost=${:.6}",
2116 tier.timeout_tier,
2117 tier.route_id,
2118 tier.outcome,
2119 tier.tool_calls,
2120 tier.model_calls,
2121 tier.latency_ms,
2122 tier.cost_usd
2123 );
2124 for reason in &tier.degradation_reasons {
2125 println!(" {reason}");
2126 }
2127 }
2128 }
2129}
2130
2131fn print_persona_ladder_report(report: &harn_vm::orchestration::PersonaEvalLadderReport) {
2132 println!(
2133 "{} ladder {} passed, {} degraded/looped, {} total",
2134 if report.pass { "PASS" } else { "FAIL" },
2135 report.passed,
2136 report.failed,
2137 report.total
2138 );
2139 println!(
2140 "first_correct: {}/{}",
2141 report.first_correct_route.as_deref().unwrap_or("<none>"),
2142 report.first_correct_tier.as_deref().unwrap_or("<none>")
2143 );
2144 println!("artifacts: {}", report.artifact_root);
2145 for tier in &report.tiers {
2146 println!(
2147 "- {} [{}] {} tools={} models={} latency={}ms cost=${:.6}",
2148 tier.timeout_tier,
2149 tier.route_id,
2150 tier.outcome,
2151 tier.tool_calls,
2152 tier.model_calls,
2153 tier.latency_ms,
2154 tier.cost_usd
2155 );
2156 for reason in &tier.degradation_reasons {
2157 println!(" {reason}");
2158 }
2159 }
2160}
2161
2162pub(crate) fn run_package_evals() {
2163 let paths = package::load_package_eval_pack_paths(None).unwrap_or_else(|error| {
2164 eprintln!("{error}");
2165 process::exit(1);
2166 });
2167 let mut all_pass = true;
2168 for path in &paths {
2169 println!("Eval pack: {}", path.display());
2170 let manifest = load_eval_pack_manifest_or_exit(path);
2171 let report = harn_vm::orchestration::evaluate_eval_pack_manifest_resumable(&manifest, None)
2172 .unwrap_or_else(|error| {
2173 eprintln!("Failed to evaluate eval pack {}: {error}", path.display());
2174 process::exit(1);
2175 });
2176 print_eval_pack_report(&report);
2177 all_pass &= report.pass;
2178 }
2179 if !all_pass {
2180 process::exit(1);
2181 }
2182}
2183
2184fn run_structural_experiment_eval(
2185 path: &Path,
2186 experiment: &str,
2187 argv: &[String],
2188 llm_mock_mode: &commands::run::CliLlmMockMode,
2189) {
2190 let baseline_dir = tempfile::Builder::new()
2191 .prefix("harn-eval-baseline-")
2192 .tempdir()
2193 .unwrap_or_else(|error| {
2194 command_error(&format!("failed to create baseline tempdir: {error}"))
2195 });
2196 let variant_dir = tempfile::Builder::new()
2197 .prefix("harn-eval-variant-")
2198 .tempdir()
2199 .unwrap_or_else(|error| {
2200 command_error(&format!("failed to create variant tempdir: {error}"))
2201 });
2202
2203 let baseline = spawn_eval_pipeline_run(path, baseline_dir.path(), None, argv, llm_mock_mode);
2204 if !baseline.status.success() {
2205 relay_subprocess_failure("baseline", &baseline);
2206 }
2207
2208 let variant = spawn_eval_pipeline_run(
2209 path,
2210 variant_dir.path(),
2211 Some(experiment),
2212 argv,
2213 llm_mock_mode,
2214 );
2215 if !variant.status.success() {
2216 relay_subprocess_failure("variant", &variant);
2217 }
2218
2219 let baseline_runs = collect_structural_eval_runs(baseline_dir.path());
2220 let variant_runs = collect_structural_eval_runs(variant_dir.path());
2221 if baseline_runs.is_empty() || variant_runs.is_empty() {
2222 eprintln!(
2223 "structural eval expected workflow run records under {} and {}, but one side was empty",
2224 baseline_dir.path().display(),
2225 variant_dir.path().display()
2226 );
2227 process::exit(1);
2228 }
2229 if baseline_runs.len() != variant_runs.len() {
2230 eprintln!(
2231 "structural eval produced different run counts: baseline={} variant={}",
2232 baseline_runs.len(),
2233 variant_runs.len()
2234 );
2235 process::exit(1);
2236 }
2237
2238 let mut baseline_ok = 0usize;
2239 let mut variant_ok = 0usize;
2240 let mut any_failures = false;
2241
2242 println!("Structural experiment: {experiment}");
2243 println!("Cases: {}", baseline_runs.len());
2244 for (baseline_run, variant_run) in baseline_runs.iter().zip(variant_runs.iter()) {
2245 let baseline_fixture = baseline_run
2246 .replay_fixture
2247 .clone()
2248 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(baseline_run));
2249 let variant_fixture = variant_run
2250 .replay_fixture
2251 .clone()
2252 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(variant_run));
2253 let baseline_report =
2254 harn_vm::orchestration::evaluate_run_against_fixture(baseline_run, &baseline_fixture);
2255 let variant_report =
2256 harn_vm::orchestration::evaluate_run_against_fixture(variant_run, &variant_fixture);
2257 let diff = harn_vm::orchestration::diff_run_records(baseline_run, variant_run);
2258 if baseline_report.pass {
2259 baseline_ok += 1;
2260 }
2261 if variant_report.pass {
2262 variant_ok += 1;
2263 }
2264 any_failures |= !baseline_report.pass || !variant_report.pass;
2265 println!(
2266 "- {} [{}]",
2267 variant_run
2268 .workflow_name
2269 .clone()
2270 .unwrap_or_else(|| variant_run.workflow_id.clone()),
2271 variant_run.task
2272 );
2273 println!(
2274 " baseline: {}",
2275 if baseline_report.pass { "PASS" } else { "FAIL" }
2276 );
2277 for failure in &baseline_report.failures {
2278 println!(" {failure}");
2279 }
2280 println!(
2281 " variant: {}",
2282 if variant_report.pass { "PASS" } else { "FAIL" }
2283 );
2284 for failure in &variant_report.failures {
2285 println!(" {failure}");
2286 }
2287 println!(" diff identical: {}", diff.identical);
2288 println!(" stage diffs: {}", diff.stage_diffs.len());
2289 println!(" tool diffs: {}", diff.tool_diffs.len());
2290 println!(" observability diffs: {}", diff.observability_diffs.len());
2291 }
2292
2293 println!("Baseline {} / {} passed", baseline_ok, baseline_runs.len());
2294 println!("Variant {} / {} passed", variant_ok, variant_runs.len());
2295
2296 if any_failures {
2297 process::exit(1);
2298 }
2299}
2300
2301fn spawn_eval_pipeline_run(
2302 path: &Path,
2303 run_dir: &Path,
2304 structural_experiment: Option<&str>,
2305 argv: &[String],
2306 llm_mock_mode: &commands::run::CliLlmMockMode,
2307) -> std::process::Output {
2308 let exe = env::current_exe().unwrap_or_else(|error| {
2309 command_error(&format!("failed to resolve current executable: {error}"))
2310 });
2311 let mut command = std::process::Command::new(exe);
2312 command.current_dir(path.parent().unwrap_or_else(|| Path::new(".")));
2313 command.arg("run");
2314 match llm_mock_mode {
2315 commands::run::CliLlmMockMode::Off => {}
2316 commands::run::CliLlmMockMode::Replay { fixture_path } => {
2317 command
2318 .arg("--llm-mock")
2319 .arg(absolute_cli_path(fixture_path));
2320 }
2321 commands::run::CliLlmMockMode::Record { fixture_path } => {
2322 command
2323 .arg("--llm-mock-record")
2324 .arg(absolute_cli_path(fixture_path));
2325 }
2326 }
2327 command.arg(path);
2328 if !argv.is_empty() {
2329 command.arg("--");
2330 command.args(argv);
2331 }
2332 command.env(harn_vm::runtime_paths::HARN_RUN_DIR_ENV, run_dir);
2333 if let Some(experiment) = structural_experiment {
2334 command.env("HARN_STRUCTURAL_EXPERIMENT", experiment);
2335 }
2336 command.output().unwrap_or_else(|error| {
2337 command_error(&format!(
2338 "failed to spawn `harn run {}` for structural eval: {error}",
2339 path.display()
2340 ))
2341 })
2342}
2343
2344fn absolute_cli_path(path: &Path) -> PathBuf {
2345 if path.is_absolute() {
2346 return path.to_path_buf();
2347 }
2348 env::current_dir()
2349 .unwrap_or_else(|_| PathBuf::from("."))
2350 .join(path)
2351}
2352
2353fn relay_subprocess_failure(label: &str, output: &std::process::Output) -> ! {
2354 let stdout = String::from_utf8_lossy(&output.stdout);
2355 let stderr = String::from_utf8_lossy(&output.stderr);
2356 if !stdout.trim().is_empty() {
2357 eprintln!("[{label}] stdout:\n{stdout}");
2358 }
2359 if !stderr.trim().is_empty() {
2360 eprintln!("[{label}] stderr:\n{stderr}");
2361 }
2362 process::exit(output.status.code().unwrap_or(1));
2363}
2364
2365fn collect_structural_eval_runs(dir: &Path) -> Vec<harn_vm::orchestration::RunRecord> {
2366 let mut paths: Vec<PathBuf> = fs::read_dir(dir)
2367 .unwrap_or_else(|error| {
2368 command_error(&format!(
2369 "failed to read structural eval run dir {}: {error}",
2370 dir.display()
2371 ))
2372 })
2373 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
2374 .filter(|entry| entry.extension().and_then(|ext| ext.to_str()) == Some("json"))
2375 .collect();
2376 paths.sort();
2377 let mut runs: Vec<_> = paths
2378 .iter()
2379 .map(|path| load_run_record_or_exit(path))
2380 .collect();
2381 runs.sort_by(|left, right| {
2382 (
2383 left.started_at.as_str(),
2384 left.workflow_id.as_str(),
2385 left.task.as_str(),
2386 )
2387 .cmp(&(
2388 right.started_at.as_str(),
2389 right.workflow_id.as_str(),
2390 right.task.as_str(),
2391 ))
2392 });
2393 runs
2394}
2395
2396pub(crate) fn parse_source_file(path: &str) -> (String, Vec<harn_parser::SNode>) {
2398 ensure_builtin_signatures_installed();
2399
2400 let source = match fs::read_to_string(path) {
2401 Ok(s) => s,
2402 Err(e) => {
2403 eprintln!("Error reading {path}: {e}");
2404 process::exit(1);
2405 }
2406 };
2407
2408 let mut lexer = Lexer::new(&source);
2409 let tokens = match lexer.tokenize() {
2410 Ok(t) => t,
2411 Err(e) => {
2412 let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
2413 &source,
2414 path,
2415 &error_span_from_lex(&e),
2416 "error",
2417 harn_parser::diagnostic::lexer_error_code(&e),
2418 &e.to_string(),
2419 Some("here"),
2420 None,
2421 );
2422 eprint!("{diagnostic}");
2423 process::exit(1);
2424 }
2425 };
2426
2427 let mut parser = Parser::new(tokens);
2428 let program = match parser.parse() {
2429 Ok(p) => p,
2430 Err(err) => {
2431 if parser.all_errors().is_empty() {
2432 let span = error_span_from_parse(&err);
2433 let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
2434 &source,
2435 path,
2436 &span,
2437 "error",
2438 harn_parser::diagnostic::parser_error_code(&err),
2439 &harn_parser::diagnostic::parser_error_message(&err),
2440 Some(harn_parser::diagnostic::parser_error_label(&err)),
2441 harn_parser::diagnostic::parser_error_help(&err),
2442 );
2443 eprint!("{diagnostic}");
2444 } else {
2445 for e in parser.all_errors() {
2446 let span = error_span_from_parse(e);
2447 let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
2448 &source,
2449 path,
2450 &span,
2451 "error",
2452 harn_parser::diagnostic::parser_error_code(e),
2453 &harn_parser::diagnostic::parser_error_message(e),
2454 Some(harn_parser::diagnostic::parser_error_label(e)),
2455 harn_parser::diagnostic::parser_error_help(e),
2456 );
2457 eprint!("{diagnostic}");
2458 }
2459 }
2460 process::exit(1);
2461 }
2462 };
2463
2464 (source, program)
2465}
2466
2467fn error_span_from_lex(e: &harn_lexer::LexerError) -> harn_lexer::Span {
2468 match e {
2469 harn_lexer::LexerError::UnexpectedCharacter(_, span)
2470 | harn_lexer::LexerError::UnterminatedString(span)
2471 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
2472 | harn_lexer::LexerError::UnterminatedBlockComment(span) => *span,
2473 }
2474}
2475
2476fn error_span_from_parse(e: &harn_parser::ParserError) -> harn_lexer::Span {
2477 match e {
2478 harn_parser::ParserError::Unexpected { span, .. } => *span,
2479 harn_parser::ParserError::UnexpectedEof { span, .. } => *span,
2480 }
2481}
2482
2483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2488pub(crate) enum ExecStage {
2489 Parse,
2490 Typecheck,
2491 Compile,
2492 Runtime,
2493}
2494
2495impl ExecStage {
2496 pub(crate) fn label(self) -> &'static str {
2498 match self {
2499 ExecStage::Parse => "parse error",
2500 ExecStage::Typecheck => "type error",
2501 ExecStage::Compile => "compile error",
2502 ExecStage::Runtime => "runtime error",
2503 }
2504 }
2505}
2506
2507#[derive(Debug, Clone)]
2512pub(crate) struct ExecError {
2513 pub(crate) stage: ExecStage,
2514 pub(crate) message: String,
2515}
2516
2517impl ExecError {
2518 fn new(stage: ExecStage, message: impl Into<String>) -> Self {
2519 Self {
2520 stage,
2521 message: message.into(),
2522 }
2523 }
2524}
2525
2526impl std::fmt::Display for ExecError {
2527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2528 f.write_str(&self.message)
2529 }
2530}
2531
2532pub(crate) async fn execute(source: &str, source_path: Option<&Path>) -> Result<String, ExecError> {
2534 execute_with_skill_dirs(source, source_path, &[]).await
2535}
2536
2537pub(crate) async fn execute_with_skill_dirs(
2538 source: &str,
2539 source_path: Option<&Path>,
2540 cli_skill_dirs: &[PathBuf],
2541) -> Result<String, ExecError> {
2542 execute_with_skill_dirs_and_optional_harness(source, source_path, cli_skill_dirs, None).await
2543}
2544
2545pub(crate) async fn execute_with_skill_dirs_and_harness(
2546 source: &str,
2547 source_path: Option<&Path>,
2548 cli_skill_dirs: &[PathBuf],
2549 harness: harn_vm::Harness,
2550) -> Result<String, ExecError> {
2551 execute_with_skill_dirs_and_optional_harness(source, source_path, cli_skill_dirs, Some(harness))
2552 .await
2553}
2554
2555async fn execute_with_skill_dirs_and_optional_harness(
2556 source: &str,
2557 source_path: Option<&Path>,
2558 cli_skill_dirs: &[PathBuf],
2559 harness: Option<harn_vm::Harness>,
2560) -> Result<String, ExecError> {
2561 let mut lexer = Lexer::new(source);
2562 let tokens = lexer
2563 .tokenize()
2564 .map_err(|e| ExecError::new(ExecStage::Parse, e.to_string()))?;
2565 let mut parser = Parser::new(tokens);
2566 let program = parser
2567 .parse()
2568 .map_err(|e| ExecError::new(ExecStage::Parse, e.to_string()))?;
2569
2570 let mut checker = TypeChecker::new();
2575 if let Some(path) = source_path {
2576 checker = crate::typecheck_imports::checker_with_resolved_imports(checker, path);
2577 }
2578 let type_diagnostics = checker.check(&program);
2579 let mut warning_lines = Vec::new();
2580 for diag in &type_diagnostics {
2581 match diag.severity {
2582 DiagnosticSeverity::Error => {
2583 return Err(ExecError::new(ExecStage::Typecheck, diag.message.clone()))
2584 }
2585 DiagnosticSeverity::Warning => {
2586 warning_lines.push(format!("warning: {}", diag.message));
2587 }
2588 }
2589 }
2590
2591 let chunk = harn_vm::Compiler::new()
2592 .compile(&program)
2593 .map_err(|e| ExecError::new(ExecStage::Compile, e.to_string()))?;
2594
2595 let local = tokio::task::LocalSet::new();
2596 local
2597 .run_until(async {
2598 let mut vm = harn_vm::Vm::new();
2599 harn_vm::register_vm_stdlib(&mut vm);
2600 install_default_hostlib(&mut vm);
2601 let source_parent = source_path
2602 .and_then(|p| p.parent())
2603 .unwrap_or(std::path::Path::new("."));
2604 let project_root = harn_vm::stdlib::process::find_project_root(source_parent);
2605 let store_base = project_root.as_deref().unwrap_or(source_parent);
2606 let execution_cwd = std::env::current_dir()
2607 .unwrap_or_else(|_| std::path::PathBuf::from("."))
2608 .to_string_lossy()
2609 .into_owned();
2610 let source_dir = source_parent.to_string_lossy().into_owned();
2611 if source_path.is_some_and(is_conformance_path) {
2612 harn_vm::event_log::install_memory_for_current_thread(64);
2613 }
2614 harn_vm::register_store_builtins(&mut vm, store_base);
2615 harn_vm::register_metadata_builtins(&mut vm, store_base);
2616 let pipeline_name = source_path
2617 .and_then(|p| p.file_stem())
2618 .and_then(|s| s.to_str())
2619 .unwrap_or("default");
2620 harn_vm::register_checkpoint_builtins(&mut vm, store_base, pipeline_name);
2621 harn_vm::stdlib::process::set_thread_execution_context(Some(
2622 harn_vm::orchestration::RunExecutionRecord {
2623 cwd: Some(execution_cwd),
2624 project_root: project_root
2625 .as_ref()
2626 .map(|root| root.to_string_lossy().into_owned()),
2627 source_dir: Some(source_dir),
2628 env: std::collections::BTreeMap::new(),
2629 adapter: None,
2630 repo_path: None,
2631 worktree_path: None,
2632 branch: None,
2633 base_ref: None,
2634 cleanup: None,
2635 grants: Vec::new(),
2636 },
2637 ));
2638 if let Some(ref root) = project_root {
2639 vm.set_project_root(root);
2640 }
2641 if let Some(path) = source_path {
2642 if let Some(parent) = path.parent() {
2643 if !parent.as_os_str().is_empty() {
2644 vm.set_source_dir(parent);
2645 }
2646 }
2647 }
2648 let loaded = skill_loader::load_skills(&skill_loader::SkillLoaderInputs {
2652 cli_dirs: cli_skill_dirs.to_vec(),
2653 source_path: source_path.map(Path::to_path_buf),
2654 });
2655 skill_loader::emit_loader_warnings(&loaded.loader_warnings);
2656 skill_loader::install_skills_global(&mut vm, &loaded);
2657 let runtime_harness = match harness {
2658 Some(harness) => harness,
2659 None => {
2660 let resolved = match source_path {
2661 Some(path) => default_harness_for_manifest_or_base_dir(path, store_base),
2662 None => default_harness_for_base_dir(store_base),
2663 };
2664 resolved.map_err(|error| {
2665 ExecError::new(
2666 ExecStage::Runtime,
2667 format!("failed to configure harness secret provider: {error}"),
2668 )
2669 })
2670 }?,
2671 };
2672 vm.set_harness(runtime_harness);
2673 if let Some(path) = source_path {
2674 let extensions = package::load_runtime_extensions(path);
2675 package::install_runtime_extensions(&extensions);
2676 package::install_manifest_triggers(&mut vm, &extensions)
2677 .await
2678 .map_err(|error| {
2679 ExecError::new(
2680 ExecStage::Runtime,
2681 format!("failed to install manifest triggers: {error}"),
2682 )
2683 })?;
2684 package::install_manifest_hooks(&mut vm, &extensions)
2685 .await
2686 .map_err(|error| {
2687 ExecError::new(
2688 ExecStage::Runtime,
2689 format!("failed to install manifest hooks: {error}"),
2690 )
2691 })?;
2692 }
2693 let _event_log = harn_vm::event_log::active_event_log()
2694 .unwrap_or_else(|| harn_vm::event_log::install_memory_for_current_thread(64));
2695 let connector_clients_installed =
2696 should_install_default_connector_clients(source, source_path);
2697 if connector_clients_installed {
2698 install_default_connector_clients(store_base)
2699 .await
2700 .map_err(|error| {
2701 ExecError::new(
2702 ExecStage::Runtime,
2703 format!("failed to initialize connector clients: {error}"),
2704 )
2705 })?;
2706 }
2707 let execution_result = vm
2708 .execute(&chunk)
2709 .await
2710 .map_err(|e| ExecError::new(ExecStage::Runtime, e.to_string()));
2711 harn_vm::egress::reset_egress_policy_for_host();
2712 if connector_clients_installed {
2713 harn_vm::clear_active_connector_clients();
2714 }
2715 harn_vm::stdlib::process::set_thread_execution_context(None);
2716 execution_result?;
2717 let mut output = String::new();
2718 for wl in &warning_lines {
2719 output.push_str(wl);
2720 output.push('\n');
2721 }
2722 output.push_str(vm.output());
2723 Ok(output)
2724 })
2725 .await
2726}
2727
2728fn should_install_default_connector_clients(source: &str, source_path: Option<&Path>) -> bool {
2729 if !source_path.is_some_and(is_conformance_path) {
2730 return true;
2731 }
2732 source.contains("connector_call")
2733 || source.contains("std/connectors")
2734 || source.contains("connectors/")
2735}
2736
2737fn is_conformance_path(path: &Path) -> bool {
2738 path.components()
2739 .any(|component| component.as_os_str() == "conformance")
2740}
2741
2742async fn install_default_connector_clients(base_dir: &Path) -> Result<(), String> {
2743 let event_log = harn_vm::event_log::active_event_log()
2744 .unwrap_or_else(|| harn_vm::event_log::install_memory_for_current_thread(64));
2745 let secret_namespace = connector_secret_namespace(base_dir);
2746 let secrets: Arc<dyn harn_vm::secrets::SecretProvider> = Arc::new(
2747 harn_vm::secrets::configured_default_chain(secret_namespace)
2748 .map_err(|error| format!("failed to configure secret providers: {error}"))?,
2749 );
2750
2751 let registry = harn_vm::ConnectorRegistry::default();
2752 let metrics = Arc::new(harn_vm::MetricsRegistry::default());
2753 let inbox = Arc::new(
2754 harn_vm::InboxIndex::new(event_log.clone(), metrics.clone())
2755 .await
2756 .map_err(|error| error.to_string())?,
2757 );
2758 registry
2759 .init_all(harn_vm::ConnectorCtx {
2760 event_log,
2761 secrets,
2762 inbox,
2763 metrics,
2764 rate_limiter: Arc::new(harn_vm::RateLimiterFactory::default()),
2765 })
2766 .await
2767 .map_err(|error| error.to_string())?;
2768 let clients = registry.client_map().await;
2769 harn_vm::install_active_connector_clients(clients);
2770 Ok(())
2771}
2772
2773fn connector_secret_namespace(base_dir: &Path) -> String {
2774 match std::env::var("HARN_SECRET_NAMESPACE") {
2775 Ok(namespace) if !namespace.trim().is_empty() => namespace,
2776 _ => {
2777 let leaf = base_dir
2778 .file_name()
2779 .and_then(|name| name.to_str())
2780 .filter(|name| !name.is_empty())
2781 .unwrap_or("workspace");
2782 format!("harn/{leaf}")
2783 }
2784 }
2785}
2786
2787pub(crate) fn default_harness_for_base_dir(base_dir: &Path) -> Result<harn_vm::Harness, String> {
2788 let secret_namespace = connector_secret_namespace(base_dir);
2789 default_harness_for_secret_namespace(secret_namespace)
2790}
2791
2792pub(crate) fn default_harness_for_manifest_or_base_dir(
2793 source_path: &Path,
2794 base_dir: &Path,
2795) -> Result<harn_vm::Harness, String> {
2796 let secret_namespace = package::find_nearest_manifest(source_path)
2797 .map(|(_, manifest_dir)| connector_secret_namespace(&manifest_dir))
2798 .unwrap_or_else(|| connector_secret_namespace(base_dir));
2799 default_harness_for_secret_namespace(secret_namespace)
2800}
2801
2802fn default_harness_for_secret_namespace(
2803 secret_namespace: String,
2804) -> Result<harn_vm::Harness, String> {
2805 let secret_provider = Arc::new(
2806 harn_vm::secrets::configured_default_chain(secret_namespace)
2807 .map_err(|error| error.to_string())?,
2808 );
2809 Ok(harn_vm::Harness::real().with_secret_provider(secret_provider))
2810}
2811
2812#[cfg(test)]
2813mod main_tests {
2814 use super::{
2815 normalize_serve_args, serve_subcommand_names, should_install_default_connector_clients,
2816 };
2817 use std::path::Path;
2818
2819 #[test]
2820 fn normalize_serve_args_inserts_a2a_for_legacy_shape() {
2821 let args = normalize_serve_args(vec![
2822 "harn".to_string(),
2823 "serve".to_string(),
2824 "--port".to_string(),
2825 "3000".to_string(),
2826 "agent.harn".to_string(),
2827 ]);
2828 assert_eq!(
2829 args,
2830 vec![
2831 "harn".to_string(),
2832 "serve".to_string(),
2833 "a2a".to_string(),
2834 "--port".to_string(),
2835 "3000".to_string(),
2836 "agent.harn".to_string(),
2837 ]
2838 );
2839 }
2840
2841 #[test]
2842 fn normalize_serve_args_preserves_explicit_subcommands() {
2843 for transport in serve_subcommand_names() {
2848 let args = normalize_serve_args(vec![
2849 "harn".to_string(),
2850 "serve".to_string(),
2851 transport.clone(),
2852 "server.harn".to_string(),
2853 ]);
2854 assert_eq!(
2855 args,
2856 vec![
2857 "harn".to_string(),
2858 "serve".to_string(),
2859 transport.clone(),
2860 "server.harn".to_string(),
2861 ],
2862 "transport `{transport}` should not be rewritten",
2863 );
2864 }
2865 }
2866
2867 #[test]
2868 fn normalize_serve_args_recognizes_site_subcommand() {
2869 let args = normalize_serve_args(vec![
2870 "harn".to_string(),
2871 "serve".to_string(),
2872 "site".to_string(),
2873 "server.harn".to_string(),
2874 ]);
2875 assert_eq!(args.get(2).map(String::as_str), Some("site"));
2876 }
2877
2878 #[test]
2879 fn conformance_skips_connector_clients_unless_fixture_uses_connectors() {
2880 let path = Path::new("conformance/tests/language/basic.harn");
2881 assert!(!should_install_default_connector_clients(
2882 "__io_println(1)",
2883 Some(path)
2884 ));
2885 assert!(!should_install_default_connector_clients(
2886 "trust_graph_verify_chain()",
2887 Some(path)
2888 ));
2889 assert!(should_install_default_connector_clients(
2890 "import { post_message } from \"std/connectors/slack\"",
2891 Some(path)
2892 ));
2893 assert!(should_install_default_connector_clients(
2894 "__io_println(1)",
2895 Some(Path::new("examples/demo.harn"))
2896 ));
2897 }
2898}