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