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