From 370b129e2f8cfff8fa4995ee384e9094348f3b2f Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 3 Jul 2026 19:15:03 -0700 Subject: [PATCH] test(core): filesystem native-parity guard (fs_probe vs native Linux) Build fs_probe for wasm and native, run the wasm one inside an AgentOs VM via openShell, and assert its stdout matches native-Linux glibc line-for-line (stat mode bits, chmod round-trip, access rwx, truncate size, append/read integrity, and a heap-growth write loop). Guards the filesystem native-parity fixes in secure-exec. --- Cargo.lock | 26 +-- Cargo.toml | 18 +- packages/core/tests/fs-native-parity.test.ts | 204 +++++++++++++++++++ 3 files changed, 226 insertions(+), 22 deletions(-) create mode 100644 packages/core/tests/fs-native-parity.test.ts diff --git a/Cargo.lock b/Cargo.lock index c959c08a4..9255e9cf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3136,7 +3136,7 @@ dependencies = [ [[package]] name = "secure-exec-bridge" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "serde", "serde_json", @@ -3145,11 +3145,11 @@ dependencies = [ [[package]] name = "secure-exec-build-support" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" [[package]] name = "secure-exec-client" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "futures", "parking_lot", @@ -3162,7 +3162,7 @@ dependencies = [ [[package]] name = "secure-exec-execution" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "base64 0.22.1", "ciborium", @@ -3179,7 +3179,7 @@ dependencies = [ [[package]] name = "secure-exec-kernel" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "base64 0.22.1", "getrandom 0.2.17", @@ -3195,7 +3195,7 @@ dependencies = [ [[package]] name = "secure-exec-sidecar" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "aes", "aes-gcm", @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "secure-exec-sidecar-browser" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "base64 0.22.1", "getrandom 0.2.17", @@ -3262,7 +3262,7 @@ dependencies = [ [[package]] name = "secure-exec-sidecar-core" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "base64 0.22.1", "secure-exec-bridge", @@ -3275,7 +3275,7 @@ dependencies = [ [[package]] name = "secure-exec-sidecar-protocol" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "rivet-vbare-compiler", "secure-exec-vm-config", @@ -3287,7 +3287,7 @@ dependencies = [ [[package]] name = "secure-exec-v8-runtime" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "ciborium", "crossbeam-channel", @@ -3302,7 +3302,7 @@ dependencies = [ [[package]] name = "secure-exec-vfs" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "async-trait", "aws-sdk-s3", @@ -3314,7 +3314,7 @@ dependencies = [ [[package]] name = "secure-exec-vfs-core" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -3327,7 +3327,7 @@ dependencies = [ [[package]] name = "secure-exec-vm-config" -version = "0.3.0-rc.1" +version = "0.3.4-rc.1" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 9c7895e1d..629e57891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,19 +23,19 @@ repository = "https://github.com/rivet-dev/agent-os" # normal crates.io dependencies so CI/publish builds do not need a sibling # checkout. [workspace.dependencies] -agentos-bridge = { package = "secure-exec-bridge", path = "../secure-exec/crates/bridge", version = "0.3.0-rc.1" } +agentos-bridge = { package = "secure-exec-bridge", path = "../secure-exec/crates/bridge", version = "0.3.4-rc.1" } agentos-protocol = { path = "crates/agentos-protocol", version = "0.2.0-rc.3" } agentos-sidecar-core = { path = "crates/agentos-sidecar-core", version = "0.2.0-rc.3" } agentos-sidecar = { path = "crates/agentos-sidecar", version = "0.2.0-rc.3" } agentos-sidecar-browser = { path = "crates/agentos-sidecar-browser", version = "0.2.0-rc.3" } -agentos-kernel = { package = "secure-exec-kernel", path = "../secure-exec/crates/kernel", version = "0.3.0-rc.1" } -agentos-execution = { package = "secure-exec-execution", path = "../secure-exec/crates/execution", version = "0.3.0-rc.1" } -agentos-v8-runtime = { package = "secure-exec-v8-runtime", path = "../secure-exec/crates/v8-runtime", version = "0.3.0-rc.1" } -secure-exec-client = { path = "../secure-exec/crates/secure-exec-client", version = "0.3.0-rc.1" } -secure-exec-bridge = { path = "../secure-exec/crates/bridge", version = "0.3.0-rc.1" } -secure-exec-sidecar = { path = "../secure-exec/crates/sidecar", version = "0.3.0-rc.1" } +agentos-kernel = { package = "secure-exec-kernel", path = "../secure-exec/crates/kernel", version = "0.3.4-rc.1" } +agentos-execution = { package = "secure-exec-execution", path = "../secure-exec/crates/execution", version = "0.3.4-rc.1" } +agentos-v8-runtime = { package = "secure-exec-v8-runtime", path = "../secure-exec/crates/v8-runtime", version = "0.3.4-rc.1" } +secure-exec-client = { path = "../secure-exec/crates/secure-exec-client", version = "0.3.4-rc.1" } +secure-exec-bridge = { path = "../secure-exec/crates/bridge", version = "0.3.4-rc.1" } +secure-exec-sidecar = { path = "../secure-exec/crates/sidecar", version = "0.3.4-rc.1" } # LOCAL-DEV: unpublished converged browser crate (web/wasm work). -secure-exec-sidecar-browser = { path = "../secure-exec/crates/sidecar-browser", version = "0.3.0-rc.1" } -secure-exec-vm-config = { path = "../secure-exec/crates/vm-config", version = "0.3.0-rc.1" } +secure-exec-sidecar-browser = { path = "../secure-exec/crates/sidecar-browser", version = "0.3.4-rc.1" } +secure-exec-vm-config = { path = "../secure-exec/crates/vm-config", version = "0.3.4-rc.1" } vbare = "0.0.4" vbare-compiler = { package = "rivet-vbare-compiler", version = "0.0.5" } diff --git a/packages/core/tests/fs-native-parity.test.ts b/packages/core/tests/fs-native-parity.test.ts new file mode 100644 index 000000000..ec049f64d --- /dev/null +++ b/packages/core/tests/fs-native-parity.test.ts @@ -0,0 +1,204 @@ +import { spawnSync } from "node:child_process"; +import { + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, test } from "vitest"; +import type { AgentOs } from "../src/index.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(__dirname, "../../.."); +const SECURE_EXEC_C_ROOT = resolve( + REPO_ROOT, + "../secure-exec/registry/native/c", +); +const WASM_PROBE_BINARY = resolve(SECURE_EXEC_C_ROOT, "build/fs_probe"); +const NATIVE_PROBE_BINARY = resolve( + SECURE_EXEC_C_ROOT, + "build/native/fs_probe", +); +const PATCHED_LIBC = resolve( + SECURE_EXEC_C_ROOT, + "sysroot/lib/wasm32-wasi/libc.a", +); +const SIDECAR_BINARY = resolve(REPO_ROOT, "target/debug/agentos-sidecar"); + +function hasCommand(command: string): boolean { + try { + return ( + spawnSync(command, ["--version"], { encoding: "utf8" }).status === 0 + ); + } catch { + return false; + } +} + +// This test builds the fs_probe fixtures (native + wasm) and the workspace +// sidecar on the fly, so it can only run with the secure-exec sibling checkout +// present (absent on release pins, where prepare-build performs no clone) plus +// make + cargo. The wasm C toolchain is the WASI SDK clang invoked by the +// sibling Makefile (not a system `clang` on PATH), so it is not probed here. +// Skip cleanly otherwise instead of hard-failing the run, matching +// vim-native-parity.test.ts. +const CAN_RUN = + existsSync(join(SECURE_EXEC_C_ROOT, "Makefile")) && + hasCommand("make") && + hasCommand("cargo"); + +function runChecked( + command: string, + args: string[], + options: { cwd: string; label: string }, +): void { + const result = spawnSync(command, args, { + cwd: options.cwd, + encoding: "utf8", + env: { ...process.env, AGENTOS_WASM_SNAPSHOT_RUNNER: "off" }, + }); + if (result.status !== 0) { + throw new Error( + [ + `${options.label} failed`, + `cwd=${options.cwd}`, + `command=${command} ${args.join(" ")}`, + `status=${result.status}`, + result.stdout, + result.stderr, + ] + .filter(Boolean) + .join("\n"), + ); + } +} + +function ensureFsProbeBuilt(): void { + if (!existsSync(PATCHED_LIBC)) { + runChecked("make", ["sysroot"], { + cwd: SECURE_EXEC_C_ROOT, + label: "failed to build patched wasi-libc sysroot", + }); + } + runChecked("make", ["build/native/fs_probe", "build/fs_probe"], { + cwd: SECURE_EXEC_C_ROOT, + label: "failed to build fs_probe parity fixtures", + }); +} + +function ensureWorkspaceSidecarBuilt(): void { + runChecked("node", ["scripts/secure-exec-dep.mjs", "prepare-build"], { + cwd: REPO_ROOT, + label: "failed to prepare secure-exec workspace dependency", + }); + runChecked("cargo", ["build", "-q", "-p", "agentos-sidecar"], { + cwd: REPO_ROOT, + label: "failed to build workspace agentos-sidecar", + }); + process.env.AGENTOS_SIDECAR_BIN = SIDECAR_BINARY; + process.env.AGENTOS_WASM_SNAPSHOT_RUNNER = "off"; +} + +function materializeFsProbePackage(): string { + const pkgDir = mkdtempSync(join(tmpdir(), "agentos-fs-probe-pkg-")); + mkdirSync(join(pkgDir, "bin")); + copyFileSync(WASM_PROBE_BINARY, join(pkgDir, "bin", "fs_probe")); + writeFileSync( + join(pkgDir, "package.json"), + JSON.stringify({ name: "fs-probe-fixture", version: "0.0.0" }), + ); + writeFileSync( + join(pkgDir, "agentos-package.json"), + JSON.stringify({ name: "fs-probe-fixture" }), + ); + return pkgDir; +} + +function normalizeProbeOutput(output: string): string { + return output + .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "") + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n") + .trimEnd(); +} + +function runNativeProbe(): string { + const scratchDir = mkdtempSync(join(tmpdir(), "fs-probe-native-")); + try { + const result = spawnSync(NATIVE_PROBE_BINARY, [scratchDir], { + encoding: "utf8", + }); + expect(result.status, result.stderr || result.stdout).toBe(0); + return normalizeProbeOutput(result.stdout); + } finally { + rmSync(scratchDir, { force: true, recursive: true }); + } +} + +describe.skipIf(!CAN_RUN)("filesystem native parity", () => { + let vm: AgentOs | undefined; + let shellId: string | undefined; + let unsubscribeShellData: (() => void) | undefined; + let probePkgDir: string | undefined; + + afterEach(async () => { + if (unsubscribeShellData) { + unsubscribeShellData(); + unsubscribeShellData = undefined; + } + if (probePkgDir) { + rmSync(probePkgDir, { force: true, recursive: true }); + probePkgDir = undefined; + } + if (vm && shellId) { + try { + vm.closeShell(shellId); + } catch { + // The probe may already have exited. + } + } + shellId = undefined; + if (vm) { + await vm.dispose(); + vm = undefined; + } + }); + + test("fs_probe output matches native Linux through openShell", async () => { + ensureWorkspaceSidecarBuilt(); + ensureFsProbeBuilt(); + const expected = runNativeProbe(); + const { AgentOs } = await import("../src/index.js"); + + probePkgDir = materializeFsProbePackage(); + vm = await AgentOs.create({ + defaultSoftware: false, + software: [probePkgDir], + }); + + let rawOutput = ""; + ({ shellId } = vm.openShell({ + command: "fs_probe", + args: ["/tmp/fs-probe-vm"], + cols: 120, + rows: 40, + })); + unsubscribeShellData = vm.onShellData(shellId, (data) => { + rawOutput += + typeof data === "string" ? data : Buffer.from(data).toString("utf8"); + }); + + const status = await vm.waitShell(shellId); + const actual = normalizeProbeOutput(rawOutput); + expect(status, actual).toBe(0); + expect(actual).toBe(expected); + // Generous timeout: the first run may build the wasi-libc sysroot and the + // debug sidecar from cold, which can exceed a 2-minute budget. Warm runs + // are near-instant (make/cargo no-ops). + }, 600_000); +});