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