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