Skip to main content

agentos_v8_runtime/
snapshot.rs

1// V8 startup snapshots: fast isolate creation from pre-compiled bridge code
2
3use std::collections::HashMap;
4use std::io::{Read, Write};
5use std::process::{Command, Stdio};
6use std::sync::{Arc, Condvar, Mutex};
7
8use sha2::{Digest, Sha256};
9
10use crate::bridge::{external_refs, register_stub_bridge_fns};
11use crate::isolate::init_v8_platform;
12use crate::session::{async_bridge_fns, sync_bridge_fns};
13
14/// Maximum allowed snapshot blob size (50MB).
15/// Prevents resource exhaustion from degenerate bridge code.
16const MAX_SNAPSHOT_BLOB_BYTES: usize = 50 * 1024 * 1024;
17const MAX_V8_BRIDGE_CODE_BYTES: usize = 16 * 1024 * 1024;
18/// Userland (agent-SDK) bundles are whole dependency graphs flattened into one
19/// IIFE, so they are larger than the bridge. Bounded so a degenerate bundle cannot
20/// exhaust memory, but generous enough for a real SDK (the pi bundle is ~7.6 MB).
21const MAX_V8_USERLAND_CODE_BYTES: usize = 32 * 1024 * 1024;
22pub(crate) const V8_BRIDGE_CODE_LIMIT_ERROR_CODE: &str = "ERR_V8_BRIDGE_CODE_LIMIT";
23pub(crate) const V8_USERLAND_CODE_LIMIT_ERROR_CODE: &str = "ERR_V8_USERLAND_CODE_LIMIT";
24
25const SNAPSHOT_HELPER_ENV: &str = "AGENTOS_V8_SNAPSHOT_HELPER";
26const SNAPSHOT_HELPER_MAGIC: &[u8; 8] = b"SEV8SNP1";
27const SNAPSHOT_HELPER_OK: &[u8; 2] = b"OK";
28const SNAPSHOT_HELPER_ERR: &[u8; 3] = b"ERR";
29const SNAPSHOT_HELPER_NONE_LEN: u64 = u64::MAX;
30
31#[cfg(any(target_os = "linux", target_os = "android"))]
32#[used]
33#[link_section = ".init_array"]
34static SNAPSHOT_HELPER_CTOR: extern "C" fn() = snapshot_helper_ctor;
35
36// Same pre-main hook on macOS: the darwin sidecar build ships this crate, and
37// without the constructor the helper child would fall through to the normal
38// main and the parent's snapshot request would fail.
39#[cfg(target_os = "macos")]
40#[used]
41#[link_section = "__DATA,__mod_init_func"]
42static SNAPSHOT_HELPER_CTOR: extern "C" fn() = snapshot_helper_ctor;
43
44#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
45extern "C" fn snapshot_helper_ctor() {
46    if std::env::var_os(SNAPSHOT_HELPER_ENV).is_some() {
47        let code = run_snapshot_helper_from_stdio();
48        std::process::exit(code);
49    }
50}
51
52pub(crate) fn validate_bridge_code_size(bridge_code: &str) -> Result<(), String> {
53    if bridge_code.len() > MAX_V8_BRIDGE_CODE_BYTES {
54        return Err(format!(
55            "{V8_BRIDGE_CODE_LIMIT_ERROR_CODE}: bridge code too large for V8 bridge setup: {} bytes (max {})",
56            bridge_code.len(),
57            MAX_V8_BRIDGE_CODE_BYTES
58        ));
59    }
60
61    Ok(())
62}
63
64pub(crate) fn validate_userland_code_size(userland_code: &str) -> Result<(), String> {
65    if userland_code.len() > MAX_V8_USERLAND_CODE_BYTES {
66        return Err(format!(
67            "{V8_USERLAND_CODE_LIMIT_ERROR_CODE}: userland snapshot code too large: {} bytes (max {})",
68            userland_code.len(),
69            MAX_V8_USERLAND_CODE_BYTES
70        ));
71    }
72
73    Ok(())
74}
75
76/// Runs after the bridge but before the userland (agent-SDK) IIFE during snapshot
77/// creation. Replaces bridge-backed lazy getters that would dispatch a host call
78/// (unavailable when bridge fns are stubs) with static, snapshot-safe values. These
79/// are environment-identity values (not per-session config), so baking them is
80/// correct; per-session config is still injected post-restore.
81const SNAPSHOT_USERLAND_PREP: &str = r#"
82(function () {
83    // Agent-SDK bundles are esbuild IIFEs that expect a global CJS `require` for
84    // their node-builtin imports, but the bridge exposes require only via module
85    // wrappers. Bind one from the bridge's namespaced createRequire so the bundle's
86    // __require resolves builtins (via the in-context loadBuiltinModule) during
87    // snapshot eval; it also works post-restore (resolution flows through the real
88    // bridge fns swapped in after restore).
89    if (typeof globalThis.require === "undefined" &&
90        typeof globalThis.__secureExecGuestCreateRequire === "function") {
91        try {
92            globalThis.require = globalThis.__secureExecGuestCreateRequire("/root/index.js");
93        } catch (e) {}
94    }
95    // `process.versions` is a bridge-backed lazy getter: it derives `.node` from the
96    // per-session `config2.version` and merges a host bridge call. During snapshot
97    // creation the bridge fns are stubs AND the default `_processConfig` has no
98    // `version`, so the live getter THROWS — an agent SDK that reads it at
99    // module-init would fail. Rather than REPLACE the getter with a static value
100    // (which would permanently shadow the real per-session version for every
101    // restored session — they'd all read a frozen, fabricated identity), WRAP it:
102    // defer to the live getter so that post-restore — once the real bridge fns and
103    // per-session config are injected — sessions read accurate per-session versions,
104    // and fall back to the static, snapshot-safe identity only while the live getter
105    // throws (i.e. during snapshot creation, before any real config exists).
106    if (typeof process !== "undefined" && process) {
107        var staticVersions = { node: "20.0.0", v8: "12.0.0", uv: "1.0.0", modules: "115" };
108        try {
109            var __verDesc = Object.getOwnPropertyDescriptor(process, "versions");
110            var __liveVersions = __verDesc && __verDesc.get;
111            Object.defineProperty(process, "versions", {
112                configurable: true,
113                enumerable: true,
114                get: function () {
115                    if (__liveVersions) {
116                        try {
117                            var v = __liveVersions.call(this);
118                            // A real per-session result has a node version; during
119                            // snapshot creation the live getter throws before here.
120                            if (v && typeof v === "object" && v.node) return v;
121                        } catch (e) {}
122                    }
123                    return staticVersions;
124                },
125            });
126        } catch (e) {}
127    }
128})();
129"#;
130
131/// Compile and run a Script in the snapshot-creation context, returning a
132/// descriptive error (with the V8 exception message) on failure. `label`
133/// identifies the phase (e.g. "bridge code" / "userland code") in error text.
134fn run_snapshot_script(
135    scope: &mut v8::HandleScope,
136    code: &str,
137    label: &str,
138    eager_compile: bool,
139) -> Result<(), String> {
140    let try_catch = &mut v8::TryCatch::new(scope);
141    let source = match v8::String::new(try_catch, code) {
142        Some(source) => source,
143        None => return Err(format!("failed to create V8 string for {label}")),
144    };
145    // NOTE(perf, measured 2026-07-02): EagerCompile moves cost into the snapshot
146    // blob: user_code_execute dropped 8.9ms -> 7.9ms on the wasm-runner floor,
147    // but isolate_new rose 4.0ms -> 7.4ms when isolate creation was still
148    // per-exec. Parked warm workers now prepay snapshot deserialization off-path,
149    // so only the userland script opts into eager compilation; bridge code and
150    // userland prep stay lazy.
151    let script = if eager_compile {
152        let resource_name = v8::String::new(try_catch, label).unwrap();
153        let origin = v8::ScriptOrigin::new(
154            try_catch,
155            resource_name.into(),
156            0,
157            0,
158            false,
159            0,
160            None,
161            false,
162            false,
163            false,
164            None,
165        );
166        let mut source = v8::script_compiler::Source::new(source, Some(&origin));
167        v8::script_compiler::compile(
168            try_catch,
169            &mut source,
170            v8::script_compiler::CompileOptions::EagerCompile,
171            v8::script_compiler::NoCacheReason::NoReason,
172        )
173    } else {
174        v8::Script::compile(try_catch, source, None)
175    };
176    let Some(script) = script else {
177        let message = try_catch
178            .exception()
179            .map(|exception| exception.to_rust_string_lossy(try_catch))
180            .unwrap_or_else(|| format!("{label} compilation failed during snapshot creation"));
181        return Err(format!(
182            "{label} compilation failed during snapshot creation: {message}"
183        ));
184    };
185    if script.run(try_catch).is_none() {
186        let message = try_catch
187            .exception()
188            .map(|exception| exception.to_rust_string_lossy(try_catch))
189            .unwrap_or_else(|| format!("{label} execution failed during snapshot creation"));
190        return Err(format!(
191            "{label} execution failed during snapshot creation: {message}"
192        ));
193    }
194    Ok(())
195}
196
197/// Create a V8 startup snapshot with a fully-initialized bridge context.
198///
199/// Registers stub bridge functions on the global, injects default config
200/// globals, then compiles and executes the bridge IIFE. The resulting
201/// context — with all bridge infrastructure set up — is snapshotted.
202///
203/// After restore, stub bridge functions are replaced with real session-local
204/// ones, and per-session config is injected via a post-restore script.
205///
206/// Returns an error if the bridge code fails to compile or the resulting
207/// snapshot exceeds MAX_SNAPSHOT_BLOB_BYTES.
208pub fn create_snapshot(bridge_code: &str) -> Result<Vec<u8>, String> {
209    create_snapshot_inner(bridge_code, None)
210}
211
212/// Create a V8 startup snapshot whose default context has BOTH the bridge
213/// infrastructure AND an evaluated userland module graph (e.g. a bundled agent
214/// SDK) captured into it.
215///
216/// `userland_code` is an IIFE (esbuild `format:'iife'`) that runs after the bridge
217/// in the same context and publishes its evaluated exports on `globalThis`. Because
218/// the bridge has already installed node-builtin polyfills and stub bridge fns at
219/// this point, the userland code can `require`/reference them while it evaluates.
220/// The whole post-evaluation heap is frozen into the blob, so restoring a fresh
221/// isolate skips re-evaluating the SDK entirely — collapsing the per-session
222/// module-load/eval tax. Per-session bridge fns and config are still swapped/injected
223/// post-restore exactly as for the bridge-only snapshot.
224pub fn create_snapshot_with_userland(
225    bridge_code: &str,
226    userland_code: &str,
227) -> Result<Vec<u8>, String> {
228    create_snapshot_inner(bridge_code, Some(userland_code))
229}
230
231fn create_snapshot_inner(
232    bridge_code: &str,
233    userland_code: Option<&str>,
234) -> Result<Vec<u8>, String> {
235    validate_bridge_code_size(bridge_code)?;
236    if let Some(userland_code) = userland_code {
237        validate_userland_code_size(userland_code)?;
238    }
239
240    create_snapshot_in_subprocess(bridge_code, userland_code)
241}
242
243fn create_snapshot_inner_in_process(
244    bridge_code: &str,
245    userland_code: Option<&str>,
246) -> Result<Vec<u8>, String> {
247    validate_bridge_code_size(bridge_code)?;
248    if let Some(userland_code) = userland_code {
249        validate_userland_code_size(userland_code)?;
250    }
251
252    init_v8_platform();
253    crate::isolate::prepare_current_thread();
254
255    crate::isolate::with_isolate_lifecycle_lock(|| {
256        let mut isolate = v8::Isolate::snapshot_creator(Some(external_refs()), None);
257        let bridge_result = {
258            let scope = &mut v8::HandleScope::new(&mut isolate);
259            let context = v8::Context::new(scope, Default::default());
260            let scope = &mut v8::ContextScope::new(scope, context);
261
262            // Register stub bridge functions so the IIFE can reference them.
263            let sync_bridge_fns = sync_bridge_fns();
264            let async_bridge_fns = async_bridge_fns();
265            register_stub_bridge_fns(scope, sync_bridge_fns, async_bridge_fns);
266
267            // Inject default config globals for bridge IIFE setup
268            inject_snapshot_defaults(scope);
269
270            // Compile and run bridge code — context captures fully-initialized state.
271            // Then, if present, run the userland (agent-SDK) IIFE in the SAME context so
272            // its evaluated graph is captured alongside the bridge.
273            let result = (|| -> Result<(), String> {
274                run_snapshot_script(scope, bridge_code, "bridge code", false)?;
275                if let Some(userland_code) = userland_code {
276                    // Some bridge-backed globals (e.g. `process.versions`) are lazy
277                    // getters that dispatch a host bridge call on first access. During
278                    // snapshot creation the bridge fns are stubs, so an agent SDK that
279                    // reads them at module-init would fail. Freeze them to static values
280                    // first; per-session config is still injected post-restore.
281                    run_snapshot_script(scope, SNAPSHOT_USERLAND_PREP, "userland prep", false)?;
282                    run_snapshot_script(scope, userland_code, "userland code", true)?;
283                }
284                Ok(())
285            })();
286
287            scope.set_default_context(context);
288            result
289        };
290        let blob = isolate
291            .create_blob(v8::FunctionCodeHandling::Keep)
292            .ok_or_else(|| "V8 snapshot creation failed".to_string())?;
293        bridge_result?;
294
295        // Reject oversized snapshots
296        if blob.len() > MAX_SNAPSHOT_BLOB_BYTES {
297            return Err(format!(
298                "snapshot blob too large: {} bytes (max {})",
299                blob.len(),
300                MAX_SNAPSHOT_BLOB_BYTES
301            ));
302        }
303
304        Ok(blob.to_vec())
305    })
306}
307
308fn create_snapshot_in_subprocess(
309    bridge_code: &str,
310    userland_code: Option<&str>,
311) -> Result<Vec<u8>, String> {
312    let current_exe =
313        std::env::current_exe().map_err(|error| format!("snapshot helper path: {error}"))?;
314    let mut child = Command::new(current_exe)
315        .env(SNAPSHOT_HELPER_ENV, "1")
316        .stdin(Stdio::piped())
317        .stdout(Stdio::piped())
318        .stderr(Stdio::piped())
319        .spawn()
320        .map_err(|error| format!("spawn snapshot helper: {error}"))?;
321
322    {
323        let mut stdin = child
324            .stdin
325            .take()
326            .ok_or_else(|| String::from("snapshot helper stdin unavailable"))?;
327        write_snapshot_helper_request(&mut stdin, bridge_code, userland_code)?;
328    }
329
330    let output = child
331        .wait_with_output()
332        .map_err(|error| format!("wait for snapshot helper: {error}"))?;
333    if !output.status.success() {
334        let stderr = String::from_utf8_lossy(&output.stderr);
335        return Err(format!(
336            "snapshot helper exited with status {}: {}",
337            output.status, stderr
338        ));
339    }
340
341    parse_snapshot_helper_response(&output.stdout, &output.stderr)
342}
343
344fn write_snapshot_helper_request(
345    writer: &mut impl Write,
346    bridge_code: &str,
347    userland_code: Option<&str>,
348) -> Result<(), String> {
349    writer
350        .write_all(SNAPSHOT_HELPER_MAGIC)
351        .map_err(|error| format!("write snapshot helper magic: {error}"))?;
352    write_u64(writer, bridge_code.len() as u64)?;
353    write_u64(
354        writer,
355        userland_code
356            .map(|code| code.len() as u64)
357            .unwrap_or(SNAPSHOT_HELPER_NONE_LEN),
358    )?;
359    writer
360        .write_all(bridge_code.as_bytes())
361        .map_err(|error| format!("write snapshot helper bridge code: {error}"))?;
362    if let Some(userland_code) = userland_code {
363        writer
364            .write_all(userland_code.as_bytes())
365            .map_err(|error| format!("write snapshot helper userland code: {error}"))?;
366    }
367    Ok(())
368}
369
370fn parse_snapshot_helper_response(stdout: &[u8], stderr: &[u8]) -> Result<Vec<u8>, String> {
371    if stdout.starts_with(SNAPSHOT_HELPER_OK) {
372        let payload = &stdout[SNAPSHOT_HELPER_OK.len()..];
373        let (len, rest) = read_u64_from_slice(payload)?;
374        let len = usize::try_from(len)
375            .map_err(|_| String::from("snapshot helper OK payload length overflows usize"))?;
376        if rest.len() != len {
377            return Err(format!(
378                "snapshot helper OK payload length mismatch: declared {}, got {}",
379                len,
380                rest.len()
381            ));
382        }
383        return Ok(rest.to_vec());
384    }
385
386    if stdout.starts_with(SNAPSHOT_HELPER_ERR) {
387        let payload = &stdout[SNAPSHOT_HELPER_ERR.len()..];
388        let (len, rest) = read_u64_from_slice(payload)?;
389        let len = usize::try_from(len)
390            .map_err(|_| String::from("snapshot helper error length overflows usize"))?;
391        if rest.len() != len {
392            return Err(format!(
393                "snapshot helper error length mismatch: declared {}, got {}",
394                len,
395                rest.len()
396            ));
397        }
398        let message = String::from_utf8_lossy(rest);
399        return Err(message.into_owned());
400    }
401
402    Err(format!(
403        "snapshot helper returned invalid response (stdout {} bytes, stderr: {})",
404        stdout.len(),
405        String::from_utf8_lossy(stderr)
406    ))
407}
408
409fn run_snapshot_helper_from_stdio() -> i32 {
410    let mut input = Vec::new();
411    if let Err(error) = std::io::stdin().read_to_end(&mut input) {
412        let _ = write_snapshot_helper_error(format!("read snapshot helper request: {error}"));
413        return 2;
414    }
415
416    let result = (|| -> Result<Vec<u8>, String> {
417        let (bridge_code, userland_code) = parse_snapshot_helper_request(&input)?;
418        create_snapshot_inner_in_process(&bridge_code, userland_code.as_deref())
419    })();
420
421    match result {
422        Ok(blob) => match write_snapshot_helper_ok(&blob) {
423            Ok(()) => 0,
424            Err(error) => {
425                eprintln!("write snapshot helper response: {error}");
426                2
427            }
428        },
429        Err(error) => match write_snapshot_helper_error(error) {
430            Ok(()) => 0,
431            Err(error) => {
432                eprintln!("write snapshot helper error response: {error}");
433                2
434            }
435        },
436    }
437}
438
439fn parse_snapshot_helper_request(input: &[u8]) -> Result<(String, Option<String>), String> {
440    let Some(rest) = input.strip_prefix(SNAPSHOT_HELPER_MAGIC) else {
441        return Err(String::from("snapshot helper request missing magic"));
442    };
443    let (bridge_len, rest) = read_u64_from_slice(rest)?;
444    let (userland_len, rest) = read_u64_from_slice(rest)?;
445    let bridge_len = usize::try_from(bridge_len)
446        .map_err(|_| String::from("snapshot helper bridge length overflows usize"))?;
447    if rest.len() < bridge_len {
448        return Err(format!(
449            "snapshot helper bridge length mismatch: declared {}, got {}",
450            bridge_len,
451            rest.len()
452        ));
453    }
454    let (bridge_bytes, rest) = rest.split_at(bridge_len);
455    let userland_code = if userland_len == SNAPSHOT_HELPER_NONE_LEN {
456        if !rest.is_empty() {
457            return Err(format!(
458                "snapshot helper request has {} trailing bytes after bridge-only payload",
459                rest.len()
460            ));
461        }
462        None
463    } else {
464        let userland_len = usize::try_from(userland_len)
465            .map_err(|_| String::from("snapshot helper userland length overflows usize"))?;
466        if rest.len() != userland_len {
467            return Err(format!(
468                "snapshot helper userland length mismatch: declared {}, got {}",
469                userland_len,
470                rest.len()
471            ));
472        }
473        Some(
474            String::from_utf8(rest.to_vec())
475                .map_err(|error| format!("snapshot helper userland is not UTF-8: {error}"))?,
476        )
477    };
478    let bridge_code = String::from_utf8(bridge_bytes.to_vec())
479        .map_err(|error| format!("snapshot helper bridge is not UTF-8: {error}"))?;
480    Ok((bridge_code, userland_code))
481}
482
483fn write_snapshot_helper_ok(blob: &[u8]) -> std::io::Result<()> {
484    let mut stdout = std::io::stdout().lock();
485    stdout.write_all(SNAPSHOT_HELPER_OK)?;
486    write_u64_io(&mut stdout, blob.len() as u64)?;
487    stdout.write_all(blob)?;
488    stdout.flush()
489}
490
491fn write_snapshot_helper_error(message: String) -> std::io::Result<()> {
492    let mut stdout = std::io::stdout().lock();
493    stdout.write_all(SNAPSHOT_HELPER_ERR)?;
494    write_u64_io(&mut stdout, message.len() as u64)?;
495    stdout.write_all(message.as_bytes())?;
496    stdout.flush()
497}
498
499fn write_u64(writer: &mut impl Write, value: u64) -> Result<(), String> {
500    write_u64_io(writer, value).map_err(|error| format!("write snapshot helper length: {error}"))
501}
502
503fn write_u64_io(writer: &mut impl Write, value: u64) -> std::io::Result<()> {
504    writer.write_all(&value.to_le_bytes())
505}
506
507fn read_u64_from_slice(input: &[u8]) -> Result<(u64, &[u8]), String> {
508    let bytes = input
509        .get(..8)
510        .ok_or_else(|| String::from("snapshot helper payload ended before u64"))?;
511    let mut value = [0_u8; 8];
512    value.copy_from_slice(bytes);
513    Ok((u64::from_le_bytes(value), &input[8..]))
514}
515
516/// Inject default config globals needed by the bridge IIFE during snapshot creation.
517///
518/// These are placeholder values so bridge code that reads _processConfig or
519/// _osConfig at setup time doesn't fail. They're overwritten per-session
520/// after snapshot restore via inject_globals_from_payload.
521///
522/// Properties are set as READ_ONLY (not DONT_DELETE) so they remain
523/// configurable — inject_globals_from_payload can redefine them with
524/// READ_ONLY | DONT_DELETE after restore.
525fn inject_snapshot_defaults(scope: &mut v8::HandleScope) {
526    let context = scope.get_current_context();
527    let global = context.global(scope);
528
529    // _processConfig: default placeholder (overwritten per-session)
530    let pc_code = r#"({
531        cwd: "/",
532        env: {},
533        timing_mitigation: "off",
534        frozen_time_ms: null,
535        high_resolution_time: false
536    })"#;
537    let pc_source = v8::String::new(scope, pc_code).unwrap();
538    let pc_script = v8::Script::compile(scope, pc_source, None).unwrap();
539    let pc_val = pc_script.run(scope).unwrap();
540    if let Some(pc_obj) = pc_val.to_object(scope) {
541        pc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
542    }
543    let pc_key = v8::String::new(scope, "_processConfig").unwrap();
544    // READ_ONLY only — no DONT_DELETE so the property remains configurable
545    // for override after snapshot restore
546    let attr = v8::PropertyAttribute::READ_ONLY;
547    global.define_own_property(scope, pc_key.into(), pc_val, attr);
548
549    // _osConfig: default placeholder (overwritten per-session)
550    let oc_code = r#"({
551        homedir: "/root",
552        tmpdir: "/tmp",
553        platform: "linux",
554        arch: "x64"
555    })"#;
556    let oc_source = v8::String::new(scope, oc_code).unwrap();
557    let oc_script = v8::Script::compile(scope, oc_source, None).unwrap();
558    let oc_val = oc_script.run(scope).unwrap();
559    if let Some(oc_obj) = oc_val.to_object(scope) {
560        oc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
561    }
562    let oc_key = v8::String::new(scope, "_osConfig").unwrap();
563    // READ_ONLY only — no DONT_DELETE so the property remains configurable
564    let attr2 = v8::PropertyAttribute::READ_ONLY;
565    global.define_own_property(scope, oc_key.into(), oc_val, attr2);
566}
567
568/// Create a V8 isolate restored from a snapshot blob.
569///
570/// The external references must match those used during snapshot creation
571/// (provided by bridge::external_refs()).
572///
573/// `blob` must be owned or 'static data — `Vec<u8>`, `Box<[u8]>`, or
574/// `v8::StartupData` all work. The data is copied into the isolate during
575/// creation; V8 does not retain a reference after `Isolate::new()` returns.
576pub fn create_isolate_from_snapshot<B>(blob: B, heap_limit_mb: Option<u32>) -> v8::OwnedIsolate
577where
578    B: std::ops::Deref<Target = [u8]> + std::borrow::Borrow<[u8]> + 'static,
579{
580    init_v8_platform();
581    crate::isolate::prepare_current_thread();
582
583    // `None` applies the bounded-by-default cap (`DEFAULT_HEAP_LIMIT_MB`), same as
584    // the fresh-isolate path — a snapshot-restored isolate is never unbounded.
585    let limit = heap_limit_mb.unwrap_or(crate::isolate::DEFAULT_HEAP_LIMIT_MB);
586    let limit_bytes = (limit as usize) * 1024 * 1024;
587    let params = v8::CreateParams::default()
588        .snapshot_blob(blob)
589        .external_references(&**external_refs())
590        .heap_limits(0, limit_bytes);
591    let mut isolate = crate::isolate::with_isolate_lifecycle_lock(|| v8::Isolate::new(params));
592    crate::isolate::configure_isolate(&mut isolate);
593    // Same OOM guard as the fresh-isolate path: terminate this isolate on heap
594    // exhaustion instead of fatal-aborting the shared process (F-003).
595    crate::isolate::install_heap_limit_guard(&mut isolate);
596    isolate
597}
598
599pub type SnapshotCacheKey = [u8; 32];
600
601/// Thread-safe snapshot cache keyed by bridge code digest.
602///
603/// Uses two-phase locking with per-key in-flight tracking so concurrent
604/// callers requesting different bridge code variants are not blocked by
605/// each other. Callers requesting the same variant wait on a condvar
606/// instead of creating duplicate snapshots.
607pub struct SnapshotCache {
608    inner: Mutex<CacheInner>,
609    max_entries: usize,
610}
611
612struct CacheInner {
613    entries: Vec<CacheEntry>,
614    /// Per-key in-flight tracking: callers for the same digest wait on the
615    /// condvar instead of creating duplicate snapshots.
616    in_flight: HashMap<SnapshotCacheKey, Arc<InFlightEntry>>,
617}
618
619struct CacheEntry {
620    key: SnapshotCacheKey,
621    /// Snapshot blob bytes (copied from v8::StartupData).
622    /// Stored as Vec<u8> rather than StartupData because StartupData
623    /// contains raw pointers that are not Send/Sync.
624    blob: Arc<Vec<u8>>,
625}
626
627/// Shared state for an in-flight snapshot creation. The creator thread
628/// populates `result` and notifies all waiters via `done`.
629struct InFlightEntry {
630    result: Mutex<Option<Result<Arc<Vec<u8>>, String>>>,
631    done: Condvar,
632}
633
634impl SnapshotCache {
635    pub fn new(max_entries: usize) -> Self {
636        SnapshotCache {
637            inner: Mutex::new(CacheInner {
638                entries: Vec::new(),
639                in_flight: HashMap::new(),
640            }),
641            max_entries,
642        }
643    }
644
645    /// Get or create a snapshot for the given bridge code.
646    ///
647    /// Two-phase locking: the cache mutex is held only for lookups and
648    /// inserts, never during snapshot creation. Per-key in-flight tracking
649    /// prevents duplicate snapshot creation for the same bridge code.
650    pub fn get_or_create(&self, bridge_code: &str) -> Result<Arc<Vec<u8>>, String> {
651        self.get_or_create_with_userland(bridge_code, None)
652    }
653
654    /// Return a cached snapshot if present. This never creates a snapshot or
655    /// waits on in-flight creation.
656    pub fn try_get_with_userland(
657        &self,
658        bridge_code: &str,
659        userland_code: Option<&str>,
660    ) -> Option<Arc<Vec<u8>>> {
661        let key = snapshot_cache_key(bridge_code, userland_code);
662        let mut inner = self.inner.lock().unwrap();
663        if let Some(pos) = inner.entries.iter().position(|e| e.key == key) {
664            let entry = inner.entries.remove(pos);
665            let blob = Arc::clone(&entry.blob);
666            inner.entries.push(entry);
667            Some(blob)
668        } else {
669            None
670        }
671    }
672
673    /// Like [`get_or_create`], but the snapshot also captures an evaluated userland
674    /// (agent-SDK) graph. The cache key is the digest of BOTH `bridge_code` and
675    /// `userland_code`, so a change to either — i.e. any change in the bundled
676    /// dependency graph — invalidates the entry and triggers exactly one rebuild.
677    pub fn get_or_create_with_userland(
678        &self,
679        bridge_code: &str,
680        userland_code: Option<&str>,
681    ) -> Result<Arc<Vec<u8>>, String> {
682        let key = snapshot_cache_key(bridge_code, userland_code);
683
684        // Phase 1: short lock — check cache, check in-flight, or claim creation
685        let in_flight = {
686            let mut inner = self.inner.lock().unwrap();
687
688            // Cache hit — move to end (most recently used)
689            if let Some(pos) = inner.entries.iter().position(|e| e.key == key) {
690                let entry = inner.entries.remove(pos);
691                let blob = Arc::clone(&entry.blob);
692                inner.entries.push(entry);
693                return Ok(blob);
694            }
695
696            // Another thread is already creating this snapshot — wait on it
697            if let Some(entry) = inner.in_flight.get(&key) {
698                Some(Arc::clone(entry))
699            } else {
700                // We're the creator — register in-flight and release the lock
701                let entry = Arc::new(InFlightEntry {
702                    result: Mutex::new(None),
703                    done: Condvar::new(),
704                });
705                inner.in_flight.insert(key, Arc::clone(&entry));
706                None
707            }
708        };
709
710        // Wait path: another thread is creating this snapshot
711        if let Some(entry) = in_flight {
712            let mut result = entry.result.lock().unwrap();
713            while result.is_none() {
714                result = entry.done.wait(result).unwrap();
715            }
716            return result.as_ref().unwrap().clone();
717        }
718
719        // Phase 2: create snapshot without holding the cache lock
720        let creation_result = create_snapshot_inner(bridge_code, userland_code).map(Arc::new);
721
722        // Phase 3: short lock — insert result, notify waiters, clean up
723        {
724            let mut inner = self.inner.lock().unwrap();
725
726            if let Ok(ref arc) = creation_result {
727                // LRU eviction: remove oldest (front) entry when at capacity
728                if inner.entries.len() >= self.max_entries {
729                    inner.entries.remove(0);
730                }
731                inner.entries.push(CacheEntry {
732                    key,
733                    blob: Arc::clone(arc),
734                });
735            }
736
737            // Publish result to waiters and remove in-flight entry
738            if let Some(entry) = inner.in_flight.remove(&key) {
739                let mut result = entry.result.lock().unwrap();
740                *result = Some(creation_result.clone());
741                entry.done.notify_all();
742            }
743        }
744
745        creation_result
746    }
747}
748
749/// Cache key over bridge + optional userland code. With no userland this is just
750/// the sha256 of the bridge code (a NUL separator is only added when userland is
751/// present), so existing bridge-only entries keep their historical keys.
752pub fn snapshot_cache_key(bridge_code: &str, userland_code: Option<&str>) -> SnapshotCacheKey {
753    match userland_code {
754        None => {
755            let mut hasher = Sha256::new();
756            hasher.update(bridge_code.as_bytes());
757            hasher.finalize().into()
758        }
759        Some(userland_code) => {
760            let mut buf = Vec::with_capacity(bridge_code.len() + 1 + userland_code.len());
761            buf.extend_from_slice(bridge_code.as_bytes());
762            buf.push(0);
763            buf.extend_from_slice(userland_code.as_bytes());
764            let mut hasher = Sha256::new();
765            hasher.update(&buf);
766            hasher.finalize().into()
767        }
768    }
769}
770
771#[doc(hidden)]
772#[cfg(any(test, feature = "test-support"))]
773pub fn run_snapshot_consolidated_checks() {
774    fn eval(isolate: &mut v8::OwnedIsolate, code: &str) -> String {
775        let scope = &mut v8::HandleScope::new(isolate);
776        let context = v8::Context::new(scope, Default::default());
777        let scope = &mut v8::ContextScope::new(scope, context);
778        let source = v8::String::new(scope, code).unwrap();
779        let script = v8::Script::compile(scope, source, None).unwrap();
780        let result = script.run(scope).unwrap();
781        result.to_rust_string_lossy(scope)
782    }
783
784    // Keep snapshot coverage in a dedicated integration-test process.
785    // Running it in the shared unit-test binary still triggers a V8 teardown
786    // SIGSEGV after the test completes.
787    init_v8_platform();
788    let _ = external_refs();
789
790    // --- Part 1: Snapshot creation returns non-empty blob ---
791    {
792        let bridge_code = "(function() { globalThis.__bridge_init = true; })();";
793        let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
794        assert!(!blob.is_empty(), "snapshot blob should be non-empty");
795    }
796
797    // --- Part 2: Restored isolate executes JS correctly ---
798    {
799        let bridge_code = "(function() { globalThis.__testValue = 42; })();";
800        let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
801        let mut isolate = create_isolate_from_snapshot(blob, None);
802        // Fresh context on restored isolate — bridge globals are in snapshot's
803        // default context, not in a new context. Verify isolate is functional.
804        assert_eq!(eval(&mut isolate, "1 + 1"), "2");
805    }
806
807    // --- Part 3: Restored isolate respects heap_limit_mb ---
808    {
809        let bridge_code = "/* empty bridge */";
810        let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
811        let mut isolate = create_isolate_from_snapshot(blob, Some(8));
812        assert_eq!(eval(&mut isolate, "'heap ok'"), "heap ok");
813    }
814
815    // --- Part 4: Normal blob is under 50MB limit ---
816    {
817        let bridge_code = "(function() { globalThis.x = 1; })();";
818        let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
819        assert!(
820            blob.len() < MAX_SNAPSHOT_BLOB_BYTES,
821            "normal bridge code should produce blob under 50MB limit"
822        );
823    }
824
825    // --- Part 5: Three sequential restores from same snapshot data ---
826    {
827        let bridge_code = "(function() { globalThis.__counter = 0; })();";
828        let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
829        let blob_bytes: Vec<u8> = blob.to_vec();
830
831        for i in 0..3 {
832            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
833            let result = eval(&mut isolate, &format!("{} + 1", i));
834            assert_eq!(result, format!("{}", i + 1));
835        }
836    }
837
838    // --- Part 6: Cache hit returns same Arc ---
839    {
840        let cache = SnapshotCache::new(4);
841        let bridge_code = "(function() { globalThis.__cached = 1; })();";
842
843        let arc1 = cache
844            .get_or_create(bridge_code)
845            .expect("first get_or_create");
846        let arc2 = cache
847            .get_or_create(bridge_code)
848            .expect("second get_or_create");
849
850        // Same Arc (same pointer) — cache hit, not a new snapshot
851        assert!(
852            Arc::ptr_eq(&arc1, &arc2),
853            "cache hit should return same Arc"
854        );
855    }
856
857    // --- Part 7: Cache miss creates new snapshot ---
858    {
859        let cache = SnapshotCache::new(4);
860        let code_a = "(function() { globalThis.__a = 1; })();";
861        let code_b = "(function() { globalThis.__b = 2; })();";
862
863        let arc_a = cache.get_or_create(code_a).expect("create A");
864        let arc_b = cache.get_or_create(code_b).expect("create B");
865
866        // Different bridge code → different Arc
867        assert!(
868            !Arc::ptr_eq(&arc_a, &arc_b),
869            "different code should produce different Arc"
870        );
871
872        // Verify both are usable
873        let mut iso_a = create_isolate_from_snapshot((*arc_a).clone(), None);
874        assert_eq!(eval(&mut iso_a, "1 + 1"), "2");
875
876        let mut iso_b = create_isolate_from_snapshot((*arc_b).clone(), None);
877        assert_eq!(eval(&mut iso_b, "2 + 2"), "4");
878    }
879
880    // --- Part 8: LRU eviction removes oldest entry ---
881    {
882        let cache = SnapshotCache::new(2);
883        let code_1 = "(function() { globalThis.__v1 = 1; })();";
884        let code_2 = "(function() { globalThis.__v2 = 2; })();";
885        let code_3 = "(function() { globalThis.__v3 = 3; })();";
886
887        let arc_1 = cache.get_or_create(code_1).expect("create 1");
888        let _arc_2 = cache.get_or_create(code_2).expect("create 2");
889
890        // Cache is full (2 entries). Adding a third should evict code_1.
891        let _arc_3 = cache.get_or_create(code_3).expect("create 3");
892
893        // code_1 should be evicted — re-requesting it should return a new Arc
894        let arc_1_new = cache.get_or_create(code_1).expect("re-create 1");
895        assert!(
896            !Arc::ptr_eq(&arc_1, &arc_1_new),
897            "evicted entry should produce a new Arc on re-creation"
898        );
899
900        // code_2 should still be cached (it was accessed before code_3 but not evicted)
901        // After eviction of code_1, cache had [code_2, code_3], then adding code_1 evicts code_2
902        // Actually: after inserting code_3, cache was [code_2, code_3] (code_1 evicted).
903        // Then inserting code_1 again: cache is full (2), evicts code_2 → cache is [code_3, code_1].
904    }
905
906    // --- Part 9: Concurrent get_or_create creates only one snapshot ---
907    {
908        use std::sync::atomic::{AtomicUsize, Ordering};
909
910        let cache = Arc::new(SnapshotCache::new(4));
911        let bridge_code = "(function() { globalThis.__concurrent = 1; })();";
912
913        // Pre-warm — to avoid measuring snapshot creation races, verify
914        // that after one creation, N threads all get the same Arc
915        let first = cache.get_or_create(bridge_code).expect("pre-warm");
916
917        let num_threads = 4;
918        let barrier = Arc::new(std::sync::Barrier::new(num_threads));
919        let same_count = Arc::new(AtomicUsize::new(0));
920
921        let mut handles = vec![];
922        for _ in 0..num_threads {
923            let cache = Arc::clone(&cache);
924            let barrier = Arc::clone(&barrier);
925            let first = Arc::clone(&first);
926            let same_count = Arc::clone(&same_count);
927            let code = bridge_code.to_string();
928
929            handles.push(std::thread::spawn(move || {
930                barrier.wait();
931                let arc = cache.get_or_create(&code).expect("concurrent get");
932                if Arc::ptr_eq(&arc, &first) {
933                    same_count.fetch_add(1, Ordering::Relaxed);
934                }
935            }));
936        }
937
938        for h in handles {
939            h.join().expect("thread join");
940        }
941
942        assert_eq!(
943            same_count.load(Ordering::Relaxed),
944            num_threads,
945            "all concurrent callers should get the same cached Arc"
946        );
947    }
948
949    // --- Part 10: Guest WebAssembly remains available after snapshot restore ---
950    {
951        let bridge_code = "(function() { globalThis.__wasm_test = true; })();";
952        let blob = create_snapshot(bridge_code).expect("snapshot creation");
953        let mut isolate = create_isolate_from_snapshot(blob, None);
954
955        let scope = &mut v8::HandleScope::new(&mut isolate);
956        let context = v8::Context::new(scope, Default::default());
957        let scope = &mut v8::ContextScope::new(scope, context);
958
959        let wasm_test_code = r#"
960                (function() {
961                    var bytes = new Uint8Array([
962                        0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
963                        0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,
964                        0x03, 0x02, 0x01, 0x00,
965                        0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,
966                        0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b,
967                    ]);
968                    var module = new WebAssembly.Module(bytes);
969                    var instance = new WebAssembly.Instance(module, {});
970                    return String(instance.exports.add(2, 3));
971                })()
972            "#;
973        let source = v8::String::new(scope, wasm_test_code).unwrap();
974        let script = v8::Script::compile(scope, source, None).unwrap();
975        let result = script.run(scope).unwrap();
976        let result_str = result.to_rust_string_lossy(scope);
977
978        assert_eq!(
979            result_str, "5",
980            "WASM should remain enabled after snapshot restore"
981        );
982    }
983
984    // --- Part 11: Session isolation — fresh contexts from same snapshot ---
985    // Verifies that state set in one session's context does not leak
986    // to another session's context (fresh context per session).
987    {
988        let bridge_code = "(function() { globalThis.__shared_bridge = 'ok'; })();";
989        let blob = create_snapshot(bridge_code).expect("snapshot creation");
990        let blob_bytes: Vec<u8> = blob.to_vec();
991
992        // "Session A": set a global variable
993        {
994            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
995            let scope = &mut v8::HandleScope::new(&mut isolate);
996            let context = v8::Context::new(scope, Default::default());
997            let scope = &mut v8::ContextScope::new(scope, context);
998
999            let source =
1000                v8::String::new(scope, "globalThis.__session_secret = 'session-a-data';").unwrap();
1001            let script = v8::Script::compile(scope, source, None).unwrap();
1002            script.run(scope);
1003
1004            // Verify session A can see its own data
1005            let check = v8::String::new(scope, "globalThis.__session_secret").unwrap();
1006            let script = v8::Script::compile(scope, check, None).unwrap();
1007            let result = script.run(scope).unwrap();
1008            assert_eq!(result.to_rust_string_lossy(scope), "session-a-data");
1009        }
1010
1011        // "Session B": fresh context from same snapshot should NOT see session A's data
1012        {
1013            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1014            let scope = &mut v8::HandleScope::new(&mut isolate);
1015            let context = v8::Context::new(scope, Default::default());
1016            let scope = &mut v8::ContextScope::new(scope, context);
1017
1018            let source = v8::String::new(scope, "typeof globalThis.__session_secret").unwrap();
1019            let script = v8::Script::compile(scope, source, None).unwrap();
1020            let result = script.run(scope).unwrap();
1021            assert_eq!(
1022                result.to_rust_string_lossy(scope),
1023                "undefined",
1024                "session B should not see session A's global state"
1025            );
1026        }
1027    }
1028
1029    // --- Part 12: External references survive snapshot restore ---
1030    // Verifies that FunctionTemplates registered on a restored isolate
1031    // correctly dispatch to Rust bridge callbacks via external_refs().
1032    {
1033        use crate::bridge::{register_async_bridge_fns, register_sync_bridge_fns, PendingPromises};
1034        use crate::host_call::BridgeCallContext;
1035
1036        let bridge_code = "(function() { globalThis.__ext_ref_test = true; })();";
1037        let blob = create_snapshot(bridge_code).expect("snapshot creation");
1038        let mut isolate = create_isolate_from_snapshot(blob, None);
1039
1040        // Create minimal BridgeCallContext (sync call will fail but we
1041        // test that the FunctionTemplate dispatches without crash)
1042        let (event_tx, _event_rx) =
1043            crossbeam_channel::bounded::<crate::session::RuntimeEventEnvelope>(16);
1044        let (_cmd_tx, _cmd_rx) = crossbeam_channel::bounded::<crate::session::SessionCommand>(1);
1045        let call_id_router: crate::host_call::CallIdRouter =
1046            Arc::new(crate::host_call::BridgeCallRegistry::with_default_limit());
1047
1048        let receiver = crate::host_call::ReaderBridgeResponseReceiver::new(Box::new(
1049            std::io::Cursor::new(Vec::<u8>::new()),
1050        ));
1051        let sender = crate::host_call::ChannelRuntimeEventSender::new(event_tx, None);
1052        let bridge_ctx = BridgeCallContext::with_receiver(
1053            Box::new(sender),
1054            Box::new(receiver),
1055            "test-session".to_string(),
1056            call_id_router,
1057            Arc::new(std::sync::atomic::AtomicU64::new(1)),
1058        );
1059        let pending = PendingPromises::new();
1060
1061        let scope = &mut v8::HandleScope::new(&mut isolate);
1062        let context = v8::Context::new(scope, Default::default());
1063        let scope = &mut v8::ContextScope::new(scope, context);
1064
1065        // Register bridge functions on the restored isolate
1066        let _sync_store = register_sync_bridge_fns(
1067            scope,
1068            &bridge_ctx as *const BridgeCallContext,
1069            &["_testSync"],
1070        );
1071        let _async_store = register_async_bridge_fns(
1072            scope,
1073            &bridge_ctx as *const BridgeCallContext,
1074            &pending as *const PendingPromises,
1075            &["_testAsync"],
1076        );
1077
1078        // Verify the functions exist as globals
1079        let check = v8::String::new(scope, "typeof _testSync").unwrap();
1080        let script = v8::Script::compile(scope, check, None).unwrap();
1081        let result = script.run(scope).unwrap();
1082        assert_eq!(
1083            result.to_rust_string_lossy(scope),
1084            "function",
1085            "_testSync should be a function on restored isolate"
1086        );
1087
1088        let check = v8::String::new(scope, "typeof _testAsync").unwrap();
1089        let script = v8::Script::compile(scope, check, None).unwrap();
1090        let result = script.run(scope).unwrap();
1091        assert_eq!(
1092            result.to_rust_string_lossy(scope),
1093            "function",
1094            "_testAsync should be a function on restored isolate"
1095        );
1096    }
1097
1098    // --- Part 13: Register stub bridge functions on V8 global ---
1099    // Verifies that register_stub_bridge_fns places functions on the global
1100    // and that they have the correct typeof without calling them.
1101    {
1102        use crate::bridge::register_stub_bridge_fns;
1103
1104        // Use a snapshot-based isolate (consistent with other parts)
1105        let bridge_code = "/* stub test */";
1106        let blob = create_snapshot(bridge_code).expect("snapshot creation");
1107        let mut isolate = create_isolate_from_snapshot(blob, None);
1108
1109        let scope = &mut v8::HandleScope::new(&mut isolate);
1110        let context = v8::Context::new(scope, Default::default());
1111        let scope = &mut v8::ContextScope::new(scope, context);
1112
1113        register_stub_bridge_fns(
1114            scope,
1115            &["_log", "_error", "_fsReadFile", "_loadPolyfill"],
1116            &["_scheduleTimer", "_dynamicImport"],
1117        );
1118
1119        let check = v8::String::new(
1120            scope,
1121            r#"
1122                (function() {
1123                    var names = ['_log', '_error', '_fsReadFile', '_loadPolyfill',
1124                                 '_scheduleTimer', '_dynamicImport'];
1125                    for (var i = 0; i < names.length; i++) {
1126                        if (typeof globalThis[names[i]] !== 'function') {
1127                            return 'FAIL: ' + names[i] + ' is ' + typeof globalThis[names[i]];
1128                        }
1129                    }
1130                    return 'OK';
1131                })()
1132            "#,
1133        )
1134        .unwrap();
1135        let script = v8::Script::compile(scope, check, None).unwrap();
1136        let result = script.run(scope).unwrap();
1137        assert_eq!(
1138            result.to_rust_string_lossy(scope),
1139            "OK",
1140            "all stub bridge functions should be registered as functions"
1141        );
1142    }
1143
1144    // --- Part 14: Bridge IIFE executes against stubs + snapshot creation ---
1145    // Verifies that setup-time code can reference stub functions (typeof,
1146    // closure wrapping, getter facade) without calling them, and that the
1147    // resulting context can be snapshotted.
1148    {
1149        use crate::bridge::register_stub_bridge_fns;
1150
1151        let mut snapshot_isolate = v8::Isolate::snapshot_creator(Some(external_refs()), None);
1152        {
1153            let scope = &mut v8::HandleScope::new(&mut snapshot_isolate);
1154            let context = v8::Context::new(scope, Default::default());
1155            let scope = &mut v8::ContextScope::new(scope, context);
1156
1157            // Register bridge functions as stubs (no External data).
1158            let sync_bridge_fns = sync_bridge_fns();
1159            let async_bridge_fns = async_bridge_fns();
1160            register_stub_bridge_fns(scope, sync_bridge_fns, async_bridge_fns);
1161
1162            // Simulate bridge IIFE: reference all bridge functions, set up
1163            // closures and getter facade, but never call any bridge function
1164            let iife_code = r#"
1165                    (function() {
1166                        // Verify bridge functions exist (like ivm-compat shim)
1167                        var syncKeys = ['_log', '_error', '_resolveModule', '_loadFile', '_moduleFormat',
1168                            '_cryptoRandomFill', '_fsReadFile', '_fsWriteFile',
1169                            '_childProcessSpawnStart', '_childProcessPoll', '_childProcessSpawnSync'];
1170                        var asyncKeys = ['_dynamicImport', '_scheduleTimer',
1171                            '_networkHttpServerListenRaw'];
1172
1173                        for (var i = 0; i < syncKeys.length; i++) {
1174                            if (typeof globalThis[syncKeys[i]] !== 'function') {
1175                                throw new Error('Missing sync: ' + syncKeys[i]);
1176                            }
1177                        }
1178                        for (var i = 0; i < asyncKeys.length; i++) {
1179                            if (typeof globalThis[asyncKeys[i]] !== 'function') {
1180                                throw new Error('Missing async: ' + asyncKeys[i]);
1181                            }
1182                        }
1183
1184                        // Simulate getter-based fs facade (setup only, no calls)
1185                        var _fs = {};
1186                        Object.defineProperties(_fs, {
1187                            readFile:  { get: function() { return globalThis._fsReadFile; },  enumerable: true },
1188                            writeFile: { get: function() { return globalThis._fsWriteFile; }, enumerable: true },
1189                        });
1190                        globalThis._fs = _fs;
1191
1192                        // Verify getter returns function reference without calling it
1193                        if (typeof _fs.readFile !== 'function') {
1194                            throw new Error('Getter should return function, got ' + typeof _fs.readFile);
1195                        }
1196
1197                        // Simulate closure wrapping (setup only, no calls)
1198                        globalThis.__wrappedLog = function() {
1199                            return globalThis._log.apply(null, arguments);
1200                        };
1201
1202                        globalThis.__bridge_setup_complete = true;
1203                    })();
1204                "#;
1205            let source = v8::String::new(scope, iife_code).unwrap();
1206            let script = v8::Script::compile(scope, source, None).unwrap();
1207            let result = script.run(scope);
1208            assert!(
1209                result.is_some(),
1210                "bridge IIFE should execute without error against stub functions"
1211            );
1212
1213            // Verify setup completed
1214            let check =
1215                v8::String::new(scope, "String(globalThis.__bridge_setup_complete)").unwrap();
1216            let script = v8::Script::compile(scope, check, None).unwrap();
1217            let val = script.run(scope).unwrap();
1218            assert_eq!(
1219                val.to_rust_string_lossy(scope),
1220                "true",
1221                "bridge setup should complete with stub functions"
1222            );
1223
1224            scope.set_default_context(context);
1225        }
1226
1227        let blob = snapshot_isolate.create_blob(v8::FunctionCodeHandling::Keep);
1228        assert!(
1229            blob.is_some(),
1230            "snapshot creation should succeed with stub bridge functions"
1231        );
1232        assert!(
1233            !blob.unwrap().is_empty(),
1234            "snapshot blob should be non-empty"
1235        );
1236    }
1237
1238    // --- Part 15: create_snapshot() auto-registers stubs and injects defaults ---
1239    // Verifies that create_snapshot() registers all bridge function stubs
1240    // and injects _processConfig/_osConfig defaults before running bridge code.
1241    {
1242        // Bridge IIFE that verifies stubs and config globals exist
1243        let iife_code = r#"
1244                (function() {
1245                    // Verify all sync bridge functions are registered as stubs
1246                    var syncFns = ['_log', '_error', '_resolveModule', '_loadFile',
1247                        '_moduleFormat', '_loadPolyfill', '_cryptoRandomFill', '_cryptoRandomUUID',
1248                        '_fsReadFile', '_fsWriteFile', '_fsReadFileBinary',
1249                        '_fsWriteFileBinary', '_fsReadDir', '_fsMkdir', '_fsRmdir',
1250                        '_fsExists', '_fsStat', '_fsUnlink', '_fsRename', '_fsChmod',
1251                        '_fsChown', '_fsLink', '_fsSymlink', '_fsReadlink', '_fsLstat',
1252                        '_fsTruncate', '_fsUtimes', '_childProcessSpawnStart',
1253                        '_childProcessPoll', '_childProcessStdinWrite', '_childProcessStdinClose',
1254                        '_childProcessKill', '_childProcessSpawnSync'];
1255                    for (var i = 0; i < syncFns.length; i++) {
1256                        if (typeof globalThis[syncFns[i]] !== 'function') {
1257                            throw new Error('Missing sync stub: ' + syncFns[i] +
1258                                ' (typeof=' + typeof globalThis[syncFns[i]] + ')');
1259                        }
1260                    }
1261
1262                    // Verify all async bridge functions are registered as stubs
1263                    var asyncFns = ['_dynamicImport', '_scheduleTimer',
1264                        '_networkDnsLookupRaw',
1265                        '_networkDnsResolveRaw',
1266                        '_networkHttpServerListenRaw',
1267                        '_networkHttpServerCloseRaw', '_networkHttpServerWaitRaw',
1268                        '_networkHttp2ServerWaitRaw', '_networkHttp2SessionWaitRaw'];
1269                    for (var i = 0; i < asyncFns.length; i++) {
1270                        if (typeof globalThis[asyncFns[i]] !== 'function') {
1271                            throw new Error('Missing async stub: ' + asyncFns[i] +
1272                                ' (typeof=' + typeof globalThis[asyncFns[i]] + ')');
1273                        }
1274                    }
1275
1276                    // Verify _processConfig default was injected
1277                    if (typeof _processConfig !== 'object' || _processConfig === null) {
1278                        throw new Error('_processConfig not injected: ' + typeof _processConfig);
1279                    }
1280                    if (_processConfig.cwd !== '/') {
1281                        throw new Error('_processConfig.cwd should be "/", got: ' + _processConfig.cwd);
1282                    }
1283
1284                    // Verify _osConfig default was injected
1285                    if (typeof _osConfig !== 'object' || _osConfig === null) {
1286                        throw new Error('_osConfig not injected: ' + typeof _osConfig);
1287                    }
1288                    if (_osConfig.platform !== 'linux') {
1289                        throw new Error('_osConfig.platform should be "linux", got: ' + _osConfig.platform);
1290                    }
1291
1292                    globalThis.__part15_ok = true;
1293                })();
1294            "#;
1295        let blob = create_snapshot(iife_code).expect(
1296            "create_snapshot should succeed with bridge code that checks stubs and defaults",
1297        );
1298        assert!(!blob.is_empty(), "snapshot blob should be non-empty");
1299
1300        // Verify the snapshot can be restored
1301        let mut isolate = create_isolate_from_snapshot(blob, None);
1302        assert_eq!(eval(&mut isolate, "1 + 1"), "2");
1303    }
1304
1305    // --- Part 16: create_snapshot() with getter facade and closures ---
1306    // Verifies that the full bridge pattern (stubs, closures, getter facade,
1307    // config globals) works through create_snapshot() and the context is
1308    // correctly snapshotted via set_default_context.
1309    {
1310        let iife_code = r#"
1311                (function() {
1312                    // Set up getter-based fs facade referencing bridge stubs
1313                    var _fs = {};
1314                    Object.defineProperties(_fs, {
1315                        readFile:  { get: function() { return globalThis._fsReadFile; },  enumerable: true },
1316                        writeFile: { get: function() { return globalThis._fsWriteFile; }, enumerable: true },
1317                    });
1318                    globalThis._fs = _fs;
1319
1320                    // Set up closure wrapping a bridge stub
1321                    globalThis.myLog = function() {
1322                        return globalThis._log.apply(null, arguments);
1323                    };
1324
1325                    // Set up a require-like function (doesn't call _loadPolyfill at setup)
1326                    globalThis.require = function(name) {
1327                        return globalThis._loadPolyfill(name);
1328                    };
1329
1330                    // Set up a console-like object
1331                    globalThis.console = {
1332                        log: function() { globalThis._log.apply(null, arguments); },
1333                        error: function() { globalThis._error.apply(null, arguments); },
1334                    };
1335
1336                    // Read _processConfig at setup time (like process.cwd initialization)
1337                    globalThis.__initialCwd = _processConfig.cwd;
1338
1339                    globalThis.__part16_setup = true;
1340                })();
1341            "#;
1342        let blob = create_snapshot(iife_code)
1343            .expect("create_snapshot should succeed with full bridge IIFE pattern");
1344        assert!(!blob.is_empty());
1345
1346        // Restore and verify default context has the bridge infrastructure
1347        let blob_bytes: Vec<u8> = blob.to_vec();
1348        let mut isolate = create_isolate_from_snapshot(blob_bytes, None);
1349        let scope = &mut v8::HandleScope::new(&mut isolate);
1350        let context = v8::Context::new(scope, Default::default());
1351        let scope = &mut v8::ContextScope::new(scope, context);
1352
1353        // Check that bridge infrastructure from the IIFE is in the default context
1354        let check_code = r#"
1355                (function() {
1356                    var results = [];
1357                    results.push('_fs=' + (typeof _fs === 'object'));
1358                    results.push('_fs.readFile=' + (typeof _fs.readFile === 'function'));
1359                    results.push('myLog=' + (typeof myLog === 'function'));
1360                    results.push('require=' + (typeof require === 'function'));
1361                    results.push('console.log=' + (typeof console.log === 'function'));
1362                    results.push('console.error=' + (typeof console.error === 'function'));
1363                    results.push('__initialCwd=' + __initialCwd);
1364                    results.push('__part16_setup=' + __part16_setup);
1365                    return results.join(';');
1366                })()
1367            "#;
1368        let source = v8::String::new(scope, check_code).unwrap();
1369        let script = v8::Script::compile(scope, source, None).unwrap();
1370        let result = script.run(scope).unwrap();
1371        let result_str = result.to_rust_string_lossy(scope);
1372
1373        assert_eq!(
1374            result_str,
1375            "_fs=true;_fs.readFile=true;myLog=true;require=true;console.log=true;console.error=true;__initialCwd=/;__part16_setup=true",
1376            "restored context should have all bridge infrastructure from the IIFE"
1377        );
1378    }
1379
1380    // --- Part 17: SnapshotCache works with context-snapshot create_snapshot ---
1381    // Verifies cache hit/miss still works now that create_snapshot registers stubs.
1382    {
1383        let cache = SnapshotCache::new(4);
1384        let code = r#"
1385                (function() {
1386                    // Verify stubs are present (create_snapshot registers them)
1387                    if (typeof _log !== 'function') throw new Error('no _log stub');
1388                    if (typeof _processConfig !== 'object') throw new Error('no _processConfig');
1389                    globalThis.__cached_context = true;
1390                })();
1391            "#;
1392
1393        let arc1 = cache.get_or_create(code).expect("first get_or_create");
1394        let arc2 = cache.get_or_create(code).expect("second get_or_create");
1395        assert!(
1396            Arc::ptr_eq(&arc1, &arc2),
1397            "cache hit should return same Arc"
1398        );
1399
1400        // Verify blob is usable
1401        let mut isolate = create_isolate_from_snapshot((*arc1).clone(), None);
1402        assert_eq!(eval(&mut isolate, "1 + 1"), "2");
1403    }
1404
1405    // --- Part 18: Context restore + replace_bridge_fns dispatches correctly ---
1406    // Verifies the full context snapshot restore flow: create snapshot with
1407    // stubs, restore, replace stubs with real bridge functions, verify the
1408    // replaced functions dispatch to the real Rust callbacks.
1409    {
1410        use crate::bridge::{replace_bridge_fns, PendingPromises};
1411        use crate::host_call::BridgeCallContext;
1412
1413        // Create snapshot with stubs + simple bridge IIFE
1414        let bridge_code = r#"
1415                (function() {
1416                    // Getter-based facade referencing globalThis._fsReadFile
1417                    var _fs = {};
1418                    Object.defineProperties(_fs, {
1419                        readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
1420                    });
1421                    globalThis._fs = _fs;
1422                    globalThis.__bridge_ready = true;
1423                })();
1424            "#;
1425        let blob = create_snapshot(bridge_code).expect("snapshot creation");
1426        let mut isolate = create_isolate_from_snapshot(blob, None);
1427
1428        // Create BridgeCallContext (sync calls will fail but we verify dispatch)
1429        let (event_tx, _event_rx) =
1430            crossbeam_channel::bounded::<crate::session::RuntimeEventEnvelope>(16);
1431        let call_id_router: crate::host_call::CallIdRouter =
1432            Arc::new(crate::host_call::BridgeCallRegistry::with_default_limit());
1433        let receiver = crate::host_call::ReaderBridgeResponseReceiver::new(Box::new(
1434            std::io::Cursor::new(Vec::<u8>::new()),
1435        ));
1436        let sender = crate::host_call::ChannelRuntimeEventSender::new(event_tx, None);
1437        let bridge_ctx = BridgeCallContext::with_receiver(
1438            Box::new(sender),
1439            Box::new(receiver),
1440            "test-session".to_string(),
1441            call_id_router,
1442            Arc::new(std::sync::atomic::AtomicU64::new(1)),
1443        );
1444        let pending = PendingPromises::new();
1445
1446        // Restore context and replace bridge functions
1447        let scope = &mut v8::HandleScope::new(&mut isolate);
1448        let context = v8::Context::new(scope, Default::default());
1449        let scope = &mut v8::ContextScope::new(scope, context);
1450
1451        let (_sync_store, _async_store) = replace_bridge_fns(
1452            scope,
1453            &bridge_ctx as *const BridgeCallContext,
1454            &pending as *const PendingPromises,
1455            &["_log", "_fsReadFile"],
1456            &["_scheduleTimer"],
1457        );
1458
1459        // Verify bridge infrastructure from IIFE survives restore
1460        let check = v8::String::new(
1461            scope,
1462            r#"
1463                (function() {
1464                    var results = [];
1465                    results.push('__bridge_ready=' + globalThis.__bridge_ready);
1466                    results.push('_fs_exists=' + (typeof _fs === 'object'));
1467                    // Getter should resolve to the REPLACED function (not stub)
1468                    results.push('_fs.readFile_type=' + typeof _fs.readFile);
1469                    // Direct global should also be the replaced function
1470                    results.push('_log_type=' + typeof _log);
1471                    results.push('_scheduleTimer_type=' + typeof _scheduleTimer);
1472                    return results.join(';');
1473                })()
1474            "#,
1475        )
1476        .unwrap();
1477        let script = v8::Script::compile(scope, check, None).unwrap();
1478        let result = script.run(scope).unwrap();
1479        assert_eq!(
1480            result.to_rust_string_lossy(scope),
1481            "__bridge_ready=true;_fs_exists=true;_fs.readFile_type=function;_log_type=function;_scheduleTimer_type=function",
1482            "restored context should have bridge IIFE state + replaced functions"
1483        );
1484    }
1485
1486    // --- Part 19: _processConfig is overridable after restore ---
1487    // Verifies that inject_snapshot_defaults uses configurable properties
1488    // so inject_globals_from_payload can override them per session.
1489    {
1490        use crate::bridge::serialize_v8_value;
1491
1492        let bridge_code = r#"
1493                (function() {
1494                    // Verify default _processConfig from snapshot
1495                    globalThis.__snapshotCwd = _processConfig.cwd;
1496                })();
1497            "#;
1498        let blob = create_snapshot(bridge_code).expect("snapshot creation");
1499        let mut isolate = create_isolate_from_snapshot(blob, None);
1500
1501        let scope = &mut v8::HandleScope::new(&mut isolate);
1502        let context = v8::Context::new(scope, Default::default());
1503        let scope = &mut v8::ContextScope::new(scope, context);
1504
1505        // Verify snapshot defaults are present
1506        let check = v8::String::new(scope, "__snapshotCwd").unwrap();
1507        let script = v8::Script::compile(scope, check, None).unwrap();
1508        let result = script.run(scope).unwrap();
1509        assert_eq!(result.to_rust_string_lossy(scope), "/");
1510
1511        // Create a V8 payload to override _processConfig
1512        let payload_code = r#"({
1513                processConfig: { cwd: "/app", env: { FOO: "bar" }, timing_mitigation: "off", frozen_time_ms: null },
1514                osConfig: { homedir: "/home/agentos", tmpdir: "/tmp", platform: "linux", arch: "arm64" }
1515            })"#;
1516        let payload_source = v8::String::new(scope, payload_code).unwrap();
1517        let payload_script = v8::Script::compile(scope, payload_source, None).unwrap();
1518        let payload_val = payload_script.run(scope).unwrap();
1519        let payload_bytes = serialize_v8_value(scope, payload_val).expect("serialize payload");
1520
1521        // Inject per-session globals (overrides snapshot defaults)
1522        crate::execution::inject_globals_from_payload(scope, &payload_bytes)
1523            .expect("inject globals payload");
1524
1525        // Verify _processConfig was overridden
1526        let check = v8::String::new(scope, "_processConfig.cwd").unwrap();
1527        let script = v8::Script::compile(scope, check, None).unwrap();
1528        let result = script.run(scope).unwrap();
1529        assert_eq!(
1530            result.to_rust_string_lossy(scope),
1531            "/app",
1532            "_processConfig.cwd should be overridden from '/' to '/app'"
1533        );
1534
1535        // Verify _osConfig was overridden
1536        let check = v8::String::new(scope, "_osConfig.arch").unwrap();
1537        let script = v8::Script::compile(scope, check, None).unwrap();
1538        let result = script.run(scope).unwrap();
1539        assert_eq!(
1540            result.to_rust_string_lossy(scope),
1541            "arm64",
1542            "_osConfig.arch should be overridden to 'arm64'"
1543        );
1544    }
1545
1546    // --- Part 19a: function globals survive snapshot restore ---
1547    {
1548        let bridge_code = r#"
1549                (function() {
1550                    globalThis.__snapshotFn = async function () { return "ok"; };
1551                })();
1552            "#;
1553        let blob = create_snapshot(bridge_code).expect("snapshot creation");
1554        let mut isolate = create_isolate_from_snapshot(blob, None);
1555
1556        let scope = &mut v8::HandleScope::new(&mut isolate);
1557        let context = v8::Context::new(scope, Default::default());
1558        let scope = &mut v8::ContextScope::new(scope, context);
1559
1560        let check = v8::String::new(
1561            scope,
1562            r#"(function() {
1563                    return JSON.stringify({
1564                        fnType: typeof globalThis.__snapshotFn,
1565                        promiseType: typeof globalThis.__snapshotFn?.(),
1566                    });
1567                })()"#,
1568        )
1569        .unwrap();
1570        let script = v8::Script::compile(scope, check, None).unwrap();
1571        let result = script.run(scope).unwrap();
1572        assert_eq!(
1573            result.to_rust_string_lossy(scope),
1574            r#"{"fnType":"function","promiseType":"object"}"#,
1575            "function-valued globals should survive snapshot restore"
1576        );
1577    }
1578
1579    // --- Part 19b: bundled bridge installs fetch globals before snapshot restore ---
1580    {
1581        let bridge_code = concat!(
1582            include_str!(concat!(env!("OUT_DIR"), "/v8-bridge.js")),
1583            "\n",
1584            include_str!(concat!(env!("OUT_DIR"), "/v8-bridge-zlib.js"))
1585        );
1586        let blob = create_snapshot(bridge_code).expect("snapshot creation");
1587        let mut isolate = create_isolate_from_snapshot(blob, None);
1588
1589        let scope = &mut v8::HandleScope::new(&mut isolate);
1590        let context = v8::Context::new(scope, Default::default());
1591        let scope = &mut v8::ContextScope::new(scope, context);
1592
1593        let check = v8::String::new(
1594            scope,
1595            r#"(function() {
1596                    return JSON.stringify({
1597                        fetchType: typeof globalThis.fetch,
1598                        headersType: typeof globalThis.Headers,
1599                        requestType: typeof globalThis.Request,
1600                        responseType: typeof globalThis.Response,
1601                    });
1602                })()"#,
1603        )
1604        .unwrap();
1605        let script = v8::Script::compile(scope, check, None).unwrap();
1606        let result = script.run(scope).unwrap();
1607        assert_eq!(
1608            result.to_rust_string_lossy(scope),
1609            r#"{"fetchType":"function","headersType":"function","requestType":"function","responseType":"function"}"#,
1610            "bundled bridge should expose fetch globals in restored contexts"
1611        );
1612    }
1613
1614    // --- Part 20a: Concurrent get_or_create with different bridge codes ---
1615    // Verifies that concurrent callers requesting different bridge code
1616    // variants are not blocked by each other (two-phase locking).
1617    {
1618        use std::sync::atomic::{AtomicBool, Ordering};
1619        use std::time::Instant;
1620
1621        let cache = Arc::new(SnapshotCache::new(4));
1622        let codes: Vec<String> = (0..3)
1623            .map(|i| {
1624                format!(
1625                    "(function() {{ globalThis.__concurrent_{} = {}; }})();",
1626                    i, i
1627                )
1628            })
1629            .collect();
1630
1631        let barrier = Arc::new(std::sync::Barrier::new(codes.len()));
1632        let all_ok = Arc::new(AtomicBool::new(true));
1633
1634        let mut handles = vec![];
1635        for code in &codes {
1636            let cache = Arc::clone(&cache);
1637            let barrier = Arc::clone(&barrier);
1638            let all_ok = Arc::clone(&all_ok);
1639            let code = code.clone();
1640
1641            handles.push(std::thread::spawn(move || {
1642                barrier.wait();
1643                let start = Instant::now();
1644                match cache.get_or_create(&code) {
1645                    Ok(arc) => {
1646                        assert!(!arc.is_empty());
1647                    }
1648                    Err(e) => {
1649                        eprintln!("get_or_create failed: {}", e);
1650                        all_ok.store(false, Ordering::Relaxed);
1651                    }
1652                }
1653                start.elapsed()
1654            }));
1655        }
1656
1657        let mut durations = vec![];
1658        for h in handles {
1659            durations.push(h.join().expect("thread join"));
1660        }
1661
1662        assert!(
1663            all_ok.load(Ordering::Relaxed),
1664            "all concurrent get_or_create calls should succeed"
1665        );
1666
1667        // Verify all entries are cached (cache hits on second request)
1668        for code in &codes {
1669            let arc1 = cache.get_or_create(code).unwrap();
1670            let arc2 = cache.get_or_create(code).unwrap();
1671            assert!(
1672                Arc::ptr_eq(&arc1, &arc2),
1673                "should be cache hit after creation"
1674            );
1675        }
1676    }
1677
1678    // --- Part 20: Multiple restores from same snapshot are independent ---
1679    // Verifies that user code in one restored context does not leak to another.
1680    {
1681        let bridge_code = r#"
1682                (function() {
1683                    globalThis.__bridge_ok = true;
1684                })();
1685            "#;
1686        let blob = create_snapshot(bridge_code).expect("snapshot creation");
1687        let blob_bytes: Vec<u8> = blob.to_vec();
1688
1689        // Restore A: set a session-specific global
1690        {
1691            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1692            let scope = &mut v8::HandleScope::new(&mut isolate);
1693            let context = v8::Context::new(scope, Default::default());
1694            let scope = &mut v8::ContextScope::new(scope, context);
1695
1696            // Bridge state from snapshot should be present
1697            let check = v8::String::new(scope, "String(__bridge_ok)").unwrap();
1698            let script = v8::Script::compile(scope, check, None).unwrap();
1699            let result = script.run(scope).unwrap();
1700            assert_eq!(result.to_rust_string_lossy(scope), "true");
1701
1702            // Set session-specific state
1703            let code = v8::String::new(scope, "globalThis.__user_data = 'session-a';").unwrap();
1704            let script = v8::Script::compile(scope, code, None).unwrap();
1705            script.run(scope);
1706        }
1707
1708        // Restore B: session A's state should not be visible
1709        {
1710            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1711            let scope = &mut v8::HandleScope::new(&mut isolate);
1712            let context = v8::Context::new(scope, Default::default());
1713            let scope = &mut v8::ContextScope::new(scope, context);
1714
1715            // Bridge state should still be present
1716            let check = v8::String::new(scope, "String(__bridge_ok)").unwrap();
1717            let script = v8::Script::compile(scope, check, None).unwrap();
1718            let result = script.run(scope).unwrap();
1719            assert_eq!(result.to_rust_string_lossy(scope), "true");
1720
1721            // Session A's data should NOT be visible
1722            let check = v8::String::new(scope, "typeof __user_data").unwrap();
1723            let script = v8::Script::compile(scope, check, None).unwrap();
1724            let result = script.run(scope).unwrap();
1725            assert_eq!(
1726                result.to_rust_string_lossy(scope),
1727                "undefined",
1728                "session B should not see session A's user data"
1729            );
1730        }
1731    }
1732
1733    // --- Part 21: Userland snapshot — evaluated graph captured, ZERO re-eval on
1734    // restore, isolation preserved (2b acceptance). ---
1735    {
1736        // Evaluate a string in a fresh context on a restored isolate. Unlike the
1737        // function-level `eval`, this takes an existing ContextScope so successive
1738        // checks observe globals set by earlier scripts in the SAME context.
1739        fn run_in(scope: &mut v8::ContextScope<v8::HandleScope>, code: &str) -> String {
1740            let source = v8::String::new(scope, code).unwrap();
1741            let script = v8::Script::compile(scope, source, None).unwrap();
1742            let result = script.run(scope).unwrap();
1743            result.to_rust_string_lossy(scope)
1744        }
1745
1746        // `userland` stands in for an esbuild IIFE bundle: it evaluates a small
1747        // module graph, references a bridge-provided global (proving the bridge is
1748        // available when userland runs), publishes exports on globalThis, and bumps
1749        // a side-effect counter so we can prove the top-level runs exactly once.
1750        let bridge_code = "(function(){ globalThis.__bridge_ok = true; })();";
1751        let userland = r#"
1752            (function () {
1753                if (typeof globalThis._fsReadFile !== "function") {
1754                    throw new Error("bridge fns missing during userland eval");
1755                }
1756                globalThis.__sideEffectCount = (globalThis.__sideEffectCount || 0) + 1;
1757                var secret = 42;
1758                globalThis.__x = { f: function () { return secret; } };
1759            })();
1760        "#;
1761
1762        let blob = create_snapshot_with_userland(bridge_code, userland)
1763            .expect("userland snapshot creation should succeed");
1764        let blob_bytes: Vec<u8> = blob.to_vec();
1765
1766        // Restore A: fresh isolate + fresh context cloned from the snapshot default
1767        // context. The userland top-level must NOT run again here.
1768        {
1769            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1770            let scope = &mut v8::HandleScope::new(&mut isolate);
1771            let context = v8::Context::new(scope, Default::default());
1772            let scope = &mut v8::ContextScope::new(scope, context);
1773
1774            assert_eq!(
1775                run_in(scope, "String(globalThis.__x.f())"),
1776                "42",
1777                "userland export __x.f() should return 42 from the snapshot"
1778            );
1779            assert_eq!(
1780                run_in(scope, "String(globalThis.__sideEffectCount)"),
1781                "1",
1782                "userland top-level must run exactly once (zero re-eval on restore)"
1783            );
1784            assert_eq!(
1785                run_in(scope, "String(globalThis.__bridge_ok)"),
1786                "true",
1787                "bridge state should coexist with userland state in the snapshot"
1788            );
1789
1790            // Mutate a global in session A.
1791            run_in(scope, "globalThis.__leak = 'session-a'; ''");
1792        }
1793
1794        // Restore B: a separate fresh isolate from the SAME blob must see the
1795        // captured userland state but NOT session A's mutation (isolation).
1796        {
1797            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1798            let scope = &mut v8::HandleScope::new(&mut isolate);
1799            let context = v8::Context::new(scope, Default::default());
1800            let scope = &mut v8::ContextScope::new(scope, context);
1801
1802            assert_eq!(
1803                run_in(scope, "String(globalThis.__x.f())"),
1804                "42",
1805                "session B should see the captured userland export"
1806            );
1807            assert_eq!(
1808                run_in(scope, "String(globalThis.__sideEffectCount)"),
1809                "1",
1810                "session B counter must still be 1 (no re-eval, no cross-session bump)"
1811            );
1812            assert_eq!(
1813                run_in(scope, "typeof globalThis.__leak"),
1814                "undefined",
1815                "session B must NOT observe session A's mutation"
1816            );
1817        }
1818
1819        // Cache: identical (bridge, userland) → same Arc; changed userland → new Arc.
1820        {
1821            let cache = SnapshotCache::new(4);
1822            let a = cache
1823                .get_or_create_with_userland(bridge_code, Some(userland))
1824                .expect("userland cache create");
1825            let b = cache
1826                .get_or_create_with_userland(bridge_code, Some(userland))
1827                .expect("userland cache hit");
1828            assert!(
1829                Arc::ptr_eq(&a, &b),
1830                "identical userland should hit the cache"
1831            );
1832
1833            let userland2 = "(function(){ globalThis.__x = { f: function(){ return 7; } }; })();";
1834            let c = cache
1835                .get_or_create_with_userland(bridge_code, Some(userland2))
1836                .expect("changed userland create");
1837            assert!(
1838                !Arc::ptr_eq(&a, &c),
1839                "changed userland (dep-graph change) should rebuild"
1840            );
1841        }
1842    }
1843
1844    // --- Part 22: REAL agent-SDK bundle snapshots + restores (env-gated). ---
1845    // End-to-end primitive validation against the actual pi SDK snapshot bundle:
1846    // the real bridge bundle + the real esbuild IIFE evaluate together into the
1847    // snapshot, and a fresh restored isolate exposes the SDK runtime global with a
1848    // working createAgentSession. Gated on PI_SNAPSHOT_BUNDLE_PATH so CI without the
1849    // bundle skips it; run with that env var pointing at dist/pi-sdk-snapshot.js.
1850    if let Ok(bundle_path) = std::env::var("PI_SNAPSHOT_BUNDLE_PATH") {
1851        let userland = std::fs::read_to_string(&bundle_path)
1852            .unwrap_or_else(|e| panic!("read pi bundle at {bundle_path}: {e}"));
1853        let bridge_code = concat!(
1854            include_str!(concat!(env!("OUT_DIR"), "/v8-bridge.js")),
1855            "\n",
1856            include_str!(concat!(env!("OUT_DIR"), "/v8-bridge-zlib.js"))
1857        );
1858
1859        let blob = create_snapshot_with_userland(bridge_code, &userland)
1860            .expect("real pi SDK bundle should snapshot cleanly (pure-JS, no top-level I/O)");
1861        let mut isolate = create_isolate_from_snapshot(blob, None);
1862        let scope = &mut v8::HandleScope::new(&mut isolate);
1863        let context = v8::Context::new(scope, Default::default());
1864        let scope = &mut v8::ContextScope::new(scope, context);
1865
1866        let check = v8::String::new(
1867            scope,
1868            "(function(){ var r = globalThis.__PI_SDK_RUNTIME__; \
1869             return r && typeof r.createAgentSession === 'function' && \
1870             typeof r.createAllTools === 'function' ? 'ok' : 'missing'; })()",
1871        )
1872        .unwrap();
1873        let script = v8::Script::compile(scope, check, None).unwrap();
1874        let result = script.run(scope).unwrap();
1875        assert_eq!(
1876            result.to_rust_string_lossy(scope),
1877            "ok",
1878            "restored isolate must expose the pi SDK runtime global from the snapshot"
1879        );
1880    }
1881
1882    // --- Part 23: cross-thread snapshot build → restore (diagnoses pre-warm). ---
1883    // Build a userland snapshot on a SEPARATE spawned+joined thread, then restore and
1884    // eval it on the main thread. If V8 fundamentally forbids restoring a blob built
1885    // on a different thread (the suspected cause of the pre-warm wedge), this aborts.
1886    {
1887        let bridge_code = "(function(){ globalThis.__xt_bridge = true; })();";
1888        let userland = "(function(){ globalThis.__xt = { f: function(){ return 99; } }; })();";
1889        let blob_bytes: Vec<u8> = std::thread::spawn(move || {
1890            create_snapshot_with_userland(bridge_code, userland)
1891                .expect("cross-thread snapshot build should succeed")
1892                .to_vec()
1893        })
1894        .join()
1895        .expect("build thread join");
1896
1897        let mut isolate = create_isolate_from_snapshot(blob_bytes, None);
1898        let scope = &mut v8::HandleScope::new(&mut isolate);
1899        let context = v8::Context::new(scope, Default::default());
1900        let scope = &mut v8::ContextScope::new(scope, context);
1901        let check = v8::String::new(scope, "String(globalThis.__xt.f())").unwrap();
1902        let script = v8::Script::compile(scope, check, None).unwrap();
1903        let result = script.run(scope).unwrap();
1904        assert_eq!(
1905            result.to_rust_string_lossy(scope),
1906            "99",
1907            "a snapshot built on another thread must restore correctly on this thread"
1908        );
1909    }
1910
1911    // --- Part 24: session-level isolation (global AND prototype). ---
1912    // Each agent session leases a FRESH context cloned from the same snapshot's
1913    // default context. This is the isolation unit: a global or built-in-prototype
1914    // mutation in "session A" must NOT be observable in "session B".
1915    {
1916        let bridge_code = "(function(){ globalThis.__iso_ok = true; })();";
1917        let userland = "(function(){ globalThis.__sdk = { v: 1 }; })();";
1918        let blob_bytes: Vec<u8> = create_snapshot_with_userland(bridge_code, userland)
1919            .expect("isolation snapshot")
1920            .to_vec();
1921
1922        // Session A: mutate a global, the captured SDK object, AND a built-in prototype.
1923        {
1924            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1925            let scope = &mut v8::HandleScope::new(&mut isolate);
1926            let context = v8::Context::new(scope, Default::default());
1927            let scope = &mut v8::ContextScope::new(scope, context);
1928            let src = v8::String::new(
1929                scope,
1930                "globalThis.__leakG = 'A'; globalThis.__sdk.v = 999; \
1931                 Array.prototype.__leakP = 'A'; ''",
1932            )
1933            .unwrap();
1934            let script = v8::Script::compile(scope, src, None).unwrap();
1935            script.run(scope);
1936        }
1937
1938        // Session B: a separate fresh context from the SAME blob sees the captured
1939        // snapshot state but NONE of session A's mutations.
1940        {
1941            let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1942            let scope = &mut v8::HandleScope::new(&mut isolate);
1943            let context = v8::Context::new(scope, Default::default());
1944            let scope = &mut v8::ContextScope::new(scope, context);
1945            let check = v8::String::new(
1946                scope,
1947                "(function(){ return [ \
1948                   String(globalThis.__iso_ok), \
1949                   typeof globalThis.__leakG, \
1950                   String(globalThis.__sdk.v), \
1951                   typeof ([].__leakP) \
1952                 ].join(','); })()",
1953            )
1954            .unwrap();
1955            let script = v8::Script::compile(scope, check, None).unwrap();
1956            let result = script.run(scope).unwrap();
1957            assert_eq!(
1958                result.to_rust_string_lossy(scope),
1959                "true,undefined,1,undefined",
1960                "session B must see snapshot state but NOT session A's global/SDK/prototype mutations"
1961            );
1962        }
1963    }
1964
1965    // --- Part 25: H-1 regression — the userland-prep `process.versions` wrapper
1966    // DEFERS to the bridge's live getter post-restore instead of freezing a static
1967    // identity. ---
1968    // `SNAPSHOT_USERLAND_PREP` wraps (does not replace) the bridge's lazy
1969    // `process.versions` getter: during snapshot creation the live getter throws
1970    // (bridge fns are stubs) so a static identity is used, but post-restore — once
1971    // the real bridge fns + per-session config are injected — sessions must read the
1972    // LIVE per-session value. A plain static pin (the prior bug) permanently shadowed
1973    // it. This models the deferral with a getter whose result depends on the
1974    // post-restore-injected `_processConfig`, so a regression back to a static pin
1975    // fails here.
1976    {
1977        use crate::bridge::serialize_v8_value;
1978
1979        // Bridge installs a lazy `process.versions` getter that derives its value from
1980        // the per-session `_processConfig` (resolved live on each access). During
1981        // snapshot creation `_processConfig` is the default (no `version`), so the
1982        // getter throws — exactly the case the prep wraps. A userland is required so
1983        // SNAPSHOT_USERLAND_PREP runs.
1984        let bridge_code = r#"
1985            (function () {
1986                globalThis.process = {
1987                    get versions() {
1988                        // Throws during creation (default _processConfig has no
1989                        // version); returns the live per-session value post-restore.
1990                        return { node: _processConfig.version.replace(/^v/, "") };
1991                    },
1992                };
1993            })();
1994        "#;
1995        let userland = "(function(){ globalThis.__sdk_ready = true; })();";
1996        let blob = create_snapshot_with_userland(bridge_code, userland)
1997            .expect("userland snapshot with a lazy process.versions getter");
1998
1999        let mut isolate = create_isolate_from_snapshot(blob, None);
2000        let scope = &mut v8::HandleScope::new(&mut isolate);
2001        let context = v8::Context::new(scope, Default::default());
2002        let scope = &mut v8::ContextScope::new(scope, context);
2003
2004        // Inject a per-session config carrying a distinctive version (as the sidecar
2005        // does post-restore via inject_globals_from_payload).
2006        let payload_code = r#"({
2007            processConfig: { cwd: "/", env: {}, version: "v99.1.2", timing_mitigation: "off", frozen_time_ms: null },
2008            osConfig: { homedir: "/root", tmpdir: "/tmp", platform: "linux", arch: "x64" }
2009        })"#;
2010        let payload_source = v8::String::new(scope, payload_code).unwrap();
2011        let payload_script = v8::Script::compile(scope, payload_source, None).unwrap();
2012        let payload_val = payload_script.run(scope).unwrap();
2013        let payload_bytes = serialize_v8_value(scope, payload_val).expect("serialize payload");
2014        crate::execution::inject_globals_from_payload(scope, &payload_bytes)
2015            .expect("inject per-session config");
2016
2017        // The wrapper must defer to the live getter → per-session "99.1.2", NOT the
2018        // snapshot-build-time static identity "20.0.0".
2019        let check = v8::String::new(scope, "String(process.versions.node)").unwrap();
2020        let script = v8::Script::compile(scope, check, None).unwrap();
2021        let result = script.run(scope).unwrap();
2022        assert_eq!(
2023            result.to_rust_string_lossy(scope),
2024            "99.1.2",
2025            "process.versions must defer to the live per-session getter post-restore, \
2026             not the frozen snapshot-build-time static identity (H-1 regression)"
2027        );
2028    }
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033    use super::*;
2034
2035    #[test]
2036    fn bridge_cache_key_uses_full_sha256_digest() {
2037        // With no userland the key is the plain sha256 of the bridge code, so
2038        // bridge-only snapshot entries keep their historical keys.
2039        assert_eq!(
2040            snapshot_cache_key("abc", None),
2041            [
2042                0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae,
2043                0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61,
2044                0xf2, 0x00, 0x15, 0xad,
2045            ]
2046        );
2047    }
2048
2049    #[test]
2050    fn create_snapshot_rejects_oversized_bridge_code_before_v8_creation() {
2051        let bridge_code = " ".repeat(MAX_V8_BRIDGE_CODE_BYTES + 1);
2052        let error = match create_snapshot(&bridge_code) {
2053            Ok(_) => panic!("oversized bridge code should be rejected"),
2054            Err(error) => error,
2055        };
2056
2057        assert!(error.contains(V8_BRIDGE_CODE_LIMIT_ERROR_CODE));
2058        assert!(error.contains("bridge code too large for V8 bridge setup"));
2059        assert!(error.contains(&MAX_V8_BRIDGE_CODE_BYTES.to_string()));
2060    }
2061
2062    #[test]
2063    fn snapshot_cache_rejects_oversized_bridge_code_without_retaining_in_flight_state() {
2064        let cache = SnapshotCache::new(1);
2065        let bridge_code = " ".repeat(MAX_V8_BRIDGE_CODE_BYTES + 1);
2066
2067        for _ in 0..2 {
2068            let error = match cache.get_or_create(&bridge_code) {
2069                Ok(_) => panic!("oversized bridge code should be rejected"),
2070                Err(error) => error,
2071            };
2072
2073            assert!(error.contains(V8_BRIDGE_CODE_LIMIT_ERROR_CODE));
2074        }
2075    }
2076
2077    #[test]
2078    fn snapshot_cache_key_is_dep_keyed_over_bridge_and_userland() {
2079        let bridge = "bridge-a";
2080        // Userland presence changes the key vs bridge-only.
2081        assert_ne!(
2082            snapshot_cache_key(bridge, None),
2083            snapshot_cache_key(bridge, Some("user-1")),
2084            "adding userland must change the key"
2085        );
2086        // Different userland → different key (any dep-graph change invalidates).
2087        assert_ne!(
2088            snapshot_cache_key(bridge, Some("user-1")),
2089            snapshot_cache_key(bridge, Some("user-2")),
2090            "different userland must produce a different key"
2091        );
2092        // Identical inputs → identical key (cache hit).
2093        assert_eq!(
2094            snapshot_cache_key(bridge, Some("user-1")),
2095            snapshot_cache_key(bridge, Some("user-1")),
2096        );
2097        // The NUL separator prevents bridge/userland boundary collisions.
2098        assert_ne!(
2099            snapshot_cache_key("ab", Some("c")),
2100            snapshot_cache_key("a", Some("bc")),
2101            "the bridge/userland split must be unambiguous"
2102        );
2103    }
2104
2105    #[test]
2106    fn create_snapshot_with_userland_rejects_oversized_userland_code() {
2107        let bridge_code = "(function(){})();";
2108        let userland = " ".repeat(MAX_V8_USERLAND_CODE_BYTES + 1);
2109        let error = match create_snapshot_with_userland(bridge_code, &userland) {
2110            Ok(_) => panic!("oversized userland code should be rejected"),
2111            Err(error) => error,
2112        };
2113
2114        assert!(error.contains(V8_USERLAND_CODE_LIMIT_ERROR_CODE));
2115        assert!(error.contains("userland snapshot code too large"));
2116    }
2117}