Skip to main content

harn_vm/
harness.rs

1//! Capability handle threaded into every Harn script as the `harness`
2//! parameter of `main`.
3//!
4//! `Harness` is the Harn-language analog of an explicit-capability handle: a
5//! single value the runtime hands to a script's `main` so that stdio,
6//! terminal, clock, filesystem, environment, randomness, network, process,
7//! crypto, system, and LLM catalog access become surface in the type system
8//! instead of ambient globals. Each sub-handle (`stdio`, `term`, `clock`, `fs`,
9//! `env`, `random`, `net`, `process`, `crypto`, `system`, `llm`) is a distinct
10//! named type that anchors the surface for its capability slice.
11//!
12//! This module defines:
13//!   * The runtime [`Harness`] value and its sub-handle wrappers.
14//!   * [`Harness::real`], the production constructor that installs the backing
15//!     state used by concrete sub-handle methods.
16//!   * [`VmHarness`], the compact `VmValue` payload that carries the same
17//!     state through the bytecode VM and distinguishes the root handle from
18//!     its sub-handles via [`HarnessKind`].
19
20use std::collections::{BTreeMap, VecDeque};
21use std::fmt;
22use std::rc::Rc;
23use std::sync::{Arc, Mutex};
24use std::time::Duration;
25
26use async_trait::async_trait;
27use harn_clock::{Clock, PausedClock, RealClock};
28use time::OffsetDateTime;
29
30/// Capability slices exposed by a [`Harness`].
31///
32/// `Root` is the parent handle; the others are the typed sub-handles users
33/// reach through field access (`harness.stdio`, `harness.clock`, ...).
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum HarnessKind {
36    Root,
37    Stdio,
38    Term,
39    Clock,
40    Fs,
41    Env,
42    Random,
43    Net,
44    Process,
45    Crypto,
46    System,
47    Llm,
48    /// Tenant sub-handle exposing the ambient `TenantId` (if any) that
49    /// the dispatching host bound to this call. See
50    /// [`crate::harness_tenant`].
51    Tenant,
52    /// Observability sub-handle: spans, counter/histogram/gauge
53    /// instruments, structured logs, and the ambient request_id
54    /// surfaced by the dispatching host. See
55    /// [`crate::observability::request_id`] and
56    /// [`crate::observability::vocabulary`].
57    Obs,
58}
59
60impl HarnessKind {
61    /// The Harn-language type name for this kind (`Harness`, `HarnessStdio`,
62    /// etc.). Used by the typechecker primitive registration and by
63    /// `VmValue::type_name`.
64    pub const fn type_name(self) -> &'static str {
65        match self {
66            HarnessKind::Root => "Harness",
67            HarnessKind::Stdio => "HarnessStdio",
68            HarnessKind::Term => "HarnessTerm",
69            HarnessKind::Clock => "HarnessClock",
70            HarnessKind::Fs => "HarnessFs",
71            HarnessKind::Env => "HarnessEnv",
72            HarnessKind::Random => "HarnessRandom",
73            HarnessKind::Net => "HarnessNet",
74            HarnessKind::Process => "HarnessProcess",
75            HarnessKind::Crypto => "HarnessCrypto",
76            HarnessKind::System => "HarnessSystem",
77            HarnessKind::Llm => "HarnessLlm",
78            HarnessKind::Tenant => "HarnessTenant",
79            HarnessKind::Obs => "HarnessObs",
80        }
81    }
82
83    /// Field name a parent `Harness` exposes for this sub-handle (e.g. the
84    /// `stdio` in `harness.stdio`). Returns `None` for the root.
85    pub const fn field_name(self) -> Option<&'static str> {
86        match self {
87            HarnessKind::Root => None,
88            HarnessKind::Stdio => Some("stdio"),
89            HarnessKind::Term => Some("term"),
90            HarnessKind::Clock => Some("clock"),
91            HarnessKind::Fs => Some("fs"),
92            HarnessKind::Env => Some("env"),
93            HarnessKind::Random => Some("random"),
94            HarnessKind::Net => Some("net"),
95            HarnessKind::Process => Some("process"),
96            HarnessKind::Crypto => Some("crypto"),
97            HarnessKind::System => Some("system"),
98            HarnessKind::Llm => Some("llm"),
99            HarnessKind::Tenant => Some("tenant"),
100            HarnessKind::Obs => Some("obs"),
101        }
102    }
103
104    /// Parse the field name a script uses to reach a sub-handle.
105    pub fn from_field_name(name: &str) -> Option<Self> {
106        match name {
107            "stdio" => Some(HarnessKind::Stdio),
108            "term" => Some(HarnessKind::Term),
109            "clock" => Some(HarnessKind::Clock),
110            "fs" => Some(HarnessKind::Fs),
111            "env" => Some(HarnessKind::Env),
112            "random" => Some(HarnessKind::Random),
113            "net" => Some(HarnessKind::Net),
114            "process" => Some(HarnessKind::Process),
115            "crypto" => Some(HarnessKind::Crypto),
116            "system" => Some(HarnessKind::System),
117            "llm" => Some(HarnessKind::Llm),
118            "tenant" => Some(HarnessKind::Tenant),
119            "obs" => Some(HarnessKind::Obs),
120            _ => None,
121        }
122    }
123
124    /// All sub-handle kinds, in the canonical field order.
125    pub const SUB_HANDLES: &'static [HarnessKind] = &[
126        HarnessKind::Stdio,
127        HarnessKind::Term,
128        HarnessKind::Clock,
129        HarnessKind::Fs,
130        HarnessKind::Env,
131        HarnessKind::Random,
132        HarnessKind::Net,
133        HarnessKind::Process,
134        HarnessKind::Crypto,
135        HarnessKind::System,
136        HarnessKind::Llm,
137        HarnessKind::Tenant,
138        HarnessKind::Obs,
139    ];
140
141    /// Every kind a Harn-script type annotation may reference.
142    pub const ALL: &'static [HarnessKind] = &[
143        HarnessKind::Root,
144        HarnessKind::Stdio,
145        HarnessKind::Term,
146        HarnessKind::Clock,
147        HarnessKind::Fs,
148        HarnessKind::Env,
149        HarnessKind::Random,
150        HarnessKind::Net,
151        HarnessKind::Process,
152        HarnessKind::Crypto,
153        HarnessKind::System,
154        HarnessKind::Llm,
155        HarnessKind::Tenant,
156        HarnessKind::Obs,
157    ];
158}
159
160/// Shared, refcounted state backing every sub-handle of a single `Harness`.
161///
162/// Method implementations (in `crate::vm::methods::harness`) borrow this to
163/// reach the concrete OS-backed primitives. Wrapped in `Arc` so handles are
164/// `Send + Sync` for VM contexts that move work onto other tasks.
165#[derive(Debug)]
166pub struct HarnessInner {
167    clock: Arc<dyn Clock>,
168    mode: HarnessMode,
169    /// Per-harness `harness.net.*` access policy. `None` means the
170    /// handle inherits the legacy unrestricted behaviour (subject to
171    /// the process-wide `crate::egress` allowlist, if configured).
172    /// See `Harness::with_net_policy` and `crate::harness_net`.
173    net_policy: Option<crate::harness_net::NetPolicy>,
174    /// `true` once a request denied under `OnViolation::Quarantine`
175    /// has fired. Sticky for the lifetime of the underlying
176    /// `Arc<HarnessInner>` so downstream consumers can pin on the
177    /// signal even after the originating call has returned. The flag
178    /// is per-`Arc` (i.e. per-`Harness` build) so unrelated harnesses
179    /// stay independent.
180    quarantined: Mutex<bool>,
181}
182
183impl HarnessInner {
184    pub fn clock(&self) -> &Arc<dyn Clock> {
185        &self.clock
186    }
187
188    pub(crate) fn mode(&self) -> &HarnessMode {
189        &self.mode
190    }
191
192    pub fn net_policy(&self) -> Option<&crate::harness_net::NetPolicy> {
193        self.net_policy.as_ref()
194    }
195
196    pub(crate) fn mark_quarantined(&self) {
197        if let Ok(mut guard) = self.quarantined.lock() {
198            *guard = true;
199        }
200    }
201
202    pub fn is_quarantined(&self) -> bool {
203        self.quarantined.lock().map(|guard| *guard).unwrap_or(false)
204    }
205}
206
207#[derive(Debug)]
208pub(crate) enum HarnessMode {
209    Real,
210    Null(NullHarnessState),
211    Mock(Arc<MockHarnessState>),
212}
213
214#[derive(Debug, Default)]
215pub(crate) struct NullHarnessState {
216    deny_events: Mutex<Vec<DenyEvent>>,
217}
218
219impl NullHarnessState {
220    pub(crate) fn record_deny(
221        &self,
222        sub_handle: HarnessKind,
223        method: &str,
224        args: &[crate::VmValue],
225    ) {
226        self.deny_events
227            .lock()
228            .expect("deny events poisoned")
229            .push(DenyEvent::new(
230                sub_handle,
231                method,
232                args.iter().map(crate::VmValue::display).collect(),
233            ));
234    }
235
236    pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
237        self.deny_events
238            .lock()
239            .expect("deny events poisoned")
240            .clone()
241    }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct DenyEvent {
246    pub sub_handle: HarnessKind,
247    pub method: String,
248    pub args: Vec<String>,
249}
250
251impl DenyEvent {
252    fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
253        Self {
254            sub_handle,
255            method: method.to_string(),
256            args,
257        }
258    }
259}
260
261#[derive(Debug)]
262pub(crate) struct MockHarnessState {
263    calls: Mutex<Vec<HarnessCall>>,
264    clock: Arc<PausedClock>,
265    env: BTreeMap<String, String>,
266    fs_reads: BTreeMap<String, Vec<u8>>,
267    net_gets: BTreeMap<String, String>,
268    random_u64: Mutex<VecDeque<u64>>,
269    stdin_lines: Mutex<VecDeque<String>>,
270    stdio: Mutex<String>,
271    stderr: Mutex<String>,
272}
273
274impl MockHarnessState {
275    pub(crate) fn record_call(
276        &self,
277        sub_handle: HarnessKind,
278        method: &str,
279        args: &[crate::VmValue],
280    ) {
281        self.calls
282            .lock()
283            .expect("calls poisoned")
284            .push(HarnessCall::new(
285                sub_handle,
286                method,
287                args.iter().map(crate::VmValue::display).collect(),
288            ));
289    }
290
291    pub(crate) fn calls(&self) -> Vec<HarnessCall> {
292        self.calls.lock().expect("calls poisoned").clone()
293    }
294
295    pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
296        self.env.get(key).map(String::as_str)
297    }
298
299    pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
300        self.fs_reads.get(path).map(Vec::as_slice)
301    }
302
303    pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
304        self.net_gets.get(url).map(String::as_str)
305    }
306
307    pub(crate) fn next_random_u64(&self) -> Option<u64> {
308        let mut values = self.random_u64.lock().expect("random values poisoned");
309        values.pop_front()
310    }
311
312    pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
313        self.clock.advance(duration);
314    }
315
316    pub(crate) fn push_stdio(&self, text: &str) {
317        self.stdio
318            .lock()
319            .expect("stdio buffer poisoned")
320            .push_str(text);
321    }
322
323    pub(crate) fn stdio(&self) -> String {
324        self.stdio.lock().expect("stdio buffer poisoned").clone()
325    }
326
327    pub(crate) fn push_stderr(&self, text: &str) {
328        self.stderr
329            .lock()
330            .expect("stderr buffer poisoned")
331            .push_str(text);
332    }
333
334    pub(crate) fn stderr(&self) -> String {
335        self.stderr.lock().expect("stderr buffer poisoned").clone()
336    }
337
338    pub(crate) fn pop_stdin_line(&self) -> Option<String> {
339        self.stdin_lines
340            .lock()
341            .expect("stdin queue poisoned")
342            .pop_front()
343    }
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct HarnessCall {
348    pub sub_handle: HarnessKind,
349    pub method: String,
350    pub args: Vec<String>,
351}
352
353impl HarnessCall {
354    fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
355        Self {
356            sub_handle,
357            method: method.to_string(),
358            args,
359        }
360    }
361}
362
363#[derive(Debug)]
364pub struct MockHarnessBuilder {
365    clock: Arc<PausedClock>,
366    env: BTreeMap<String, String>,
367    fs_reads: BTreeMap<String, Vec<u8>>,
368    net_gets: BTreeMap<String, String>,
369    random_u64: Vec<u64>,
370    stdin_lines: Vec<String>,
371}
372
373impl MockHarnessBuilder {
374    fn new() -> Self {
375        Self {
376            clock: paused_clock_at_unix_ms(0),
377            env: BTreeMap::new(),
378            fs_reads: BTreeMap::new(),
379            net_gets: BTreeMap::new(),
380            random_u64: Vec::new(),
381            stdin_lines: Vec::new(),
382        }
383    }
384
385    pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
386        self.clock = paused_clock_at_unix_ms(unix_ms);
387        self
388    }
389
390    pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
391        self.clock = PausedClock::new(origin);
392        self
393    }
394
395    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
396        self.env.insert(key.into(), value.into());
397        self
398    }
399
400    pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
401        self.fs_reads.insert(path.into(), data.into());
402        self
403    }
404
405    pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
406        self.net_gets.insert(url.into(), body.into());
407        self
408    }
409
410    pub fn random_u64(mut self, value: u64) -> Self {
411        self.random_u64.push(value);
412        self
413    }
414
415    /// Queue a line that `harness.stdio.read_line()` or
416    /// `harness.stdio.prompt(...)` will return next. Lines are dequeued
417    /// FIFO; once the queue is empty subsequent reads surface EOF
418    /// (`nil` for the unstructured form, `{ok: false, status: "eof"}`
419    /// for the structured form).
420    pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
421        self.stdin_lines.push(line.into());
422        self
423    }
424
425    pub fn build(self) -> Harness {
426        let clock = self.clock;
427        Harness::with_mode(
428            clock.clone() as Arc<dyn Clock>,
429            HarnessMode::Mock(Arc::new(MockHarnessState {
430                calls: Mutex::new(Vec::new()),
431                clock,
432                env: self.env,
433                fs_reads: self.fs_reads,
434                net_gets: self.net_gets,
435                random_u64: Mutex::new(self.random_u64.into()),
436                stdin_lines: Mutex::new(self.stdin_lines.into()),
437                stdio: Mutex::new(String::new()),
438                stderr: Mutex::new(String::new()),
439            })),
440        )
441    }
442}
443
444/// The runtime handle threaded into `main(harness: Harness)`.
445///
446/// Cheap to clone; sub-handles share the same `Arc` inner state.
447#[derive(Debug, Clone)]
448pub struct Harness {
449    inner: Arc<HarnessInner>,
450}
451
452impl Harness {
453    /// Build the production handle wired to wall-clock time. Filesystem,
454    /// environment, randomness, and network access are layered on by the
455    /// E4.2-E4.4 migration tickets; the constructor only needs to succeed
456    /// without panicking today (per the E4.1 exit criteria).
457    ///
458    /// The production clock is wrapped in [`MockAwareClock`] so existing
459    /// `mock_time(...)` / `advance_time(...)` test fixtures observe
460    /// `harness.clock.*` reads identically to the ambient builtins. The
461    /// shim is part of the E4.3-E4.6 migration window and goes away once
462    /// the ambient `mock_time` test utility is retired by E4.5.
463    pub fn real() -> Self {
464        Self::with_mode(
465            Arc::new(MockAwareClock::new(RealClock::new())),
466            HarnessMode::Real,
467        )
468    }
469
470    /// Build a deny-by-default test handle. Every sub-handle method records a
471    /// [`DenyEvent`] and fails with a categorized VM error.
472    pub fn null() -> Self {
473        Self::with_mode(
474            paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
475            HarnessMode::Null(NullHarnessState::default()),
476        )
477    }
478
479    /// Build a record/replay test handle backed by a paused clock.
480    pub fn mock() -> MockHarnessBuilder {
481        MockHarnessBuilder::new()
482    }
483
484    /// Build a handle wired to a caller-supplied clock. Most callers want
485    /// [`Self::test`] (which constructs the `PausedClock` for you);
486    /// reach for this when an existing `Arc<dyn Clock>` is already in
487    /// hand — e.g. a `RecordedClock` wrapper.
488    pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
489        Self::with_mode(clock, HarnessMode::Real)
490    }
491
492    /// Construct a `Harness` from a pre-built `Arc<HarnessInner>`.
493    /// Used by VM method dispatch when it needs to re-wrap a sub-handle's
494    /// inner state into a root `Harness` (e.g. to invoke
495    /// [`Self::with_net_policy`] from inside the method dispatcher).
496    pub fn from_inner(inner: Arc<HarnessInner>) -> Self {
497        Self { inner }
498    }
499
500    fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
501        // `HarnessInner` becomes !Send/!Sync once a `NetPolicy` with a
502        // `Rc<VmClosure>` callback is attached (issue #1913). The
503        // closure is only invoked on the VM thread that originated
504        // the harness method call, so the practical safety of the Arc
505        // is unchanged; the clippy lint is suppressed at the
506        // construction sites that legitimately store the inner state
507        // in shared ownership.
508        #[allow(clippy::arc_with_non_send_sync)]
509        let inner = Arc::new(HarnessInner {
510            clock,
511            mode,
512            net_policy: None,
513            quarantined: Mutex::new(false),
514        });
515        Self { inner }
516    }
517
518    /// Attach a per-harness `harness.net.*` access policy.
519    ///
520    /// Returns a new `Harness` value whose sub-handles share a fresh
521    /// `Arc<HarnessInner>`. Existing handles built off the prior inner
522    /// keep operating without the policy — so calling
523    /// `harness.with_net_policy(...)` does NOT retroactively gate
524    /// references to `harness` held elsewhere. Per issue #1913.
525    ///
526    /// The clock and mode are propagated verbatim. Mock canned
527    /// responses (`net_gets`, `random_u64`, etc.) live behind the
528    /// shared `HarnessMode::Mock` payload, so the new handle observes
529    /// the same recorded calls and the same canned responses as the
530    /// source handle.
531    pub fn with_net_policy(&self, policy: crate::harness_net::NetPolicy) -> Self {
532        let clock = Arc::clone(&self.inner.clock);
533        let mode = match &self.inner.mode {
534            HarnessMode::Real => HarnessMode::Real,
535            HarnessMode::Null(_) => HarnessMode::Null(NullHarnessState::default()),
536            HarnessMode::Mock(state) => HarnessMode::Mock(Arc::clone(state)),
537        };
538        // See `with_mode` for the rationale on this suppression.
539        #[allow(clippy::arc_with_non_send_sync)]
540        let inner = Arc::new(HarnessInner {
541            clock,
542            mode,
543            net_policy: Some(policy),
544            quarantined: Mutex::new(self.is_quarantined()),
545        });
546        Self { inner }
547    }
548
549    /// `true` if the harness has been marked quarantined by an
550    /// `OnViolation::Quarantine` deny event.
551    pub fn is_quarantined(&self) -> bool {
552        self.inner.is_quarantined()
553    }
554
555    pub fn deny_events(&self) -> Vec<DenyEvent> {
556        match self.inner.mode() {
557            HarnessMode::Null(state) => state.deny_events(),
558            HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
559        }
560    }
561
562    pub fn calls(&self) -> Vec<HarnessCall> {
563        match self.inner.mode() {
564            HarnessMode::Mock(state) => state.calls(),
565            HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
566        }
567    }
568
569    pub fn captured_stdio(&self) -> String {
570        match self.inner.mode() {
571            HarnessMode::Mock(state) => state.stdio(),
572            HarnessMode::Real | HarnessMode::Null(_) => String::new(),
573        }
574    }
575
576    pub fn captured_stderr(&self) -> String {
577        match self.inner.mode() {
578            HarnessMode::Mock(state) => state.stderr(),
579            HarnessMode::Real | HarnessMode::Null(_) => String::new(),
580        }
581    }
582
583    /// Build a deterministic test handle wired to a fresh
584    /// [`PausedClock`] pinned at the Unix epoch.
585    ///
586    /// Returns the harness paired with the underlying `PausedClock` so
587    /// tests can drive virtual time through `PausedClock::advance`
588    /// while passing the same `Harness` value into the VM. The two
589    /// share the underlying `Arc<dyn Clock>`, so the harness reflects
590    /// every advance immediately.
591    ///
592    /// Pairs with [`PausedClock::advance`] / [`PausedClock::set`] — see
593    /// [`Self::with_paused_clock`] for picking a non-epoch origin.
594    pub fn test() -> (Self, Arc<PausedClock>) {
595        Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
596    }
597
598    /// Like [`Self::test`], but pins the paused clock's wall origin to
599    /// `origin`. Lets tests anchor virtual time to a meaningful date
600    /// without manually advancing past the epoch first.
601    pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
602        let paused = PausedClock::new(origin);
603        let as_dyn: Arc<dyn Clock> = paused.clone();
604        (Self::with_clock(as_dyn), paused)
605    }
606
607    /// Field access for `harness.stdio`.
608    pub fn stdio(&self) -> HarnessStdio {
609        HarnessStdio {
610            inner: Arc::clone(&self.inner),
611        }
612    }
613
614    /// Field access for `harness.term`.
615    pub fn term(&self) -> HarnessTerm {
616        HarnessTerm {
617            inner: Arc::clone(&self.inner),
618        }
619    }
620
621    /// Field access for `harness.clock`.
622    pub fn clock(&self) -> HarnessClock {
623        HarnessClock {
624            inner: Arc::clone(&self.inner),
625        }
626    }
627
628    /// Field access for `harness.fs`.
629    pub fn fs(&self) -> HarnessFs {
630        HarnessFs {
631            inner: Arc::clone(&self.inner),
632        }
633    }
634
635    /// Field access for `harness.env`.
636    pub fn env(&self) -> HarnessEnv {
637        HarnessEnv {
638            inner: Arc::clone(&self.inner),
639        }
640    }
641
642    /// Field access for `harness.random`.
643    pub fn random(&self) -> HarnessRandom {
644        HarnessRandom {
645            inner: Arc::clone(&self.inner),
646        }
647    }
648
649    /// Field access for `harness.net`.
650    pub fn net(&self) -> HarnessNet {
651        HarnessNet {
652            inner: Arc::clone(&self.inner),
653        }
654    }
655
656    /// Field access for `harness.process`.
657    pub fn process(&self) -> HarnessProcess {
658        HarnessProcess {
659            inner: Arc::clone(&self.inner),
660        }
661    }
662
663    /// Field access for `harness.crypto`.
664    pub fn crypto(&self) -> HarnessCrypto {
665        HarnessCrypto {
666            inner: Arc::clone(&self.inner),
667        }
668    }
669
670    /// Field access for `harness.system`.
671    pub fn system(&self) -> HarnessSystem {
672        HarnessSystem {
673            inner: Arc::clone(&self.inner),
674        }
675    }
676
677    /// Field access for `harness.llm`.
678    pub fn llm(&self) -> HarnessLlm {
679        HarnessLlm {
680            inner: Arc::clone(&self.inner),
681        }
682    }
683
684    /// Field access for `harness.tenant`.
685    pub fn tenant(&self) -> HarnessTenant {
686        HarnessTenant {
687            inner: Arc::clone(&self.inner),
688        }
689    }
690
691    /// Field access for `harness.obs`.
692    pub fn obs(&self) -> HarnessObs {
693        HarnessObs {
694            inner: Arc::clone(&self.inner),
695        }
696    }
697
698    /// Lower this handle into the `VmValue::Harness` payload.
699    pub fn into_vm_value(self) -> crate::value::VmValue {
700        crate::value::VmValue::harness(VmHarness {
701            inner: self.inner,
702            kind: HarnessKind::Root,
703        })
704    }
705}
706
707fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
708    let nanos = (unix_ms as i128).saturating_mul(1_000_000);
709    let origin =
710        OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
711    PausedClock::new(origin)
712}
713
714pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
715    crate::VmValue::String(Rc::from(value.into()))
716}
717
718impl Default for Harness {
719    fn default() -> Self {
720        Self::real()
721    }
722}
723
724/// stdio sub-handle: `print`, `println`, `eprint`, `eprintln`, `prompt`,
725/// `read_line`.
726#[derive(Debug, Clone)]
727pub struct HarnessStdio {
728    inner: Arc<HarnessInner>,
729}
730
731/// term sub-handle: `width`, `height`, `read_password`.
732#[derive(Debug, Clone)]
733pub struct HarnessTerm {
734    inner: Arc<HarnessInner>,
735}
736
737/// clock sub-handle: `now`, `monotonic_now`, `sleep`.
738#[derive(Debug, Clone)]
739pub struct HarnessClock {
740    inner: Arc<HarnessInner>,
741}
742
743impl HarnessClock {
744    pub fn clock(&self) -> &Arc<dyn Clock> {
745        self.inner.clock()
746    }
747}
748
749/// fs sub-handle: `read_file`, `write_file`, `exists`, `list_dir`,
750/// `delete_file`, ...
751#[derive(Debug, Clone)]
752pub struct HarnessFs {
753    inner: Arc<HarnessInner>,
754}
755
756/// env sub-handle: `get`, `set`, `vars`.
757#[derive(Debug, Clone)]
758pub struct HarnessEnv {
759    inner: Arc<HarnessInner>,
760}
761
762/// random sub-handle: `gen_u64`, `gen_range`, `gen_f64`, ...
763#[derive(Debug, Clone)]
764pub struct HarnessRandom {
765    inner: Arc<HarnessInner>,
766}
767
768/// net sub-handle: `http_get`, `http_post`, ...
769#[derive(Debug, Clone)]
770pub struct HarnessNet {
771    inner: Arc<HarnessInner>,
772}
773
774/// process sub-handle: `spawn_captured`.
775#[derive(Debug, Clone)]
776pub struct HarnessProcess {
777    inner: Arc<HarnessInner>,
778}
779
780/// crypto sub-handle: deterministic digest helpers such as `sha256`.
781#[derive(Debug, Clone)]
782pub struct HarnessCrypto {
783    inner: Arc<HarnessInner>,
784}
785
786/// system sub-handle: `cpu`, `memory`, `gpus`, `temperature`, `platform`,
787/// `processes`. Read-only host introspection — no side effects on the host
788/// system. Gated by the harness handle so scripts running under
789/// `Harness::null()` or restricted policies cannot fingerprint the runner
790/// without an explicit grant (issue #1912 / epic #1765).
791#[derive(Debug, Clone)]
792pub struct HarnessSystem {
793    inner: Arc<HarnessInner>,
794}
795
796/// llm sub-handle: `catalog`, `providers`.
797#[derive(Debug, Clone)]
798pub struct HarnessLlm {
799    inner: Arc<HarnessInner>,
800}
801
802/// tenant sub-handle: `id`, `try_id`. Surfaces the ambient `TenantId`
803/// bound by the dispatching host (see [`crate::harness_tenant`]). No
804/// host state — the methods consult a thread-local stack — but the
805/// handle still rides the shared `Arc<HarnessInner>` so null/mock-mode
806/// gating in [`crate::vm::methods::harness`] applies uniformly.
807#[derive(Debug, Clone)]
808pub struct HarnessTenant {
809    inner: Arc<HarnessInner>,
810}
811
812/// obs sub-handle: `span` / `start_span` / `end_span` / `counter` /
813/// `histogram` / `gauge` / `log` / `request_id`. Wraps the existing
814/// `__obs_*` builtins and the request_id ambient pushed by the
815/// dispatching host (see [`crate::observability::request_id`]) behind a
816/// typed surface so handlers don't reach into the lower-level builtins
817/// directly. Backend selection / exporter wiring still lives in
818/// [`crate::events`] (OTel sink) and `std/observability` (`configure`,
819/// backend factories) — the sub-handle is the *emit-side* surface that
820/// every harn-serve primitive shares.
821#[derive(Debug, Clone)]
822pub struct HarnessObs {
823    inner: Arc<HarnessInner>,
824}
825
826macro_rules! sub_handle_inner {
827    ($($ty:ty),* $(,)?) => {
828        $(
829            impl $ty {
830                #[allow(dead_code)]
831                pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
832                    &self.inner
833                }
834            }
835        )*
836    };
837}
838sub_handle_inner!(
839    HarnessStdio,
840    HarnessTerm,
841    HarnessFs,
842    HarnessEnv,
843    HarnessRandom,
844    HarnessNet,
845    HarnessProcess,
846    HarnessCrypto,
847    HarnessSystem,
848    HarnessLlm,
849    HarnessTenant,
850    HarnessObs,
851);
852
853impl HarnessClock {
854    #[allow(dead_code)]
855    pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
856        &self.inner
857    }
858}
859
860/// Compact `VmValue` payload for a `Harness` or any of its sub-handles.
861///
862/// All handle variants share one `Arc<HarnessInner>`; `kind` discriminates the
863/// surface the VM exposes for property access and method dispatch.
864#[derive(Clone)]
865pub struct VmHarness {
866    inner: Arc<HarnessInner>,
867    kind: HarnessKind,
868}
869
870impl VmHarness {
871    pub fn kind(&self) -> HarnessKind {
872        self.kind
873    }
874
875    pub fn type_name(&self) -> &'static str {
876        self.kind.type_name()
877    }
878
879    pub fn inner(&self) -> &Arc<HarnessInner> {
880        &self.inner
881    }
882
883    /// Get the sub-handle reached by a field name (`stdio`, `clock`, etc.).
884    /// Returns `None` when the receiver is itself a sub-handle or the field
885    /// is unknown.
886    pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
887        if self.kind != HarnessKind::Root {
888            return None;
889        }
890        let kind = HarnessKind::from_field_name(field)?;
891        Some(VmHarness {
892            inner: Arc::clone(&self.inner),
893            kind,
894        })
895    }
896}
897
898impl fmt::Debug for VmHarness {
899    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
900        f.debug_struct("VmHarness")
901            .field("kind", &self.kind)
902            .finish_non_exhaustive()
903    }
904}
905
906/// Clock wrapper that consults the crate-wide `clock_mock` thread-local
907/// before delegating to an inner [`Clock`]. Used by [`Harness::real`] so
908/// `harness.clock.*` reads honor `mock_time(...)` / `advance_time(...)`
909/// during the E4.3-E4.6 migration. New tests should prefer
910/// [`Harness::test`] / [`PausedClock`] directly.
911#[derive(Debug)]
912pub struct MockAwareClock<C: Clock + 'static> {
913    inner: C,
914}
915
916impl<C: Clock + 'static> MockAwareClock<C> {
917    pub fn new(inner: C) -> Self {
918        Self { inner }
919    }
920}
921
922#[async_trait]
923impl<C: Clock + 'static> Clock for MockAwareClock<C> {
924    fn now_utc(&self) -> OffsetDateTime {
925        if let Some(mock) = crate::clock_mock::active_mock_clock() {
926            return mock.now_utc();
927        }
928        self.inner.now_utc()
929    }
930
931    fn monotonic_ms(&self) -> i64 {
932        if let Some(mock) = crate::clock_mock::active_mock_clock() {
933            return mock.monotonic_ms();
934        }
935        self.inner.monotonic_ms()
936    }
937
938    async fn sleep(&self, duration: Duration) {
939        if duration.is_zero() {
940            return;
941        }
942        if let Some(mock) = crate::clock_mock::active_mock_clock() {
943            // Single-script tests under `mock_time(...)` rely on `sleep(...)`
944            // advancing the mock and returning immediately — the same
945            // semantics as the legacy ambient `sleep_ms` builtin. Waiting
946            // on `mock.sleep` would deadlock because nothing else is
947            // driving `advance(...)` in the same task.
948            mock.advance_std_sync(duration);
949            return;
950        }
951        self.inner.sleep(duration).await;
952    }
953
954    async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
955        if let Some(mock) = crate::clock_mock::active_mock_clock() {
956            let now = mock.now_utc();
957            if deadline > now {
958                if let Ok(delta) = Duration::try_from(deadline - now) {
959                    mock.advance_std_sync(delta);
960                }
961            }
962            return;
963        }
964        self.inner.sleep_until_utc(deadline).await;
965    }
966}
967
968#[cfg(test)]
969mod tests {
970    use super::*;
971
972    #[test]
973    fn real_constructs_without_panic() {
974        let _harness = Harness::real();
975    }
976
977    #[test]
978    fn sub_handles_share_inner_state() {
979        let harness = Harness::real();
980        let stdio_inner = Arc::as_ptr(harness.stdio().inner());
981        let clock_inner = Arc::as_ptr(harness.clock().inner());
982        assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
983    }
984
985    #[test]
986    fn kinds_round_trip_through_field_names() {
987        for kind in HarnessKind::SUB_HANDLES {
988            let field = kind.field_name().unwrap();
989            assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
990        }
991        assert!(HarnessKind::from_field_name("nope").is_none());
992        assert!(HarnessKind::Root.field_name().is_none());
993    }
994
995    #[test]
996    fn vm_harness_property_access_returns_sub_handle() {
997        let root = match Harness::real().into_vm_value() {
998            crate::value::VmValue::Harness(h) => h,
999            other => panic!("expected Harness variant, got {}", other.type_name()),
1000        };
1001        let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
1002        assert_eq!(stdio.kind(), HarnessKind::Stdio);
1003        assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
1004        assert!(root.sub_handle("not_a_field").is_none());
1005    }
1006
1007    #[test]
1008    fn test_constructor_clock_advances_under_paused_clock_advance() {
1009        let (harness, paused) = Harness::test();
1010        let clock = harness.clock();
1011        let start_wall = clock.clock().now_utc();
1012        assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
1013        assert_eq!(clock.clock().monotonic_ms(), 0);
1014
1015        paused.advance(Duration::from_millis(1_500));
1016        assert_eq!(clock.clock().monotonic_ms(), 1_500);
1017        let after_wall = clock.clock().now_utc();
1018        assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
1019    }
1020
1021    #[test]
1022    fn with_paused_clock_pins_origin() {
1023        let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
1024        let (harness, paused) = Harness::with_paused_clock(origin);
1025        assert_eq!(harness.clock().clock().now_utc(), origin);
1026        paused.advance(Duration::from_mins(1));
1027        assert_eq!(
1028            harness.clock().clock().now_utc() - origin,
1029            time::Duration::seconds(60)
1030        );
1031    }
1032
1033    #[test]
1034    fn null_harness_records_deny_events_for_every_sub_handle() {
1035        let harness = Harness::null();
1036        for source in [
1037            r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
1038            r"fn main(harness: Harness) { harness.term.width() }",
1039            r"fn main(harness: Harness) { harness.clock.now_ms() }",
1040            r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
1041            r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
1042            r"fn main(harness: Harness) { harness.random.gen_u64() }",
1043            r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
1044            r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1045            r#"fn main(harness: Harness) { harness.crypto.sha256("") }"#,
1046            r"fn main(harness: Harness) { harness.system.cpu() }",
1047            r"fn main(harness: Harness) { harness.llm.catalog() }",
1048            r"fn main(harness: Harness) { harness.tenant.id() }",
1049            r#"fn main(harness: Harness) { harness.obs.log("blocked", "info", {}) }"#,
1050        ] {
1051            let error = run_harness_source(source, harness.clone()).expect_err("call denied");
1052            assert!(
1053                error.contains("NullHarness denied"),
1054                "unexpected deny error: {error}"
1055            );
1056        }
1057
1058        let events = harness.deny_events();
1059        let observed: Vec<_> = events
1060            .iter()
1061            .map(|event| (event.sub_handle, event.method.as_str()))
1062            .collect();
1063        assert_eq!(
1064            observed,
1065            vec![
1066                (HarnessKind::Stdio, "println"),
1067                (HarnessKind::Term, "width"),
1068                (HarnessKind::Clock, "now_ms"),
1069                (HarnessKind::Fs, "read_text"),
1070                (HarnessKind::Env, "get"),
1071                (HarnessKind::Random, "gen_u64"),
1072                (HarnessKind::Net, "get"),
1073                (HarnessKind::Process, "spawn_captured"),
1074                (HarnessKind::Crypto, "sha256"),
1075                (HarnessKind::System, "cpu"),
1076                (HarnessKind::Llm, "catalog"),
1077                (HarnessKind::Tenant, "id"),
1078                (HarnessKind::Obs, "log"),
1079            ]
1080        );
1081        assert_eq!(events[0].args, vec!["blocked"]);
1082        assert_eq!(events[3].args, vec!["/x"]);
1083    }
1084
1085    #[test]
1086    fn mock_harness_replays_canned_responses_and_records_calls() {
1087        let harness = Harness::mock()
1088            .clock_at_unix_ms(1_700_000_000_000)
1089            .env("KEY", "value")
1090            .fs_read("/x", b"data".to_vec())
1091            .random_u64(42)
1092            .net_get("https://example.test", "body")
1093            .build();
1094
1095        let output = run_harness_source(
1096            r#"
1097fn main(harness: Harness) {
1098  harness.stdio.print("partial ")
1099  harness.stdio.println("line")
1100  __io_println(harness.term.width())
1101  __io_println(harness.term.height())
1102  __io_println(harness.clock.now_ms())
1103  harness.clock.sleep_ms(250)
1104  __io_println(harness.clock.now_ms())
1105  __io_println(harness.clock.monotonic_ms())
1106  __io_println(harness.env.get("KEY"))
1107  __io_println(harness.fs.read_text("/x"))
1108  __io_println(harness.fs.exists("/missing"))
1109  __io_println(harness.random.gen_u64())
1110  __io_println(harness.net.get("https://example.test"))
1111  __io_println(harness.crypto.sha256(""))
1112  __io_println(len(harness.llm.catalog()) > 0)
1113}
1114"#,
1115            harness.clone(),
1116        )
1117        .expect("mock harness run succeeds");
1118
1119        assert_eq!(harness.captured_stdio(), "partial line\n");
1120        assert_eq!(
1121            output,
1122            "80\n24\n1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ntrue\n"
1123        );
1124        let observed: Vec<_> = harness
1125            .calls()
1126            .into_iter()
1127            .map(|call| (call.sub_handle, call.method))
1128            .collect();
1129        assert_eq!(
1130            observed,
1131            vec![
1132                (HarnessKind::Stdio, "print".to_string()),
1133                (HarnessKind::Stdio, "println".to_string()),
1134                (HarnessKind::Term, "width".to_string()),
1135                (HarnessKind::Term, "height".to_string()),
1136                (HarnessKind::Clock, "now_ms".to_string()),
1137                (HarnessKind::Clock, "sleep_ms".to_string()),
1138                (HarnessKind::Clock, "now_ms".to_string()),
1139                (HarnessKind::Clock, "monotonic_ms".to_string()),
1140                (HarnessKind::Env, "get".to_string()),
1141                (HarnessKind::Fs, "read_text".to_string()),
1142                (HarnessKind::Fs, "exists".to_string()),
1143                (HarnessKind::Random, "gen_u64".to_string()),
1144                (HarnessKind::Net, "get".to_string()),
1145                (HarnessKind::Crypto, "sha256".to_string()),
1146                (HarnessKind::Llm, "catalog".to_string()),
1147            ]
1148        );
1149    }
1150
1151    #[test]
1152    fn mock_harness_replays_random_values_fifo() {
1153        let harness = Harness::mock()
1154            .random_u64(7)
1155            .random_u64(11)
1156            .random_u64(u64::MAX)
1157            .build();
1158
1159        let output = run_harness_source(
1160            r"
1161fn main(harness: Harness) {
1162  __io_println(harness.random.gen_u64())
1163  __io_println(harness.random.gen_u64())
1164  __io_println(harness.random.gen_u64())
1165}
1166",
1167            harness,
1168        )
1169        .expect("mock random succeeds");
1170
1171        assert_eq!(output, "7\n11\n9223372036854775807\n");
1172    }
1173
1174    #[test]
1175    fn mock_harness_reports_missing_canned_responses() {
1176        let cases = [
1177            (
1178                r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
1179                "MockHarness has no fs_read response for /missing",
1180            ),
1181            (
1182                r"fn main(harness: Harness) { harness.random.gen_u64() }",
1183                "MockHarness has no random_u64 response",
1184            ),
1185            (
1186                r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1187                "MockHarness has no net_get response for https://missing.test",
1188            ),
1189            (
1190                r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1191                "MockHarness has no process spawn response",
1192            ),
1193        ];
1194
1195        for (source, expected) in cases {
1196            let error = run_harness_source(source, Harness::mock().build())
1197                .expect_err("missing mock response fails");
1198            assert!(
1199                error.contains(expected),
1200                "expected `{expected}` in `{error}`"
1201            );
1202        }
1203    }
1204
1205    #[test]
1206    fn mock_harness_records_failed_calls() {
1207        let harness = Harness::mock().build();
1208        let error = run_harness_source(
1209            r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1210            harness.clone(),
1211        )
1212        .expect_err("missing mock response fails");
1213
1214        assert!(error.contains("MockHarness has no net_get response"));
1215        assert_eq!(
1216            harness.calls(),
1217            vec![HarnessCall {
1218                sub_handle: HarnessKind::Net,
1219                method: "get".to_string(),
1220                args: vec!["https://missing.test".to_string()],
1221            }]
1222        );
1223    }
1224
1225    #[test]
1226    fn mock_harness_captures_stderr_separately_from_stdout() {
1227        let harness = Harness::mock().build();
1228        run_harness_source(
1229            r#"
1230fn main(harness: Harness) {
1231  harness.stdio.println("stdout line")
1232  harness.stdio.eprint("err ")
1233  harness.stdio.eprintln("trail")
1234}
1235"#,
1236            harness.clone(),
1237        )
1238        .expect("stderr capture run succeeds");
1239        assert_eq!(harness.captured_stdio(), "stdout line\n");
1240        assert_eq!(harness.captured_stderr(), "err trail\n");
1241    }
1242
1243    #[test]
1244    fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
1245        let harness = Harness::mock()
1246            .stdin_line("first")
1247            .stdin_line("second")
1248            .build();
1249        let output = run_harness_source(
1250            r#"
1251fn main(harness: Harness) {
1252  harness.stdio.println(harness.stdio.read_line())
1253  harness.stdio.println(harness.stdio.prompt("answer: "))
1254  let eof = harness.stdio.read_line({trim: false})
1255  harness.stdio.println(eof.status)
1256}
1257"#,
1258            harness.clone(),
1259        )
1260        .expect("stdin replay succeeds");
1261        // All stdio writes route to the mock capture buffer; vm.output stays empty.
1262        assert_eq!(output, "");
1263        assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
1264    }
1265
1266    #[test]
1267    fn mock_harness_replays_password_input_without_stdout_echo() {
1268        let harness = Harness::mock().stdin_line("secret").build();
1269        let output = run_harness_source(
1270            r#"
1271fn main(harness: Harness) {
1272  __io_println(harness.term.read_password("password: "))
1273}
1274"#,
1275            harness.clone(),
1276        )
1277        .expect("stdin replay succeeds");
1278
1279        assert_eq!(output, "secret\n");
1280        assert_eq!(harness.captured_stdio(), "");
1281        assert_eq!(harness.captured_stderr(), "password: ");
1282        assert_eq!(
1283            harness.calls(),
1284            vec![HarnessCall {
1285                sub_handle: HarnessKind::Term,
1286                method: "read_password".to_string(),
1287                args: vec!["password: ".to_string()],
1288            }]
1289        );
1290    }
1291
1292    #[test]
1293    fn mock_harness_rejects_wrong_argument_types() {
1294        let error = run_harness_source(
1295            r"fn main(harness: Harness) { harness.fs.read_text(1) }",
1296            Harness::mock().build(),
1297        )
1298        .expect_err("wrong argument type fails");
1299
1300        // A wrong-typed argument is rejected by one of two layers, and which
1301        // one fires depends on whether the process-global builtin-signature
1302        // registry was already populated (by any prior `register_vm_stdlib`
1303        // call in the test binary) when `compile_source` ran:
1304        //   - empty registry  -> the harness runtime guard (`string_arg`)
1305        //   - populated registry -> static type-check at compile time, which
1306        //     matches the `read_text` method against the same-named stdlib
1307        //     `read_text(path: string)` signature.
1308        // Both correctly reject the int, so accept either message.
1309        let runtime_rejection =
1310            error.contains("HarnessFs.read_text expects string argument 1, got int");
1311        let static_rejection = error.contains("argument 1 `path`: expected string, found int");
1312        assert!(
1313            runtime_rejection || static_rejection,
1314            "expected a string/int type rejection for read_text, got: {error}"
1315        );
1316    }
1317
1318    #[test]
1319    fn real_harness_fs_write_outside_workspace_roots_surfaces_cap_201() {
1320        use crate::orchestration::{
1321            clear_execution_policy_stacks, push_execution_policy, CapabilityPolicy, SandboxProfile,
1322        };
1323        clear_execution_policy_stacks();
1324        let temp = tempfile::tempdir().unwrap();
1325        let policy = CapabilityPolicy {
1326            sandbox_profile: SandboxProfile::Worktree,
1327            workspace_roots: vec![temp.path().to_string_lossy().into_owned()],
1328            ..CapabilityPolicy::default()
1329        };
1330        push_execution_policy(policy);
1331        let outside = std::env::temp_dir().join("harn_e4_4_cap_201_outside.txt");
1332        let source = format!(
1333            r#"fn main(harness: Harness) {{ harness.fs.write_text("{}", "x") }}"#,
1334            outside.to_string_lossy().replace('\\', "/"),
1335        );
1336        let error = run_harness_source(&source, Harness::real())
1337            .expect_err("write outside workspace_roots must reject");
1338        clear_execution_policy_stacks();
1339        assert!(
1340            error.contains("HARN-CAP-201"),
1341            "expected HARN-CAP-201 prefix, got: {error}"
1342        );
1343        assert!(
1344            error.contains("sandbox violation"),
1345            "deny should keep the underlying sandbox-rejection message, got: {error}"
1346        );
1347    }
1348
1349    fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1350        let rt = tokio::runtime::Builder::new_current_thread()
1351            .enable_all()
1352            .build()
1353            .unwrap();
1354        rt.block_on(async move {
1355            let local = tokio::task::LocalSet::new();
1356            local
1357                .run_until(async move {
1358                    let chunk = crate::compile_source(source)?;
1359                    let mut vm = crate::Vm::new();
1360                    crate::stdlib::register_vm_stdlib(&mut vm);
1361                    vm.set_harness(harness);
1362                    vm.execute(&chunk)
1363                        .await
1364                        .map_err(|error| error.to_string())?;
1365                    Ok(vm.output().to_string())
1366                })
1367                .await
1368        })
1369    }
1370}