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