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