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