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};
46mod bounded_files;
47pub mod builtin_profile;
48pub mod bytecode_cache;
49pub mod call_budget;
50pub mod canonical_json;
51pub mod channel_guardrails;
52pub mod channels;
53pub mod checkpoint;
54mod chunk;
55mod compiler;
56pub mod composition;
57pub mod conditional_replace;
58pub mod config;
59pub mod connectors;
60pub mod context_manifest;
61pub mod corrections;
62pub mod coverage;
63pub(crate) mod durable_rate_limit;
64pub mod duration_parse;
65pub mod egress;
66pub mod environment_registry;
67pub mod event_log;
68pub mod events;
69pub mod external_agent;
70pub mod flight_recorder;
71pub mod flow;
72pub mod harness;
73pub mod harness_auth;
74pub(crate) mod harness_crypto;
75pub mod harness_net;
76pub mod harness_system;
77pub mod harness_tenant;
78pub mod host_attachments;
79
80/// Placement policy for child-interpreter subtasks.
81///
82/// Worker placement is the default. Embedders with a deliberately
83/// single-threaded host may scope an execution tree to current-thread
84/// placement explicitly.
85pub mod subtask {
86    pub use crate::vm::subtask::{
87        placement, scope_placement, SubtaskPlacement, SubtaskPlacementParseError, PLACEMENT_ENV,
88        PLACEMENT_VALUES,
89    };
90}
91mod http;
92pub mod jsonrpc;
93pub(crate) mod limits;
94pub mod linked_program;
95pub mod llm;
96pub mod llm_config;
97pub mod local_selection;
98pub mod mcp;
99pub mod mcp_allowlist;
100pub mod mcp_auth;
101pub mod mcp_bulk_auth;
102pub mod mcp_card;
103pub mod mcp_client_roots;
104pub mod mcp_elicit;
105pub mod mcp_host;
106pub mod mcp_identity;
107pub mod mcp_input;
108pub mod mcp_json_discovery;
109pub mod mcp_oauth;
110pub mod mcp_presets;
111pub mod mcp_progress;
112pub mod mcp_protocol;
113pub mod mcp_registry;
114pub mod mcp_sampling;
115pub mod mcp_server;
116pub mod mcp_tasks;
117pub mod metadata;
118pub mod module_artifact;
119pub mod module_source;
120pub mod observability;
121pub mod op_interrupt;
122pub mod orchestration;
123pub mod runtime_content;
124pub use runtime_content::{
125    runtime_content_fingerprint, RuntimeBuildFeatures, RuntimeCompatibilityFingerprint,
126    RuntimeContentFingerprint,
127};
128mod persistent_state;
129pub mod personas;
130pub mod portable;
131mod prepared_module;
132pub mod prepared_run;
133pub mod process_sandbox;
134pub mod profile;
135pub mod provenance;
136pub mod provider_catalog;
137pub mod receipts;
138pub mod record_filter;
139pub mod redact;
140pub mod run_events;
141pub mod runtime_context;
142pub(crate) mod runtime_guards;
143pub mod runtime_limits;
144pub mod runtime_paths;
145pub(crate) mod runtime_sqlite;
146pub mod schema;
147pub(crate) mod secret_patterns;
148pub mod secrets;
149pub mod security;
150pub mod session_bundle;
151pub mod session_recap;
152pub mod session_timeline;
153pub mod sessions;
154pub(crate) mod shared_state;
155pub mod shells;
156pub mod skills;
157pub mod stdlib;
158/// Session-metadata change notification for surfaces that project a session.
159pub use stdlib::session_change::{
160    subscribe as subscribe_session_changes, SessionChangeSubscription,
161};
162pub use stdlib::session_store::open_canonical_store;
163pub mod stdlib_modules;
164pub mod step_runtime;
165pub mod store;
166pub(crate) mod synchronization;
167pub mod tenant;
168pub(crate) mod term;
169pub(crate) mod test_env;
170pub mod testbench;
171pub mod text;
172pub mod text_diff;
173pub mod tool_annotations;
174pub mod tool_call_cancellations;
175pub mod tool_registry;
176pub mod tool_surface;
177pub mod tracing;
178pub mod triggers;
179pub mod trust_graph;
180pub(crate) mod url_encoding;
181pub mod user_dirs;
182
183/// Initialize process-wide assets whose construction should happen before an
184/// embedding host enters an async request or VM execution stack.
185///
186/// New embedding hosts should call [`initialize_runtime`] instead so startup
187/// also validates the Harn-owned environment namespace. This asset-only
188/// operation remains for compatibility, and VM construction retains it as a
189/// fallback for embedders without an explicit bootstrap phase.
190pub fn initialize_runtime_assets() {
191    secret_patterns::initialize_default_secret_patterns();
192}
193
194/// A startup condition that must stop the process before any work begins.
195#[derive(Debug)]
196pub enum RuntimeInitError {
197    /// The Harn-owned environment namespace holds an unknown or malformed key.
198    Environment(environment_registry::EnvironmentValidationError),
199    /// A configured cache directory cannot be honored.
200    CacheDir(bytecode_cache::CacheDirError),
201}
202
203impl std::fmt::Display for RuntimeInitError {
204    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        match self {
206            Self::Environment(error) => error.fmt(formatter),
207            Self::CacheDir(error) => error.fmt(formatter),
208        }
209    }
210}
211
212impl std::error::Error for RuntimeInitError {}
213
214/// Validate the Harn-owned environment namespace and initialize process-wide
215/// runtime assets through the same bootstrap boundary used by the CLI.
216///
217/// Returns a warning the caller should print once when startup succeeded but
218/// something degraded — today, that caching is off because no cache directory
219/// resolves. A *configured* value that cannot be honored is an `Err` instead:
220/// an operator who set `HARN_CACHE_DIR` gets a hard failure rather than a
221/// silent downgrade to no caching.
222pub fn initialize_runtime() -> Result<Option<&'static str>, RuntimeInitError> {
223    environment_registry::validate_startup_environment().map_err(RuntimeInitError::Environment)?;
224    let warning = bytecode_cache::check_cache_config().map_err(RuntimeInitError::CacheDir)?;
225    initialize_runtime_assets();
226    Ok(warning)
227}
228
229/// Crate-wide deterministic clock mock used by stdlib time builtins, the
230/// trigger dispatcher, the cron scheduler, and Rust-side tests. Re-exports
231/// the long-lived implementation under `triggers::test_util::clock` so all
232/// callers go through one source of truth.
233pub mod clock_mock {
234    pub(crate) use crate::triggers::test_util::clock::scope_capability_clock;
235    pub use crate::triggers::test_util::clock::{
236        active_clock, active_mock_clock, advance, clear_overrides, install_override, instant_now,
237        is_mocked, now_ms, now_utc, sleep, ClockInstant, ClockOverrideGuard, MockClock,
238    };
239
240    /// Runtime audit for capabilities that observe real wall-clock or
241    /// monotonic time while a testbench mock is installed. See the module
242    /// docs for the full design.
243    pub mod leak_audit {
244        pub use crate::triggers::test_util::clock_leak::{
245            drain, enter_scope, install_scope, instant_now, reset, snapshot, wall_now, ClockLeak,
246            ClockLeakScope, ClockLeakScopeGuard,
247        };
248    }
249}
250
251pub(crate) mod text_index;
252pub mod typecheck;
253pub mod value;
254pub mod verification;
255pub mod visible_text;
256mod vm;
257pub(crate) mod wait_for_graph;
258pub mod waitpoints;
259pub mod windows_path;
260pub mod workspace_anchor;
261pub mod workspace_path;
262
263pub use persistent_state::{
264    register_persistent_state_builtins_at_root, scope_persistent_state_root, PersistentStateRoot,
265    ScopedPersistentStateRoot,
266};
267pub use prepared_module::{PreparedModuleCache, PreparedModuleCacheStats};
268
269pub use actor_chain::{
270    ActorChain, ActorChainEntry, ActorChainError, Principal, ScopeAttenuationMode,
271    ScopeAttenuationPolicy, ScopeAttenuationViolation,
272};
273pub use call_budget::{
274    charge_mcp_call, charge_pg_query, install_mcp_call_budget, install_pg_query_budget,
275    McpCallBudgetGuard, PgQueryBudgetGuard,
276};
277pub use checkpoint::register_checkpoint_builtins;
278pub use chunk::*;
279pub use compiler::*;
280pub use connectors::{
281    active_connector_client, active_metrics_registry, clear_active_connector_clients,
282    clear_active_metrics_registry, connector_export_denied_builtin_reason,
283    connector_export_denied_harness_method_reason, connector_export_effect_class,
284    cron::{CatchupMode, CronConnector},
285    declared_secret_ids, default_connector_export_policy,
286    harn_module::{
287        load_contract as load_harn_connector_contract, HarnConnector, HarnConnectorContract,
288    },
289    hmac::{verify_hmac_signed, SIGNATURE_VERIFY_AUDIT_TOPIC},
290    install_active_connector_clients, install_active_metrics_registry,
291    postprocess_normalized_event, scope_active_connector_clients, ActivationHandle,
292    ActiveConnectorClientsGuard, ClientError, Connector, ConnectorClient, ConnectorClientResolver,
293    ConnectorCtx, ConnectorError, ConnectorExportEffectClass, ConnectorHttpResponse,
294    ConnectorMetricsSnapshot, ConnectorNormalizeResult, ConnectorRegistry, GenericWebhookConnector,
295    HarnConnectorEffectPolicies, MetricsRegistry, PostNormalizeOutcome, ProviderPayloadSchema,
296    RateLimitConfig, RateLimiterFactory, RawInbound, StreamConnector, TriggerBinding, TriggerKind,
297    TriggerRegistry, VmConnectorClients, WebhookSignatureVariant,
298};
299pub use corrections::{
300    append_correction_record, apply_corrections_to_policy, correction_query_filters_from_json,
301    correction_record_from_json, policy_with_corrections, query_correction_records,
302    CorrectionQueryFilters, CorrectionRecord, CorrectionScope, CORRECTIONS_TOPIC,
303    CORRECTION_EVENT_KIND, CORRECTION_SCHEMA_V0,
304};
305pub use harn_kernel::BuiltinId;
306pub use harness::{
307    DenyEvent, Harness, HarnessAgent, HarnessCall, HarnessChannels, HarnessClock, HarnessEnv,
308    HarnessFs, HarnessKind, HarnessLlm, HarnessMemory, HarnessNet, HarnessObs, HarnessPostgres,
309    HarnessProcess, HarnessRandom, HarnessSecrets, HarnessSqlite, HarnessStdio, HarnessSystem,
310    HarnessTenant, HarnessTerm, HarnessTesting, MockHarnessBuilder, VmHarness,
311};
312pub use harness_auth::{
313    current_auth_principal, enter_auth_principal, AuthPrincipal, AuthPrincipalScopeGuard,
314    MISSING_PRINCIPAL_MESSAGE,
315};
316pub use harness_net::{
317    bypass_enabled as net_policy_bypass_enabled, NetMatcher, NetPolicy, NetPolicyAudit,
318    NetPolicyDecision, NetPolicyDefault, NetPolicyRule, OnViolation, HARN_NET_POLICY_BYPASS_ENV,
319    NET_POLICY_AUDIT_TOPIC,
320};
321pub use harness_tenant::{
322    current_tenant_id, enter_tenant, TenantScopeGuard, MISSING_TENANT_MESSAGE,
323};
324pub use http::{register_http_builtins, reset_http_state};
325pub use llm::register_llm_builtins;
326pub use llm::trigger_predicate::TriggerPredicateBudget;
327pub use llm::{
328    current_agent_session_id, install_llm_cost_budget, install_llm_token_budget,
329    peek_llm_cost_budget, peek_llm_token_budget, register_session_end_hook, set_llm_cost_budget,
330    set_llm_token_budget, LlmBudgetGuard, LlmTokenBudgetGuard, SessionEndHookRegistration,
331};
332pub use mcp::{connect_mcp_server_from_json, connect_mcp_server_from_spec, register_mcp_builtins};
333pub use mcp_allowlist::{
334    build_catalog as build_mcp_catalog, catalog_for_request as mcp_catalog_for_request,
335    AdvertisedItem as McpAdvertisedItem, CatalogRequest as McpCatalogRequest, McpAllowlist,
336    McpAllowlistItem, McpCatalog, McpCatalogItem, McpCatalogServer, McpItemKind,
337    MCP_ALLOWLIST_SCHEMA_VERSION,
338};
339pub use mcp_card::{fetch_server_card, load_server_card_from_path, CardError};
340pub use mcp_host::{
341    cache_stats as mcp_host_cache_stats, set_allowlist as set_mcp_host_allowlist,
342    AllowlistDecision as McpHostAllowlistDecision, AllowlistGuard as McpHostAllowlistGuard,
343    BreakerState as McpHostBreakerState, CacheStats as McpHostCacheStats, McpHostStatus,
344    SpawnOptions as McpHostSpawnOptions, SupervisionPolicy as McpHostSupervisionPolicy,
345};
346pub use mcp_registry::{
347    active_handle as mcp_active_handle, ensure_active as mcp_ensure_active,
348    get_registration as mcp_get_registration, install_active as mcp_install_active,
349    is_registered as mcp_is_registered, register_servers as mcp_register_servers,
350    release as mcp_release, reset as mcp_reset_registry, snapshot_status as mcp_snapshot_status,
351    sweep_expired as mcp_sweep_expired, RegisteredMcpServer, RegistryStatus,
352};
353pub use mcp_server::{
354    take_mcp_serve_metadata, take_mcp_serve_prompts, take_mcp_serve_registry,
355    take_mcp_serve_resource_templates, take_mcp_serve_resources, tool_registry_to_mcp_tools,
356    McpPromptDef, McpResourceDef, McpResourceTemplateDef, McpServer, McpServerMetadata,
357};
358pub use metadata::register_metadata_builtins;
359pub use observability::audit::{audit_events as audit_obs_events, AuditFinding, AuditFindingKind};
360pub use observability::execution_scope::{
361    current_execution_scope, enter_execution_scope, mint_execution_scope, ExecutionScopeGuard,
362};
363pub use observability::request_id::{current_request_id, enter_request_id, RequestIdScopeGuard};
364pub use orchestration::{
365    benchmark_adapted_replay_pair, benchmark_replay_trace, build_replay_benchmark_report,
366    OpenCodeJsonlAdapter, ReplayBenchmarkCloudIngest, ReplayBenchmarkError,
367    ReplayBenchmarkFixtureReceipt, ReplayBenchmarkFixtureReport, ReplayBenchmarkMetrics,
368    ReplayBenchmarkReport, ReplayBenchmarkSuiteIdentity, ReplayBenchmarkSummary,
369    ReplayCategoryMetric, ReplayDebuggingProxyMetrics, ReplayRuntimeCostMetrics,
370    ReplayTraceAdapter, OPENCODE_JSONL_ADAPTER_ID, OPENCODE_JSONL_ADAPTER_SCHEMA_VERSION,
371    REPLAY_BENCHMARK_CLOUD_INGEST_KIND, REPLAY_BENCHMARK_REPORT_SCHEMA_VERSION,
372};
373pub use orchestration::{
374    canonicalize_run, first_divergence, run_replay_oracle_trace, ReplayAllowlistRule,
375    ReplayDivergence, ReplayExpectation, ReplayOracleError, ReplayOracleReport, ReplayOracleTrace,
376    ReplayTraceRun, ReplayTraceRunCounts, REPLAY_TRACE_SCHEMA_VERSION,
377};
378pub use orchestration::{
379    install_handoff_routes, snapshot_handoff_routes, HandoffRouteConfig,
380    HandoffRouteDecisionRecord, HandoffRouteTargetConfig,
381};
382pub use personas::{
383    disable_persona, fire_schedule as fire_persona_schedule, fire_trigger as fire_persona_trigger,
384    format_ms as format_persona_ms, now_ms as persona_now_ms, parse_rfc3339_ms as parse_persona_ms,
385    pause_persona, persona_status, record_persona_spend, register_persona_supervision_sink,
386    register_persona_value_sink, report_repair_worker_status, restore_persona_checkpoint,
387    resume_persona, PersonaAssignmentStatus, PersonaBudgetPolicy, PersonaBudgetStatus,
388    PersonaCheckpointAction, PersonaCheckpointRestoreOutcome, PersonaCheckpointRestoreRequest,
389    PersonaCheckpointResume, PersonaCheckpointUpdate, PersonaHandoffInboxItem, PersonaLease,
390    PersonaLifecycleState, PersonaQueuePositionUpdate, PersonaQueuedWork, PersonaReceiptUpdate,
391    PersonaRepairWorkerLifecycle, PersonaRepairWorkerStatusUpdate, PersonaRunCost,
392    PersonaRunReceipt, PersonaRuntimeBinding, PersonaStatus, PersonaSupervisionEvent,
393    PersonaSupervisionSink, PersonaSupervisionSinkRegistration, PersonaTriggerEnvelope,
394    PersonaValueEvent, PersonaValueEventKind, PersonaValueReceipt, PersonaValueSink,
395    PersonaValueSinkRegistration, StageDecl, StageExit, PERSONA_RUNTIME_TOPIC,
396};
397pub use provenance::{
398    build_signed_receipt, load_or_generate_agent_signing_key, verify_receipt, ProvenanceReceipt,
399    ReceiptBuildOptions, ReceiptVerificationReport,
400};
401pub use receipts::{
402    Receipt, ReceiptSink, ReceiptStatus, ReceiptValidationError, RedactingReceiptSink,
403    RedactionClass, RECEIPT_SCHEMA_ID, RECEIPT_SCHEMA_JSON, RECEIPT_SCHEMA_VERSION,
404};
405pub use record_filter::{normalize_record_filter_expression, CompiledRecordFilter};
406pub use runtime_limits::{
407    RuntimeLimitDescription, RuntimeLimitEntry, RuntimeLimits, RuntimeLimitsReport,
408    RUNTIME_LIMIT_DESCRIPTIONS,
409};
410pub use schema::json_to_vm_value;
411pub use sessions::{
412    CreateSession, ExpireSession, Session, SessionAttributes, SessionError, SessionStore,
413    TouchSession, SESSIONS_TOPIC,
414};
415/// The single owner of ignore policy for every Harn filesystem walk.
416///
417/// Re-exported so embedders that enumerate files on behalf of Harn scripts
418/// (today: the `harn-hostlib` deterministic-tool builtins) skip exactly the
419/// same paths the in-VM builtins do.
420pub use stdlib::fs::ignore_policy;
421#[doc(hidden)]
422pub use stdlib::fs::invalidate_cached_file_text;
423pub use stdlib::hitl::{
424    append_hitl_response, ApprovalRequest, HitlHostResponse, HITL_APPROVALS_TOPIC,
425    HITL_DUAL_CONTROL_TOPIC, HITL_ESCALATIONS_TOPIC, HITL_QUESTIONS_TOPIC,
426};
427/// Per-turn memo for turn-stable host reads. See [`stdlib::host::turn_cache`].
428pub use stdlib::host::turn_cache as host_turn_cache;
429pub use stdlib::host::{
430    clear_host_call_bridge, dispatch_host_operation, host_call_ready, install_host_call_bridge,
431    set_host_call_bridge, HostCallBridge, HostCallBridgeGuard, HostCallDispatchFuture,
432};
433pub use stdlib::http_response::{
434    parse_envelope as parse_http_envelope, HttpEnvelope, HttpHeaderValue, WsUpgradeSpec,
435    HTTP_RESPONSE_TAG_KEY, HTTP_RESPONSE_TAG_VERSION,
436};
437#[cfg(feature = "postgres")]
438pub use stdlib::install_shared_pool_registry;
439pub use stdlib::io::{
440    reserve_stdio_for_current_thread, set_stdout_passthrough, take_stderr_buffer,
441    StdioReservationGuard,
442};
443pub use stdlib::long_running::cancel_handle as cancel_long_running_handle;
444pub use stdlib::observability::install_default_backend as install_obs_default_backend;
445pub use stdlib::secret_scan::{
446    append_secret_scan_audit, audit_secret_scan_active, scan_content as secret_scan_content,
447    SecretFinding, SECRET_SCAN_AUDIT_TOPIC,
448};
449pub use stdlib::template::{
450    lookup_prompt_consumers, lookup_prompt_span, prompt_render_indices, record_prompt_render_index,
451    PromptSourceSpan, PromptSpanKind,
452};
453pub use stdlib::waitpoint::{
454    process_waitpoint_resume_event, service_waitpoints_once, WAITPOINT_RESUME_TOPIC,
455};
456pub use stdlib::workflow_messages::{
457    workflow_pause_for_base, workflow_publish_query_for_base, workflow_query_for_base,
458    workflow_respond_update_for_base, workflow_resume_for_base, workflow_signal_for_base,
459    workflow_update_for_base, WorkflowMailboxState,
460};
461pub use stdlib::{
462    register_agent_stdlib, register_core_stdlib, register_io_stdlib, register_vm_stdlib,
463};
464pub use store::register_store_builtins;
465pub use tenant::{
466    tenant_event_topic_prefix, tenant_secret_namespace, tenant_topic, validate_tenant_id, ApiKeyId,
467    TenantApiKeyRecord, TenantBudget, TenantEventLog, TenantRecord, TenantRegistrySnapshot,
468    TenantResolutionError, TenantScope, TenantSecretProvider, TenantStatus, TenantStore,
469    TENANT_EVENT_TOPIC_PREFIX, TENANT_REGISTRY_DIR, TENANT_REGISTRY_FILE,
470    TENANT_SECRET_NAMESPACE_PREFIX,
471};
472pub use triggers::{
473    append_dispatch_cancel_request, begin_in_flight, binding_autonomy_budget_would_exceed,
474    binding_budget_would_exceed, binding_version_as_of, classify_trigger_dlq_error,
475    clear_dispatcher_state, clear_orchestrator_budget, clear_trigger_registry, drain,
476    dynamic_deregister, dynamic_register, expected_predicate_cost_usd_micros, finish_in_flight,
477    install_manifest_triggers, install_orchestrator_budget, micros_to_usd,
478    note_autonomous_decision, note_orchestrator_budget_cost, orchestrator_budget_would_exceed,
479    parse_flow_control_duration, pause, pin_trigger_binding, provider_metadata,
480    record_predicate_cost_sample, redact_headers, register_provider_schemas,
481    registered_provider_metadata, registered_provider_schema_names, reset_binding_budget_windows,
482    reset_provider_catalog, resolve_live_or_as_of, resolve_live_trigger_binding,
483    resolve_trigger_binding_as_of, resume, run_trigger_harness_fixture, scheduler_in_flight_by_key,
484    scheduler_ready_stats_by_key, snapshot_dispatcher_stats, snapshot_orchestrator_budget,
485    snapshot_trigger_bindings, unpin_trigger_binding, usd_to_micros, worker_claims_topic_name,
486    worker_job_topic_name, worker_response_topic_name, ClaimedWorkerJob, DispatchCancelRequest,
487    DispatchError, DispatchOutcome, DispatchStatus, Dispatcher, DispatcherDrainReport,
488    DispatcherStatsSnapshot, ExtensionProviderPayload, FairnessKey, HeaderRedactionPolicy,
489    InboxIndex, OrchestratorBudgetConfig, OrchestratorBudgetSnapshot, ProviderCatalog,
490    ProviderCatalogError, ProviderId, ProviderMetadata, ProviderOutboundMethod, ProviderPayload,
491    ProviderRuntimeMetadata, ProviderSchema, ProviderSecretRequirement, ReadyKeyStats,
492    RecordedTriggerBinding, RetryPolicy, SchedulableJob, SchedulerKeyStat, SchedulerPolicy,
493    SchedulerSnapshot, SchedulerState, SchedulerStrategy, SignatureStatus,
494    SignatureVerificationMetadata, StreamEventPayload, TenantId, TraceId, TriggerBatchConfig,
495    TriggerBindingSnapshot, TriggerBindingSource, TriggerBindingSpec,
496    TriggerBudgetExhaustionStrategy, TriggerConcurrencyConfig, TriggerDebounceConfig,
497    TriggerDispatchOutcome, TriggerEvent, TriggerEventId, TriggerExpressionSpec,
498    TriggerFlowControlConfig, TriggerHandlerSpec, TriggerHarnessResult, TriggerId,
499    TriggerMetricsSnapshot, TriggerPredicateSpec, TriggerPriorityOrderConfig,
500    TriggerRateLimitConfig, TriggerRegistryError, TriggerRetryConfig, TriggerSingletonConfig,
501    TriggerState, TriggerThrottleConfig, WorkerQueue, WorkerQueueClaimHandle,
502    WorkerQueueEnqueueReceipt, WorkerQueueInspectSnapshot, WorkerQueueJob, WorkerQueueJobState,
503    WorkerQueuePriority, WorkerQueueResponseRecord, WorkerQueueState, WorkerQueueSummary,
504    DEFAULT_INBOX_RETENTION_DAYS, DEFAULT_STARVATION_AGE_MS, TRIGGERS_LIFECYCLE_TOPIC,
505    TRIGGER_ATTEMPTS_TOPIC, TRIGGER_CANCEL_REQUESTS_TOPIC, TRIGGER_DLQ_TOPIC,
506    TRIGGER_INBOX_CLAIMS_TOPIC, TRIGGER_INBOX_ENVELOPES_TOPIC, TRIGGER_INBOX_LEGACY_TOPIC,
507    TRIGGER_INBOX_OBSERVABILITY_TOPIC, TRIGGER_OPERATION_AUDIT_TOPIC, TRIGGER_OUTBOX_TOPIC,
508    TRIGGER_TEST_FIXTURES, WORKER_QUEUE_CATALOG_TOPIC,
509};
510pub use trust_graph::{
511    append_active_scope_attenuation_alert, append_active_trust_record,
512    append_scope_attenuation_alert, append_trust_record, export_trust_chain,
513    group_trust_records_by_trace, policy_for_agent, policy_for_autonomy_tier,
514    query_trust_graph_records, query_trust_records, resolve_agent_autonomy_tier,
515    summarize_trust_records, topic_for_agent, trust_score_for, verify_trust_chain, AutonomyTier,
516    TrustAgentSummary, TrustChainExport, TrustChainExportMetadata, TrustChainExportProducer,
517    TrustChainReport, TrustGraphRecord, TrustOutcome, TrustQueryFilters, TrustRecord,
518    TrustRecordActionKind, TrustScore, TrustTraceGroup, METADATA_KEY_ACTOR_CHAIN,
519    METADATA_KEY_ACTOR_CHAIN_ALERT, METADATA_KEY_EFFECTS_GRANT, METADATA_KEY_EFFECTS_USED,
520    METADATA_KEY_PARENT_RECORD_ID, OPENTRUSTGRAPH_ACCEPTED_SCHEMAS, OPENTRUSTGRAPH_CHAIN_SCHEMA_V0,
521    OPENTRUSTGRAPH_SCHEMA_V0, OPENTRUSTGRAPH_SCHEMA_V0_1, TRUST_ACTION_RELEASE,
522    TRUST_GRAPH_GLOBAL_TOPIC, TRUST_GRAPH_LEGACY_GLOBAL_TOPIC, TRUST_GRAPH_LEGACY_TOPIC_PREFIX,
523    TRUST_GRAPH_RECORDS_TOPIC, TRUST_GRAPH_TOPIC_PREFIX,
524};
525pub use value::*;
526pub use vm::*;
527
528#[cfg(feature = "vm-bench-internals")]
529#[doc(hidden)]
530pub mod bench_internals;
531
532/// Lex, parse, type-check, and compile source to bytecode in one call.
533/// Bails on the first type error. For callers that need diagnostics
534/// rather than early exit, use `harn_parser::check_source` directly
535/// and then call `Compiler::new().compile(&program)`.
536pub fn compile_source(source: &str) -> Result<Chunk, String> {
537    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
538    Compiler::new().compile(&program).map_err(|e| e.to_string())
539}
540
541/// Same as [`compile_source`] but compiles a specific named pipeline as
542/// the program entry point instead of the default-pipeline-or-first
543/// selection rule. Returns a runtime error when no pipeline with
544/// `pipeline_name` exists in the source.
545pub fn compile_source_named(source: &str, pipeline_name: &str) -> Result<Chunk, String> {
546    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
547    let has_pipeline = program.iter().any(|sn| {
548        let (_, inner) = harn_parser::peel_attributes(sn);
549        matches!(&inner.node, harn_parser::Node::Pipeline { name, .. } if name == pipeline_name)
550    });
551    if !has_pipeline {
552        return Err(format!("no pipeline named `{pipeline_name}` in source"));
553    }
554    Compiler::new()
555        .compile_named(&program, pipeline_name)
556        .map_err(|e| e.to_string())
557}
558
559/// Lowers resolved Harn declarations to JSON Schema for public host boundaries.
560///
561/// The resolver owns aliases, structs, enums, imports, and generic
562/// instantiation. Building it once from the visible module declarations keeps
563/// transports and SDK generators from reproducing Harn's type system.
564pub struct TypeSchemaResolver {
565    compiler: compiler::Compiler,
566    nominal_types: std::collections::BTreeMap<String, SchemaNominalType>,
567}
568
569#[derive(Clone)]
570enum SchemaNominalType {
571    Struct {
572        type_params: Vec<harn_parser::TypeParam>,
573        fields: Vec<harn_parser::StructField>,
574    },
575    Enum {
576        type_params: Vec<harn_parser::TypeParam>,
577        variants: Vec<harn_parser::EnumVariant>,
578    },
579}
580
581impl TypeSchemaResolver {
582    /// A resolver with no user declarations in scope.
583    pub fn empty() -> Self {
584        Self {
585            compiler: compiler::Compiler::new(),
586            nominal_types: std::collections::BTreeMap::new(),
587        }
588    }
589
590    /// Collect every visible type declaration in `program`.
591    pub fn from_program(program: &[harn_parser::SNode]) -> Self {
592        let mut compiler = compiler::Compiler::new();
593        compiler.collect_type_aliases(program);
594        let mut nominal_types = std::collections::BTreeMap::new();
595        for node in program {
596            let (_, declaration) = harn_parser::peel_attributes(node);
597            match &declaration.node {
598                harn_parser::Node::StructDecl {
599                    name,
600                    type_params,
601                    fields,
602                    ..
603                } => {
604                    nominal_types.insert(
605                        name.clone(),
606                        SchemaNominalType::Struct {
607                            type_params: type_params.clone(),
608                            fields: fields.clone(),
609                        },
610                    );
611                }
612                harn_parser::Node::EnumDecl {
613                    name,
614                    type_params,
615                    variants,
616                    ..
617                } => {
618                    nominal_types.insert(
619                        name.clone(),
620                        SchemaNominalType::Enum {
621                            type_params: type_params.clone(),
622                            variants: variants.clone(),
623                        },
624                    );
625                }
626                _ => {}
627            }
628        }
629        Self {
630            compiler,
631            nominal_types,
632        }
633    }
634
635    /// JSON Schema for one `TypeExpr`, expanding any named alias first. `None`
636    /// when the (expanded) type has no JSON-Schema form (function types, ...).
637    pub fn json_schema_for_type_expr(
638        &self,
639        type_expr: &harn_parser::TypeExpr,
640    ) -> Option<serde_json::Value> {
641        self.json_schema_for_type_expr_inner(type_expr, &mut Vec::new())
642    }
643
644    /// Input projection keeps the established structural contract: inline
645    /// shapes and aliases lower to JSON Schema, while nominal declarations do
646    /// not advertise a wire form until the argument bridge can hydrate that
647    /// form into a nominal runtime value.
648    pub fn json_schema_for_input_type_expr(
649        &self,
650        type_expr: &harn_parser::TypeExpr,
651    ) -> Option<serde_json::Value> {
652        let expanded = self.compiler.expand_alias(type_expr);
653        let schema = compiler::Compiler::type_expr_to_schema_value(&expanded)?;
654        let json_schema = schema::schema_to_json_schema_value(&schema).ok()?;
655        Some(llm::vm_value_to_json(&json_schema))
656    }
657
658    fn json_schema_for_type_expr_inner(
659        &self,
660        type_expr: &harn_parser::TypeExpr,
661        resolving: &mut Vec<harn_parser::TypeExpr>,
662    ) -> Option<serde_json::Value> {
663        const MAX_SCHEMA_TYPE_NEST: usize = 128;
664        let expanded = self.compiler.expand_alias(type_expr);
665        if resolving.len() >= MAX_SCHEMA_TYPE_NEST || resolving.contains(&expanded) {
666            return Some(serde_json::json!({}));
667        }
668
669        if let Some((name, args)) = nominal_reference(&expanded) {
670            if let Some(declaration) = self.nominal_types.get(name).cloned() {
671                resolving.push(expanded.clone());
672                let schema = self.json_schema_for_nominal(name, &declaration, args, resolving);
673                resolving.pop();
674                return schema;
675            }
676        }
677
678        if !contains_nominal_reference(&expanded, &self.nominal_types) {
679            let schema = compiler::Compiler::type_expr_to_schema_value(&expanded)?;
680            let json_schema = schema::schema_to_json_schema_value(&schema).ok()?;
681            return Some(llm::vm_value_to_json(&json_schema));
682        }
683
684        use harn_parser::TypeExpr;
685        match expanded {
686            TypeExpr::Shape(fields) => {
687                let mut properties = serde_json::Map::new();
688                let mut required = Vec::new();
689                for field in fields {
690                    let mut field_schema =
691                        self.json_schema_for_type_expr_inner(&field.type_expr, resolving)?;
692                    if field.optional {
693                        field_schema = serde_json::json!({
694                            "anyOf": [field_schema, {"type": "null"}],
695                        });
696                    } else {
697                        required.push(serde_json::Value::String(field.name.clone()));
698                    }
699                    properties.insert(field.name, field_schema);
700                }
701                Some(serde_json::json!({
702                    "type": "object",
703                    "properties": properties,
704                    "required": required,
705                }))
706            }
707            TypeExpr::List(inner) => Some(serde_json::json!({
708                "type": "array",
709                "items": self.json_schema_for_type_expr_inner(&inner, resolving)?,
710            })),
711            TypeExpr::Tuple(elements) => {
712                let prefix_items = elements
713                    .iter()
714                    .map(|element| self.json_schema_for_type_expr_inner(element, resolving))
715                    .collect::<Option<Vec<_>>>()?;
716                Some(serde_json::json!({
717                    "type": "array",
718                    "prefixItems": prefix_items,
719                    "items": false,
720                    "minItems": elements.len(),
721                    "maxItems": elements.len(),
722                }))
723            }
724            TypeExpr::DictType(key, value) if matches!(key.as_ref(), TypeExpr::Named(name) if name == "string") => {
725                Some(serde_json::json!({
726                    "type": "object",
727                    "additionalProperties": self
728                        .json_schema_for_type_expr_inner(&value, resolving)?,
729                }))
730            }
731            TypeExpr::Union(members) => Some(serde_json::json!({
732                "anyOf": members
733                    .iter()
734                    .map(|member| self.json_schema_for_type_expr_inner(member, resolving))
735                    .collect::<Option<Vec<_>>>()?,
736            })),
737            TypeExpr::Intersection(members) => Some(serde_json::json!({
738                "allOf": members
739                    .iter()
740                    .map(|member| self.json_schema_for_type_expr_inner(member, resolving))
741                    .collect::<Option<Vec<_>>>()?,
742            })),
743            TypeExpr::Owned(inner) => self.json_schema_for_type_expr_inner(&inner, resolving),
744            _ => None,
745        }
746    }
747
748    fn json_schema_for_nominal(
749        &self,
750        name: &str,
751        declaration: &SchemaNominalType,
752        args: &[harn_parser::TypeExpr],
753        resolving: &mut Vec<harn_parser::TypeExpr>,
754    ) -> Option<serde_json::Value> {
755        let type_params = match declaration {
756            SchemaNominalType::Struct { type_params, .. }
757            | SchemaNominalType::Enum { type_params, .. } => type_params,
758        };
759        if type_params.len() != args.len() {
760            return None;
761        }
762        let bindings = type_params
763            .iter()
764            .zip(args.iter().cloned())
765            .map(|(param, arg)| (param.name.clone(), arg))
766            .collect::<std::collections::BTreeMap<_, _>>();
767
768        match declaration {
769            SchemaNominalType::Struct { fields, .. } => {
770                let fields = fields
771                    .iter()
772                    .map(|field| harn_parser::ShapeField {
773                        name: field.name.clone(),
774                        type_expr: field
775                            .type_expr
776                            .as_ref()
777                            .map(|ty| harn_parser::substitute_type_expr(ty, &bindings))
778                            .unwrap_or_else(|| harn_parser::TypeExpr::Named("unknown".into())),
779                        optional: field.optional,
780                        span: field.span,
781                    })
782                    .collect();
783                self.json_schema_for_type_expr_inner(
784                    &harn_parser::TypeExpr::Shape(fields),
785                    resolving,
786                )
787            }
788            SchemaNominalType::Enum { variants, .. } => {
789                let branches = variants
790                    .iter()
791                    .map(|variant| {
792                        let prefix_items = variant
793                            .fields
794                            .iter()
795                            .map(|field| {
796                                let type_expr = field.type_expr.as_ref()?;
797                                let instantiated =
798                                    harn_parser::substitute_type_expr(type_expr, &bindings);
799                                let mut schema =
800                                    self.json_schema_for_type_expr_inner(&instantiated, resolving)?;
801                                if let serde_json::Value::Object(object) = &mut schema {
802                                    object.insert(
803                                        "title".to_string(),
804                                        serde_json::Value::String(field.name.clone()),
805                                    );
806                                }
807                                Some(schema)
808                            })
809                            .collect::<Option<Vec<_>>>()?;
810                        Some(serde_json::json!({
811                            "type": "object",
812                            "properties": {
813                                "enum": {"const": name},
814                                "variant": {"const": variant.name},
815                                "fields": {
816                                    "type": "array",
817                                    "prefixItems": prefix_items,
818                                    "items": false,
819                                    "minItems": variant.fields.len(),
820                                    "maxItems": variant.fields.len(),
821                                },
822                            },
823                            "required": ["enum", "variant", "fields"],
824                            "additionalProperties": false,
825                        }))
826                    })
827                    .collect::<Option<Vec<_>>>()?;
828                Some(serde_json::json!({"oneOf": branches}))
829            }
830        }
831    }
832
833    /// JSON Schema `object` for a parameter list (a served tool's `inputSchema`),
834    /// expanding aliases per parameter.
835    pub fn json_schema_for_typed_params(
836        &self,
837        params: &[harn_parser::TypedParam],
838    ) -> serde_json::Value {
839        let mut properties = serde_json::Map::new();
840        let mut required = Vec::new();
841
842        for param in params {
843            let param_schema = param
844                .type_expr
845                .as_ref()
846                .and_then(|type_expr| self.json_schema_for_input_type_expr(type_expr))
847                .unwrap_or_else(|| serde_json::json!({}));
848            if param.default_value.is_none() {
849                required.push(serde_json::Value::String(param.name.clone()));
850            }
851            properties.insert(param.name.clone(), param_schema);
852        }
853
854        let mut schema = serde_json::Map::new();
855        schema.insert(
856            "type".to_string(),
857            serde_json::Value::String("object".to_string()),
858        );
859        schema.insert(
860            "properties".to_string(),
861            serde_json::Value::Object(properties),
862        );
863        if !required.is_empty() {
864            schema.insert("required".to_string(), serde_json::Value::Array(required));
865        }
866        serde_json::Value::Object(schema)
867    }
868}
869
870fn nominal_reference(
871    type_expr: &harn_parser::TypeExpr,
872) -> Option<(&str, &[harn_parser::TypeExpr])> {
873    match type_expr {
874        harn_parser::TypeExpr::Named(name) => Some((name, &[])),
875        harn_parser::TypeExpr::Applied { name, args } => Some((name, args)),
876        _ => None,
877    }
878}
879
880fn contains_nominal_reference(
881    type_expr: &harn_parser::TypeExpr,
882    nominal_types: &std::collections::BTreeMap<String, SchemaNominalType>,
883) -> bool {
884    use harn_parser::TypeExpr;
885    match type_expr {
886        TypeExpr::Named(name) => nominal_types.contains_key(name),
887        TypeExpr::Applied { name, args } => {
888            nominal_types.contains_key(name)
889                || args
890                    .iter()
891                    .any(|arg| contains_nominal_reference(arg, nominal_types))
892        }
893        TypeExpr::Union(types) | TypeExpr::Intersection(types) | TypeExpr::Tuple(types) => types
894            .iter()
895            .any(|ty| contains_nominal_reference(ty, nominal_types)),
896        TypeExpr::Shape(fields) => fields
897            .iter()
898            .any(|field| contains_nominal_reference(&field.type_expr, nominal_types)),
899        TypeExpr::OpenShape { fields, rests } => {
900            fields
901                .iter()
902                .any(|field| contains_nominal_reference(&field.type_expr, nominal_types))
903                || rests
904                    .iter()
905                    .any(|rest| contains_nominal_reference(rest, nominal_types))
906        }
907        TypeExpr::List(inner)
908        | TypeExpr::Iter(inner)
909        | TypeExpr::Generator(inner)
910        | TypeExpr::Stream(inner)
911        | TypeExpr::Owned(inner) => contains_nominal_reference(inner, nominal_types),
912        TypeExpr::DictType(key, value) => {
913            contains_nominal_reference(key, nominal_types)
914                || contains_nominal_reference(value, nominal_types)
915        }
916        TypeExpr::FnType {
917            params,
918            return_type,
919        } => {
920            params
921                .iter()
922                .any(|param| contains_nominal_reference(param, nominal_types))
923                || contains_nominal_reference(return_type, nominal_types)
924        }
925        TypeExpr::Never | TypeExpr::LitString(_) | TypeExpr::LitInt(_) => false,
926    }
927}
928
929/// Raw lowering with no program declarations in scope. Prefer
930/// [`TypeSchemaResolver::from_program`] when serving a module so named
931/// declarations resolve instead of erasing to `{}`.
932pub fn json_schema_for_type_expr(type_expr: &harn_parser::TypeExpr) -> Option<serde_json::Value> {
933    TypeSchemaResolver::empty().json_schema_for_type_expr(type_expr)
934}
935
936pub fn json_schema_for_typed_params(params: &[harn_parser::TypedParam]) -> serde_json::Value {
937    TypeSchemaResolver::empty().json_schema_for_typed_params(params)
938}
939
940#[cfg(test)]
941mod schema_alias_resolver_tests {
942    use super::*;
943
944    fn fn_params_schema(src: &str) -> serde_json::Value {
945        let program = harn_parser::parse_source(src).expect("parse test source");
946        let resolver = TypeSchemaResolver::from_program(&program);
947        for node in &program {
948            let (_, inner) = harn_parser::peel_attributes(node);
949            if let harn_parser::Node::FnDecl { params, .. } = &inner.node {
950                return resolver.json_schema_for_typed_params(params);
951            }
952        }
953        panic!("no fn decl in test source");
954    }
955
956    #[test]
957    fn named_shape_alias_projects_like_inline_shape() {
958        let inline = fn_params_schema("pub fn f(p: {kind: string, path: string}) {}");
959        let aliased =
960            fn_params_schema("type Src = {kind: string, path: string}\npub fn f(p: Src) {}");
961        assert_eq!(
962            aliased, inline,
963            "a named shape alias must project the same inputSchema as its inline shape",
964        );
965        assert_ne!(
966            aliased["properties"]["p"],
967            serde_json::json!({}),
968            "the alias parameter must not erase to an empty schema",
969        );
970    }
971
972    #[test]
973    fn literal_union_alias_projects_json_enum() {
974        let schema = fn_params_schema("type Kind = \"local\" | \"ssh\"\npub fn f(p: Kind) {}");
975        let p = &schema["properties"]["p"];
976        assert_eq!(p["type"], "string");
977        assert_eq!(p["enum"], serde_json::json!(["local", "ssh"]));
978    }
979
980    #[test]
981    fn unknown_named_type_still_erases_to_empty() {
982        // No alias declared: unchanged behavior — an unknown named type lowers to {}.
983        let schema = fn_params_schema("pub fn f(p: Unknown) {}");
984        assert_eq!(schema["properties"]["p"], serde_json::json!({}));
985    }
986}
987
988fn reset_llm_state_for_thread_reset() {
989    llm::reset_llm_state();
990    #[cfg(test)]
991    reset_thread_local_state_test_hooks::before_llm_global_reset();
992    // This full wipe is necessary between Harn programs to clear durable
993    // cooldowns that would otherwise stall a later run under a paused clock.
994    llm::reset_rate_limit_registry();
995    llm_config::clear_user_overrides();
996    llm_config::clear_runtime_provider_endpoint_overrides();
997}
998
999#[cfg(test)]
1000mod reset_thread_local_state_test_hooks {
1001    use std::sync::{Arc, Mutex, OnceLock};
1002
1003    type Hook = Arc<dyn Fn() + Send + Sync + 'static>;
1004
1005    static BEFORE_LLM_GLOBAL_RESET: OnceLock<Mutex<Option<Hook>>> = OnceLock::new();
1006
1007    fn before_llm_global_reset_hook() -> &'static Mutex<Option<Hook>> {
1008        BEFORE_LLM_GLOBAL_RESET.get_or_init(|| Mutex::new(None))
1009    }
1010
1011    pub(crate) struct HookGuard;
1012
1013    impl Drop for HookGuard {
1014        fn drop(&mut self) {
1015            let mut hook = before_llm_global_reset_hook()
1016                .lock()
1017                .unwrap_or_else(std::sync::PoisonError::into_inner);
1018            *hook = None;
1019        }
1020    }
1021
1022    pub(crate) fn install_before_llm_global_reset(hook: Hook) -> HookGuard {
1023        let mut slot = before_llm_global_reset_hook()
1024            .lock()
1025            .unwrap_or_else(std::sync::PoisonError::into_inner);
1026        *slot = Some(hook);
1027        HookGuard
1028    }
1029
1030    pub(crate) fn before_llm_global_reset() {
1031        let hook = before_llm_global_reset_hook()
1032            .lock()
1033            .unwrap_or_else(std::sync::PoisonError::into_inner)
1034            .clone();
1035        if let Some(hook) = hook {
1036            hook();
1037        }
1038    }
1039}
1040
1041/// Reset all thread-local state that can leak between test runs.
1042pub fn reset_thread_local_state() {
1043    #[cfg(test)]
1044    {
1045        // `reset_thread_local_state` is also used by in-process unit tests. It
1046        // clears process-global LLM config/rate-limit state, so share the same
1047        // lock used by LLM env tests; otherwise a sibling reset can erase a
1048        // parked rate-limit test's registry while the test still owns a permit.
1049        let _guard = llm::env_guard();
1050        reset_llm_state_for_thread_reset();
1051    }
1052    #[cfg(not(test))]
1053    reset_llm_state_for_thread_reset();
1054
1055    http::reset_http_state();
1056    channels::reset_channel_state();
1057    event_log::reset_active_event_log();
1058    egress::clear_explicit_egress_policy_requirement_for_host();
1059    egress::clear_ssrf_guard_requirement_for_host();
1060    stdlib::reset_stdlib_state();
1061    connectors::clear_active_connector_clients();
1062    orchestration::clear_runtime_hooks();
1063    orchestration::clear_file_edit_queue();
1064    orchestration::clear_execution_policy_stacks();
1065    orchestration::clear_command_policies();
1066    orchestration::clear_pipeline_on_finish();
1067    orchestration::reset_lifecycle_receipt_registry();
1068    orchestration::agent_inbox::reset();
1069    tool_call_cancellations::reset_registry();
1070    redact::clear_policy_stack();
1071    security::reset_thread_state();
1072    triggers::clear_dispatcher_state();
1073    triggers::clear_trigger_registry();
1074    events::reset_event_sinks();
1075    tracing::set_tracing_enabled(false);
1076    tracing::reset_tracing();
1077    // `builtin_profile` is deliberately NOT reset here. Its recorder is
1078    // process-global (`static ENABLED` / `static TOTALS`), and this function
1079    // runs from ~150 test setups and from production entry points like
1080    // `execute_conformance_source` and the orchestrator lifecycle. Every one
1081    // of those calls disarmed the recorder that a concurrently running
1082    // profiled run had just enabled, so `harn run --profile` reported
1083    // `vm/residual 100%` and named nothing. `builtin_profile::enable()`
1084    // already discards the previous run's totals, so the profiling entry
1085    // point owns the lifecycle without help from here. Same reasoning as
1086    // `llm::rate_limit::reset_runtime_rate_limit_overrides` and the
1087    // `long_running::reset_state` exclusion in `stdlib::reset_stdlib_state`.
1088    agent_events::reset_all_sinks();
1089    agent_sessions::reset_session_store();
1090    mcp_registry::reset();
1091    mcp_host::reset_for_tests();
1092    call_budget::reset_call_budget_state();
1093    clock_mock::leak_audit::reset();
1094}
1095
1096#[cfg(test)]
1097mod reset_leak_tests {
1098    //! Regression coverage for harn#2660: process-/thread-global
1099    //! registries that accumulated one entry per test because they were
1100    //! never drained by `reset_thread_local_state`. Each case populates a
1101    //! registry through its real entry point, runs the reset, and asserts
1102    //! the registry is empty again.
1103    use super::*;
1104    use crate::value::VmValue;
1105
1106    #[test]
1107    fn reset_drains_pending_file_edit_notifications() {
1108        orchestration::queue_file_edited("stale.harn", serde_json::json!({"operation": "write"}));
1109
1110        reset_thread_local_state();
1111
1112        assert!(
1113            orchestration::drain_file_edits().is_empty(),
1114            "a later VM run must not receive file edits queued by the previous run"
1115        );
1116    }
1117
1118    /// The recorder is enabled per RUN but lives for the PROCESS, so an
1119    /// embedder that runs one script with `--profile` and the next without it
1120    /// would keep paying for bookkeeping nobody reads and fold the second
1121    /// run's builtins into the first run's totals. Enablement therefore ends
1122    /// with the run that asked for it — the guard `enable()` returns — and NOT
1123    /// in `reset_thread_local_state`, which fires from ~150 test setups and
1124    /// from production entry points that know nothing about an in-flight
1125    /// profiled run.
1126    #[test]
1127    fn builtin_profile_recording_ends_with_its_run_not_with_a_global_reset() {
1128        let _lock = builtin_profile::test_lock()
1129            .lock()
1130            .unwrap_or_else(std::sync::PoisonError::into_inner);
1131        let recording = builtin_profile::enable();
1132        builtin_profile::record("run_shell", std::time::Duration::from_millis(5));
1133        assert!(builtin_profile::is_enabled());
1134        assert!(!builtin_profile::snapshot().is_empty());
1135
1136        reset_thread_local_state();
1137
1138        assert!(
1139            builtin_profile::is_enabled(),
1140            "an unrelated global reset must not disarm an in-flight profiled run"
1141        );
1142        assert!(
1143            !builtin_profile::snapshot().is_empty(),
1144            "an unrelated global reset must not drop totals the run still owns"
1145        );
1146
1147        drop(recording);
1148
1149        assert!(
1150            !builtin_profile::is_enabled(),
1151            "a profiled run must not leave the recorder on for the next one"
1152        );
1153        assert!(
1154            builtin_profile::snapshot().is_empty(),
1155            "builtin totals must be empty once the run's guard drops"
1156        );
1157    }
1158
1159    /// The changed-path map is the authoritative source for a sub-agent's
1160    /// `files_written` receipt, is process-global, and is drained only at
1161    /// teardown — which a session that errors never reaches. A later session
1162    /// reusing the id would report writes it never made.
1163    #[test]
1164    fn reset_drains_session_changed_paths() {
1165        let session = "sess-leak";
1166        agent_sessions::open_or_create(Some(session.to_string()));
1167        agent_sessions::record_session_changed_path(session, "/tmp/written-by-a-dead-run.txt");
1168        assert!(!agent_sessions::session_changed_paths(session).is_empty());
1169        reset_thread_local_state();
1170        assert!(
1171            agent_sessions::session_changed_paths(session).is_empty(),
1172            "a receipt must not inherit an abandoned session's writes"
1173        );
1174    }
1175
1176    #[test]
1177    fn reset_drains_agent_inbox() {
1178        orchestration::agent_inbox::reset();
1179        orchestration::agent_inbox::push("sess-2660", "note", "leak", "test");
1180        assert!(orchestration::agent_inbox::session_count() > 0);
1181        reset_thread_local_state();
1182        assert_eq!(
1183            orchestration::agent_inbox::session_count(),
1184            0,
1185            "agent_inbox must be empty after reset"
1186        );
1187    }
1188
1189    #[test]
1190    fn reset_drains_tool_call_cancellation_registry() {
1191        tool_call_cancellations::reset_registry();
1192        // Leak the guard so the entry survives until the reset runs —
1193        // this mirrors a dispatch abandoned mid-flight.
1194        let registered = tool_call_cancellations::register("sess-2660", "call-1", "tool");
1195        if let Some((_handle, guard)) = registered {
1196            std::mem::forget(guard);
1197        }
1198        assert!(tool_call_cancellations::registry_len() > 0);
1199        reset_thread_local_state();
1200        assert_eq!(
1201            tool_call_cancellations::registry_len(),
1202            0,
1203            "tool-call cancellation registry must be empty after reset"
1204        );
1205    }
1206
1207    #[test]
1208    fn reset_drains_routing_policy_registry() {
1209        llm::routing::clear_policy_registry();
1210        let mut config: crate::value::DictMap = crate::value::DictMap::new();
1211        config.insert(
1212            crate::value::intern_key("chain"),
1213            VmValue::List(std::sync::Arc::new(vec![VmValue::String(
1214                arcstr::ArcStr::from("mock:mock"),
1215            )])),
1216        );
1217        llm::routing::build_routing_policy(&config).expect("intern a routing policy");
1218        assert!(llm::routing::policy_registry_len() > 0);
1219        reset_thread_local_state();
1220        assert_eq!(
1221            llm::routing::policy_registry_len(),
1222            0,
1223            "routing policy registry must be empty after reset"
1224        );
1225    }
1226
1227    #[test]
1228    fn reset_holds_llm_env_guard_while_wiping_llm_globals() {
1229        let observed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1230        let observed_hook = std::sync::Arc::clone(&observed);
1231        let _hook = reset_thread_local_state_test_hooks::install_before_llm_global_reset(
1232            std::sync::Arc::new(move || {
1233                assert!(
1234                    matches!(
1235                        llm::env_lock().try_lock(),
1236                        Err(std::sync::TryLockError::WouldBlock)
1237                    ),
1238                    "reset_thread_local_state must hold env_guard before wiping LLM globals"
1239                );
1240                observed_hook.store(true, std::sync::atomic::Ordering::SeqCst);
1241            }),
1242        );
1243
1244        reset_thread_local_state();
1245        assert!(
1246            observed.load(std::sync::atomic::Ordering::SeqCst),
1247            "LLM global reset hook should have run"
1248        );
1249    }
1250}