feat(harness): secure workspace and custom tools
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import * as harness from "../src/index.js";
|
||||
|
||||
type Workspace = {
|
||||
resolveReadable(path: string): Promise<string>;
|
||||
writeReportDraft(input: { title?: string; body: string }): Promise<{ relativePath: "report.md"; byteSize: number }>;
|
||||
remove(): Promise<void>;
|
||||
};
|
||||
|
||||
type WorkspaceConstructor = {
|
||||
create(root: string): Promise<Workspace>;
|
||||
};
|
||||
|
||||
let root = "";
|
||||
let outside = "";
|
||||
let workspace: Workspace;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), "agent-studio-workspace-"));
|
||||
outside = await mkdtemp(join(tmpdir(), "agent-studio-outside-"));
|
||||
await writeFile(join(root, "inside.txt"), "inside", "utf8");
|
||||
await writeFile(join(outside, "secret.txt"), "secret", "utf8");
|
||||
const Constructor = Reflect.get(harness, "SafeWorkspace") as WorkspaceConstructor | undefined;
|
||||
expect(Constructor, "SafeWorkspace must be exported").toBeDefined();
|
||||
workspace = await Constructor!.create(root);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await rm(outside, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("SafeWorkspace", () => {
|
||||
it("resolves an existing regular file inside the Session root", async () => {
|
||||
await expect(workspace.resolveReadable("inside.txt")).resolves.toBe(await realpath(join(root, "inside.txt")));
|
||||
});
|
||||
|
||||
it.each(["../outside/secret.txt", "/tmp/outside.txt"])("rejects path escape %s", async (path) => {
|
||||
await expect(workspace.resolveReadable(path)).rejects.toMatchObject({ code: "PATH_OUTSIDE_WORKSPACE" });
|
||||
});
|
||||
|
||||
it("rejects symlinks before following them", async () => {
|
||||
await symlink(outside, join(root, "link"));
|
||||
await expect(workspace.resolveReadable("link/secret.txt")).rejects.toMatchObject({ code: "SYMLINK_NOT_ALLOWED" });
|
||||
});
|
||||
|
||||
it("rejects protected path segments", async () => {
|
||||
await mkdir(join(root, ".git"));
|
||||
await writeFile(join(root, ".git", "config"), "credential", "utf8");
|
||||
await expect(workspace.resolveReadable(".git/config")).rejects.toMatchObject({ code: "PROTECTED_PATH" });
|
||||
});
|
||||
|
||||
it("writes only the fixed report.md path", async () => {
|
||||
const result = await workspace.writeReportDraft({ title: "广告账户报告", body: "账户总结" });
|
||||
expect(result.relativePath).toBe("report.md");
|
||||
await expect(readFile(join(root, "report.md"), "utf8")).resolves.toBe("# 广告账户报告\n\n账户总结\n");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user