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