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