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