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