Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/src/agent-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ export async function runCommand(
if (dangerReason) throw new Error(dangerReason);

const cwd = resolveSafePath(workspaceRoot, relativeCwd);
const wrappedCommand = applySandbox(command, { workspaceRoot, allowNetwork: network });
const wrappedCommand = applySandbox(command, { workspaceRoot, allowNetwork: network, cwd });
const settings = settingsStore.getSettings();
let stopMonitor = () => {};
try {
Expand Down Expand Up @@ -913,7 +913,7 @@ export function startBackgroundCommand(
}

const cwd = resolveSafePath(workspaceRoot, relativeCwd);
const wrappedCommand = applySandbox(command, { workspaceRoot, allowNetwork: network });
const wrappedCommand = applySandbox(command, { workspaceRoot, allowNetwork: network, cwd });
// detached so the shell becomes its own process group leader — lets
// killProcessTree() below signal the whole group (shell + whatever it
// spawned, e.g. `npm run dev` spawning `node`) instead of just the shell
Expand Down
23 changes: 23 additions & 0 deletions app/src/command-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,29 @@ describe("wrapCommand", () => {
expect(wrapped?.args[1]).toContain("(allow network*)");
expect(wrapped?.args[1]).not.toContain("(deny network*)");
});

// --chdir takes precedence over the working directory bwrap inherits from
// the spawning process, so pinning it to the workspace root silently
// overrode the subdirectory the caller had already resolved.
it("chdirs to the requested subdirectory, not just the workspace root", () => {
const wrapped = wrapCommand(
"npm test",
{ workspaceRoot: "/home/user/project", allowNetwork: false, cwd: "/home/user/project/packages/api" },
"linux",
has(["bwrap"])
);
const chdirIndex = wrapped!.args.indexOf("--chdir");
expect(chdirIndex).toBeGreaterThan(-1);
expect(wrapped!.args[chdirIndex + 1]).toBe("/home/user/project/packages/api");
// The workspace root is still what gets bound writable.
expect(wrapped!.args).toEqual(expect.arrayContaining(["--bind", "/home/user/project", "/home/user/project"]));
});

it("falls back to the workspace root when no cwd is given", () => {
const wrapped = wrapCommand("npm test", { workspaceRoot: "/home/user/project", allowNetwork: false }, "linux", has(["bwrap"]));
const chdirIndex = wrapped!.args.indexOf("--chdir");
expect(wrapped!.args[chdirIndex + 1]).toBe("/home/user/project");
});
});

describe("applySandbox", () => {
Expand Down
11 changes: 10 additions & 1 deletion app/src/command-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ export function detectSandboxCapabilities(
export interface WrapCommandOptions {
workspaceRoot: string;
allowNetwork: boolean;
// Directory the command should start in. Defaults to the workspace root.
// bubblewrap is passed --chdir, which takes precedence over the working
// directory inherited from the spawning process, so a caller that resolved
// a subdirectory has to pass it through here — setting it only on the
// spawn options would be silently overridden. Callers are responsible for
// confining this to the workspace (they go through resolveSafePath); it is
// not re-validated here.
cwd?: string;
}

export interface WrappedCommand {
Expand All @@ -92,6 +100,7 @@ export function wrapCommand(
): WrappedCommand | null {
const caps = detectSandboxCapabilities(platform, hasCommand);
const root = path.resolve(opts.workspaceRoot);
const startDir = opts.cwd ? path.resolve(opts.cwd) : root;

if (caps.mechanism === "bubblewrap") {
const args = [
Expand All @@ -113,7 +122,7 @@ export function wrapCommand(
root,
root,
"--chdir",
root,
startDir,
"--die-with-parent",
"--unshare-all",
];
Expand Down
2 changes: 1 addition & 1 deletion app/src/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function createTerminal(
// ordinary shell use (curl, ping, package managers) confusingly fail.
// Filesystem confinement to the workspace still applies where the
// platform supports it.
const wrapped = wrapCommand(shell, { workspaceRoot, allowNetwork: true });
const wrapped = wrapCommand(shell, { workspaceRoot, allowNetwork: true, cwd });
const ptyOptions = { name: "xterm-256color", cols: 80, rows: 24, cwd, env: process.env };
const ptyProcess = wrapped ? pty.spawn(wrapped.command, wrapped.args, ptyOptions) : pty.spawn(shell, [], ptyOptions);

Expand Down