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