From 4269f56689b444a25ce0a4756a80d98038c1175f Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 9 Jun 2026 17:19:01 +0200 Subject: [PATCH] Revert "fix(sandbox): install bubblewrap/socat + probe so bash isolation actually engages (#58)" This reverts commit 30e1237b8ff0c2b7629028032e7519dd804c50e7. --- server/.ocd-deploy.json | 1 - server/Dockerfile | 11 -- .../pi/extensions/project-sandbox/index.ts | 101 +----------------- 3 files changed, 2 insertions(+), 111 deletions(-) diff --git a/server/.ocd-deploy.json b/server/.ocd-deploy.json index 396ead1..f496936 100644 --- a/server/.ocd-deploy.json +++ b/server/.ocd-deploy.json @@ -13,7 +13,6 @@ "size": 20, "path": "/app/data" }, - "userns": true, "env": [ { "key": "PI_PROJECTS_ROOT", diff --git a/server/Dockerfile b/server/Dockerfile index 67e3366..a01340d 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -19,19 +19,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ less \ tree \ tini \ - bubblewrap \ - socat \ && rm -rf /var/lib/apt/lists/* -# bubblewrap + socat back the agent's bash sandbox (@anthropic-ai/sandbox-runtime -# on Linux). They confine `bash` to the current project dir so a prompt-injected -# command can't read sibling projects. NOTE: bubblewrap also needs the container -# to permit user+mount namespaces — i.e. the platform must launch this container -# with `--security-opt seccomp=unconfined` (or grant CAP_SYS_ADMIN / userns). -# Without that, the binaries are present but `bwrap --unshare-*` fails at runtime; -# the sandbox extension probes for this at session start and falls back rather -# than breaking bash (see server/lib/pi/extensions/project-sandbox/index.ts). - # Install fd from upstream releases rather than apt: Debian bookworm pins # fd-find at 8.7.0, which predates the `--no-require-git` flag the agent's # `find` tool relies on (added in fd 9.0). Provide both `fd` and the diff --git a/server/lib/pi/extensions/project-sandbox/index.ts b/server/lib/pi/extensions/project-sandbox/index.ts index 7187709..0fb6af5 100644 --- a/server/lib/pi/extensions/project-sandbox/index.ts +++ b/server/lib/pi/extensions/project-sandbox/index.ts @@ -50,61 +50,6 @@ import { DANGEROUS_DIRECTORIES, } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js"; import { resolveZeroPackageRoot } from "../../zero-cli.ts"; -import { log } from "../../../utils/logger.ts"; - -const sandboxLog = log.child({ module: "project-sandbox" }); - -// When set truthy, bash is *disabled* (rather than run unsandboxed) if the -// filesystem sandbox can't engage. Use this once the deploy is known to grant -// bubblewrap the namespaces it needs, so a future regression (missing dep, -// dropped capability) can't silently re-open cross-project bash access. -const REQUIRE_BASH_SANDBOX = /^(1|true|yes)$/i.test( - process.env.PI_REQUIRE_BASH_SANDBOX ?? "", -); - -/** - * Confirm the sandbox actually *runs*, not just that its binaries exist. - * - * `SandboxManager.initialize()` only checks that `bwrap`/`socat`/`rg` are on - * PATH (a `which`). But a present `bwrap` still fails at runtime if the - * container forbids user/mount namespaces (no CAP_SYS_ADMIN, or seccomp blocks - * `unshare`) — and then *every* real bash command would die with "Operation - * not permitted", losing the agent its bash tool entirely. So we wrap a - * trivial command and run it: only trust the sandbox if it exits cleanly. - */ -async function sandboxActuallyRuns(projectDir: string): Promise { - let wrapped: string; - try { - wrapped = await SandboxManager.wrapWithSandbox("true"); - } catch { - return false; - } - return new Promise((resolve) => { - const child = spawn("bash", ["-c", wrapped], { cwd: projectDir, stdio: "ignore" }); - let settled = false; - const done = (ok: boolean) => { - if (settled) return; - settled = true; - resolve(ok); - }; - const timer = setTimeout(() => { - try { - child.kill("SIGKILL"); - } catch { - // ignore - } - done(false); - }, 5000); - child.on("error", () => { - clearTimeout(timer); - done(false); - }); - child.on("close", (code) => { - clearTimeout(timer); - done(code === 0); - }); - }); -} const UNBLOCK_FILES = new Set([".mcp.json", ".gitmodules"]); const UNBLOCK_DIRS = new Set([".vscode", ".idea"]); @@ -426,13 +371,6 @@ export function createProjectSandboxExtension( ...localBashTemplate, label: "bash (sandboxed)", async execute(id, params, signal, onUpdate, _ctx) { - if (!sandboxReady && REQUIRE_BASH_SANDBOX) { - throw new Error( - "bash is disabled: filesystem sandbox is unavailable and " + - "PI_REQUIRE_BASH_SANDBOX is set. Cross-project access can't be " + - "guaranteed, so bash is refused rather than run unsandboxed.", - ); - } const bash = createBashTool(projectDir, { operations: createBashOps(bashEnv, { sandboxed: sandboxReady }), }); @@ -447,13 +385,6 @@ export function createProjectSandboxExtension( pi.on("session_start", async (_event, ctx) => { const platform = process.platform; if (platform !== "darwin" && platform !== "linux") { - sandboxReady = false; - sandboxLog.error( - "bash sandbox unsupported on this platform — bash is running UNSANDBOXED " + - "(cross-project reads/writes are NOT blocked)", - undefined, - { platform, projectDir }, - ); ctx.ui.notify( `Sandbox not supported on ${platform} — bash runs unsandboxed`, "warning", @@ -487,19 +418,6 @@ export function createProjectSandboxExtension( allowGitConfig: true, }, }); - - // initialize() only `which`-checks the binaries; confirm the sandbox - // can actually create its namespaces before we trust it (see - // sandboxActuallyRuns). Without this, a deploy that ships bwrap but - // forbids user namespaces would break *every* bash command. - if (!(await sandboxActuallyRuns(projectDir))) { - throw new Error( - "bubblewrap is installed but cannot create user/mount namespaces — " + - "the container likely needs `--security-opt seccomp=unconfined` " + - "(or CAP_SYS_ADMIN / userns). Falling back to unsandboxed bash.", - ); - } - sandboxReady = true; ctx.ui.setStatus( "sandbox", @@ -507,24 +425,9 @@ export function createProjectSandboxExtension( ); } catch (err) { sandboxReady = false; - if (REQUIRE_BASH_SANDBOX) { - sandboxLog.error( - "bash sandbox unavailable — bash is DISABLED (PI_REQUIRE_BASH_SANDBOX set)", - err, - { projectDir }, - ); - } else { - sandboxLog.error( - "bash sandbox unavailable — bash is running UNSANDBOXED " + - "(cross-project reads/writes are NOT blocked). Set " + - "PI_REQUIRE_BASH_SANDBOX=1 to disable bash instead.", - err, - { projectDir }, - ); - } ctx.ui.notify( - `Bash sandbox unavailable: ${err instanceof Error ? err.message : err}`, - REQUIRE_BASH_SANDBOX ? "error" : "warning", + `Bash sandbox init failed: ${err instanceof Error ? err.message : err}`, + "error", ); } });