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_metadata, take_mcp_serve_prompts, take_mcp_serve_registry,
244    take_mcp_serve_resource_templates, take_mcp_serve_resources, tool_registry_to_mcp_tools,
245    McpServer, McpServerMetadata,
246};
247pub use metadata::register_metadata_builtins;
248pub use observability::audit::{audit_events as audit_obs_events, AuditFinding, AuditFindingKind};
249pub use observability::request_id::{current_request_id, enter_request_id, RequestIdScopeGuard};
250pub use orchestration::{
251    benchmark_adapted_replay_pair, benchmark_replay_trace, build_replay_benchmark_report,
252    OpenCodeJsonlAdapter, ReplayBenchmarkCloudIngest, ReplayBenchmarkError,
253    ReplayBenchmarkFixtureReceipt, ReplayBenchmarkFixtureReport, ReplayBenchmarkMetrics,
254    ReplayBenchmarkReport, ReplayBenchmarkSuiteIdentity, ReplayBenchmarkSummary,
255    ReplayCategoryMetric, ReplayDebuggingProxyMetrics, ReplayRuntimeCostMetrics,
256    ReplayTraceAdapter, OPENCODE_JSONL_ADAPTER_ID, OPENCODE_JSONL_ADAPTER_SCHEMA_VERSION,
257    REPLAY_BENCHMARK_CLOUD_INGEST_KIND, REPLAY_BENCHMARK_REPORT_SCHEMA_VERSION,
258};
259pub use orchestration::{
260    canonicalize_run, first_divergence, run_replay_oracle_trace, ReplayAllowlistRule,
261    ReplayDivergence, ReplayExpectation, ReplayOracleError, ReplayOracleReport, ReplayOracleTrace,
262    ReplayTraceRun, ReplayTraceRunCounts, REPLAY_TRACE_SCHEMA_VERSION,
263};
264pub use orchestration::{
265    install_handoff_routes, snapshot_handoff_routes, HandoffRouteConfig,
266    HandoffRouteDecisionRecord, HandoffRouteTargetConfig,
267};
268pub use personas::{
269    disable_persona, fire_schedule as fire_persona_schedule, fire_trigger as fire_persona_trigger,
270    format_ms as format_persona_ms, now_ms as persona_now_ms, parse_rfc3339_ms as parse_persona_ms,
271    pause_persona, persona_status, record_persona_spend, register_persona_supervision_sink,
272    register_persona_value_sink, report_repair_worker_status, restore_persona_checkpoint,
273    resume_persona, PersonaAssignmentStatus, PersonaBudgetPolicy, PersonaBudgetStatus,
274    PersonaCheckpointAction, PersonaCheckpointRestoreOutcome, PersonaCheckpointRestoreRequest,
275    PersonaCheckpointResume, PersonaCheckpointUpdate, PersonaHandoffInboxItem, PersonaLease,
276    PersonaLifecycleState, PersonaQueuePositionUpdate, PersonaQueuedWork, PersonaReceiptUpdate,
277    PersonaRepairWorkerLifecycle, PersonaRepairWorkerStatusUpdate, PersonaRunCost,
278    PersonaRunReceipt, PersonaRuntimeBinding, PersonaStatus, PersonaSupervisionEvent,
279    PersonaSupervisionSink, PersonaSupervisionSinkRegistration, PersonaTriggerEnvelope,
280    PersonaValueEvent, PersonaValueEventKind, PersonaValueReceipt, PersonaValueSink,
281    PersonaValueSinkRegistration, StageDecl, StageExit, PERSONA_RUNTIME_TOPIC,
282};
283pub use provenance::{
284    build_signed_receipt, load_or_generate_agent_signing_key, verify_receipt, ProvenanceReceipt,
285    ReceiptBuildOptions, ReceiptVerificationReport,
286};
287pub use receipts::{
288    Receipt, ReceiptSink, ReceiptStatus, ReceiptValidationError, RedactingReceiptSink,
289    RedactionClass, RECEIPT_SCHEMA_ID, RECEIPT_SCHEMA_JSON, RECEIPT_SCHEMA_VERSION,
290};
291pub use record_filter::{normalize_record_filter_expression, CompiledRecordFilter};
292pub use runtime_limits::{
293    RuntimeLimitDescription, RuntimeLimitEntry, RuntimeLimits, RuntimeLimitsReport,
294    RUNTIME_LIMIT_DESCRIPTIONS,
295};
296pub use schema::json_to_vm_value;
297pub use sessions::{
298    CreateSession, ExpireSession, Session, SessionAttributes, SessionError, SessionStore,
299    TouchSession, SESSIONS_TOPIC,
300};
301pub use stdlib::hitl::{
302    append_hitl_response, ApprovalRequest, HitlHostResponse, HITL_APPROVALS_TOPIC,
303    HITL_DUAL_CONTROL_TOPIC, HITL_ESCALATIONS_TOPIC, HITL_QUESTIONS_TOPIC,
304};
305pub use stdlib::host::{clear_host_call_bridge, set_host_call_bridge, HostCallBridge};
306pub use stdlib::http_response::{
307    parse_envelope as parse_http_envelope, HttpEnvelope, HttpHeaderValue, WsUpgradeSpec,
308    HTTP_RESPONSE_TAG_KEY, HTTP_RESPONSE_TAG_VERSION,
309};
310#[cfg(feature = "postgres")]
311pub use stdlib::install_shared_pool_registry;
312pub use stdlib::io::{set_stdout_passthrough, take_stderr_buffer};
313pub use stdlib::long_running::cancel_handle as cancel_long_running_handle;
314pub use stdlib::observability::install_default_backend as install_obs_default_backend;
315pub use stdlib::secret_scan::{
316    append_secret_scan_audit, audit_secret_scan_active, scan_content as secret_scan_content,
317    SecretFinding, SECRET_SCAN_AUDIT_TOPIC,
318};
319pub use stdlib::template::{
320    lookup_prompt_consumers, lookup_prompt_span, prompt_render_indices, record_prompt_render_index,
321    PromptSourceSpan, PromptSpanKind,
322};
323pub use stdlib::waitpoint::{
324    process_waitpoint_resume_event, service_waitpoints_once, WAITPOINT_RESUME_TOPIC,
325};
326pub use stdlib::workflow_messages::{
327    workflow_pause_for_base, workflow_publish_query_for_base, workflow_query_for_base,
328    workflow_respond_update_for_base, workflow_resume_for_base, workflow_signal_for_base,
329    workflow_update_for_base, WorkflowMailboxState,
330};
331pub use stdlib::{
332    register_agent_stdlib, register_core_stdlib, register_io_stdlib, register_vm_stdlib,
333};
334pub use store::register_store_builtins;
335pub use tenant::{
336    tenant_event_topic_prefix, tenant_secret_namespace, tenant_topic, validate_tenant_id, ApiKeyId,
337    TenantApiKeyRecord, TenantBudget, TenantEventLog, TenantRecord, TenantRegistrySnapshot,
338    TenantResolutionError, TenantScope, TenantSecretProvider, TenantStatus, TenantStore,
339    TENANT_EVENT_TOPIC_PREFIX, TENANT_REGISTRY_DIR, TENANT_REGISTRY_FILE,
340    TENANT_SECRET_NAMESPACE_PREFIX,
341};
342pub use triggers::{
343    append_dispatch_cancel_request, begin_in_flight, binding_autonomy_budget_would_exceed,
344    binding_budget_would_exceed, binding_version_as_of, classify_trigger_dlq_error,
345    clear_dispatcher_state, clear_orchestrator_budget, clear_trigger_registry, drain,
346    dynamic_deregister, dynamic_register, expected_predicate_cost_usd_micros, finish_in_flight,
347    install_manifest_triggers, install_orchestrator_budget, install_provider_catalog,
348    micros_to_usd, note_autonomous_decision, note_orchestrator_budget_cost,
349    orchestrator_budget_would_exceed, parse_flow_control_duration, pause, pin_trigger_binding,
350    provider_metadata, record_predicate_cost_sample, redact_headers, register_provider_schema,
351    registered_provider_metadata, registered_provider_schema_names, reset_binding_budget_windows,
352    reset_provider_catalog, reset_provider_catalog_with, resolve_live_or_as_of,
353    resolve_live_trigger_binding, resolve_trigger_binding_as_of, resume,
354    run_trigger_harness_fixture, scheduler_in_flight_by_key, scheduler_ready_stats_by_key,
355    snapshot_dispatcher_stats, snapshot_orchestrator_budget, snapshot_trigger_bindings,
356    unpin_trigger_binding, usd_to_micros, worker_claims_topic_name, worker_job_topic_name,
357    worker_response_topic_name, ClaimedWorkerJob, DispatchCancelRequest, DispatchError,
358    DispatchOutcome, DispatchStatus, Dispatcher, DispatcherDrainReport, DispatcherStatsSnapshot,
359    FairnessKey, HeaderRedactionPolicy, InboxIndex, NotionPolledChangeEvent,
360    OrchestratorBudgetConfig, OrchestratorBudgetSnapshot, ProviderCatalog, ProviderCatalogError,
361    ProviderId, ProviderMetadata, ProviderOutboundMethod, ProviderPayload, ProviderRuntimeMetadata,
362    ProviderSchema, ProviderSecretRequirement, ReadyKeyStats, RecordedTriggerBinding, RetryPolicy,
363    SchedulableJob, SchedulerKeyStat, SchedulerPolicy, SchedulerSnapshot, SchedulerState,
364    SchedulerStrategy, SignatureStatus, SignatureVerificationMetadata, StreamEventPayload,
365    TenantId, TraceId, TriggerBatchConfig, TriggerBindingSnapshot, TriggerBindingSource,
366    TriggerBindingSpec, TriggerBudgetExhaustionStrategy, TriggerConcurrencyConfig,
367    TriggerDebounceConfig, TriggerDispatchOutcome, TriggerEvent, TriggerEventId,
368    TriggerExpressionSpec, TriggerFlowControlConfig, TriggerHandlerSpec, TriggerHarnessResult,
369    TriggerId, TriggerMetricsSnapshot, TriggerPredicateSpec, TriggerPriorityOrderConfig,
370    TriggerRateLimitConfig, TriggerRegistryError, TriggerRetryConfig, TriggerSingletonConfig,
371    TriggerState, TriggerThrottleConfig, WorkerQueue, WorkerQueueClaimHandle,
372    WorkerQueueEnqueueReceipt, WorkerQueueInspectSnapshot, WorkerQueueJob, WorkerQueueJobState,
373    WorkerQueuePriority, WorkerQueueResponseRecord, WorkerQueueState, WorkerQueueSummary,
374    DEFAULT_INBOX_RETENTION_DAYS, DEFAULT_STARVATION_AGE_MS, TRIGGERS_LIFECYCLE_TOPIC,
375    TRIGGER_ATTEMPTS_TOPIC, TRIGGER_CANCEL_REQUESTS_TOPIC, TRIGGER_DLQ_TOPIC,
376    TRIGGER_INBOX_CLAIMS_TOPIC, TRIGGER_INBOX_ENVELOPES_TOPIC, TRIGGER_INBOX_LEGACY_TOPIC,
377    TRIGGER_INBOX_OBSERVABILITY_TOPIC, TRIGGER_OPERATION_AUDIT_TOPIC, TRIGGER_OUTBOX_TOPIC,
378    TRIGGER_TEST_FIXTURES, WORKER_QUEUE_CATALOG_TOPIC,
379};
380pub use trust_graph::{
381    append_active_scope_attenuation_alert, append_active_trust_record,
382    append_scope_attenuation_alert, append_trust_record, export_trust_chain,
383    group_trust_records_by_trace, policy_for_agent, policy_for_autonomy_tier,
384    query_trust_graph_records, query_trust_records, resolve_agent_autonomy_tier,
385    summarize_trust_records, topic_for_agent, trust_score_for, verify_trust_chain, AutonomyTier,
386    TrustAgentSummary, TrustChainExport, TrustChainExportMetadata, TrustChainExportProducer,
387    TrustChainReport, TrustGraphRecord, TrustOutcome, TrustQueryFilters, TrustRecord,
388    TrustRecordActionKind, TrustScore, TrustTraceGroup, METADATA_KEY_ACTOR_CHAIN,
389    METADATA_KEY_ACTOR_CHAIN_ALERT, METADATA_KEY_EFFECTS_GRANT, METADATA_KEY_EFFECTS_USED,
390    METADATA_KEY_PARENT_RECORD_ID, OPENTRUSTGRAPH_ACCEPTED_SCHEMAS, OPENTRUSTGRAPH_CHAIN_SCHEMA_V0,
391    OPENTRUSTGRAPH_SCHEMA_V0, OPENTRUSTGRAPH_SCHEMA_V0_1, TRUST_ACTION_RELEASE,
392    TRUST_GRAPH_GLOBAL_TOPIC, TRUST_GRAPH_LEGACY_GLOBAL_TOPIC, TRUST_GRAPH_LEGACY_TOPIC_PREFIX,
393    TRUST_GRAPH_RECORDS_TOPIC, TRUST_GRAPH_TOPIC_PREFIX,
394};
395pub use value::*;
396pub use vm::*;
397
398#[cfg(feature = "vm-bench-internals")]
399#[doc(hidden)]
400pub mod bench_internals;
401
402/// Lex, parse, type-check, and compile source to bytecode in one call.
403/// Bails on the first type error. For callers that need diagnostics
404/// rather than early exit, use `harn_parser::check_source` directly
405/// and then call `Compiler::new().compile(&program)`.
406pub fn compile_source(source: &str) -> Result<Chunk, String> {
407    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
408    Compiler::new().compile(&program).map_err(|e| e.to_string())
409}
410
411/// Same as [`compile_source`] but compiles a specific named pipeline as
412/// the program entry point instead of the default-pipeline-or-first
413/// selection rule. Returns a runtime error when no pipeline with
414/// `pipeline_name` exists in the source.
415pub fn compile_source_named(source: &str, pipeline_name: &str) -> Result<Chunk, String> {
416    let program = harn_parser::check_source_strict(source).map_err(|e| e.to_string())?;
417    let has_pipeline = program.iter().any(|sn| {
418        let (_, inner) = harn_parser::peel_attributes(sn);
419        matches!(&inner.node, harn_parser::Node::Pipeline { name, .. } if name == pipeline_name)
420    });
421    if !has_pipeline {
422        return Err(format!("no pipeline named `{pipeline_name}` in source"));
423    }
424    Compiler::new()
425        .compile_named(&program, pipeline_name)
426        .map_err(|e| e.to_string())
427}
428
429pub fn json_schema_for_type_expr(type_expr: &harn_parser::TypeExpr) -> Option<serde_json::Value> {
430    let schema = compiler::Compiler::type_expr_to_schema_value(type_expr)?;
431    let json_schema = schema::schema_to_json_schema_value(&schema).ok()?;
432    Some(llm::vm_value_to_json(&json_schema))
433}
434
435pub fn json_schema_for_typed_params(params: &[harn_parser::TypedParam]) -> serde_json::Value {
436    let mut properties = serde_json::Map::new();
437    let mut required = Vec::new();
438
439    for param in params {
440        let param_schema = param
441            .type_expr
442            .as_ref()
443            .and_then(json_schema_for_type_expr)
444            .unwrap_or_else(|| serde_json::json!({}));
445        if param.default_value.is_none() {
446            required.push(serde_json::Value::String(param.name.clone()));
447        }
448        properties.insert(param.name.clone(), param_schema);
449    }
450
451    let mut schema = serde_json::Map::new();
452    schema.insert(
453        "type".to_string(),
454        serde_json::Value::String("object".to_string()),
455    );
456    schema.insert(
457        "properties".to_string(),
458        serde_json::Value::Object(properties),
459    );
460    if !required.is_empty() {
461        schema.insert("required".to_string(), serde_json::Value::Array(required));
462    }
463    serde_json::Value::Object(schema)
464}
465
466fn reset_llm_state_for_thread_reset() {
467    llm::reset_llm_state();
468    #[cfg(test)]
469    reset_thread_local_state_test_hooks::before_llm_global_reset();
470    // This full wipe is necessary between Harn programs to clear durable
471    // cooldowns that would otherwise stall a later run under a paused clock.
472    llm::reset_rate_limit_registry();
473    llm_config::clear_user_overrides();
474}
475
476#[cfg(test)]
477mod reset_thread_local_state_test_hooks {
478    use std::sync::{Arc, Mutex, OnceLock};
479
480    type Hook = Arc<dyn Fn() + Send + Sync + 'static>;
481
482    static BEFORE_LLM_GLOBAL_RESET: OnceLock<Mutex<Option<Hook>>> = OnceLock::new();
483
484    fn before_llm_global_reset_hook() -> &'static Mutex<Option<Hook>> {
485        BEFORE_LLM_GLOBAL_RESET.get_or_init(|| Mutex::new(None))
486    }
487
488    pub(crate) struct HookGuard;
489
490    impl Drop for HookGuard {
491        fn drop(&mut self) {
492            let mut hook = before_llm_global_reset_hook()
493                .lock()
494                .unwrap_or_else(std::sync::PoisonError::into_inner);
495            *hook = None;
496        }
497    }
498
499    pub(crate) fn install_before_llm_global_reset(hook: Hook) -> HookGuard {
500        let mut slot = before_llm_global_reset_hook()
501            .lock()
502            .unwrap_or_else(std::sync::PoisonError::into_inner);
503        *slot = Some(hook);
504        HookGuard
505    }
506
507    pub(crate) fn before_llm_global_reset() {
508        let hook = before_llm_global_reset_hook()
509            .lock()
510            .unwrap_or_else(std::sync::PoisonError::into_inner)
511            .clone();
512        if let Some(hook) = hook {
513            hook();
514        }
515    }
516}
517
518/// Reset all thread-local state that can leak between test runs.
519pub fn reset_thread_local_state() {
520    #[cfg(test)]
521    {
522        // `reset_thread_local_state` is also used by in-process unit tests. It
523        // clears process-global LLM config/rate-limit state, so share the same
524        // lock used by LLM env tests; otherwise a sibling reset can erase a
525        // parked rate-limit test's registry while the test still owns a permit.
526        let _guard = llm::env_guard();
527        reset_llm_state_for_thread_reset();
528    }
529    #[cfg(not(test))]
530    reset_llm_state_for_thread_reset();
531
532    http::reset_http_state();
533    channels::reset_channel_state();
534    event_log::reset_active_event_log();
535    egress::clear_explicit_egress_policy_requirement_for_host();
536    egress::clear_ssrf_guard_requirement_for_host();
537    stdlib::reset_stdlib_state();
538    connectors::clear_active_connector_clients();
539    orchestration::clear_runtime_hooks();
540    orchestration::clear_execution_policy_stacks();
541    orchestration::clear_command_policies();
542    orchestration::clear_pipeline_on_finish();
543    orchestration::reset_lifecycle_receipt_registry();
544    orchestration::agent_inbox::reset();
545    tool_call_cancellations::reset_registry();
546    redact::clear_policy_stack();
547    security::reset_thread_state();
548    triggers::clear_dispatcher_state();
549    triggers::clear_trigger_registry();
550    events::reset_event_sinks();
551    tracing::set_tracing_enabled(false);
552    tracing::reset_tracing();
553    agent_events::reset_all_sinks();
554    agent_sessions::reset_session_store();
555    mcp_registry::reset();
556    mcp_host::reset_for_tests();
557    call_budget::reset_call_budget_state();
558    clock_mock::leak_audit::reset();
559}
560
561#[cfg(test)]
562mod reset_leak_tests {
563    //! Regression coverage for harn#2660: process-/thread-global
564    //! registries that accumulated one entry per test because they were
565    //! never drained by `reset_thread_local_state`. Each case populates a
566    //! registry through its real entry point, runs the reset, and asserts
567    //! the registry is empty again.
568    use super::*;
569    use crate::value::VmValue;
570
571    #[test]
572    fn reset_drains_agent_inbox() {
573        orchestration::agent_inbox::reset();
574        orchestration::agent_inbox::push("sess-2660", "note", "leak", "test");
575        assert!(orchestration::agent_inbox::session_count() > 0);
576        reset_thread_local_state();
577        assert_eq!(
578            orchestration::agent_inbox::session_count(),
579            0,
580            "agent_inbox must be empty after reset"
581        );
582    }
583
584    #[test]
585    fn reset_drains_tool_call_cancellation_registry() {
586        tool_call_cancellations::reset_registry();
587        // Leak the guard so the entry survives until the reset runs —
588        // this mirrors a dispatch abandoned mid-flight.
589        let registered = tool_call_cancellations::register("sess-2660", "call-1", "tool");
590        if let Some((_handle, guard)) = registered {
591            std::mem::forget(guard);
592        }
593        assert!(tool_call_cancellations::registry_len() > 0);
594        reset_thread_local_state();
595        assert_eq!(
596            tool_call_cancellations::registry_len(),
597            0,
598            "tool-call cancellation registry must be empty after reset"
599        );
600    }
601
602    #[test]
603    fn reset_drains_routing_policy_registry() {
604        llm::routing::clear_policy_registry();
605        let mut config: crate::value::DictMap = crate::value::DictMap::new();
606        config.insert(
607            crate::value::intern_key("chain"),
608            VmValue::List(std::sync::Arc::new(vec![VmValue::String(
609                arcstr::ArcStr::from("mock:mock"),
610            )])),
611        );
612        llm::routing::build_routing_policy(&config).expect("intern a routing policy");
613        assert!(llm::routing::policy_registry_len() > 0);
614        reset_thread_local_state();
615        assert_eq!(
616            llm::routing::policy_registry_len(),
617            0,
618            "routing policy registry must be empty after reset"
619        );
620    }
621
622    #[test]
623    fn reset_holds_llm_env_guard_while_wiping_llm_globals() {
624        let observed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
625        let observed_hook = std::sync::Arc::clone(&observed);
626        let _hook = reset_thread_local_state_test_hooks::install_before_llm_global_reset(
627            std::sync::Arc::new(move || {
628                assert!(
629                    matches!(
630                        llm::env_lock().try_lock(),
631                        Err(std::sync::TryLockError::WouldBlock)
632                    ),
633                    "reset_thread_local_state must hold env_guard before wiping LLM globals"
634                );
635                observed_hook.store(true, std::sync::atomic::Ordering::SeqCst);
636            }),
637        );
638
639        reset_thread_local_state();
640        assert!(
641            observed.load(std::sync::atomic::Ordering::SeqCst),
642            "LLM global reset hook should have run"
643        );
644    }
645}