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