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