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