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