feat(FN-4638): complete Step 3 — add sandbox-exec policy adapter

Fusion-Task-Id: FN-4638
Fusion-Task-Lineage: a6dcc3d9-b7ab-42c1-af88-c9e09f770d92
This commit is contained in:
Fusion
2026-05-15 11:02:57 -07:00
committed by gsxdsm
parent cfc95c8ea8
commit c087ee4b3e
2 changed files with 175 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import {
fusionWorktreePreset,
policyToSbplProfile,
SandboxPolicyError,
sbplEscape,
type SandboxExecContext,
} from "../../sandbox/sandbox-exec-policy.js";
const ctx: SandboxExecContext = {
worktreePath: "/tmp/worktree",
repoRootPath: "/tmp/repo",
pnpmStorePath: "/Users/test/Library/pnpm/store",
nodeBinPath: "/usr/local/bin/node",
homeDir: "/Users/test",
};
describe("sandbox-exec policy", () => {
it("escapes sbpl paths", () => {
expect(sbplEscape('a\\b"c d')).toBe('a\\\\b\\"c d');
expect(sbplEscape("emoji-📦")).toContain("\\x");
});
it.each([
{ allowNetwork: true, expected: "(allow network-outbound)" },
{ allowNetwork: false, expected: "(deny network*)" },
])("emits network clauses", ({ allowNetwork, expected }) => {
const profile = policyToSbplProfile({ allowNetwork }, ctx);
expect(profile).toContain("(version 1)");
expect(profile).toContain(expected);
});
it("includes defaults plus custom read/write paths", () => {
const profile = policyToSbplProfile(
{
allowNetwork: true,
allowedWritePaths: ["/tmp/custom write"],
allowedReadPaths: ["/opt/custom-read"],
},
ctx,
);
expect(profile).toContain("(allow file-write* (subpath \"/tmp/worktree\"))");
expect(profile).toContain("(allow file-write* (subpath \"/Users/test/Library/pnpm/store\"))");
expect(profile).toContain("(allow file-write* (subpath \"/tmp/custom write\"))");
expect(profile).toContain("(allow file-read* (subpath \"/opt/custom-read\"))");
});
it("guards port 4040", () => {
expect(() => policyToSbplProfile({ allowNetwork: true, allowedPorts: [4040] }, ctx)).toThrow(SandboxPolicyError);
});
it("guards fusion writes", () => {
expect(() =>
policyToSbplProfile(
{
allowNetwork: true,
allowedWritePaths: ["/tmp/repo/.fusion/tasks"],
},
ctx,
),
).toThrow(SandboxPolicyError);
});
it("preset enables pnpm-friendly paths", () => {
const profile = policyToSbplProfile(fusionWorktreePreset(ctx), ctx);
expect(profile).toContain("(allow file-write* (subpath \"/tmp/worktree\"))");
expect(profile).toContain("(allow file-write* (subpath \"/Users/test/Library/pnpm/store\"))");
expect(profile).toContain("(deny network-bind (local ip \"*:4040\"))");
});
});

View File

@@ -0,0 +1,103 @@
import { dirname, resolve } from "node:path";
import {
SBPL_BASE_ALLOW,
SBPL_FILE_READ_BASE,
SBPL_HEADER,
SBPL_NETWORK_ALLOW_OUTBOUND,
SBPL_NETWORK_DENY_ALL,
SBPL_TMP_WRITE,
} from "./sandbox-exec-profile-templates.js";
import type { SandboxPolicy } from "./types.js";
export class SandboxPolicyError extends Error {
constructor(message: string) {
super(message);
this.name = "SandboxPolicyError";
}
}
export interface SandboxExecPolicy extends SandboxPolicy {
failureMode?: "fail-hard" | "fallback-native";
allowedPorts?: number[];
allowPort4040Override?: boolean;
}
export interface SandboxExecContext {
worktreePath: string;
repoRootPath: string;
pnpmStorePath: string;
nodeBinPath: string;
homeDir: string;
tmpDirOverride?: string;
}
function uniq(paths: string[]): string[] {
return [...new Set(paths.map((p) => resolve(p)))];
}
export function sbplEscape(value: string): string {
const bytes = Buffer.from(value, "utf8");
let out = "";
for (const byte of bytes) {
if (byte === 0x22) out += '\\"';
else if (byte === 0x5c) out += "\\\\";
else if (byte >= 0x20 && byte <= 0x7e) out += String.fromCharCode(byte);
else out += `\\x${byte.toString(16).padStart(2, "0")}`;
}
return out;
}
function asSubpath(path: string): string {
return `(subpath "${sbplEscape(path)}")`;
}
function ensureNoFusionWrites(paths: string[], repoRootPath: string): void {
const fusionRoot = resolve(repoRootPath, ".fusion");
const fusionDb = resolve(fusionRoot, "fusion.db");
for (const candidate of paths) {
const resolved = resolve(candidate);
if (resolved === fusionRoot || resolved === fusionDb || resolved.startsWith(`${fusionRoot}/`)) {
throw new SandboxPolicyError("Sandbox policy cannot include writable paths under .fusion/.");
}
}
}
export function fusionWorktreePreset(ctx: SandboxExecContext): SandboxExecPolicy {
return {
allowNetwork: true,
allowedReadPaths: [ctx.repoRootPath],
allowedWritePaths: [ctx.worktreePath, ctx.pnpmStorePath],
};
}
export function policyToSbplProfile(policy: SandboxExecPolicy, ctx: SandboxExecContext): string {
if (policy.allowedPorts?.includes(4040) && policy.allowPort4040Override !== true) {
throw new SandboxPolicyError("Port 4040 is reserved and cannot be allowed in sandbox policy.");
}
const tmpDir = resolve(ctx.tmpDirOverride ?? "/private/tmp");
const nodeDir = dirname(ctx.nodeBinPath);
const writePaths = uniq([ctx.worktreePath, ctx.pnpmStorePath, ...(policy.allowedWritePaths ?? [])]);
ensureNoFusionWrites(writePaths, ctx.repoRootPath);
const readPaths = uniq([
...((policy.allowedReadPaths ?? []).length ? (policy.allowedReadPaths ?? []) : [ctx.repoRootPath]),
...(ctx.repoRootPath !== ctx.worktreePath ? [ctx.repoRootPath] : []),
nodeDir,
]).filter((path) => !writePaths.includes(path));
const lines = [SBPL_HEADER, SBPL_BASE_ALLOW, SBPL_FILE_READ_BASE, SBPL_TMP_WRITE, `(allow file-read* ${asSubpath(tmpDir)})`, `(allow file-write* ${asSubpath(tmpDir)})`];
for (const readPath of readPaths) {
lines.push(`(allow file-read* ${asSubpath(readPath)})`);
}
for (const writePath of writePaths) {
lines.push(`(allow file-write* ${asSubpath(writePath)})`);
}
lines.push(policy.allowNetwork ? SBPL_NETWORK_ALLOW_OUTBOUND : SBPL_NETWORK_DENY_ALL);
return `${lines.join("\n")}\n`;
}