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