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