feat(FN-4636): complete Step 2 — add native sandbox backend

Fusion-Task-Id: FN-4636
Fusion-Task-Lineage: 38ff2f48-4cb1-42c2-8f64-eb3b8d3d7f2c
This commit is contained in:
Fusion
2026-05-15 10:02:04 -07:00
committed by gsxdsm
parent 92be0b4217
commit b1de46e1c1
2 changed files with 141 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import { cwd } from "node:process";
import { describe, expect, it } from "vitest";
import { NativeSandboxBackend } from "../native.js";
describe("NativeSandboxBackend", () => {
it("returns stdout on success", async () => {
const backend = new NativeSandboxBackend();
const result = await backend.run("node -e 'process.stdout.write(\"ok\")'", {
cwd: cwd(),
timeoutMs: 5_000,
maxBuffer: 1024 * 1024,
encoding: "utf-8",
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe("ok");
expect(result.timedOut).toBe(false);
expect(result.bufferExceeded).toBe(false);
});
it("maps timeout failures", async () => {
const backend = new NativeSandboxBackend();
const result = await backend.run("node -e 'setTimeout(() => {}, 1000)'", {
cwd: cwd(),
timeoutMs: 50,
maxBuffer: 1024 * 1024,
encoding: "utf-8",
});
expect(result.exitCode).toBeNull();
expect(result.timedOut).toBe(true);
expect(result.signal).toBe("SIGTERM");
});
it("maps non-zero exits", async () => {
const backend = new NativeSandboxBackend();
const result = await backend.run("node -e 'process.stderr.write(\"fail\"); process.exit(7)'", {
cwd: cwd(),
timeoutMs: 5_000,
maxBuffer: 1024 * 1024,
encoding: "utf-8",
});
expect(result.exitCode).toBe(7);
expect(result.stderr).toContain("fail");
expect(result.timedOut).toBe(false);
});
it("maps maxBuffer failures", async () => {
const backend = new NativeSandboxBackend();
const result = await backend.run("node -e 'process.stdout.write(\"x\".repeat(5000))'", {
cwd: cwd(),
timeoutMs: 5_000,
maxBuffer: 512,
encoding: "utf-8",
});
expect(result.bufferExceeded).toBe(true);
expect(result.exitCode).toBeNull();
});
it("prepare/dispose are idempotent no-ops", async () => {
const backend = new NativeSandboxBackend();
await expect(backend.prepare({ allowNetwork: true })).resolves.toBeUndefined();
await expect(backend.prepare({ allowNetwork: false })).resolves.toBeUndefined();
await expect(backend.dispose()).resolves.toBeUndefined();
await expect(backend.dispose()).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,69 @@
import { exec } from "node:child_process";
import { promisify } from "node:util";
import type { SandboxBackend, SandboxCapabilities, SandboxPolicy, SandboxRunOptions, SandboxRunResult } from "./types.js";
const execAsync = promisify(exec);
export class NativeSandboxBackend implements SandboxBackend {
capabilities(): SandboxCapabilities {
return {
id: "native",
supportsNetworkPolicy: false,
supportsFilesystemPolicy: false,
platform: "any",
};
}
async prepare(_policy: SandboxPolicy): Promise<void> {
return Promise.resolve();
}
async run(command: string, options: SandboxRunOptions): Promise<SandboxRunResult> {
try {
const { stdout, stderr } = await execAsync(command, {
cwd: options.cwd,
timeout: options.timeoutMs,
maxBuffer: options.maxBuffer,
...(options.encoding !== undefined && { encoding: options.encoding }),
...(options.shell !== undefined && { shell: options.shell }),
...(options.env !== undefined && { env: options.env }),
...(options.signal !== undefined && { signal: options.signal }),
});
return {
stdout: stdout?.toString?.() ?? "",
stderr: stderr?.toString?.() ?? "",
exitCode: 0,
signal: null,
timedOut: false,
bufferExceeded: false,
};
} catch (error) {
const errObj = error as Record<string, unknown>;
const code = errObj.code;
const status = typeof errObj.status === "number" ? errObj.status : null;
const exitCode = typeof code === "number" ? code : status;
const message = String(errObj.message ?? "");
return {
stdout: typeof (errObj.stdout as { toString?: unknown })?.toString === "function" ? String(errObj.stdout) : "",
stderr: typeof (errObj.stderr as { toString?: unknown })?.toString === "function" ? String(errObj.stderr) : "",
exitCode,
signal: (errObj.signal as NodeJS.Signals | null | undefined) ?? null,
bufferExceeded:
code === "ENOBUFS"
|| code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|| message.includes("maxBuffer"),
timedOut:
code === "ETIMEDOUT"
|| (errObj.killed === true && (errObj.signal === "SIGTERM" || message.includes("timed out"))),
spawnError: code === "ENOENT" || code === "EACCES" ? (error as Error) : undefined,
};
}
}
async dispose(): Promise<void> {
return Promise.resolve();
}
}