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