mirror of
https://github.com/yuangyaa/openclaw.git
synced 2026-07-14 11:07:29 +08:00
144 lines
5.5 KiB
TypeScript
144 lines
5.5 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { SANDBOX_PINNED_MUTATION_PYTHON } from "./fs-bridge-mutation-helper.js";
|
|
|
|
async function withTempRoot<T>(prefix: string, run: (root: string) => Promise<T>): Promise<T> {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
try {
|
|
return await run(root);
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function runMutation(args: string[], input?: string) {
|
|
return spawnSync("python3", ["-c", SANDBOX_PINNED_MUTATION_PYTHON, ...args], {
|
|
input,
|
|
encoding: "utf8",
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
}
|
|
|
|
describe("sandbox pinned mutation helper", () => {
|
|
it("writes through a pinned directory fd", async () => {
|
|
await withTempRoot("openclaw-mutation-helper-", async (root) => {
|
|
const workspace = path.join(root, "workspace");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
|
|
const result = runMutation(["write", workspace, "nested/deeper", "note.txt", "1"], "hello");
|
|
|
|
expect(result.status).toBe(0);
|
|
await expect(
|
|
fs.readFile(path.join(workspace, "nested", "deeper", "note.txt"), "utf8"),
|
|
).resolves.toBe("hello");
|
|
});
|
|
});
|
|
|
|
it.runIf(process.platform !== "win32")(
|
|
"rejects symlink-parent writes instead of materializing a temp file outside the mount",
|
|
async () => {
|
|
await withTempRoot("openclaw-mutation-helper-", async (root) => {
|
|
const workspace = path.join(root, "workspace");
|
|
const outside = path.join(root, "outside");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await fs.mkdir(outside, { recursive: true });
|
|
await fs.symlink(outside, path.join(workspace, "alias"));
|
|
|
|
const result = runMutation(["write", workspace, "alias", "escape.txt", "0"], "owned");
|
|
|
|
expect(result.status).not.toBe(0);
|
|
await expect(fs.readFile(path.join(outside, "escape.txt"), "utf8")).rejects.toThrow();
|
|
});
|
|
},
|
|
);
|
|
|
|
it.runIf(process.platform !== "win32")("rejects symlink segments during mkdirp", async () => {
|
|
await withTempRoot("openclaw-mutation-helper-", async (root) => {
|
|
const workspace = path.join(root, "workspace");
|
|
const outside = path.join(root, "outside");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await fs.mkdir(outside, { recursive: true });
|
|
await fs.symlink(outside, path.join(workspace, "alias"));
|
|
|
|
const result = runMutation(["mkdirp", workspace, "alias/nested"]);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
await expect(fs.readFile(path.join(outside, "nested"), "utf8")).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
it.runIf(process.platform !== "win32")("remove unlinks the symlink itself", async () => {
|
|
await withTempRoot("openclaw-mutation-helper-", async (root) => {
|
|
const workspace = path.join(root, "workspace");
|
|
const outside = path.join(root, "outside");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await fs.mkdir(outside, { recursive: true });
|
|
await fs.writeFile(path.join(outside, "secret.txt"), "classified", "utf8");
|
|
await fs.symlink(path.join(outside, "secret.txt"), path.join(workspace, "link.txt"));
|
|
|
|
const result = runMutation(["remove", workspace, "", "link.txt", "0", "0"]);
|
|
|
|
expect(result.status).toBe(0);
|
|
await expect(fs.readlink(path.join(workspace, "link.txt"))).rejects.toThrow();
|
|
await expect(fs.readFile(path.join(outside, "secret.txt"), "utf8")).resolves.toBe(
|
|
"classified",
|
|
);
|
|
});
|
|
});
|
|
|
|
it.runIf(process.platform !== "win32")(
|
|
"rejects symlink destination parents during rename",
|
|
async () => {
|
|
await withTempRoot("openclaw-mutation-helper-", async (root) => {
|
|
const workspace = path.join(root, "workspace");
|
|
const outside = path.join(root, "outside");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await fs.mkdir(outside, { recursive: true });
|
|
await fs.writeFile(path.join(workspace, "from.txt"), "payload", "utf8");
|
|
await fs.symlink(outside, path.join(workspace, "alias"));
|
|
|
|
const result = runMutation([
|
|
"rename",
|
|
workspace,
|
|
"",
|
|
"from.txt",
|
|
workspace,
|
|
"alias",
|
|
"escape.txt",
|
|
"1",
|
|
]);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
await expect(fs.readFile(path.join(workspace, "from.txt"), "utf8")).resolves.toBe(
|
|
"payload",
|
|
);
|
|
await expect(fs.readFile(path.join(outside, "escape.txt"), "utf8")).rejects.toThrow();
|
|
});
|
|
},
|
|
);
|
|
|
|
it.runIf(process.platform !== "win32")(
|
|
"copies directories across different mount roots during rename fallback",
|
|
async () => {
|
|
await withTempRoot("openclaw-mutation-helper-", async (root) => {
|
|
const sourceRoot = path.join(root, "source");
|
|
const destRoot = path.join(root, "dest");
|
|
await fs.mkdir(path.join(sourceRoot, "dir", "nested"), { recursive: true });
|
|
await fs.mkdir(destRoot, { recursive: true });
|
|
await fs.writeFile(path.join(sourceRoot, "dir", "nested", "file.txt"), "payload", "utf8");
|
|
|
|
const result = runMutation(["rename", sourceRoot, "", "dir", destRoot, "", "moved", "1"]);
|
|
|
|
expect(result.status).toBe(0);
|
|
await expect(
|
|
fs.readFile(path.join(destRoot, "moved", "nested", "file.txt"), "utf8"),
|
|
).resolves.toBe("payload");
|
|
await expect(fs.stat(path.join(sourceRoot, "dir"))).rejects.toThrow();
|
|
});
|
|
},
|
|
);
|
|
});
|