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