forked from bai/curriculum-project-hub
fix: prove Linux Agent sandbox boundary
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createAgentSdkStderrSink } from "../../src/agent/diagnostics.js";
|
||||
|
||||
describe("agent SDK stderr diagnostics", () => {
|
||||
it("redacts a credential split across chunks and correlates the log", () => {
|
||||
const warn = vi.fn();
|
||||
const sink = createAgentSdkStderrSink({
|
||||
logger: { warn },
|
||||
runId: "run-1",
|
||||
projectId: "project-1",
|
||||
sensitiveValues: ["provider-secret"],
|
||||
});
|
||||
|
||||
sink.write("sandbox startup token=provider-");
|
||||
sink.write("secret\nremaining diagnostic");
|
||||
sink.flush();
|
||||
|
||||
const serialized = JSON.stringify(warn.mock.calls);
|
||||
expect(serialized).not.toContain("provider-secret");
|
||||
expect(serialized).toContain("[REDACTED]");
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ runId: "run-1", projectId: "project-1" }),
|
||||
"agent SDK stderr",
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds line length, line count, and an unterminated line", () => {
|
||||
const warn = vi.fn();
|
||||
const sink = createAgentSdkStderrSink({
|
||||
logger: { warn },
|
||||
runId: "run-2",
|
||||
projectId: "project-2",
|
||||
sensitiveValues: [],
|
||||
});
|
||||
|
||||
sink.write(`${"x".repeat(3_000)}\n`);
|
||||
sink.write(Array.from({ length: 30 }, (_, index) => `line-${index}`).join("\n"));
|
||||
sink.flush();
|
||||
|
||||
expect(warn.mock.calls.length).toBeLessThanOrEqual(21);
|
||||
const diagnostics = warn.mock.calls
|
||||
.map(([bindings]) => (bindings as { sdkStderr?: string }).sdkStderr)
|
||||
.filter((value): value is string => value !== undefined);
|
||||
expect(diagnostics.every((value) => value.length <= 2_000)).toBe(true);
|
||||
expect(warn.mock.calls.some(([, message]) => String(message).includes("limit reached"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -46,7 +46,7 @@ describe("agent subprocess security policy", () => {
|
||||
expect(policy.env).not.toHaveProperty("HUB_SESSION_SECRET");
|
||||
expect(policy.env.HOME).toMatch(new RegExp(`^${escapeRegExp(canonicalWorkspace)}/`));
|
||||
expect(policy.env.CLAUDE_CONFIG_DIR).toMatch(new RegExp(`^${escapeRegExp(canonicalWorkspace)}/`));
|
||||
expect(policy.env.CLAUDE_CODE_TMPDIR).toMatch(new RegExp(`^${escapeRegExp(canonicalWorkspace)}/`));
|
||||
expect(policy.env.CLAUDE_CODE_TMPDIR).toBe(join(".cph", "t"));
|
||||
|
||||
expect(policy.sandbox).toMatchObject({
|
||||
enabled: true,
|
||||
@@ -81,6 +81,24 @@ describe("agent subprocess security policy", () => {
|
||||
})).rejects.toThrow("unsupported provider environment variable: DATABASE_URL");
|
||||
});
|
||||
|
||||
it("keeps the short SDK socket directory inside the project workspace", async () => {
|
||||
const { workspaceRoot, workspace } = await makeWorkspace();
|
||||
const policy = await createAgentSecurityPolicy({
|
||||
workspaceRoot,
|
||||
workspaceDir: workspace,
|
||||
hostEnv: { PATH: "/usr/bin:/bin" },
|
||||
});
|
||||
|
||||
expect(policy.env.CLAUDE_CODE_TMPDIR).toBe(join(".cph", "t"));
|
||||
const absoluteTemp = join(await realpath(workspace), ".cph", "t");
|
||||
expect(policy.env.TMPDIR).toBe(absoluteTemp);
|
||||
expect(policy.env.TMP).toBe(absoluteTemp);
|
||||
expect(policy.env.TEMP).toBe(absoluteTemp);
|
||||
expect(policy.sandbox.filesystem.allowWrite).toEqual([await realpath(workspace)]);
|
||||
expect(policy.sandbox.filesystem.denyWrite).toEqual(["/"]);
|
||||
expect(policy.sandbox.filesystem.allowRead).not.toContain(expect.stringMatching(/^\/run\//));
|
||||
});
|
||||
|
||||
it("rejects a project workspace whose real path escapes the configured workspace root", async () => {
|
||||
const { root, workspaceRoot } = await makeWorkspace();
|
||||
const outside = join(root, "outside");
|
||||
|
||||
Reference in New Issue
Block a user