forked from bai/curriculum-project-hub
fix: restore bounded agent sandbox execution
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, lstat, mkdir, realpath } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { isAbsolute, join, relative, resolve } from "node:path";
|
||||
@@ -30,7 +29,7 @@ const SANDBOX_HIDDEN_ENV_KEYS = [
|
||||
// Linux sockaddr_un.sun_path is 108 bytes including the terminator. Claude's
|
||||
// sandbox appends its own user directory and randomized bridge socket names,
|
||||
// so keep our prefix well below that hard limit.
|
||||
const MAX_AGENT_TMP_PREFIX_BYTES = 64;
|
||||
const MAX_AGENT_TMP_PREFIX_BYTES = 56;
|
||||
|
||||
export interface AgentSecurityInput {
|
||||
readonly workspaceRoot: string;
|
||||
@@ -78,7 +77,8 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
const agentCache = await ensureDirectoryTree(runtimeRoot, ["cache"]);
|
||||
const agentConfig = await ensureDirectoryTree(runtimeRoot, ["config"]);
|
||||
const agentState = await ensureDirectoryTree(runtimeRoot, ["state"]);
|
||||
const agentTmp = await resolveShortAgentTemp(workspaceDir);
|
||||
const agentTmp = await ensureDirectoryTree(cphRoot, ["t"]);
|
||||
assertShortAgentTemp(agentTmp);
|
||||
|
||||
const path = hostEnv["PATH"]?.trim();
|
||||
if (path === undefined || path === "") {
|
||||
@@ -125,10 +125,7 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
autoAllowBashIfSandboxed: true,
|
||||
allowUnsandboxedCommands: false,
|
||||
filesystem: {
|
||||
// The temp directory is service-owned, project-keyed, mode 0700, and
|
||||
// contains only SDK sockets/scratch. User deliverables remain confined
|
||||
// to workspaceDir.
|
||||
allowWrite: [workspaceDir, agentTmp],
|
||||
allowWrite: [workspaceDir],
|
||||
// Reject every write path by default, then re-open only the canonical
|
||||
// workspace. This prevents bubblewrap's ordinary temp exceptions from
|
||||
// turning an unauthorized path into a successful ephemeral write.
|
||||
@@ -137,7 +134,7 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
// workspace plus the named system runtime needed to execute tools.
|
||||
// SDK allowRead takes precedence over matching denyRead paths.
|
||||
denyRead: ["/"],
|
||||
allowRead: [workspaceDir, agentTmp, ...runtimeReadPaths],
|
||||
allowRead: [workspaceDir, ...runtimeReadPaths],
|
||||
},
|
||||
credentials: {
|
||||
files: sensitiveReadPaths.map((path) => ({ path, mode: "deny" as const })),
|
||||
@@ -147,22 +144,13 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveShortAgentTemp(workspaceDir: string): Promise<string> {
|
||||
// Do not use os.tmpdir() on macOS: its per-user path can itself exceed the
|
||||
// Unix socket budget. /tmp canonicalizes to /private/tmp there and /tmp on
|
||||
// Linux. The UID boundary prevents Organizations' service accounts sharing
|
||||
// a writable runtime root; the workspace hash separates Projects.
|
||||
const systemTmp = await realpath(process.platform === "win32" ? homedir() : "/tmp");
|
||||
const uid = process.getuid?.() ?? "user";
|
||||
const workspaceKey = createHash("sha256").update(workspaceDir).digest("hex").slice(0, 12);
|
||||
const agentTmp = await ensureDirectoryTree(systemTmp, [`cph-agent-${uid}`, workspaceKey]);
|
||||
function assertShortAgentTemp(agentTmp: string): void {
|
||||
const prefixBytes = Buffer.byteLength(agentTmp);
|
||||
if (prefixBytes > MAX_AGENT_TMP_PREFIX_BYTES) {
|
||||
throw new Error(
|
||||
`Agent temp path is too long for sandbox bridge sockets (${prefixBytes} > ${MAX_AGENT_TMP_PREFIX_BYTES} bytes): ${agentTmp}`,
|
||||
);
|
||||
}
|
||||
return agentTmp;
|
||||
}
|
||||
|
||||
function hostRuntimeReadPaths(env: Readonly<Record<string, string | undefined>>): string[] {
|
||||
|
||||
Reference in New Issue
Block a user