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
4/// Re-export of the unified clock substrate so downstream crates (CLI,
5/// orchestrator, harn-cloud) can depend on a single canonical `Clock`
6/// trait without each adding `harn-clock` as a direct dependency.
7pub use harn_clock as clock;
8
9pub mod a2a;
10pub mod agent_events;
11pub mod agent_sessions;
12pub mod atomic_io;
13pub mod autonomy;
14pub(crate) mod aws_sigv4;
15pub mod bridge;
16mod builtin_id;
17pub mod bytecode_cache;
18pub mod call_budget;
19pub mod channel_guardrails;
20pub mod channels;
21pub mod checkpoint;
22mod chunk;
23mod compiler;
24pub mod composition;
25pub mod config;
26pub mod connectors;
27pub mod corrections;
28pub(crate) mod durable_rate_limit;
29pub mod egress;
30pub mod event_log;
31pub mod events;
32pub mod external_agent;
33pub mod flow;
34pub mod harness;
35pub mod harness_auth;
36pub(crate) mod harness_crypto;
37pub mod harness_net;
38pub mod harness_system;
39pub mod harness_tenant;
40mod http;
41pub mod jsonrpc;
42pub(crate) mod limits;
43pub mod llm;
44pub mod llm_config;
45pub mod mcp;
46pub mod mcp_allowlist;
47pub mod mcp_auth;
48pub mod mcp_bulk_auth;
49pub mod mcp_card;
50pub mod mcp_elicit;
51pub mod mcp_file_upload;
52pub mod mcp_host;
53pub mod mcp_identity;
54pub mod mcp_oauth;
55pub mod mcp_presets;
56pub mod mcp_progress;
57pub mod mcp_protocol;
58pub mod mcp_registry;
59pub mod mcp_sampling;
60pub mod mcp_server;
61pub mod metadata;
62pub mod module_artifact;
63pub mod observability;
64pub mod orchestration;
65pub mod personas;
66pub mod process_sandbox;
67pub mod profile;
68pub mod provenance;
69pub mod provider_catalog;
70pub mod receipts;
71pub mod record_filter;
72pub mod redact;
73pub mod run_events;
74pub mod runtime_context;
75pub(crate) mod runtime_guards;
76pub mod runtime_limits;
77pub mod runtime_paths;
78pub mod schema;
79pub(crate) mod secret_patterns;
80pub mod secrets;
81pub mod security;
82pub mod session_bundle;
83pub mod session_timeline;
84pub mod sessions;
85pub(crate) mod shared_state;
86pub mod shells;
87pub mod skills;
88pub mod stdlib;
89pub mod stdlib_modules;
90pub mod step_runtime;
91pub mod store;
92pub(crate) mod synchronization;
93pub mod tenant;
94pub(crate) mod term;
95pub mod testbench;
96pub mod tool_annotations;
97pub mod tool_call_cancellations;
98pub mod tool_surface;
99pub mod tracing;
100pub mod triggers;
101pub mod trust_graph;
102pub(crate) mod url_encoding;
103pub mod user_dirs;
104
105/// Crate-wide deterministic clock mock used by stdlib time builtins, the
106/// trigger dispatcher, the cron scheduler, and Rust-side tests. Re-exports
107/// the long-lived implementation under `triggers::test_util::clock` so all
108/// callers go through one source of truth.
109pub mod clock_mock {
110    pub use crate::triggers::test_util::clock::{
111        active_mock_clock, advance, clear_overrides, install_override, instant_now, is_mocked,
112        now_ms, now_utc, sleep, ClockInstant, ClockOverrideGuard, MockClock,
113    };
114
115    /// Runtime audit for capabilities that observe real wall-clock or
116    /// monotonic time while a testbench mock is installed. See the module
117    /// docs for the full design.
118    pub mod leak_audit {
119        #[cfg(test)]
120        pub use crate::triggers::test_util::clock_leak::TEST_LOCK;
121        pub use crate::triggers::test_util::clock_leak::{
122            drain, instant_now, reset, snapshot, wall_now, ClockLeak,
123        };
124    }
125}
126
127pub mod typecheck;
128pub mod value;
129pub mod visible_text;
130mod vm;
131pub(crate) mod wait_for_graph;
132pub mod waitpoints;
133pub mod workspace_anchor;
134pub mod workspace_path;
135
136pub use builtin_id::BuiltinId;
137pub use call_budget::{
138    charge_mcp_call, charge_pg_query, install_mcp_call_budget, install_pg_query_budget,
139    McpCallBudgetGuard, PgQueryBudgetGuard,
140};
141pub use checkpoint::register_checkpoint_builtins;
142pub use chunk::*;
143pub use compiler::*;
144pub use connectors::{
145    active_connector_client, active_metrics_registry, clear_active_connector_clients,
146    clear_active_metrics_registry, connector_export_denied_builtin_reason,
147    connector_export_effect_class,
148    cron::{CatchupMode, CronConnector},
149    default_connector_export_policy,
150    harn_module::{
151        load_contract as load_harn_connector_contract, HarnConnector, HarnConnectorContract,
152    },
153    hmac::{verify_hmac_signed, SIGNATURE_VERIFY_AUDIT_TOPIC},
154    install_active_connector_clients, install_active_metrics_registry,
155    postprocess_normalized_event, ActivationHandle, ClientError, Connector, ConnectorClient,
156    ConnectorCtx, ConnectorError, ConnectorExportEffectClass, ConnectorHttpResponse,
157    ConnectorMetricsSnapshot, ConnectorNormalizeResult, ConnectorRegistry, GenericWebhookConnector,
158    HarnConnectorEffectPolicies, MetricsRegistry, PostNormalizeOutcome, ProviderPayloadSchema,
159    RateLimitConfig, RateLimiterFactory, RawInbound, StreamConnector, TriggerBinding, TriggerKind,
160    TriggerRegistry, WebhookSignatureVariant,
161};
162pub use corrections::{
163    append_correction_record, apply_corrections_to_policy, correction_query_filters_from_json,
164    correction_record_from_json, policy_with_corrections, query_correction_records,
165    CorrectionQueryFilters, CorrectionRecord, CorrectionScope, CORRECTIONS_TOPIC,
166    CORRECTION_EVENT_KIND, CORRECTION_SCHEMA_V0,
167};
168pub use harness::{
169    DenyEvent, Harness, HarnessCall, HarnessClock, HarnessCrypto, HarnessEnv, HarnessFs,
170    HarnessKind, HarnessLlm, HarnessNet, HarnessObs, HarnessProcess, HarnessRandom, HarnessStdio,
171    HarnessSystem, HarnessTenant, HarnessTerm, MockAwareClock, MockHarnessBuilder, VmHarness,
172};
173pub use harness_auth::{
174    current_auth_principal, enter_auth_principal, AuthPrincipal, AuthPrincipalScopeGuard,
175    MISSING_PRINCIPAL_MESSAGE,
176};
177pub use harness_net::{
178    bypass_enabled as net_policy_bypass_enabled, NetMatcher, NetPolicy, NetPolicyAudit,
179    NetPolicyDecision, NetPolicyDefault, NetPolicyRule, OnViolation, HARN_NET_POLICY_BYPASS_ENV,
180    NET_POLICY_AUDIT_TOPIC,
181};
182pub use harness_tenant::{
183    current_tenant_id, enter_tenant, TenantScopeGuard, MISSING_TENANT_MESSAGE,
184};
185pub use http::{register_http_builtins, reset_http_state};
186pub use llm::register_llm_builtins;
187pub use llm::trigger_predicate::TriggerPredicateBudget;
188pub use llm::{
189    current_agent_session_id, install_llm_cost_budget, install_llm_token_budget,
190    peek_llm_cost_budget, peek_llm_token_budget, register_session_end_hook, set_llm_cost_budget,
191    set_llm_token_budget, LlmBudgetGuard, LlmTokenBudgetGuard,
192};
193pub use mcp::{connect_mcp_server_from_json, connect_mcp_server_from_spec, register_mcp_builtins};
194pub use mcp_allowlist::{
195    build_catalog as build_mcp_catalog, catalog_for_request as mcp_catalog_for_request,
196    AdvertisedItem as McpAdvertisedItem, CatalogRequest as McpCatalogRequest, McpAllowlist,
197    McpAllowlistItem, McpCatalog, McpCatalogItem, McpCatalogServer, McpItemKind,
198    MCP_ALLOWLIST_SCHEMA_VERSION,
199};
200pub use mcp_card::{fetch_server_card, load_server_card_from_path, CardError};
201pub use mcp_host::{
202    cache_stats as mcp_host_cache_stats, set_allowlist as set_mcp_host_allowlist,
203    AllowlistDecision as McpHostAllowlistDecision, AllowlistGuard as McpHostAllowlistGuard,
204    BreakerState as McpHostBreakerState, CacheStats as McpHostCacheStats, McpHostStatus,
205    SpawnOptions as McpHostSpawnOptions, SupervisionPolicy as McpHostSupervisionPolicy,
206};
207pub use mcp_registry::{
208    active_handle as mcp_active_handle, ensure_active as mcp_ensure_active,
209    get_registration as mcp_get_registration, install_active as mcp_install_active,
210    is_registered as mcp_is_registered, register_servers as mcp_register_servers,
211    release as mcp_release, reset as mcp_reset_registry, snapshot_status as mcp_snapshot_status,
212    sweep_expired as mcp_sweep_expired, RegisteredMcpServer, RegistryStatus,
213};
214pub use mcp_server::{
215    take_mcp_serve_prompts, take_mcp_serve_registry, take_mcp_serve_resource_templates,
216    take_mcp_serve_resources, tool_registry_to_mcp_tools, McpServer,
217};
218pub use metadata::register_metadata_builtins;
219pub use observability::audit::{audit_events as audit_obs_events, AuditFinding, AuditFindingKind};
220pub use observability::request_id::{current_request_id, enter_request_id, RequestIdScopeGuard};
221pub use orchestration::{
222    benchmark_adapted_replay_pair, benchmark_replay_trace, build_replay_benchmark_report,
223    OpenCodeJsonlAdapter, ReplayBenchmarkCloudIngest, ReplayBenchmarkError,
224    ReplayBenchmarkFixtureReceipt, ReplayBenchmarkFixtureReport, ReplayBenchmarkMetrics,
225    ReplayBenchmarkReport, ReplayBenchmarkSuiteIdentity, ReplayBenchmarkSummary,
226    ReplayCategoryMetric, ReplayDebuggingProxyMetrics, ReplayRuntimeCostMetrics,
227    ReplayTraceAdapter, OPENCODE_JSONL_ADAPTER_ID, OPENCODE_JSONL_ADAPTER_SCHEMA_VERSION,
228    REPLAY_BENCHMARK_CLOUD_INGEST_KIND, REPLAY_BENCHMARK_REPORT_SCHEMA_VERSION,
229};
230pub use orchestration::{
231    canonicalize_run, first_divergence, run_replay_oracle_trace, ReplayAllowlistRule,
232    ReplayDivergence, ReplayExpectation, ReplayOracleError, ReplayOracleReport, ReplayOracleTrace,
233    ReplayTraceRun, ReplayTraceRunCounts, REPLAY_TRACE_SCHEMA_VERSION,
234};
235pub use orchestration::{
236    install_handoff_routes, snapshot_handoff_routes, HandoffRouteConfig,
237    HandoffRouteDecisionRecord, HandoffRouteTargetConfig,
238};
239pub use personas::{
240    disable_persona, fire_schedule as fire_persona_schedule, fire_trigger as fire_persona_trigger,
241    format_ms as format_persona_ms, now_ms as persona_now_ms, parse_rfc3339_ms as parse_persona_ms,
242    pause_persona, persona_status, record_persona_spend, register_persona_supervision_sink,
243    register_persona_value_sink, report_repair_worker_status, restore_persona_checkpoint,
244    resume_persona, PersonaAssignmentStatus, PersonaBudgetPolicy, PersonaBudgetStatus,
245    PersonaCheckpointAction, PersonaCheckpointRestoreOutcome, PersonaCheckpointRestoreRequest,
246    PersonaCheckpointResume, PersonaCheckpointUpdate, PersonaHandoffInboxItem, PersonaLease,
247    PersonaLifecycleState, PersonaQueuePositionUpdate, PersonaQueuedWork, PersonaReceiptUpdate,
248    PersonaRepairWorkerLifecycle, PersonaRepairWorkerStatusUpdate, PersonaRunCost,
249    PersonaRunReceipt, PersonaRuntimeBinding, PersonaStatus, PersonaSupervisionEvent,
250    PersonaSupervisionSink, PersonaSupervisionSinkRegistration, PersonaTriggerEnvelope,
251    PersonaValueEvent, PersonaValueEventKind, PersonaValueReceipt, PersonaValueSink,
252    PersonaValueSinkRegistration, StageDecl, StageExit, PERSONA_RUNTIME_TOPIC,
253};
254pub use provenance::{
255    build_signed_receipt, load_or_generate_agent_signing_key, verify_receipt, ProvenanceReceipt,
256    ReceiptBuildOptions, ReceiptVerificationReport,
257};
258pub use receipts::{
259    Receipt, ReceiptSink, ReceiptStatus, ReceiptValidationError, RedactingReceiptSink,
260    RedactionClass, RECEIPT_SCHEMA_ID, RECEIPT_SCHEMA_JSON, RECEIPT_SCHEMA_VERSION,
261};
262pub use record_filter::{normalize_record_filter_expression, CompiledRecordFilter};
263pub use runtime_limits::{
264    RuntimeLimitDescription, RuntimeLimitEntry, RuntimeLimits, RuntimeLimitsReport,
265    RUNTIME_LIMIT_DESCRIPTIONS,
266};
267pub use schema::json_to_vm_value;
268pub use sessions::{
269    CreateSession, ExpireSession, Session, SessionAttributes, SessionError, SessionStore,
270    TouchSession, SESSIONS_TOPIC,
271};
272pub use stdlib::hitl::{
273    append_hitl_response, ApprovalRequest, HitlHostResponse, HITL_APPROVALS_TOPIC,
274    HITL_DUAL_CONTROL_TOPIC, HITL_ESCALATIONS_TOPIC, HITL_QUESTIONS_TOPIC,
275};
276pub use stdlib::host::{clear_host_call_bridge, set_host_call_bridge, HostCallBridge};
277pub use stdlib::http_response::{
278    parse_envelope as parse_http_envelope, HttpEnvelope, HttpHeaderValue, WsUpgradeSpec,
279    HTTP_RESPONSE_TAG_KEY, HTTP_RESPONSE_TAG_VERSION,
280};
281#[cfg(feature = "postgres")]
282pub use stdlib::install_shared_pool_registry;
283pub use stdlib::io::{set_stdout_passthrough, take_stderr_buffer};
284pub use stdlib::long_running::cancel_handle as cancel_long_running_handle;
285pub use stdlib::observability::install_default_backend as install_obs_default_backend;
286pub use stdlib::secret_scan::{
287    append_secret_scan_audit, audit_secret_scan_active, scan_content as secret_scan_content,
288    SecretFinding, SECRET_SCAN_AUDIT_TOPIC,
289};
290pub use stdlib::template::{
291    lookup_prompt_consumers, lookup_prompt_span, prompt_render_indices, record_prompt_render_index,
292    PromptSourceSpan, PromptSpanKind,
293};
294pub use stdlib::waitpoint::{
295    process_waitpoint_resume_event, service_waitpoints_once, WAITPOINT_RESUME_TOPIC,
296};
297pub use stdlib::workflow_messages::{
298    workflow_pause_for_base, workflow_publish_query_for_base, workflow_query_for_base,
299    workflow_respond_update_for_base, workflow_resume_for_base, workflow_signal_for_base,
300    workflow_update_for_base, WorkflowMailboxState,
301};
302pub use stdlib::{
303    register_agent_stdlib, register_core_stdlib, register_io_stdlib, register_vm_stdlib,
304};
305pub use store::register_store_builtins;
306pub use tenant::{
307    tenant_event_topic_prefix, tenant_secret_namespace, tenant_topic, validate_tenant_id, ApiKeyId,
308    TenantApiKeyRecord, TenantBudget, TenantEventLog, TenantRecord, TenantRegistrySnapshot,
309    TenantResolutionError, TenantScope, TenantSecretProvider, TenantStatus, TenantStore,
310    TENANT_EVENT_TOPIC_PREFIX, TENANT_REGISTRY_DIR, TENANT_REGISTRY_FILE,
311    TENANT_SECRET_NAMESPACE_PREFIX,
312};
313pub use triggers::{
314    append_dispatch_cancel_request, begin_in_flight, binding_autonomy_budget_would_exceed,
315    binding_budget_would_exceed, binding_version_as_of, classify_trigger_dlq_error,
316    clear_dispatcher_state, clear_orchestrator_budget, clear_trigger_registry, drain,
317    dynamic_deregister, dynamic_register, expected_predicate_cost_usd_micros, finish_in_flight,
318    install_manifest_triggers, install_orchestrator_budget, install_provider_catalog,
319    micros_to_usd, note_autonomous_decision, note_orchestrator_budget_cost,
320    orchestrator_budget_would_exceed, parse_flow_control_duration, pause, pin_trigger_binding,
321    provider_metadata, record_predicate_cost_sample, redact_headers, register_provider_schema,
322    registered_provider_metadata, registered_provider_schema_names, reset_binding_budget_windows,
323    reset_provider_catalog, reset_provider_catalog_with, resolve_live_or_as_of,
324    resolve_live_trigger_binding, resolve_trigger_binding_as_of, resume,
325    run_trigger_harness_fixture, scheduler_in_flight_by_key, scheduler_ready_stats_by_key,
326    snapshot_dispatcher_stats, snapshot_orchestrator_budget, snapshot_trigger_bindings,
327    unpin_trigger_binding, usd_to_micros, worker_claims_topic_name, worker_job_topic_name,
328    worker_response_topic_name, ClaimedWorkerJob, DispatchCancelRequest, DispatchError,
329    DispatchOutcome, DispatchStatus, Dispatcher, DispatcherDrainReport, DispatcherStatsSnapshot,
330    FairnessKey, HeaderRedactionPolicy, InboxIndex, NotionPolledChangeEvent,
331    OrchestratorBudgetConfig, OrchestratorBudgetSnapshot, ProviderCatalog, ProviderCatalogError,
332    ProviderId, ProviderMetadata, ProviderOutboundMethod, ProviderPayload, ProviderRuntimeMetadata,
333    ProviderSchema, ProviderSecretRequirement, ReadyKeyStats, RecordedTriggerBinding, RetryPolicy,
334    SchedulableJob, SchedulerKeyStat, SchedulerPolicy, SchedulerSnapshot, SchedulerState,
335    SchedulerStrategy, SignatureStatus, SignatureVerificationMetadata, StreamEventPayload,
336    TenantId, TraceId, TriggerBatchConfig, TriggerBindingSnapshot, TriggerBindingSource,
337    TriggerBindingSpec, TriggerBudgetExhaustionStrategy, TriggerConcurrencyConfig,
338    TriggerDebounceConfig, TriggerDispatchOutcome, TriggerEvent, TriggerEventId,
339    TriggerExpressionSpec, TriggerFlowControlConfig, TriggerHandlerSpec, TriggerHarnessResult,
340    TriggerId, TriggerMetricsSnapshot, TriggerPredicateSpec, TriggerPriorityOrderConfig,
341    TriggerRateLimitConfig, TriggerRegistryError, TriggerRetryConfig, TriggerSingletonConfig,
342    TriggerState, TriggerThrottleConfig, WorkerQueue, WorkerQueueClaimHandle,
343    WorkerQueueEnqueueReceipt, WorkerQueueInspectSnapshot, WorkerQueueJob, WorkerQueueJobState,
344    WorkerQueuePriority, WorkerQueueResponseRecord, WorkerQueueState, WorkerQueueSummary,
345    DEFAULT_INBOX_RETENTION_DAYS, DEFAULT_STARVATION_AGE_MS, TRIGGERS_LIFECYCLE_TOPIC,
346    TRIGGER_ATTEMPTS_TOPIC, TRIGGER_CANCEL_REQUESTS_TOPIC, TRIGGER_DLQ_TOPIC,
347    TRIGGER_INBOX_CLAIMS_TOPIC, TRIGGER_INBOX_ENVELOPES_TOPIC, TRIGGER_INBOX_LEGACY_TOPIC,
348    TRIGGER_OPERATION_AUDIT_TOPIC, TRIGGER_OUTBOX_TOPIC, TRIGGER_TEST_FIXTURES,
349    WORKER_QUEUE_CATALOG_TOPIC,
350};
351pub use trust_graph::{
352    append_active_trust_record, append_trust_record, export_trust_chain,
353    group_trust_records_by_trace, policy_for_agent, policy_for_autonomy_tier,
354    query_trust_graph_records, query_trust_records, resolve_agent_autonomy_tier,
355    summarize_trust_records, topic_for_agent, trust_score_for, verify_trust_chain, AutonomyTier,
356    TrustAgentSummary, TrustChainExport, TrustChainExportMetadata, TrustChainExportProducer,
357    TrustChainReport, TrustGraphRecord, TrustOutcome, TrustQueryFilters, TrustRecord,
358    TrustRecordActionKind, TrustScore, TrustTraceGroup, METADATA_KEY_EFFECTS_GRANT,
359    METADATA_KEY_EFFECTS_USED, METADATA_KEY_PARENT_RECORD_ID, OPENTRUSTGRAPH_ACCEPTED_SCHEMAS,
360    OPENTRUSTGRAPH_CHAIN_SCHEMA_V0, OPENTRUSTGRAPH_SCHEMA_V0, OPENTRUSTGRAPH_SCHEMA_V0_1,
361    TRUST_ACTION_RELEASE, TRUST_GRAPH_GLOBAL_TOPIC, TRUST_GRAPH_LEGACY_GLOBAL_TOPIC,
362    TRUST_GRAPH_LEGACY_TOPIC_PREFIX, TRUST_GRAPH_RECORDS_TOPIC, TRUST_GRAPH_TOPIC_PREFIX,
363};
364pub use value::*;
365pub use vm::*;
366
367#[cfg(feature = "vm-bench-internals")]
368#[doc(hidden)]
369pub mod bench_internals;
370
371/// Lex, parse, type-check, and compile source to bytecode in one call.
372/// Bails on the first type error. For callers that need diagnostics
373/// rather than early exit, use `harn_parser::check_source` directly
374/// and then call `Compiler::new().compile(&program)`.
375pub fn compile_source(source: &str) -> Result<Chunk, String> {
376    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
377    Compiler::new().compile(&program).map_err(|e| e.to_string())
378}
379
380/// Same as [`compile_source`] but compiles a specific named pipeline as
381/// the program entry point instead of the default-pipeline-or-first
382/// selection rule. Returns a runtime error when no pipeline with
383/// `pipeline_name` exists in the source.
384pub fn compile_source_named(source: &str, pipeline_name: &str) -> Result<Chunk, String> {
385    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
386    let has_pipeline = program.iter().any(|sn| {
387        let (_, inner) = harn_parser::peel_attributes(sn);
388        matches!(&inner.node, harn_parser::Node::Pipeline { name, .. } if name == pipeline_name)
389    });
390    if !has_pipeline {
391        return Err(format!("no pipeline named `{pipeline_name}` in source"));
392    }
393    Compiler::new()
394        .compile_named(&program, pipeline_name)
395        .map_err(|e| e.to_string())
396}
397
398pub fn json_schema_for_type_expr(type_expr: &harn_parser::TypeExpr) -> Option<serde_json::Value> {
399    let schema = compiler::Compiler::type_expr_to_schema_value(type_expr)?;
400    let json_schema = schema::schema_to_json_schema_value(&schema).ok()?;
401    Some(llm::vm_value_to_json(&json_schema))
402}
403
404pub fn json_schema_for_typed_params(params: &[harn_parser::TypedParam]) -> serde_json::Value {
405    let mut properties = serde_json::Map::new();
406    let mut required = Vec::new();
407
408    for param in params {
409        let param_schema = param
410            .type_expr
411            .as_ref()
412            .and_then(json_schema_for_type_expr)
413            .unwrap_or_else(|| serde_json::json!({}));
414        if param.default_value.is_none() {
415            required.push(serde_json::Value::String(param.name.clone()));
416        }
417        properties.insert(param.name.clone(), param_schema);
418    }
419
420    let mut schema = serde_json::Map::new();
421    schema.insert(
422        "type".to_string(),
423        serde_json::Value::String("object".to_string()),
424    );
425    schema.insert(
426        "properties".to_string(),
427        serde_json::Value::Object(properties),
428    );
429    if !required.is_empty() {
430        schema.insert("required".to_string(), serde_json::Value::Array(required));
431    }
432    serde_json::Value::Object(schema)
433}
434
435/// Reset all thread-local state that can leak between test runs.
436pub fn reset_thread_local_state() {
437    llm::reset_llm_state();
438    // `reset_thread_local_state` only runs in sequential / separate-process
439    // contexts (the CLI test runner between cases, integration-test binaries,
440    // per-job worker resets) — never from the parallel in-process unit tests
441    // that share the rate-limiter registry. So unlike `reset_llm_state`, it is
442    // safe (and necessary for test isolation) to fully wipe the registry here,
443    // clearing retry-after cooldowns that would otherwise stall a later test's
444    // LLM call under a paused clock.
445    llm::reset_rate_limit_registry();
446    llm_config::clear_user_overrides();
447    http::reset_http_state();
448    channels::reset_channel_state();
449    event_log::reset_active_event_log();
450    egress::clear_explicit_egress_policy_requirement_for_host();
451    egress::clear_ssrf_guard_requirement_for_host();
452    stdlib::reset_stdlib_state();
453    connectors::clear_active_connector_clients();
454    orchestration::clear_runtime_hooks();
455    orchestration::clear_execution_policy_stacks();
456    orchestration::clear_command_policies();
457    orchestration::clear_pipeline_on_finish();
458    orchestration::reset_lifecycle_receipt_registry();
459    orchestration::agent_inbox::reset();
460    tool_call_cancellations::reset_registry();
461    redact::clear_policy_stack();
462    security::reset_thread_state();
463    triggers::clear_dispatcher_state();
464    triggers::clear_trigger_registry();
465    events::reset_event_sinks();
466    tracing::set_tracing_enabled(false);
467    tracing::reset_tracing();
468    agent_events::reset_all_sinks();
469    agent_sessions::reset_session_store();
470    mcp_registry::reset();
471    mcp_host::reset_for_tests();
472    call_budget::reset_call_budget_state();
473    clock_mock::leak_audit::reset();
474}
475
476#[cfg(test)]
477mod reset_leak_tests {
478    //! Regression coverage for harn#2660: process-/thread-global
479    //! registries that accumulated one entry per test because they were
480    //! never drained by `reset_thread_local_state`. Each case populates a
481    //! registry through its real entry point, runs the reset, and asserts
482    //! the registry is empty again.
483    use super::*;
484    use crate::value::VmValue;
485
486    #[test]
487    fn reset_drains_agent_inbox() {
488        orchestration::agent_inbox::reset();
489        orchestration::agent_inbox::push("sess-2660", "note", "leak", "test");
490        assert!(orchestration::agent_inbox::session_count() > 0);
491        reset_thread_local_state();
492        assert_eq!(
493            orchestration::agent_inbox::session_count(),
494            0,
495            "agent_inbox must be empty after reset"
496        );
497    }
498
499    #[test]
500    fn reset_drains_tool_call_cancellation_registry() {
501        tool_call_cancellations::reset_registry();
502        // Leak the guard so the entry survives until the reset runs —
503        // this mirrors a dispatch abandoned mid-flight.
504        let registered = tool_call_cancellations::register("sess-2660", "call-1", "tool");
505        if let Some((_handle, guard)) = registered {
506            std::mem::forget(guard);
507        }
508        assert!(tool_call_cancellations::registry_len() > 0);
509        reset_thread_local_state();
510        assert_eq!(
511            tool_call_cancellations::registry_len(),
512            0,
513            "tool-call cancellation registry must be empty after reset"
514        );
515    }
516
517    #[test]
518    fn reset_drains_routing_policy_registry() {
519        llm::routing::clear_policy_registry();
520        let mut config: crate::value::DictMap = crate::value::DictMap::new();
521        config.insert(
522            "chain".to_string(),
523            VmValue::List(std::sync::Arc::new(vec![VmValue::String(
524                std::sync::Arc::from("mock:mock"),
525            )])),
526        );
527        llm::routing::build_routing_policy(&config).expect("intern a routing policy");
528        assert!(llm::routing::policy_registry_len() > 0);
529        reset_thread_local_state();
530        assert_eq!(
531            llm::routing::policy_registry_len(),
532            0,
533            "routing policy registry must be empty after reset"
534        );
535    }
536}