feat(FN-4637): complete Step 2 — add bubblewrap policy adapter

Fusion-Task-Id: FN-4637
Fusion-Task-Lineage: 564c5692-3aaf-4396-9306-a395703cf365
This commit is contained in:
Fusion
2026-05-15 10:34:48 -07:00
committed by gsxdsm
parent 063977e23e
commit b755553bd6
2 changed files with 200 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import { SandboxPolicyError, fusionWorktreePreset, policyToBwrapArgs, type BubblewrapPolicyContext } from "../../sandbox/bubblewrap-policy.js";
function baseCtx(overrides: Partial<BubblewrapPolicyContext> = {}): BubblewrapPolicyContext {
return {
worktreePath: "/repo/.worktrees/fn-1",
repoRootPath: "/repo",
pnpmStorePath: "/home/u/.pnpm-store",
nodeBinPath: "/usr/bin/node",
homeDir: "/home/u",
pathExists: (path) => !path.includes("missing"),
envSource: {
PATH: "/usr/bin",
HOME: "/home/u",
USER: "u",
LANG: "en_US.UTF-8",
FUSION_RUN_ID: "run-1",
SECRET_TOKEN: "hidden",
},
...overrides,
};
}
describe("policyToBwrapArgs", () => {
it.each([
{ allowNetwork: true, expected: false },
{ allowNetwork: false, expected: true },
])("maps allowNetwork=$allowNetwork to --unshare-net=$expected", ({ allowNetwork, expected }) => {
const args = policyToBwrapArgs({ allowNetwork }, baseCtx());
expect(args.includes("--unshare-net")).toBe(expected);
});
it("includes defaults for writable mounts and env allowlist", () => {
const args = policyToBwrapArgs({ allowNetwork: true }, baseCtx());
expect(args).toContain("--bind");
expect(args).toContain("/repo/.worktrees/fn-1");
expect(args).toContain("/home/u/.pnpm-store");
expect(args).toContain("--tmpfs");
expect(args).toContain("/tmp");
expect(args).toContain("--setenv");
expect(args.join(" ")).toContain("FUSION_RUN_ID run-1");
expect(args.join(" ")).not.toContain("SECRET_TOKEN");
});
it("supports additional writable paths and skips missing readonly sources", () => {
const args = policyToBwrapArgs(
{
allowNetwork: true,
allowedWritePaths: ["/custom/write"],
allowedReadPaths: ["/missing/readonly", "/custom/ro"],
},
baseCtx(),
);
expect(args.join(" ")).toContain("--bind /custom/write /custom/write");
expect(args.join(" ")).toContain("--ro-bind /custom/ro /custom/ro");
expect(args.join(" ")).not.toContain("/missing/readonly");
});
it("guards port 4040 unless explicitly overridden", () => {
expect(() =>
policyToBwrapArgs({ allowNetwork: true, allowedPorts: [4040] }, baseCtx()),
).toThrow(SandboxPolicyError);
expect(() =>
policyToBwrapArgs({ allowNetwork: true, allowedPorts: [4040], allowPort4040Override: true }, baseCtx()),
).not.toThrow();
});
it("fusionWorktreePreset includes worktree and pnpm store but not .fusion db path", () => {
const preset = fusionWorktreePreset(baseCtx());
expect(preset.allowedWritePaths).toContain("/repo/.worktrees/fn-1");
expect(preset.allowedWritePaths).toContain("/home/u/.pnpm-store");
expect((preset.allowedWritePaths ?? []).some((path) => path.includes(".fusion"))).toBe(false);
});
});

View File

@@ -0,0 +1,122 @@
import { dirname, resolve } from "node:path";
import type { SandboxPolicy } from "./types.js";
export class SandboxPolicyError extends Error {
constructor(message: string) {
super(message);
this.name = "SandboxPolicyError";
}
}
export interface BubblewrapPolicy extends SandboxPolicy {
allowedPorts?: number[];
allowPort4040Override?: boolean;
}
export interface BubblewrapPolicyContext {
worktreePath: string;
repoRootPath: string;
pnpmStorePath: string;
nodeBinPath: string;
homeDir: string;
tmpDirOverride?: string;
pathExists?: (path: string) => boolean;
envSource?: NodeJS.ProcessEnv;
}
function shouldPassEnv(key: string): boolean {
return key === "PATH"
|| key === "HOME"
|| key === "USER"
|| key === "LANG"
|| key.startsWith("LC_")
|| key.startsWith("NODE_")
|| key.startsWith("npm_")
|| key.startsWith("PNPM_")
|| key === "CI"
|| key.startsWith("FUSION_");
}
function uniq(paths: string[]): string[] {
return [...new Set(paths.map((p) => resolve(p)))];
}
export function fusionWorktreePreset(ctx: BubblewrapPolicyContext): BubblewrapPolicy {
return {
allowNetwork: true,
allowedReadPaths: [ctx.repoRootPath],
allowedWritePaths: [ctx.worktreePath, ctx.pnpmStorePath],
};
}
export function policyToBwrapArgs(policy: BubblewrapPolicy, ctx: BubblewrapPolicyContext): string[] {
if (policy.allowedPorts?.includes(4040) && policy.allowPort4040Override !== true) {
throw new SandboxPolicyError("Port 4040 is reserved and cannot be allowed in sandbox policy.");
}
const pathExists = ctx.pathExists ?? (() => true);
const tmpDir = ctx.tmpDirOverride ?? "/tmp";
const args = [
"--die-with-parent",
"--unshare-pid",
"--unshare-uts",
"--unshare-ipc",
"--new-session",
"--proc",
"/proc",
"--dev",
"/dev",
"--clearenv",
"--tmpfs",
tmpDir,
];
if (!policy.allowNetwork) {
args.push("--unshare-net");
}
const writablePaths = uniq([
ctx.worktreePath,
ctx.pnpmStorePath,
...(policy.allowedWritePaths ?? []),
]);
for (const path of writablePaths) {
if (!pathExists(path)) continue;
args.push("--bind", path, path);
}
const nodeInstallDir = dirname(ctx.nodeBinPath);
const readonlyPaths = uniq([
...((policy.allowedReadPaths ?? []).length ? (policy.allowedReadPaths ?? []) : [ctx.repoRootPath]),
...(ctx.repoRootPath !== ctx.worktreePath ? [ctx.repoRootPath] : []),
"/usr",
"/bin",
"/lib",
"/lib64",
"/etc/resolv.conf",
"/etc/ssl",
"/etc/ca-certificates",
nodeInstallDir,
]).filter((path) => !writablePaths.includes(path));
for (const path of readonlyPaths) {
if (!pathExists(path)) continue;
args.push("--ro-bind", path, path);
}
const env = {
...(ctx.envSource ?? process.env),
...(policy.env ?? {}),
};
for (const [key, value] of Object.entries(env)) {
if (!value || !shouldPassEnv(key)) continue;
args.push("--setenv", key, value);
}
args.push("--chdir", ctx.worktreePath);
return args;
}