localharness 0.33.0

A Rust-native agent SDK with pluggable LLM backends (Gemini today). Streaming, custom tools, safety policies, background triggers — zero external binaries.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
//! Compositor scheduling for `host::compose` (roadmap Track A / Phase 1a) —
//! the part that is pure control flow, so it lives here and is native-tested,
//! independent of the wasm `Instance`/`Memory` it will hold in `app::display`.
//!
//! The hazard the adversarial critique flagged as the most likely first crash:
//! a child module's `frame()` issues `spawn`/`close`/`move` on the table WHILE
//! the compositor is iterating it — a re-entrant mutation that double-borrows
//! the live `RefCell` (single-threaded wasm can't deadlock, but it *can* panic
//! the whole tab). The fix is structural: during a tick a child can only queue
//! ops into a separate [`Pending`](crate::compose::Pending) buffer; the table applies them AFTER every
//! module has ticked. The iteration never sees a mid-flight mutation.
//!
//! `H` is the opaque per-module runtime handle (a wasm instance + its memory in
//! `app::display`; a stand-in in tests). The table is generic over it so the
//! scheduling logic carries zero browser dependencies.

use crate::raster::Viewport;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

/// Content-addressed cache for fetched module artifacts (compiled wasm /
/// instances in `app::display`; anything in tests). Keyed by a hash of the
/// WASM BYTES — never by tokenId or name. The critique flagged tokenId-keying
/// as a silent-staleness bug: an on-chain republish (new bytes, same name)
/// would hit a stale entry forever. Content-addressing makes the new bytes a
/// new key, so a republish is a cache miss → a fresh fetch. The on-chain TRUST
/// commitment is keccak256 (the registry capability seam); this LOCAL cache
/// only needs to distinguish different bytes, so a fast std hash suffices.
pub struct WasmCache<V> {
    map: HashMap<u64, V>,
}

impl<V> Default for WasmCache<V> {
    fn default() -> Self {
        Self::new()
    }
}

impl<V> WasmCache<V> {
    pub fn new() -> Self {
        Self { map: HashMap::new() }
    }

    /// The content key for `bytes` — a hash of the bytes themselves, so
    /// identical bytes share a key and any change produces a different one.
    pub fn content_key(bytes: &[u8]) -> u64 {
        let mut h = DefaultHasher::new();
        bytes.hash(&mut h);
        h.finish()
    }

    pub fn get(&self, key: u64) -> Option<&V> {
        self.map.get(&key)
    }

    pub fn insert(&mut self, key: u64, value: V) {
        self.map.insert(key, value);
    }

    pub fn contains(&self, key: u64) -> bool {
        self.map.contains_key(&key)
    }

    pub fn len(&self) -> usize {
        self.map.len()
    }

    pub fn is_empty(&self) -> bool {
        self.map.is_empty()
    }
}

/// One composited child: its runtime handle and the sub-rectangle it draws to.
pub struct Module<H> {
    pub handle: H,
    pub viewport: Viewport,
}

/// Resource caps for a composition — the security gate that stops an
/// attacker-authored or runaway compose graph from exhausting the host (linear
/// memory) or the sponsor (per-mount fees). The adversarial critique flagged
/// ALL three frontier designs as leaving these uncapped (its #2 top risk:
/// "sponsor-key drain… uncapped in all three designs"). Checked when a spawn is
/// requested, BEFORE any fetch/instantiate/settle.
#[derive(Clone, Copy, Debug)]
pub struct ComposeBudget {
    pub max_children: usize,
    pub max_bytes_per_child: usize,
    pub max_total_bytes: usize,
}

impl ComposeBudget {
    /// Conservative v1 caps (8 children, 16 KB each, 64 KB total).
    pub fn v1() -> Self {
        Self { max_children: 8, max_bytes_per_child: 16 * 1024, max_total_bytes: 64 * 1024 }
    }

    /// Whether a new child of `child_bytes` may be admitted given the `count`
    /// children and `total_bytes` already mounted. `Err` carries the reason so
    /// the host can log WHY a spawn was refused (silent caps read as "worked").
    pub fn admit(&self, count: usize, total_bytes: usize, child_bytes: usize) -> Result<(), String> {
        if count >= self.max_children {
            return Err(format!("compose: at the {}-child cap", self.max_children));
        }
        if child_bytes > self.max_bytes_per_child {
            return Err(format!(
                "compose: child is {child_bytes} bytes, over the {}-byte per-child cap",
                self.max_bytes_per_child
            ));
        }
        if total_bytes.saturating_add(child_bytes) > self.max_total_bytes {
            return Err(format!(
                "compose: mounting {child_bytes} more bytes would exceed the {}-byte total cap",
                self.max_total_bytes
            ));
        }
        Ok(())
    }
}

/// Tile `n` module viewports across an `fb_w` x `fb_h` framebuffer in a near-
/// square grid (1 -> full screen, 2 -> side-by-side, 3-4 -> 2x2, 5-9 -> 3x3, …).
/// Cells fill left-to-right, top-to-bottom. Integer division can leave a thin
/// remainder strip on the right/bottom edge, which the compositor paints black.
/// Cells never overlap and stay within the framebuffer. Pure + native-tested so
/// the wasm-only `app::display` compositor carries no untested layout math.
pub fn grid_viewports(n: usize, fb_w: i32, fb_h: i32) -> Vec<Viewport> {
    if n == 0 {
        return Vec::new();
    }
    let cols = (n as f64).sqrt().ceil() as i32;
    let rows = (n as i32 + cols - 1) / cols; // ceil(n / cols); cols >= 1
    let (cw, ch) = (fb_w / cols, fb_h / rows);
    (0..n as i32)
        .map(|i| Viewport { ox: (i % cols) * cw, oy: (i / cols) * ch, w: cw, h: ch })
        .collect()
}

/// A deferred-op buffer handed to a module during a tick. A child issues
/// spawn/close/move here; nothing mutates the table until the tick completes.
pub struct Pending<H> {
    ops: Vec<Op<H>>,
}

enum Op<H> {
    Spawn(Module<H>),
    Close(usize),
    SetViewport(usize, Viewport),
}

impl<H> Pending<H> {
    fn new() -> Self {
        Self { ops: Vec::new() }
    }

    /// Queue a new child module to be added after the tick.
    pub fn spawn(&mut self, handle: H, viewport: Viewport) {
        self.ops.push(Op::Spawn(Module { handle, viewport }));
    }

    /// Queue the removal of the module at `idx` (resolved against the table as
    /// it stands when ops are applied).
    pub fn close(&mut self, idx: usize) {
        self.ops.push(Op::Close(idx));
    }

    /// Queue a viewport change for the module at `idx`.
    pub fn set_viewport(&mut self, idx: usize, viewport: Viewport) {
        self.ops.push(Op::SetViewport(idx, viewport));
    }

    fn is_empty(&self) -> bool {
        self.ops.is_empty()
    }
}

/// The live set of composited child modules + the deferred-mutation discipline.
pub struct ModuleTable<H> {
    modules: Vec<Module<H>>,
}

impl<H> Default for ModuleTable<H> {
    fn default() -> Self {
        Self::new()
    }
}

impl<H> ModuleTable<H> {
    pub fn new() -> Self {
        Self { modules: Vec::new() }
    }

    pub fn len(&self) -> usize {
        self.modules.len()
    }

    pub fn is_empty(&self) -> bool {
        self.modules.is_empty()
    }

    /// Add a module immediately (use outside a tick — e.g. the initial layout).
    pub fn push(&mut self, handle: H, viewport: Viewport) -> usize {
        self.modules.push(Module { handle, viewport });
        self.modules.len() - 1
    }

    /// Tick every module in order. `f` receives each module's handle + viewport
    /// and a [`Pending`] buffer to issue spawn/close/move on. Those mutations
    /// are applied only after the whole pass, so a child mutating the table
    /// during its own frame cannot invalidate the in-progress iteration.
    pub fn tick(&mut self, mut f: impl FnMut(usize, &H, &Viewport, &mut Pending<H>)) {
        let mut pending = Pending::new();
        for (i, m) in self.modules.iter().enumerate() {
            f(i, &m.handle, &m.viewport, &mut pending);
        }
        if !pending.is_empty() {
            self.apply(pending);
        }
    }

    /// The topmost module whose viewport contains global point `(x, y)`, with
    /// the pointer translated to that module's LOCAL coords. Last-pushed =
    /// topmost (z-order). Pointer events route only to the focused child
    /// (roadmap Phase 1c) so a click in one panel can't drive a sibling.
    pub fn focus_at(&self, x: i32, y: i32) -> Option<(usize, i32, i32)> {
        for i in (0..self.modules.len()).rev() {
            let vp = &self.modules[i].viewport;
            if x >= vp.ox && y >= vp.oy && x < vp.ox + vp.w && y < vp.oy + vp.h {
                return Some((i, x - vp.ox, y - vp.oy));
            }
        }
        None
    }

    fn apply(&mut self, pending: Pending<H>) {
        // Spawns and viewport sets first (stable indices), then closes in
        // DESCENDING index order so each removal can't shift a later one.
        let mut closes = Vec::new();
        for op in pending.ops {
            match op {
                Op::Spawn(m) => self.modules.push(m),
                Op::SetViewport(i, vp) => {
                    if let Some(m) = self.modules.get_mut(i) {
                        m.viewport = vp;
                    }
                }
                Op::Close(i) => closes.push(i),
            }
        }
        closes.sort_unstable();
        closes.dedup();
        for i in closes.into_iter().rev() {
            if i < self.modules.len() {
                self.modules.remove(i);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn vp() -> Viewport {
        Viewport::full(256, 144)
    }

    #[test]
    fn push_adds_immediately() {
        let mut t: ModuleTable<&str> = ModuleTable::new();
        assert!(t.is_empty());
        let i = t.push("a", vp());
        assert_eq!(i, 0);
        assert_eq!(t.len(), 1);
    }

    #[test]
    fn tick_visits_every_module_with_its_index() {
        let mut t: ModuleTable<i32> = ModuleTable::new();
        t.push(10, vp());
        t.push(20, vp());
        let mut seen = Vec::new();
        t.tick(|i, h, _vp, _p| seen.push((i, *h)));
        assert_eq!(seen, vec![(0, 10), (1, 20)]);
    }

    #[test]
    fn spawn_during_tick_is_deferred_then_applied() {
        let mut t: ModuleTable<i32> = ModuleTable::new();
        t.push(1, vp());
        // Module 0's frame() spawns a child. The table must NOT grow mid-tick
        // (that's the double-borrow crash), and the child appears after.
        let mut len_seen_during = None;
        t.tick(|_i, _h, _vp, p| {
            // (we can't read t.len() here — that's the whole point — but the
            // iteration is over a snapshot of 1, so f runs exactly once)
            len_seen_during = Some(true);
            p.spawn(2, vp());
        });
        assert_eq!(len_seen_during, Some(true));
        assert_eq!(t.len(), 2, "spawned child applied after the tick");
    }

    #[test]
    fn tick_runs_once_per_preexisting_module_not_for_spawned() {
        let mut t: ModuleTable<i32> = ModuleTable::new();
        t.push(1, vp());
        let mut ticks = 0;
        t.tick(|_i, _h, _vp, p| {
            ticks += 1;
            p.spawn(99, vp()); // each spawn must NOT be ticked this pass
        });
        assert_eq!(ticks, 1, "only the pre-existing module ticked");
        assert_eq!(t.len(), 2);
    }

    #[test]
    fn close_during_tick_applies_descending_so_indices_stay_valid() {
        let mut t: ModuleTable<i32> = ModuleTable::new();
        t.push(0, vp());
        t.push(1, vp());
        t.push(2, vp());
        // Close 0 and 2 during the tick; descending-order apply keeps it sound.
        t.tick(|i, _h, _vp, p| {
            if i == 0 || i == 2 {
                p.close(i);
            }
        });
        assert_eq!(t.len(), 1, "modules 0 and 2 removed, 1 remains");
        let mut left = None;
        t.tick(|_i, h, _vp, _p| left = Some(*h));
        assert_eq!(left, Some(1));
    }

    #[test]
    fn compose_budget_admits_within_caps_and_refuses_past_them() {
        let b = ComposeBudget::v1();
        // Within all caps.
        assert!(b.admit(0, 0, 1024).is_ok());
        assert!(b.admit(7, 1024, 1024).is_ok()); // last allowed child
        // Too many children.
        assert!(b.admit(8, 0, 1).is_err());
        // Child too big.
        assert!(b.admit(0, 0, 16 * 1024 + 1).is_err());
        // Total would overflow the aggregate cap.
        assert!(b.admit(1, 60 * 1024, 8 * 1024).is_err());
        // saturating_add can't be tricked into wrapping past the cap.
        assert!(b.admit(0, usize::MAX, 1).is_err());
    }

    #[test]
    fn focus_at_routes_to_containing_module_in_local_coords() {
        let mut t: ModuleTable<i32> = ModuleTable::new();
        t.push(0, Viewport { ox: 0, oy: 0, w: 100, h: 100 });
        t.push(1, Viewport { ox: 100, oy: 50, w: 64, h: 32 });
        // Inside module 1 → its index + pointer translated to local coords.
        assert_eq!(t.focus_at(110, 60), Some((1, 10, 10)));
        // Inside module 0 only.
        assert_eq!(t.focus_at(5, 5), Some((0, 5, 5)));
        // Outside every viewport.
        assert_eq!(t.focus_at(200, 200), None);
    }

    #[test]
    fn focus_at_picks_topmost_on_overlap() {
        let mut t: ModuleTable<i32> = ModuleTable::new();
        t.push(0, Viewport { ox: 0, oy: 0, w: 100, h: 100 });
        t.push(1, Viewport { ox: 0, oy: 0, w: 100, h: 100 }); // same rect, on top
        // Last-pushed (index 1) wins the click.
        assert_eq!(t.focus_at(10, 10), Some((1, 10, 10)));
    }

    #[test]
    fn cache_content_key_is_deterministic_and_byte_sensitive() {
        let a = WasmCache::<()>::content_key(b"abc");
        assert_eq!(a, WasmCache::<()>::content_key(b"abc"));
        assert_ne!(a, WasmCache::<()>::content_key(b"abd"));
        assert_ne!(a, WasmCache::<()>::content_key(b""));
    }

    #[test]
    fn republish_changes_the_key_so_no_stale_hit() {
        // The whole point: same name/tokenId, new bytes (a republish) → a new
        // content key → cache MISS → fresh fetch. A tokenId-keyed cache would
        // have served the stale v1 forever.
        let mut cache: WasmCache<&str> = WasmCache::new();
        let k1 = WasmCache::<&str>::content_key(b"app-wasm-v1");
        cache.insert(k1, "compiled-v1");
        assert!(cache.contains(k1));

        let k2 = WasmCache::<&str>::content_key(b"app-wasm-v2");
        assert_ne!(k1, k2);
        assert!(cache.get(k2).is_none(), "republished bytes must not hit the v1 entry");
        assert_eq!(cache.get(k1), Some(&"compiled-v1"), "the v1 bytes still resolve to v1");
    }

    #[test]
    fn set_viewport_during_tick_is_deferred() {
        let mut t: ModuleTable<i32> = ModuleTable::new();
        t.push(7, Viewport::full(256, 144));
        t.tick(|i, _h, _vp, p| p.set_viewport(i, Viewport { ox: 10, oy: 20, w: 64, h: 32 }));
        let mut got = None;
        t.tick(|_i, _h, v, _p| got = Some(*v));
        assert_eq!(got, Some(Viewport { ox: 10, oy: 20, w: 64, h: 32 }));
    }

    #[test]
    fn grid_one_module_is_the_full_framebuffer() {
        assert_eq!(grid_viewports(1, 256, 144), vec![Viewport { ox: 0, oy: 0, w: 256, h: 144 }]);
    }

    #[test]
    fn grid_two_modules_split_side_by_side_without_overlap() {
        let v = grid_viewports(2, 256, 144);
        assert_eq!(v, vec![
            Viewport { ox: 0, oy: 0, w: 128, h: 144 },
            Viewport { ox: 128, oy: 0, w: 128, h: 144 },
        ]);
        assert!(v[0].ox + v[0].w <= v[1].ox, "left cell ends before the right begins");
    }

    #[test]
    fn grid_four_modules_are_a_2x2() {
        let v = grid_viewports(4, 256, 144); // cols=2, rows=2, cw=128, ch=72
        assert_eq!(v.len(), 4);
        assert_eq!(v[0], Viewport { ox: 0, oy: 0, w: 128, h: 72 });
        assert_eq!(v[3], Viewport { ox: 128, oy: 72, w: 128, h: 72 });
    }

    #[test]
    fn grid_cells_stay_in_bounds_and_zero_is_empty() {
        assert!(grid_viewports(0, 256, 144).is_empty());
        for n in 1..=9 {
            for vp in grid_viewports(n, 256, 144) {
                assert!(vp.ox >= 0 && vp.oy >= 0);
                assert!(vp.ox + vp.w <= 256 && vp.oy + vp.h <= 144, "cell {vp:?} escapes the framebuffer for n={n}");
            }
        }
    }
}