when a shell tool call invokes apply_patch, resolve relative paths against workdir, if specified (#556)

Previously, we were ignoring the `workdir` field in an `ExecInput` when
running it through `canAutoApprove()`. For ordinary `exec()` calls, that
was sufficient, but for `apply_patch`, we need the `workdir` to resolve
relative paths in the `apply_patch` argument so that we can check them
in `isPathConstrainedTowritablePaths()`.

Likewise, we also need the workdir when running `execApplyPatch()`
because the paths need to be resolved again.

Ideally, the `ApplyPatchCommand` returned by `canAutoApprove()` would
not be a simple `patch: string`, but the parsed patch with all of the
paths resolved, in which case `execApplyPatch()` could expect absolute
paths and would not need `workdir`.
This commit is contained in:
Michael Bolin
2025-04-22 14:07:47 -07:00
committed by GitHub
parent a30e79b768
commit 7c1f2d7deb
4 changed files with 63 additions and 13 deletions

View File

@@ -10,6 +10,7 @@ import { formatCommandForDisplay } from "../../format-command.js";
import fs from "fs";
import os from "os";
import { parse } from "shell-quote";
import { resolvePathAgainstWorkdir } from "src/approvals.js";
const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds
@@ -60,16 +61,20 @@ export function exec(
return execForSandbox(cmd, opts, writableRoots, abortSignal);
}
export function execApplyPatch(patchText: string): ExecResult {
export function execApplyPatch(
patchText: string,
workdir: string | undefined,
): ExecResult {
// This is a temporary measure to understand what are the common base commands
// until we start persisting and uploading rollouts
try {
const result = process_patch(
patchText,
(p) => fs.readFileSync(p, "utf8"),
(p, c) => fs.writeFileSync(p, c, "utf8"),
(p) => fs.unlinkSync(p),
(p) => fs.readFileSync(resolvePathAgainstWorkdir(p, workdir), "utf8"),
(p, c) =>
fs.writeFileSync(resolvePathAgainstWorkdir(p, workdir), c, "utf8"),
(p) => fs.unlinkSync(resolvePathAgainstWorkdir(p, workdir)),
);
return {
stdout: result,

View File

@@ -81,7 +81,7 @@ export async function handleExecCommand(
) => Promise<CommandConfirmation>,
abortSignal?: AbortSignal,
): Promise<HandleExecCommandResult> {
const { cmd: command } = args;
const { cmd: command, workdir } = args;
const key = deriveCommandKey(command);
@@ -103,7 +103,7 @@ export async function handleExecCommand(
// working directory so that edits are constrained to the project root. If
// the caller wishes to broaden or restrict the set it can be made
// configurable in the future.
const safety = canAutoApprove(command, policy, [process.cwd()]);
const safety = canAutoApprove(command, workdir, policy, [process.cwd()]);
let runInSandbox: boolean;
switch (safety.type) {
@@ -247,7 +247,7 @@ async function execCommand(
const start = Date.now();
const execResult =
applyPatchCommand != null
? execApplyPatch(applyPatchCommand.patch)
? execApplyPatch(applyPatchCommand.patch, workdir)
: await exec(
{ ...execInput, additionalWritableRoots },
await getSandbox(runInSandbox),