fix(runner): fail closed when the Pi permission extension cannot install#5318
Conversation
|
@coderabbitai review |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Action performedReview finished.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Railway Preview Environment
Updated at 2026-07-14T19:29:37.701Z |
The published runner image runs as user node (uid 1000) but never created /pi-agent (the configured PI_CODING_AGENT_DIR), so installing Pi's permission-enforcing extension failed with EACCES. The install swallowed that error and returned void, so the runner continued with NO enforcement: a deny/ask policy silently ran every built-in tool. Three parts: - Image: create /pi-agent owned by node in Dockerfile.gh before USER node. - Routing: managed/no-key local Pi runs install the extension into a per-run temp dir the runtime user owns, so the install no longer depends on the configured agent dir being writable. - Fail closed: installPiExtensionLocal / uploadPiExtensionToSandbox now report success; session setup throws a named error when the policy could gate a Pi built-in and the extension did not install, surfaced as an error frame. Claude-Session: https://claude.ai/code/session_01Hyn9365BLPXDmNZShrQkmH
100e8fe to
6ae2d91
Compare
Context
Setting a permission policy that should block or ask before a tool ran did nothing on the published runner image. A user could set the Pi policy to
deny(orask), send a message that runs a shell command, and the command still ran with no prompt. The user got no signal that their policy was a no-op.The cause was a fail-open. Pi enforces the policy through a small extension the runner installs into Pi's agent directory at the start of each run. The published image runs as the unprivileged user
node(uid 1000), but the image never created/pi-agent(the deployment's configuredPI_CODING_AGENT_DIR) or grantednodewrite access to it. Somkdir /pi-agent/extensionsfailed withEACCES. The install caught that error, logged one line, and returned void. The session then continued with no enforcement. Because the default policy isallow_reads(notallow), gating is active on nearly every run, so the blast radius was wide.Changes
Three parts, all in the runner.
Image.
Dockerfile.ghnow creates/pi-agentowned bynodebefore theUSER nodeline, so the install succeeds on a fresh pull of the default path.Writable-directory routing. Managed-key and no-key local Pi runs now install the extension into a per-run temporary directory the runtime user owns (under the system temp path), instead of straight into the configured
/pi-agent. This generalizes the temp-dir approach the runner already used for runs carrying custom skills or a system prompt, so the install no longer depends on any operator-set directory being writable. The subscription path is unchanged: it must run out of the operator's mounted login so Pi can refresh its token mid-run.Fail closed. The install functions now report success instead of returning void:
Before:
installPiExtensionLocal(agentDir): void(swallowed the failure)After:
installPiExtensionLocal(agentDir): boolean(false when it could not install)uploadPiExtensionToSandboxgains the same boolean. In session setup (acquireEnvironment), when the policy could gate a Pi built-in tool (builtinGatingActive) and the extension did not install, the run throws a named error constant. The engine's existing catch turns it into{ ok: false, error }and a visible error frame, following theTOOL_MCP_UNAVAILABLE_MESSAGEpattern. When the policy is allow-everything the extension is not needed, so a failed install stays harmless and the run continues.The error names the cause, states that nothing ran unprotected, and points the operator at the remedy: "The agent could not enforce its permission policy: the permission component failed to install, so no built-in tool could be gated. The run was stopped so no tool ran outside your policy. Ask your deployment operator to make the runner's Pi agent directory writable, or rebuild and republish the runner image."
Tests / notes
pi-permission-failclosed.test.ts: gating active plus a failed install returns{ ok: false }with the new message; an allow-everything policy plus a failed install continues to sandbox startup.pnpm test, 1158 tests) andpnpm run typecheckclean.SANDBOX_AGENT_EXTENSION_BUNDLEat a stub file, because CI does not runbuild:extensionbefore the unit suite and the new fail-closed path would otherwise trip on the absent artifact.Rollout
Runs on an old, unpatched image now fail loudly with the remedy in the error message, instead of running unprotected. This is the point of the fix. Because gating is active on nearly every default run, the image fix and the temp-dir routing ship together so the install succeeds in the common cases and fail-closed only fires when a directory is genuinely unwritable. The image change takes effect once the runner image is rebuilt and the published charts point at it.
The related Claude built-in gap (the "Deny all" control not governing Claude's own shell) is a separate change in a different file and ships in its own PR.
https://claude.ai/code/session_01Hyn9365BLPXDmNZShrQkmH