Skip to main content

harn_vm/
lib.rs

1#![recursion_limit = "256"]
2#![allow(clippy::result_large_err, clippy::cloned_ref_to_slice_refs)]
3//! # harn-vm
4//!
5//! The Harn compiler, virtual machine, standard library, provider/LLM layer,
6//! orchestration runtime, and host bridge.
7//!
8//! ## Stability
9//!
10//! This crate is consumed both by the in-tree surfaces (`harn-cli`,
11//! `harn-serve`, the LSP and DAP) and by external embedders. The intended
12//! embedding entry points are `Vm`, `Harness`, `compile_source`, and the
13//! `llm`, `orchestration`, `agent_events`, `agent_sessions`, `config`, and
14//! `security` modules. Other public items exist primarily for in-workspace use
15//! and may change between minor releases; anything marked `#[doc(hidden)]` is
16//! an implementation detail with no stability guarantee. The crate follows the
17//! workspace version and is pre-1.0, so the public surface may still evolve.
18
19/// Re-export of the unified clock substrate so downstream crates (CLI,
20/// orchestrator, and cloud runtimes) can depend on a single canonical `Clock`
21/// trait without each adding `harn-clock` as a direct dependency.
22pub use harn_clock as clock;
23
24pub mod a2a;
25pub mod actor_chain;
26pub mod agent_events;
27pub mod agent_sessions;
28pub mod agent_transcript_budget;
29pub mod atomic_io;
30pub mod autonomy;
31pub(crate) mod aws_sigv4;
32pub mod bridge;
33mod builtin_id;
34pub mod bytecode_cache;
35pub mod call_budget;
36pub mod channel_guardrails;
37pub mod channels;
38pub mod checkpoint;
39mod chunk;
40mod compiler;
41pub mod composition;
42pub mod config;
43pub mod connectors;
44pub mod corrections;
45pub mod coverage;
46pub(crate) mod durable_rate_limit;
47pub(crate) mod duration_parse;
48pub mod egress;
49pub mod event_log;
50pub mod events;
51pub mod external_agent;
52pub mod flow;
53pub mod harness;
54pub mod harness_auth;
55pub(crate) mod harness_crypto;
56pub mod harness_net;
57pub mod harness_system;
58pub mod harness_tenant;
59pub mod host_attachments;
60mod http;
61pub mod jsonrpc;
62pub(crate) mod limits;
63pub mod llm;
64pub mod llm_config;
65pub mod mcp;
66pub mod mcp_allowlist;
67pub mod mcp_auth;
68pub mod mcp_bulk_auth;
69pub mod mcp_card;
70pub mod mcp_client_request;
71pub mod mcp_client_roots;
72pub mod mcp_elicit;
73pub mod mcp_file_upload;
74pub mod mcp_host;
75pub mod mcp_identity;
76pub mod mcp_json_discovery;
77pub mod mcp_oauth;
78pub mod mcp_presets;
79pub mod mcp_progress;
80pub mod mcp_protocol;
81pub mod mcp_registry;
82pub mod mcp_sampling;
83pub mod mcp_server;
84pub mod metadata;
85pub mod module_artifact;
86pub mod observability;
87pub mod op_interrupt;
88pub mod orchestration;
89mod persistent_state;
90pub mod personas;
91mod prepared_module;
92pub mod process_sandbox;
93pub mod profile;
94pub mod provenance;
95pub mod provider_catalog;
96pub mod receipts;
97pub mod record_filter;
98pub mod redact;
99pub mod run_events;
100pub mod runtime_context;
101pub(crate) mod runtime_guards;
102pub mod runtime_limits;
103pub mod runtime_paths;
104pub(crate) mod runtime_sqlite;
105pub mod schema;
106pub(crate) mod secret_patterns;
107pub mod secrets;
108pub mod security;
109pub mod session_bundle;
110pub mod session_timeline;
111pub mod sessions;
112pub(crate) mod shared_state;
113pub mod shells;
114pub mod skills;
115pub mod stdlib;
116pub mod stdlib_modules;
117pub mod step_runtime;
118pub mod store;
119pub(crate) mod synchronization;
120pub mod tenant;
121pub(crate) mod term;
122pub mod testbench;
123pub mod tool_annotations;
124pub mod tool_call_cancellations;
125pub mod tool_surface;
126pub mod tracing;
127pub mod triggers;
128pub mod trust_graph;
129pub(crate) mod url_encoding;
130pub mod user_dirs;
131
132/// Crate-wide deterministic clock mock used by stdlib time builtins, the
133/// trigger dispatcher, the cron scheduler, and Rust-side tests. Re-exports
134/// the long-lived implementation under `triggers::test_util::clock` so all
135/// callers go through one source of truth.
136pub mod clock_mock {
137    pub use crate::triggers::test_util::clock::{
138        active_mock_clock, advance, clear_overrides, install_override, instant_now, is_mocked,
139        now_ms, now_utc, sleep, ClockInstant, ClockOverrideGuard, MockClock,
140    };
141
142    /// Runtime audit for capabilities that observe real wall-clock or
143    /// monotonic time while a testbench mock is installed. See the module
144    /// docs for the full design.
145    pub mod leak_audit {
146        pub use crate::triggers::test_util::clock_leak::{
147            drain, enter_scope, install_scope, instant_now, reset, snapshot, wall_now, ClockLeak,
148            ClockLeakScope, ClockLeakScopeGuard,
149        };
150    }
151}
152
153pub mod typecheck;
154pub mod value;
155pub mod verification;
156pub mod visible_text;
157mod vm;
158pub(crate) mod wait_for_graph;
159pub mod waitpoints;
160pub mod workspace_anchor;
161pub mod workspace_path;
162
163pub use persistent_state::{register_persistent_state_builtins_at_root, PersistentStateRoot};
164pub use prepared_module::{PreparedModuleCache, PreparedModuleCacheStats};
165
166pub use actor_chain::{
167    ActorChain, ActorChainEntry, ActorChainError, Principal, ScopeAttenuationMode,
168    ScopeAttenuationPolicy, ScopeAttenuationViolation,
169};
170pub use builtin_id::BuiltinId;
171pub use call_budget::{
172    charge_mcp_call, charge_pg_query, install_mcp_call_budget, install_pg_query_budget,
173    McpCallBudgetGuard, PgQueryBudgetGuard,
174};
175pub use checkpoint::register_checkpoint_builtins;
176pub use chunk::*;
177pub use compiler::*;
178pub use connectors::{
179    active_connector_client, active_metrics_registry, clear_active_connector_clients,
180    clear_active_metrics_registry, connector_export_denied_builtin_reason,
181    connector_export_effect_class,
182    cron::{CatchupMode, CronConnector},
183    default_connector_export_policy,
184    harn_module::{
185        load_contract as load_harn_connector_contract, HarnConnector, HarnConnectorContract,
186    },
187    hmac::{verify_hmac_signed, SIGNATURE_VERIFY_AUDIT_TOPIC},
188    install_active_connector_clients, install_active_metrics_registry,
189    postprocess_normalized_event, ActivationHandle, ClientError, Connector, ConnectorClient,
190    ConnectorCtx, ConnectorError, ConnectorExportEffectClass, ConnectorHttpResponse,
191    ConnectorMetricsSnapshot, ConnectorNormalizeResult, ConnectorRegistry, GenericWebhookConnector,
192    HarnConnectorEffectPolicies, MetricsRegistry, PostNormalizeOutcome, ProviderPayloadSchema,
193    RateLimitConfig, RateLimiterFactory, RawInbound, StreamConnector, TriggerBinding, TriggerKind,
194    TriggerRegistry, WebhookSignatureVariant,
195};
196pub use corrections::{
197    append_correction_record, apply_corrections_to_policy, correction_query_filters_from_json,
198    correction_record_from_json, policy_with_corrections, query_correction_records,
199    CorrectionQueryFilters, CorrectionRecord, CorrectionScope, CORRECTIONS_TOPIC,
200    CORRECTION_EVENT_KIND, CORRECTION_SCHEMA_V0,
201};
202pub use harness::{
203    DenyEvent, Harness, HarnessCall, HarnessClock, HarnessCrypto, HarnessEnv, HarnessFs,
204    HarnessKind, HarnessLlm, HarnessNet, HarnessObs, HarnessProcess, HarnessRandom, HarnessSecrets,
205    HarnessStdio, HarnessSystem, HarnessTenant, HarnessTerm, MockAwareClock, MockHarnessBuilder,
206    VmHarness,
207};
208pub use harness_auth::{
209    current_auth_principal, enter_auth_principal, AuthPrincipal, AuthPrincipalScopeGuard,
210    MISSING_PRINCIPAL_MESSAGE,
211};
212pub use harness_net::{
213    bypass_enabled as net_policy_bypass_enabled, NetMatcher, NetPolicy, NetPolicyAudit,
214    NetPolicyDecision, NetPolicyDefault, NetPolicyRule, OnViolation, HARN_NET_POLICY_BYPASS_ENV,
215    NET_POLICY_AUDIT_TOPIC,
216};
217pub use harness_tenant::{
218    current_tenant_id, enter_tenant, TenantScopeGuard, MISSING_TENANT_MESSAGE,
219};
220pub use http::{register_http_builtins, reset_http_state};
221pub use llm::register_llm_builtins;
222pub use llm::trigger_predicate::TriggerPredicateBudget;
223pub use llm::{
224    current_agent_session_id, install_llm_cost_budget, install_llm_token_budget,
225    peek_llm_cost_budget, peek_llm_token_budget, register_session_end_hook, set_llm_cost_budget,
226    set_llm_token_budget, LlmBudgetGuard, LlmTokenBudgetGuard, SessionEndHookRegistration,
227};
228pub use mcp::{connect_mcp_server_from_json, connect_mcp_server_from_spec, register_mcp_builtins};
229pub use mcp_allowlist::{
230    build_catalog as build_mcp_catalog, catalog_for_request as mcp_catalog_for_request,
231    AdvertisedItem as McpAdvertisedItem, CatalogRequest as McpCatalogRequest, McpAllowlist,
232    McpAllowlistItem, McpCatalog, McpCatalogItem, McpCatalogServer, McpItemKind,
233    MCP_ALLOWLIST_SCHEMA_VERSION,
234};
235pub use mcp_card::{fetch_server_card, load_server_card_from_path, CardError};
236pub use mcp_host::{
237    cache_stats as mcp_host_cache_stats, set_allowlist as set_mcp_host_allowlist,
238    AllowlistDecision as McpHostAllowlistDecision, AllowlistGuard as McpHostAllowlistGuard,
239    BreakerState as McpHostBreakerState, CacheStats as McpHostCacheStats, McpHostStatus,
240    SpawnOptions as McpHostSpawnOptions, SupervisionPolicy as McpHostSupervisionPolicy,
241};
242pub use mcp_registry::{
243    active_handle as mcp_active_handle, ensure_active as mcp_ensure_active,
244    get_registration as mcp_get_registration, install_active as mcp_install_active,
245    is_registered as mcp_is_registered, register_servers as mcp_register_servers,
246    release as mcp_release, reset as mcp_reset_registry, snapshot_status as mcp_snapshot_status,
247    sweep_expired as mcp_sweep_expired, RegisteredMcpServer, RegistryStatus,
248};
249pub use mcp_server::{
250    take_mcp_serve_metadata, take_mcp_serve_prompts, take_mcp_serve_registry,
251    take_mcp_serve_resource_templates, take_mcp_serve_resources, tool_registry_to_mcp_tools,
252    McpServer, McpServerMetadata,
253};
254pub use metadata::register_metadata_builtins;
255pub use observability::audit::{audit_events as audit_obs_events, AuditFinding, AuditFindingKind};
256pub use observability::request_id::{current_request_id, enter_request_id, RequestIdScopeGuard};
257pub use orchestration::{
258    benchmark_adapted_replay_pair, benchmark_replay_trace, build_replay_benchmark_report,
259    OpenCodeJsonlAdapter, ReplayBenchmarkCloudIngest, ReplayBenchmarkError,
260    ReplayBenchmarkFixtureReceipt, ReplayBenchmarkFixtureReport, ReplayBenchmarkMetrics,
261    ReplayBenchmarkReport, ReplayBenchmarkSuiteIdentity, ReplayBenchmarkSummary,
262    ReplayCategoryMetric, ReplayDebuggingProxyMetrics, ReplayRuntimeCostMetrics,
263    ReplayTraceAdapter, OPENCODE_JSONL_ADAPTER_ID, OPENCODE_JSONL_ADAPTER_SCHEMA_VERSION,
264    REPLAY_BENCHMARK_CLOUD_INGEST_KIND, REPLAY_BENCHMARK_REPORT_SCHEMA_VERSION,
265};
266pub use orchestration::{
267    canonicalize_run, first_divergence, run_replay_oracle_trace, ReplayAllowlistRule,
268    ReplayDivergence, ReplayExpectation, ReplayOracleError, ReplayOracleReport, ReplayOracleTrace,
269    ReplayTraceRun, ReplayTraceRunCounts, REPLAY_TRACE_SCHEMA_VERSION,
270};
271pub use orchestration::{
272    install_handoff_routes, snapshot_handoff_routes, HandoffRouteConfig,
273    HandoffRouteDecisionRecord, HandoffRouteTargetConfig,
274};
275pub use personas::{
276    disable_persona, fire_schedule as fire_persona_schedule, fire_trigger as fire_persona_trigger,
277    format_ms as format_persona_ms, now_ms as persona_now_ms, parse_rfc3339_ms as parse_persona_ms,
278    pause_persona, persona_status, record_persona_spend, register_persona_supervision_sink,
279    register_persona_value_sink, report_repair_worker_status, restore_persona_checkpoint,
280    resume_persona, PersonaAssignmentStatus, PersonaBudgetPolicy, PersonaBudgetStatus,
281    PersonaCheckpointAction, PersonaCheckpointRestoreOutcome, PersonaCheckpointRestoreRequest,
282    PersonaCheckpointResume, PersonaCheckpointUpdate, PersonaHandoffInboxItem, PersonaLease,
283    PersonaLifecycleState, PersonaQueuePositionUpdate, PersonaQueuedWork, PersonaReceiptUpdate,
284    PersonaRepairWorkerLifecycle, PersonaRepairWorkerStatusUpdate, PersonaRunCost,
285    PersonaRunReceipt, PersonaRuntimeBinding, PersonaStatus, PersonaSupervisionEvent,
286    PersonaSupervisionSink, PersonaSupervisionSinkRegistration, PersonaTriggerEnvelope,
287    PersonaValueEvent, PersonaValueEventKind, PersonaValueReceipt, PersonaValueSink,
288    PersonaValueSinkRegistration, StageDecl, StageExit, PERSONA_RUNTIME_TOPIC,
289};
290pub use provenance::{
291    build_signed_receipt, load_or_generate_agent_signing_key, verify_receipt, ProvenanceReceipt,
292    ReceiptBuildOptions, ReceiptVerificationReport,
293};
294pub use receipts::{
295    Receipt, ReceiptSink, ReceiptStatus, ReceiptValidationError, RedactingReceiptSink,
296    RedactionClass, RECEIPT_SCHEMA_ID, RECEIPT_SCHEMA_JSON, RECEIPT_SCHEMA_VERSION,
297};
298pub use record_filter::{normalize_record_filter_expression, CompiledRecordFilter};
299pub use runtime_limits::{
300    RuntimeLimitDescription, RuntimeLimitEntry, RuntimeLimits, RuntimeLimitsReport,
301    RUNTIME_LIMIT_DESCRIPTIONS,
302};
303pub use schema::json_to_vm_value;
304pub use sessions::{
305    CreateSession, ExpireSession, Session, SessionAttributes, SessionError, SessionStore,
306    TouchSession, SESSIONS_TOPIC,
307};
308pub use stdlib::hitl::{
309    append_hitl_response, ApprovalRequest, HitlHostResponse, HITL_APPROVALS_TOPIC,
310    HITL_DUAL_CONTROL_TOPIC, HITL_ESCALATIONS_TOPIC, HITL_QUESTIONS_TOPIC,
311};
312pub use stdlib::host::{clear_host_call_bridge, set_host_call_bridge, HostCallBridge};
313pub use stdlib::http_response::{
314    parse_envelope as parse_http_envelope, HttpEnvelope, HttpHeaderValue, WsUpgradeSpec,
315    HTTP_RESPONSE_TAG_KEY, HTTP_RESPONSE_TAG_VERSION,
316};
317#[cfg(feature = "postgres")]
318pub use stdlib::install_shared_pool_registry;
319pub use stdlib::io::{
320    reserve_stdio_for_current_thread, set_stdout_passthrough, take_stderr_buffer,
321    StdioReservationGuard,
322};
323pub use stdlib::long_running::cancel_handle as cancel_long_running_handle;
324pub use stdlib::observability::install_default_backend as install_obs_default_backend;
325pub use stdlib::secret_scan::{
326    append_secret_scan_audit, audit_secret_scan_active, scan_content as secret_scan_content,
327    SecretFinding, SECRET_SCAN_AUDIT_TOPIC,
328};
329pub use stdlib::template::{
330    lookup_prompt_consumers, lookup_prompt_span, prompt_render_indices, record_prompt_render_index,
331    PromptSourceSpan, PromptSpanKind,
332};
333pub use stdlib::waitpoint::{
334    process_waitpoint_resume_event, service_waitpoints_once, WAITPOINT_RESUME_TOPIC,
335};
336pub use stdlib::workflow_messages::{
337    workflow_pause_for_base, workflow_publish_query_for_base, workflow_query_for_base,
338    workflow_respond_update_for_base, workflow_resume_for_base, workflow_signal_for_base,
339    workflow_update_for_base, WorkflowMailboxState,
340};
341pub use stdlib::{
342    register_agent_stdlib, register_core_stdlib, register_io_stdlib, register_vm_stdlib,
343};
344pub use store::register_store_builtins;
345pub use tenant::{
346    tenant_event_topic_prefix, tenant_secret_namespace, tenant_topic, validate_tenant_id, ApiKeyId,
347    TenantApiKeyRecord, TenantBudget, TenantEventLog, TenantRecord, TenantRegistrySnapshot,
348    TenantResolutionError, TenantScope, TenantSecretProvider, TenantStatus, TenantStore,
349    TENANT_EVENT_TOPIC_PREFIX, TENANT_REGISTRY_DIR, TENANT_REGISTRY_FILE,
350    TENANT_SECRET_NAMESPACE_PREFIX,
351};
352pub use triggers::{
353    append_dispatch_cancel_request, begin_in_flight, binding_autonomy_budget_would_exceed,
354    binding_budget_would_exceed, binding_version_as_of, classify_trigger_dlq_error,
355    clear_dispatcher_state, clear_orchestrator_budget, clear_trigger_registry, drain,
356    dynamic_deregister, dynamic_register, expected_predicate_cost_usd_micros, finish_in_flight,
357    install_manifest_triggers, install_orchestrator_budget, install_provider_catalog,
358    micros_to_usd, note_autonomous_decision, note_orchestrator_budget_cost,
359    orchestrator_budget_would_exceed, parse_flow_control_duration, pause, pin_trigger_binding,
360    provider_metadata, record_predicate_cost_sample, redact_headers, register_provider_schema,
361    registered_provider_metadata, registered_provider_schema_names, reset_binding_budget_windows,
362    reset_provider_catalog, reset_provider_catalog_with, resolve_live_or_as_of,
363    resolve_live_trigger_binding, resolve_trigger_binding_as_of, resume,
364    run_trigger_harness_fixture, scheduler_in_flight_by_key, scheduler_ready_stats_by_key,
365    snapshot_dispatcher_stats, snapshot_orchestrator_budget, snapshot_trigger_bindings,
366    unpin_trigger_binding, usd_to_micros, worker_claims_topic_name, worker_job_topic_name,
367    worker_response_topic_name, ClaimedWorkerJob, DispatchCancelRequest, DispatchError,
368    DispatchOutcome, DispatchStatus, Dispatcher, DispatcherDrainReport, DispatcherStatsSnapshot,
369    FairnessKey, HeaderRedactionPolicy, InboxIndex, NotionPolledChangeEvent,
370    OrchestratorBudgetConfig, OrchestratorBudgetSnapshot, ProviderCatalog, ProviderCatalogError,
371    ProviderId, ProviderMetadata, ProviderOutboundMethod, ProviderPayload, ProviderRuntimeMetadata,
372    ProviderSchema, ProviderSecretRequirement, ReadyKeyStats, RecordedTriggerBinding, RetryPolicy,
373    SchedulableJob, SchedulerKeyStat, SchedulerPolicy, SchedulerSnapshot, SchedulerState,
374    SchedulerStrategy, SignatureStatus, SignatureVerificationMetadata, StreamEventPayload,
375    TenantId, TraceId, TriggerBatchConfig, TriggerBindingSnapshot, TriggerBindingSource,
376    TriggerBindingSpec, TriggerBudgetExhaustionStrategy, TriggerConcurrencyConfig,
377    TriggerDebounceConfig, TriggerDispatchOutcome, TriggerEvent, TriggerEventId,
378    TriggerExpressionSpec, TriggerFlowControlConfig, TriggerHandlerSpec, TriggerHarnessResult,
379    TriggerId, TriggerMetricsSnapshot, TriggerPredicateSpec, TriggerPriorityOrderConfig,
380    TriggerRateLimitConfig, TriggerRegistryError, TriggerRetryConfig, TriggerSingletonConfig,
381    TriggerState, TriggerThrottleConfig, WorkerQueue, WorkerQueueClaimHandle,
382    WorkerQueueEnqueueReceipt, WorkerQueueInspectSnapshot, WorkerQueueJob, WorkerQueueJobState,
383    WorkerQueuePriority, WorkerQueueResponseRecord, WorkerQueueState, WorkerQueueSummary,
384    DEFAULT_INBOX_RETENTION_DAYS, DEFAULT_STARVATION_AGE_MS, TRIGGERS_LIFECYCLE_TOPIC,
385    TRIGGER_ATTEMPTS_TOPIC, TRIGGER_CANCEL_REQUESTS_TOPIC, TRIGGER_DLQ_TOPIC,
386    TRIGGER_INBOX_CLAIMS_TOPIC, TRIGGER_INBOX_ENVELOPES_TOPIC, TRIGGER_INBOX_LEGACY_TOPIC,
387    TRIGGER_INBOX_OBSERVABILITY_TOPIC, TRIGGER_OPERATION_AUDIT_TOPIC, TRIGGER_OUTBOX_TOPIC,
388    TRIGGER_TEST_FIXTURES, WORKER_QUEUE_CATALOG_TOPIC,
389};
390pub use trust_graph::{
391    append_active_scope_attenuation_alert, append_active_trust_record,
392    append_scope_attenuation_alert, append_trust_record, export_trust_chain,
393    group_trust_records_by_trace, policy_for_agent, policy_for_autonomy_tier,
394    query_trust_graph_records, query_trust_records, resolve_agent_autonomy_tier,
395    summarize_trust_records, topic_for_agent, trust_score_for, verify_trust_chain, AutonomyTier,
396    TrustAgentSummary, TrustChainExport, TrustChainExportMetadata, TrustChainExportProducer,
397    TrustChainReport, TrustGraphRecord, TrustOutcome, TrustQueryFilters, TrustRecord,
398    TrustRecordActionKind, TrustScore, TrustTraceGroup, METADATA_KEY_ACTOR_CHAIN,
399    METADATA_KEY_ACTOR_CHAIN_ALERT, METADATA_KEY_EFFECTS_GRANT, METADATA_KEY_EFFECTS_USED,
400    METADATA_KEY_PARENT_RECORD_ID, OPENTRUSTGRAPH_ACCEPTED_SCHEMAS, OPENTRUSTGRAPH_CHAIN_SCHEMA_V0,
401    OPENTRUSTGRAPH_SCHEMA_V0, OPENTRUSTGRAPH_SCHEMA_V0_1, TRUST_ACTION_RELEASE,
402    TRUST_GRAPH_GLOBAL_TOPIC, TRUST_GRAPH_LEGACY_GLOBAL_TOPIC, TRUST_GRAPH_LEGACY_TOPIC_PREFIX,
403    TRUST_GRAPH_RECORDS_TOPIC, TRUST_GRAPH_TOPIC_PREFIX,
404};
405pub use value::*;
406pub use vm::*;
407
408#[cfg(feature = "vm-bench-internals")]
409#[doc(hidden)]
410pub mod bench_internals;
411
412/// Lex, parse, type-check, and compile source to bytecode in one call.
413/// Bails on the first type error. For callers that need diagnostics
414/// rather than early exit, use `harn_parser::check_source` directly
415/// and then call `Compiler::new().compile(&program)`.
416pub fn compile_source(source: &str) -> Result<Chunk, String> {
417    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
418    Compiler::new().compile(&program).map_err(|e| e.to_string())
419}
420
421/// Same as [`compile_source`] but compiles a specific named pipeline as
422/// the program entry point instead of the default-pipeline-or-first
423/// selection rule. Returns a runtime error when no pipeline with
424/// `pipeline_name` exists in the source.
425pub fn compile_source_named(source: &str, pipeline_name: &str) -> Result<Chunk, String> {
426    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
427    let has_pipeline = program.iter().any(|sn| {
428        let (_, inner) = harn_parser::peel_attributes(sn);
429        matches!(&inner.node, harn_parser::Node::Pipeline { name, .. } if name == pipeline_name)
430    });
431    if !has_pipeline {
432        return Err(format!("no pipeline named `{pipeline_name}` in source"));
433    }
434    Compiler::new()
435        .compile_named(&program, pipeline_name)
436        .map_err(|e| e.to_string())
437}
438
439/// Lowers Harn `TypeExpr`s to JSON Schema with `type`-alias EXPANSION, built once
440/// from a parsed program's alias declarations. Without expansion, a tool parameter
441/// typed as a user alias (`p: EvalSource`, `p: FunnelStage`) erases to an empty
442/// `{}` schema because the low-level lowering only recognizes built-in type names —
443/// the exporter must first resolve the alias to its underlying shape/union (a
444/// literal-union alias then round-trips as a JSON `enum`). Cycle-safe via the same
445/// `expand_alias` guard the compiler and typechecker share.
446pub struct SchemaAliasResolver {
447    compiler: compiler::Compiler,
448}
449
450impl SchemaAliasResolver {
451    /// A resolver with no aliases in scope — lowering is identical to the raw
452    /// (unexpanded) form, so `Named(alias)` still lowers to `{}` when unknown.
453    pub fn empty() -> Self {
454        Self {
455            compiler: compiler::Compiler::new(),
456        }
457    }
458
459    /// Collect every `type` alias declared in `program`, so named references in
460    /// tool signatures resolve to their bodies.
461    pub fn from_program(program: &[harn_parser::SNode]) -> Self {
462        let mut compiler = compiler::Compiler::new();
463        compiler.collect_type_aliases(program);
464        Self { compiler }
465    }
466
467    /// JSON Schema for one `TypeExpr`, expanding any named alias first. `None`
468    /// when the (expanded) type has no JSON-Schema form (function types, ...).
469    pub fn json_schema_for_type_expr(
470        &self,
471        type_expr: &harn_parser::TypeExpr,
472    ) -> Option<serde_json::Value> {
473        let expanded = self.compiler.expand_alias(type_expr);
474        let schema = compiler::Compiler::type_expr_to_schema_value(&expanded)?;
475        let json_schema = schema::schema_to_json_schema_value(&schema).ok()?;
476        Some(llm::vm_value_to_json(&json_schema))
477    }
478
479    /// JSON Schema `object` for a parameter list (a served tool's `inputSchema`),
480    /// expanding aliases per parameter.
481    pub fn json_schema_for_typed_params(
482        &self,
483        params: &[harn_parser::TypedParam],
484    ) -> serde_json::Value {
485        let mut properties = serde_json::Map::new();
486        let mut required = Vec::new();
487
488        for param in params {
489            let param_schema = param
490                .type_expr
491                .as_ref()
492                .and_then(|type_expr| self.json_schema_for_type_expr(type_expr))
493                .unwrap_or_else(|| serde_json::json!({}));
494            if param.default_value.is_none() {
495                required.push(serde_json::Value::String(param.name.clone()));
496            }
497            properties.insert(param.name.clone(), param_schema);
498        }
499
500        let mut schema = serde_json::Map::new();
501        schema.insert(
502            "type".to_string(),
503            serde_json::Value::String("object".to_string()),
504        );
505        schema.insert(
506            "properties".to_string(),
507            serde_json::Value::Object(properties),
508        );
509        if !required.is_empty() {
510            schema.insert("required".to_string(), serde_json::Value::Array(required));
511        }
512        serde_json::Value::Object(schema)
513    }
514}
515
516/// Raw lowering with no program aliases in scope. Prefer
517/// [`SchemaAliasResolver::from_program`] when serving a module so named-alias
518/// parameters resolve instead of erasing to `{}`.
519pub fn json_schema_for_type_expr(type_expr: &harn_parser::TypeExpr) -> Option<serde_json::Value> {
520    SchemaAliasResolver::empty().json_schema_for_type_expr(type_expr)
521}
522
523pub fn json_schema_for_typed_params(params: &[harn_parser::TypedParam]) -> serde_json::Value {
524    SchemaAliasResolver::empty().json_schema_for_typed_params(params)
525}
526
527#[cfg(test)]
528mod schema_alias_resolver_tests {
529    use super::*;
530
531    fn fn_params_schema(src: &str) -> serde_json::Value {
532        let program = harn_parser::parse_source(src).expect("parse test source");
533        let resolver = SchemaAliasResolver::from_program(&program);
534        for node in &program {
535            let (_, inner) = harn_parser::peel_attributes(node);
536            if let harn_parser::Node::FnDecl { params, .. } = &inner.node {
537                return resolver.json_schema_for_typed_params(params);
538            }
539        }
540        panic!("no fn decl in test source");
541    }
542
543    #[test]
544    fn named_shape_alias_projects_like_inline_shape() {
545        let inline = fn_params_schema("pub fn f(p: {kind: string, path: string}) {}");
546        let aliased =
547            fn_params_schema("type Src = {kind: string, path: string}\npub fn f(p: Src) {}");
548        assert_eq!(
549            aliased, inline,
550            "a named shape alias must project the same inputSchema as its inline shape",
551        );
552        assert_ne!(
553            aliased["properties"]["p"],
554            serde_json::json!({}),
555            "the alias parameter must not erase to an empty schema",
556        );
557    }
558
559    #[test]
560    fn literal_union_alias_projects_json_enum() {
561        let schema = fn_params_schema("type Kind = \"local\" | \"ssh\"\npub fn f(p: Kind) {}");
562        let p = &schema["properties"]["p"];
563        assert_eq!(p["type"], "string");
564        assert_eq!(p["enum"], serde_json::json!(["local", "ssh"]));
565    }
566
567    #[test]
568    fn unknown_named_type_still_erases_to_empty() {
569        // No alias declared: unchanged behavior — an unknown named type lowers to {}.
570        let schema = fn_params_schema("pub fn f(p: Unknown) {}");
571        assert_eq!(schema["properties"]["p"], serde_json::json!({}));
572    }
573}
574
575fn reset_llm_state_for_thread_reset() {
576    llm::reset_llm_state();
577    #[cfg(test)]
578    reset_thread_local_state_test_hooks::before_llm_global_reset();
579    // This full wipe is necessary between Harn programs to clear durable
580    // cooldowns that would otherwise stall a later run under a paused clock.
581    llm::reset_rate_limit_registry();
582    llm_config::clear_user_overrides();
583    llm_config::clear_runtime_provider_endpoint_overrides();
584}
585
586#[cfg(test)]
587mod reset_thread_local_state_test_hooks {
588    use std::sync::{Arc, Mutex, OnceLock};
589
590    type Hook = Arc<dyn Fn() + Send + Sync + 'static>;
591
592    static BEFORE_LLM_GLOBAL_RESET: OnceLock<Mutex<Option<Hook>>> = OnceLock::new();
593
594    fn before_llm_global_reset_hook() -> &'static Mutex<Option<Hook>> {
595        BEFORE_LLM_GLOBAL_RESET.get_or_init(|| Mutex::new(None))
596    }
597
598    pub(crate) struct HookGuard;
599
600    impl Drop for HookGuard {
601        fn drop(&mut self) {
602            let mut hook = before_llm_global_reset_hook()
603                .lock()
604                .unwrap_or_else(std::sync::PoisonError::into_inner);
605            *hook = None;
606        }
607    }
608
609    pub(crate) fn install_before_llm_global_reset(hook: Hook) -> HookGuard {
610        let mut slot = before_llm_global_reset_hook()
611            .lock()
612            .unwrap_or_else(std::sync::PoisonError::into_inner);
613        *slot = Some(hook);
614        HookGuard
615    }
616
617    pub(crate) fn before_llm_global_reset() {
618        let hook = before_llm_global_reset_hook()
619            .lock()
620            .unwrap_or_else(std::sync::PoisonError::into_inner)
621            .clone();
622        if let Some(hook) = hook {
623            hook();
624        }
625    }
626}
627
628/// Reset all thread-local state that can leak between test runs.
629pub fn reset_thread_local_state() {
630    #[cfg(test)]
631    {
632        // `reset_thread_local_state` is also used by in-process unit tests. It
633        // clears process-global LLM config/rate-limit state, so share the same
634        // lock used by LLM env tests; otherwise a sibling reset can erase a
635        // parked rate-limit test's registry while the test still owns a permit.
636        let _guard = llm::env_guard();
637        reset_llm_state_for_thread_reset();
638    }
639    #[cfg(not(test))]
640    reset_llm_state_for_thread_reset();
641
642    http::reset_http_state();
643    channels::reset_channel_state();
644    event_log::reset_active_event_log();
645    egress::clear_explicit_egress_policy_requirement_for_host();
646    egress::clear_ssrf_guard_requirement_for_host();
647    stdlib::reset_stdlib_state();
648    connectors::clear_active_connector_clients();
649    orchestration::clear_runtime_hooks();
650    orchestration::clear_execution_policy_stacks();
651    orchestration::clear_command_policies();
652    orchestration::clear_pipeline_on_finish();
653    orchestration::reset_lifecycle_receipt_registry();
654    orchestration::agent_inbox::reset();
655    tool_call_cancellations::reset_registry();
656    redact::clear_policy_stack();
657    security::reset_thread_state();
658    triggers::clear_dispatcher_state();
659    triggers::clear_trigger_registry();
660    events::reset_event_sinks();
661    tracing::set_tracing_enabled(false);
662    tracing::reset_tracing();
663    agent_events::reset_all_sinks();
664    agent_sessions::reset_session_store();
665    mcp_registry::reset();
666    mcp_host::reset_for_tests();
667    call_budget::reset_call_budget_state();
668    clock_mock::leak_audit::reset();
669}
670
671#[cfg(test)]
672mod reset_leak_tests {
673    //! Regression coverage for harn#2660: process-/thread-global
674    //! registries that accumulated one entry per test because they were
675    //! never drained by `reset_thread_local_state`. Each case populates a
676    //! registry through its real entry point, runs the reset, and asserts
677    //! the registry is empty again.
678    use super::*;
679    use crate::value::VmValue;
680
681    #[test]
682    fn reset_drains_agent_inbox() {
683        orchestration::agent_inbox::reset();
684        orchestration::agent_inbox::push("sess-2660", "note", "leak", "test");
685        assert!(orchestration::agent_inbox::session_count() > 0);
686        reset_thread_local_state();
687        assert_eq!(
688            orchestration::agent_inbox::session_count(),
689            0,
690            "agent_inbox must be empty after reset"
691        );
692    }
693
694    #[test]
695    fn reset_drains_tool_call_cancellation_registry() {
696        tool_call_cancellations::reset_registry();
697        // Leak the guard so the entry survives until the reset runs —
698        // this mirrors a dispatch abandoned mid-flight.
699        let registered = tool_call_cancellations::register("sess-2660", "call-1", "tool");
700        if let Some((_handle, guard)) = registered {
701            std::mem::forget(guard);
702        }
703        assert!(tool_call_cancellations::registry_len() > 0);
704        reset_thread_local_state();
705        assert_eq!(
706            tool_call_cancellations::registry_len(),
707            0,
708            "tool-call cancellation registry must be empty after reset"
709        );
710    }
711
712    #[test]
713    fn reset_drains_routing_policy_registry() {
714        llm::routing::clear_policy_registry();
715        let mut config: crate::value::DictMap = crate::value::DictMap::new();
716        config.insert(
717            crate::value::intern_key("chain"),
718            VmValue::List(std::sync::Arc::new(vec![VmValue::String(
719                arcstr::ArcStr::from("mock:mock"),
720            )])),
721        );
722        llm::routing::build_routing_policy(&config).expect("intern a routing policy");
723        assert!(llm::routing::policy_registry_len() > 0);
724        reset_thread_local_state();
725        assert_eq!(
726            llm::routing::policy_registry_len(),
727            0,
728            "routing policy registry must be empty after reset"
729        );
730    }
731
732    #[test]
733    fn reset_holds_llm_env_guard_while_wiping_llm_globals() {
734        let observed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
735        let observed_hook = std::sync::Arc::clone(&observed);
736        let _hook = reset_thread_local_state_test_hooks::install_before_llm_global_reset(
737            std::sync::Arc::new(move || {
738                assert!(
739                    matches!(
740                        llm::env_lock().try_lock(),
741                        Err(std::sync::TryLockError::WouldBlock)
742                    ),
743                    "reset_thread_local_state must hold env_guard before wiping LLM globals"
744                );
745                observed_hook.store(true, std::sync::atomic::Ordering::SeqCst);
746            }),
747        );
748
749        reset_thread_local_state();
750        assert!(
751            observed.load(std::sync::atomic::Ordering::SeqCst),
752            "LLM global reset hook should have run"
753        );
754    }
755}