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