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