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