forked from EduCraft/curriculum-project-hub
fix: prove Linux Agent sandbox boundary
This commit is contained in:
@@ -21,7 +21,10 @@ const SAFE_HOST_ENV_KEYS = [
|
||||
"CPH_BIN",
|
||||
] as const;
|
||||
|
||||
const PROVIDER_SECRET_ENV_KEYS = ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"] as const;
|
||||
const SANDBOX_HIDDEN_ENV_KEYS = [
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_API_KEY",
|
||||
] as const;
|
||||
|
||||
export interface AgentSecurityInput {
|
||||
readonly workspaceRoot: string;
|
||||
@@ -37,6 +40,7 @@ export interface AgentSandboxPolicy {
|
||||
readonly allowUnsandboxedCommands: false;
|
||||
readonly filesystem: {
|
||||
readonly allowWrite: string[];
|
||||
readonly denyWrite: string[];
|
||||
readonly denyRead: string[];
|
||||
readonly allowRead: string[];
|
||||
};
|
||||
@@ -61,12 +65,16 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
const hostEnv = input.hostEnv ?? process.env;
|
||||
const { workspaceRoot, workspaceDir } = await resolveProjectWorkspace(input.workspaceRoot, input.workspaceDir);
|
||||
|
||||
const runtimeRoot = await ensureDirectoryTree(workspaceDir, [".cph", "agent-runtime"]);
|
||||
const cphRoot = await ensureDirectoryTree(workspaceDir, [".cph"]);
|
||||
const runtimeRoot = await ensureDirectoryTree(cphRoot, ["agent-runtime"]);
|
||||
const agentHome = await ensureDirectoryTree(runtimeRoot, ["home"]);
|
||||
const agentCache = await ensureDirectoryTree(runtimeRoot, ["cache"]);
|
||||
const agentConfig = await ensureDirectoryTree(runtimeRoot, ["config"]);
|
||||
const agentState = await ensureDirectoryTree(runtimeRoot, ["state"]);
|
||||
const agentTmp = await ensureDirectoryTree(runtimeRoot, ["tmp"]);
|
||||
// Keep the SDK temp root both short enough for Linux AF_UNIX sockets and
|
||||
// physically inside the project boundary pinned by AgentFileOp.Authorized.
|
||||
const agentTmp = await ensureDirectoryTree(cphRoot, ["t"]);
|
||||
const claudeCodeTmp = join(".cph", "t");
|
||||
|
||||
const path = hostEnv["PATH"]?.trim();
|
||||
if (path === undefined || path === "") {
|
||||
@@ -82,11 +90,14 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
env.XDG_CACHE_HOME = agentCache;
|
||||
env.XDG_CONFIG_HOME = agentConfig;
|
||||
env.XDG_STATE_HOME = agentState;
|
||||
// General subprocess temp paths stay absolute so tools continue to work
|
||||
// after `cd`. Only the SDK's socket prefix is relative: Claude resolves it
|
||||
// from the canonical workspace cwd before Bash commands can change cwd.
|
||||
env.TMPDIR = agentTmp;
|
||||
env.TMP = agentTmp;
|
||||
env.TEMP = agentTmp;
|
||||
env.CLAUDE_CONFIG_DIR = agentConfig;
|
||||
env.CLAUDE_CODE_TMPDIR = agentTmp;
|
||||
env.CLAUDE_CODE_TMPDIR = claudeCodeTmp;
|
||||
env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
||||
env.DISABLE_TELEMETRY = "1";
|
||||
env.DO_NOT_TRACK = "1";
|
||||
@@ -112,6 +123,10 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
allowUnsandboxedCommands: false,
|
||||
filesystem: {
|
||||
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.
|
||||
denyWrite: ["/"],
|
||||
// Deny host reads by default, then re-open exactly this run's
|
||||
// workspace plus the named system runtime needed to execute tools.
|
||||
// SDK allowRead takes precedence over matching denyRead paths.
|
||||
@@ -120,7 +135,7 @@ export async function createAgentSecurityPolicy(input: AgentSecurityInput): Prom
|
||||
},
|
||||
credentials: {
|
||||
files: sensitiveReadPaths.map((path) => ({ path, mode: "deny" as const })),
|
||||
envVars: PROVIDER_SECRET_ENV_KEYS.map((name) => ({ name, mode: "deny" as const })),
|
||||
envVars: SANDBOX_HIDDEN_ENV_KEYS.map((name) => ({ name, mode: "deny" as const })),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -144,8 +159,10 @@ function hostRuntimeReadPaths(env: Readonly<Record<string, string | undefined>>)
|
||||
]
|
||||
: [
|
||||
"/bin",
|
||||
"/sbin",
|
||||
"/usr/bin",
|
||||
"/usr/local/bin",
|
||||
"/usr/sbin",
|
||||
"/lib",
|
||||
"/lib64",
|
||||
"/usr/lib",
|
||||
|
||||
Reference in New Issue
Block a user