Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@brycehanscomb/toolgate",
"version": "0.4.0",
"version": "0.5.0",
"devDependencies": {
"bun-types": "latest"
},
Expand Down
24 changes: 17 additions & 7 deletions policies/allow-npx-safe.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { allow, next, type Policy } from "../src";
import { safeBashCommand, safeBashCommandOrPipeline, getAndChainSegments, getArgs, parseShell } from "./parse-bash-ast";

/** Whitelisted npx packages — add entries as needed. */
const SAFE_NPX_PACKAGES = new Set([
"next",
"playwright",
"vitest",
]);
/**
* Whitelisted npx packages.
* - `true` means all subcommands are safe
* - A `Set<string>` lists destructive subcommands that should NOT be auto-allowed
*/
const SAFE_NPX_PACKAGES: Record<string, true | Set<string>> = {
next: true,
playwright: true,
vitest: true,
cdk: new Set(["deploy", "destroy"]),
};

function isAllowedNpx(tokens: string[]): boolean {
return tokens[0] === "npx" && SAFE_NPX_PACKAGES.has(tokens[1]);
if (tokens[0] !== "npx") return false;
const rule = SAFE_NPX_PACKAGES[tokens[1]];
if (!rule) return false;
if (rule === true) return true;
// Block if any argument matches a destructive subcommand
return !tokens.slice(2).some(t => rule.has(t));
}

const allowNpxSafe: Policy = {
Expand Down
71 changes: 71 additions & 0 deletions policies/tests/allow-npx-safe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from "bun:test";
import { ALLOW, NEXT, type ToolCall } from "@brycehanscomb/toolgate";
import allowNpxSafe from "../allow-npx-safe";

const makeCall = (tool: string, args: Record<string, unknown> = {}): ToolCall => ({
tool,
args,
context: { cwd: "/tmp", env: {}, projectRoot: "/tmp" },
});

describe("allow-npx-safe", () => {
it("allows npx playwright test", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx playwright test" }));
expect(result.verdict).toBe(ALLOW);
});

it("allows npx vitest", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx vitest" }));
expect(result.verdict).toBe(ALLOW);
});

it("allows npx next build", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx next build" }));
expect(result.verdict).toBe(ALLOW);
});

it("allows npx cdk synth", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx cdk synth" }));
expect(result.verdict).toBe(ALLOW);
});

it("allows npx cdk diff", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx cdk diff" }));
expect(result.verdict).toBe(ALLOW);
});

it("allows npx cdk list", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx cdk list" }));
expect(result.verdict).toBe(ALLOW);
});

it("passes through npx cdk deploy", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx cdk deploy" }));
expect(result.verdict).toBe(NEXT);
});

it("passes through npx cdk deploy with stack name", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx cdk deploy MyStack" }));
expect(result.verdict).toBe(NEXT);
});

it("passes through npx cdk destroy", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx cdk destroy" }));
expect(result.verdict).toBe(NEXT);
});

it("passes through unknown npx packages", async () => {
const result = await allowNpxSafe.handler(makeCall("Bash", { command: "npx some-unknown-pkg" }));
expect(result.verdict).toBe(NEXT);
});

it("allows mcp__playwright__ tools", async () => {
const result = await allowNpxSafe.handler(makeCall("mcp__playwright__browser_snapshot"));
expect(result.verdict).toBe(ALLOW);
});

it("passes through non-Bash tools", async () => {
const result = await allowNpxSafe.handler(makeCall("Read", { file_path: "/tmp/test" }));
expect(result.verdict).toBe(NEXT);
});
});