2025-04-16 12:56:08 -04:00
|
|
|
|
import type { ExecInput, ExecResult } from "./sandbox/interface.js";
|
|
|
|
|
|
import type { SpawnOptions } from "child_process";
|
2025-04-19 14:42:19 +09:00
|
|
|
|
import type { ParseEntry } from "shell-quote";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
import { process_patch } from "./apply-patch.js";
|
|
|
|
|
|
import { SandboxType } from "./sandbox/interface.js";
|
|
|
|
|
|
import { execWithSeatbelt } from "./sandbox/macos-seatbelt.js";
|
|
|
|
|
|
import { exec as rawExec } from "./sandbox/raw-exec.js";
|
2025-04-16 14:16:53 -07:00
|
|
|
|
import { formatCommandForDisplay } from "../../format-command.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import fs from "fs";
|
|
|
|
|
|
import os from "os";
|
2025-04-22 16:45:17 -07:00
|
|
|
|
import path from "path";
|
2025-04-19 14:42:19 +09:00
|
|
|
|
import { parse } from "shell-quote";
|
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`.
2025-04-22 14:07:47 -07:00
|
|
|
|
import { resolvePathAgainstWorkdir } from "src/approvals.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds
|
|
|
|
|
|
|
2025-04-19 14:42:19 +09:00
|
|
|
|
function requiresShell(cmd: Array<string>): boolean {
|
2025-04-21 14:11:19 +10:00
|
|
|
|
// If the command is a single string that contains shell operators,
|
|
|
|
|
|
// it needs to be run with shell: true
|
|
|
|
|
|
if (cmd.length === 1 && cmd[0] !== undefined) {
|
|
|
|
|
|
const tokens = parse(cmd[0]) as Array<ParseEntry>;
|
2025-04-19 14:42:19 +09:00
|
|
|
|
return tokens.some((token) => typeof token === "object" && "op" in token);
|
2025-04-21 14:11:19 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If the command is split into multiple arguments, we don't need shell: true
|
|
|
|
|
|
// even if one of the arguments is a shell operator like '|'
|
|
|
|
|
|
return false;
|
2025-04-19 14:42:19 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* This function should never return a rejected promise: errors should be
|
|
|
|
|
|
* mapped to a non-zero exit code and the error message should be in stderr.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function exec(
|
2025-04-17 15:39:26 -07:00
|
|
|
|
{
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
workdir,
|
|
|
|
|
|
timeoutInMillis,
|
|
|
|
|
|
additionalWritableRoots,
|
|
|
|
|
|
}: ExecInput & { additionalWritableRoots: ReadonlyArray<string> },
|
2025-04-16 12:56:08 -04:00
|
|
|
|
sandbox: SandboxType,
|
|
|
|
|
|
abortSignal?: AbortSignal,
|
|
|
|
|
|
): Promise<ExecResult> {
|
|
|
|
|
|
// This is a temporary measure to understand what are the common base commands
|
|
|
|
|
|
// until we start persisting and uploading rollouts
|
|
|
|
|
|
|
|
|
|
|
|
const opts: SpawnOptions = {
|
|
|
|
|
|
timeout: timeoutInMillis || DEFAULT_TIMEOUT_MS,
|
2025-04-19 14:42:19 +09:00
|
|
|
|
...(requiresShell(cmd) ? { shell: true } : {}),
|
2025-04-16 12:56:08 -04:00
|
|
|
|
...(workdir ? { cwd: workdir } : {}),
|
|
|
|
|
|
};
|
2025-04-17 15:39:26 -07:00
|
|
|
|
// Merge default writable roots with any user-specified ones.
|
|
|
|
|
|
const writableRoots = [
|
|
|
|
|
|
process.cwd(),
|
|
|
|
|
|
os.tmpdir(),
|
|
|
|
|
|
...additionalWritableRoots,
|
|
|
|
|
|
];
|
2025-04-30 14:08:27 -07:00
|
|
|
|
if (sandbox === SandboxType.MACOS_SEATBELT) {
|
|
|
|
|
|
return execWithSeatbelt(cmd, opts, writableRoots, abortSignal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SandboxType.NONE (or any other) falls back to the raw exec implementation
|
|
|
|
|
|
return rawExec(cmd, opts, abortSignal);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
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`.
2025-04-22 14:07:47 -07:00
|
|
|
|
export function execApplyPatch(
|
|
|
|
|
|
patchText: string,
|
2025-04-22 16:45:17 -07:00
|
|
|
|
workdir: string | undefined = undefined,
|
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`.
2025-04-22 14:07:47 -07:00
|
|
|
|
): ExecResult {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// 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,
|
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`.
2025-04-22 14:07:47 -07:00
|
|
|
|
(p) => fs.readFileSync(resolvePathAgainstWorkdir(p, workdir), "utf8"),
|
2025-04-22 16:45:17 -07:00
|
|
|
|
(p, c) => {
|
|
|
|
|
|
const resolvedPath = resolvePathAgainstWorkdir(p, workdir);
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure the parent directory exists before writing the file. This
|
|
|
|
|
|
// mirrors the behaviour of the standalone apply_patch CLI (see
|
|
|
|
|
|
// write_file() in apply-patch.ts) and prevents errors when adding a
|
|
|
|
|
|
// new file in a not‑yet‑created sub‑directory.
|
|
|
|
|
|
const dir = path.dirname(resolvedPath);
|
|
|
|
|
|
if (dir !== ".") {
|
|
|
|
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(resolvedPath, c, "utf8");
|
|
|
|
|
|
},
|
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`.
2025-04-22 14:07:47 -07:00
|
|
|
|
(p) => fs.unlinkSync(resolvePathAgainstWorkdir(p, workdir)),
|
2025-04-16 12:56:08 -04:00
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
stdout: result,
|
|
|
|
|
|
stderr: "",
|
|
|
|
|
|
exitCode: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
// @ts-expect-error error might not be an object or have a message property.
|
|
|
|
|
|
const stderr = String(error.message ?? error);
|
|
|
|
|
|
return {
|
|
|
|
|
|
stdout: "",
|
|
|
|
|
|
stderr: stderr,
|
|
|
|
|
|
exitCode: 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function getBaseCmd(cmd: Array<string>): string {
|
|
|
|
|
|
const formattedCommand = formatCommandForDisplay(cmd);
|
|
|
|
|
|
return formattedCommand.split(" ")[0] || cmd[0] || "<unknown>";
|
|
|
|
|
|
}
|