secure-exec-sidecar 0.3.1

Native Secure Exec sidecar runtime
Documentation
mod support;

use secure_exec_sidecar::wire::{
    BootstrapRootFilesystemRequest, ConfigureVmRequest, DisposeReason, DisposeVmRequest,
    GuestFilesystemCallRequest, GuestFilesystemOperation, GuestRuntimeKind, MountDescriptor,
    MountPluginDescriptor, RequestPayload, ResponsePayload, RootFilesystemEntry,
    RootFilesystemEntryEncoding, RootFilesystemEntryKind,
};
use std::collections::HashMap;
use std::fs;
use std::os::unix::fs::symlink;
use std::time::Duration;
use support::{
    authenticate_wire, collect_process_output_wire_with_timeout, create_vm_wire, execute_wire,
    new_sidecar, open_session_wire, temp_dir, wire_request, wire_vm,
};

/// Companion to `node_modules_host_mount_resolution.rs` (issue #109): that test
/// projects a *plain* host `node_modules` (real package directories). Real
/// installs are rarely plain — pnpm (and yarn's node-linker) lay out
/// `node_modules` as symlinks into a virtual `.pnpm` store, with scoped
/// packages nested under an `@scope/` directory. This locks in that the
/// `host_dir` mount `NodeRuntime.create({ nodeModules })` projects follows those
/// in-tree relative symlinks, so a pnpm-installed package resolves from guest
/// `import` exactly like the plain layout does.
///
/// Note the boundary this does *not* relax: the mount confines reads to its
/// root (anchored `openat2(RESOLVE_BENEATH)`), so a symlink that escapes the
/// mounted tree (a workspace/`file:` dep linked to the workspace root or an
/// external store) is deliberately not followed. The fix for that case is to
/// mount a directory that contains the symlink targets, not to widen the mount.
///
/// One guest V8 isolate per process, so this lives in its own integration-test
/// binary (see the sibling test for the multi-isolate teardown rationale).
#[test]
fn pnpm_symlinked_packages_resolve_from_guest_import() {
    support::assert_node_available();

    let mut sidecar = new_sidecar("node-modules-symlink");

    // Seed a host `node_modules` shaped like a pnpm install: real package
    // directories live in the `.pnpm` virtual store, and the top-level entries
    // are relative symlinks pointing into it. Both a plain and a scoped package
    // are covered, since scoped packages take a separate `@scope/` resolution
    // path.
    let host_root = temp_dir("node-modules-symlink-host");
    let node_modules = host_root.join("node_modules");
    let store = node_modules.join(".pnpm");

    let plain_real = store.join("is-number@7.0.0/node_modules/is-number");
    fs::create_dir_all(&plain_real).expect("create plain store dir");
    fs::write(
        plain_real.join("package.json"),
        r#"{"name":"is-number","version":"7.0.0","type":"module","main":"index.js"}"#,
    )
    .expect("write plain package.json");
    fs::write(
        plain_real.join("index.js"),
        "export default () => \"plain-symlink-resolved\";\n",
    )
    .expect("write plain entry");

    let scoped_real = store.join("@scope+pkg@1.0.0/node_modules/@scope/pkg");
    fs::create_dir_all(&scoped_real).expect("create scoped store dir");
    fs::write(
        scoped_real.join("package.json"),
        r#"{"name":"@scope/pkg","version":"1.0.0","type":"module","main":"index.js"}"#,
    )
    .expect("write scoped package.json");
    fs::write(
        scoped_real.join("index.js"),
        "export default () => \"scoped-symlink-resolved\";\n",
    )
    .expect("write scoped entry");

    // Top-level symlinks, relative and contained within `node_modules` exactly
    // as pnpm writes them.
    symlink(
        "./.pnpm/is-number@7.0.0/node_modules/is-number",
        node_modules.join("is-number"),
    )
    .expect("link plain package");
    fs::create_dir_all(node_modules.join("@scope")).expect("create @scope dir");
    symlink(
        "../.pnpm/@scope+pkg@1.0.0/node_modules/@scope/pkg",
        node_modules.join("@scope/pkg"),
    )
    .expect("link scoped package");

    let cwd = temp_dir("node-modules-symlink-cwd");

    let connection_id = authenticate_wire(&mut sidecar, "conn-1");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
    let (vm_id, _) = create_vm_wire(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
    );

    // Provide the mountpoint, then project the host `node_modules` at guest
    // `/tmp/node_modules` (read-only) — the default `nodeModules` projection.
    sidecar
        .dispatch_wire_blocking(wire_request(
            4,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::BootstrapRootFilesystemRequest(BootstrapRootFilesystemRequest {
                entries: vec![RootFilesystemEntry {
                    path: String::from("/tmp/node_modules"),
                    kind: RootFilesystemEntryKind::Directory,
                    mode: None,
                    uid: None,
                    gid: None,
                    content: None,
                    encoding: None,
                    target: None,
                    executable: false,
                }],
            }),
        ))
        .expect("bootstrap mountpoint");

    sidecar
        .dispatch_wire_blocking(wire_request(
            5,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::ConfigureVmRequest(ConfigureVmRequest {
                mounts: vec![MountDescriptor {
                    guest_path: String::from("/tmp/node_modules"),
                    read_only: true,
                    plugin: MountPluginDescriptor {
                        id: String::from("host_dir"),
                        config: serde_json::to_string(&serde_json::json!({
                            "hostPath": node_modules.to_string_lossy(),
                            "readOnly": true,
                        }))
                        .expect("serialize host_dir mount config"),
                    },
                }],
                software: Vec::new(),
                permissions: None,
                module_access_cwd: None,
                instructions: Vec::new(),
                projected_modules: Vec::new(),
                command_permissions: HashMap::new(),
                loopback_exempt_ports: Vec::new(),
            }),
        ))
        .expect("mount host node_modules");

    // Program under `/tmp` so the resolution walk reaches `/tmp/node_modules`,
    // importing both the plain and the scoped symlinked package.
    let entry_source = r#"
import plain from "is-number";
import scoped from "@scope/pkg";
console.log(plain());
console.log(scoped());
"#;
    let write = sidecar
        .dispatch_wire_blocking(wire_request(
            6,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::GuestFilesystemCallRequest(GuestFilesystemCallRequest {
                operation: GuestFilesystemOperation::WriteFile,
                path: String::from("/tmp/entry.mjs"),
                destination_path: None,
                target: None,
                content: Some(String::from(entry_source)),
                encoding: Some(RootFilesystemEntryEncoding::Utf8),
                recursive: false,
                mode: None,
                uid: None,
                gid: None,
                atime_ms: None,
                mtime_ms: None,
                len: None,
                offset: None,
            }),
        ))
        .expect("write guest entry");
    match write.response.payload {
        ResponsePayload::GuestFilesystemResultResponse(_) => {}
        other => panic!("unexpected guest write response: {other:?}"),
    }

    execute_wire(
        &mut sidecar,
        7,
        &connection_id,
        &session_id,
        &vm_id,
        "proc-resolve",
        GuestRuntimeKind::JavaScript,
        std::path::Path::new("/tmp/entry.mjs"),
        Vec::new(),
    );
    let (stdout, stderr, exit) = collect_process_output_wire_with_timeout(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "proc-resolve",
        Duration::from_secs(10),
    );

    assert_eq!(
        stdout.trim(),
        "plain-symlink-resolved\nscoped-symlink-resolved",
        "guest import of pnpm-symlinked packages should resolve; stderr: {stderr}"
    );
    assert_eq!(exit, 0, "stderr: {stderr}");

    sidecar
        .dispatch_wire_blocking(wire_request(
            8,
            wire_vm(&connection_id, &session_id, &vm_id),
            RequestPayload::DisposeVmRequest(DisposeVmRequest {
                reason: DisposeReason::Requested,
            }),
        ))
        .expect("dispose vm");
}