fix: keep agent sandbox sockets on short paths

This commit is contained in:
2026-07-11 02:22:19 +08:00
parent 38d9a9c6cb
commit 035c264179
7 changed files with 72 additions and 44 deletions
@@ -70,8 +70,7 @@ describe("real Claude SDK sandbox boundary", () => {
const canonicalServiceSecret = await realpath(serviceSecret);
const resultPath = join(canonicalWorkspace, "sandbox-result.txt");
const cphVersionPath = join(canonicalWorkspace, "cph-version.txt");
const effectiveTempProbePath = join(canonicalWorkspace, ".cph", "t", "effective-temp.txt");
const denialLogPath = join(canonicalWorkspace, ".cph", "t", "denials.log");
const denialLogPath = join(canonicalWorkspace, ".cph", "denials.log");
const siblingEscapePath = join(canonicalSibling, "escape.txt");
const unsandboxedEscapePath = join(root, `unsandboxed-escape-${nonce}`);
const hostTmpEscapePath = `/tmp/cph-host-temp-${nonce}`;
@@ -95,12 +94,12 @@ describe("real Claude SDK sandbox boundary", () => {
`if service_value=$(cat ${shellQuote(canonicalServiceSecret)} 2>> ${shellQuote(denialLogPath)}); then exit 23; fi`,
`mkdir ${shellQuote(join(canonicalWorkspace, "subdir"))}`,
`cd ${shellQuote(join(canonicalWorkspace, "subdir"))}`,
`test "\${TMPDIR-unset}" = ${shellQuote(join(canonicalWorkspace, ".cph", "t"))}`,
`test "\${TMP-unset}" = ${shellQuote(join(canonicalWorkspace, ".cph", "t"))}`,
`test "\${TEMP-unset}" = ${shellQuote(join(canonicalWorkspace, ".cph", "t"))}`,
'test "${CLAUDE_CODE_TMPDIR-unset}" = ".cph/t"',
`test "$(realpath "$TMPDIR")" = ${shellQuote(join(canonicalWorkspace, ".cph", "t"))}`,
`printf 'workspace-temp\\n' > "$TMPDIR/effective-temp.txt"`,
'test "${TMPDIR-unset}" = "${TMP-unset}"',
'test "${TMPDIR-unset}" = "${TEMP-unset}"',
'test "${TMPDIR-unset}" = "${CLAUDE_CODE_TMPDIR-unset}"',
'test "${#TMPDIR}" -le 64',
`case "$TMPDIR" in ${shellQuote(canonicalWorkspace)}/*) exit 35;; esac`,
`printf 'sdk-temp\\n' > "$TMPDIR/effective-temp.txt"`,
`if printf 'host-temp\\n' > ${shellQuote(hostTmpEscapePath)} 2>> ${shellQuote(denialLogPath)}; then exit 33; fi`,
`if printf 'host-var-temp\\n' > ${shellQuote(hostVarTmpEscapePath)} 2>> ${shellQuote(denialLogPath)}; then exit 34; fi`,
'test "${DATABASE_URL-unset}" = unset',
@@ -159,7 +158,6 @@ describe("real Claude SDK sandbox boundary", () => {
});
expect(resultContents).toBe("sandbox-ok\n");
await expect(readFile(cphVersionPath, "utf8")).resolves.toMatch(/^cph /);
await expect(readFile(effectiveTempProbePath, "utf8")).resolves.toBe("workspace-temp\n");
const denialLog = await readFile(denialLogPath, "utf8");
expect(denialLog).not.toContain("sibling-secret");
expect(denialLog).not.toContain("platform-secret");
+16 -10
View File
@@ -46,7 +46,11 @@ 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).toBe(join(".cph", "t"));
expect(policy.env.CLAUDE_CODE_TMPDIR).toBe(policy.env.TMPDIR);
expect(policy.env.TMP).toBe(policy.env.TMPDIR);
expect(policy.env.TEMP).toBe(policy.env.TMPDIR);
expect(policy.env.TMPDIR).not.toMatch(new RegExp(`^${escapeRegExp(canonicalWorkspace)}/`));
expect(Buffer.byteLength(policy.env.TMPDIR!)).toBeLessThanOrEqual(64);
expect(policy.sandbox).toMatchObject({
enabled: true,
@@ -54,7 +58,7 @@ describe("agent subprocess security policy", () => {
autoAllowBashIfSandboxed: true,
allowUnsandboxedCommands: false,
filesystem: {
allowWrite: [canonicalWorkspace],
allowWrite: expect.arrayContaining([canonicalWorkspace, policy.env.TMPDIR]),
denyRead: ["/"],
allowRead: expect.arrayContaining([canonicalWorkspace, "/usr/bin"]),
},
@@ -81,7 +85,7 @@ 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 () => {
it("keeps every SDK temp variable on a short isolated path outside a long project workspace", async () => {
const { workspaceRoot, workspace } = await makeWorkspace();
const policy = await createAgentSecurityPolicy({
workspaceRoot,
@@ -89,14 +93,16 @@ describe("agent subprocess security policy", () => {
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)]);
const canonicalWorkspace = await realpath(workspace);
const temp = policy.env.TMPDIR!;
expect(policy.env.CLAUDE_CODE_TMPDIR).toBe(temp);
expect(policy.env.TMP).toBe(temp);
expect(policy.env.TEMP).toBe(temp);
expect(temp).not.toMatch(new RegExp(`^${escapeRegExp(canonicalWorkspace)}/`));
expect(Buffer.byteLength(temp)).toBeLessThanOrEqual(64);
expect(policy.sandbox.filesystem.allowWrite).toEqual([canonicalWorkspace, temp]);
expect(policy.sandbox.filesystem.denyWrite).toEqual(["/"]);
expect(policy.sandbox.filesystem.allowRead).not.toContain(expect.stringMatching(/^\/run\//));
expect(policy.sandbox.filesystem.allowRead).toContain(temp);
});
it("rejects a project workspace whose real path escapes the configured workspace root", async () => {
+1 -1
View File
@@ -108,7 +108,7 @@ describe("runAgent", () => {
failIfUnavailable: true,
allowUnsandboxedCommands: false,
filesystem: expect.objectContaining({
allowWrite: [workspace],
allowWrite: expect.arrayContaining([workspace]),
denyRead: ["/"],
allowRead: expect.arrayContaining([workspace]),
}),