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