Skip to main content

harn_vm/
lib.rs

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