diff --git a/src/__tests__/tools-security.test.ts b/src/__tests__/tools-security.test.ts index 6a9eecd7..a0d70c88 100644 --- a/src/__tests__/tools-security.test.ts +++ b/src/__tests__/tools-security.test.ts @@ -654,7 +654,7 @@ describe("package install inline validation", () => { const tool = tools.find((t) => t.name === "install_npm_package")!; await tool.execute({ package: "axios" }, ctx); expect(conway.execCalls.length).toBe(1); - expect(conway.execCalls[0].command).toBe("npm install -g axios"); + expect(conway.execCalls[0].command).toBe("npm install -g 'axios'"); }); it("install_npm_package allows scoped packages", async () => { diff --git a/src/agent/tools.ts b/src/agent/tools.ts index 0d2db864..d38fee1c 100644 --- a/src/agent/tools.ts +++ b/src/agent/tools.ts @@ -568,7 +568,10 @@ export function createBuiltinTools(sandboxId: string): AutomatonTool[] { if (!/^[@a-zA-Z0-9._\/-]+$/.test(pkg)) { return `Blocked: invalid package name "${pkg}"`; } - const result = await ctx.conway.exec(`npm install -g ${pkg}`, 60000); + const result = await ctx.conway.exec( + `npm install -g ${escapeShellArg(pkg)}`, + 60000, + ); const { ulid } = await import("ulid"); ctx.db.insertModification({ @@ -964,7 +967,10 @@ Model: ${ctx.inference.getDefaultModel()} if (!/^[@a-zA-Z0-9._\/-]+$/.test(pkg)) { return `Blocked: invalid package name "${pkg}"`; } - const result = await ctx.conway.exec(`npm install -g ${pkg}`, 60000); + const result = await ctx.conway.exec( + `npm install -g ${escapeShellArg(pkg)}`, + 60000, + ); if (result.exitCode !== 0) { return `Failed to install MCP server: ${result.stderr}`; diff --git a/src/self-mod/tools-manager.ts b/src/self-mod/tools-manager.ts index 6fa7ed84..9a5a48f2 100644 --- a/src/self-mod/tools-manager.ts +++ b/src/self-mod/tools-manager.ts @@ -12,6 +12,11 @@ import type { import { logModification } from "./audit-log.js"; import { ulid } from "ulid"; +/** Escape a string for safe shell interpolation. */ +function escapeShellArg(arg: string): string { + return `'${arg.replace(/'/g, "'\\''")}'`; +} + /** * Install an npm package globally in the sandbox. */ @@ -29,7 +34,7 @@ export async function installNpmPackage( } const result = await conway.exec( - `npm install -g ${packageName}`, + `npm install -g ${escapeShellArg(packageName)}`, 120000, );