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