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