diff --git a/apps/pi-extension/index.ts b/apps/pi-extension/index.ts index d2524aba7..6b2bbc80a 100644 --- a/apps/pi-extension/index.ts +++ b/apps/pi-extension/index.ts @@ -68,6 +68,7 @@ import { import { applyPhaseTools, isPlanWritePathAllowed, + isPlannotatorSubmitDevicePath, PLAN_MARK_DONE_TOOL, PLAN_SUBMIT_TOOL, releasePhaseTools, @@ -1464,6 +1465,7 @@ export default function plannotator(pi: ExtensionAPI): void { if (event.toolName !== "write" && event.toolName !== "edit") return; const inputPath = event.input.path as string; + if (isPlannotatorSubmitDevicePath(inputPath)) return; if (!isPlanWritePathAllowed(inputPath, ctx.cwd)) { const verb = event.toolName === "write" ? "writes" : "edits"; return { diff --git a/apps/pi-extension/tool-scope.test.ts b/apps/pi-extension/tool-scope.test.ts index 83ad3c3b3..86a9280db 100644 --- a/apps/pi-extension/tool-scope.test.ts +++ b/apps/pi-extension/tool-scope.test.ts @@ -2,7 +2,9 @@ import { describe, expect, test } from "bun:test"; import { applyPhaseTools, isPlanWritePathAllowed, + isPlannotatorSubmitDevicePath, PLAN_MARK_DONE_TOOL, + PLAN_SUBMIT_DEVICE_URI, PLAN_SUBMIT_TOOL, releasePhaseTools, stripPlanningOnlyTools, @@ -87,3 +89,55 @@ describe("plan write path gate", () => { expect(isPlanWritePathAllowed("notes.MdX", cwd)).toBe(true); }); }); + +describe("plannotator submit device path gate", () => { + test("recognizes the exact submit device URI", () => { + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plan")).toBe(true); + expect(isPlannotatorSubmitDevicePath(PLAN_SUBMIT_DEVICE_URI)).toBe(true); + }); + + test("rejects lookalikes: suffix, subpath, or wrong case", () => { + // Suffix + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plan2")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plans")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plan_extra")).toBe(false); + // Subpath + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plan/")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plan/extra")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plan/sub/path")).toBe(false); + // Wrong scheme case + expect(isPlannotatorSubmitDevicePath("XD://plannotator_submit_plan")).toBe(false); + expect(isPlannotatorSubmitDevicePath("Xd://plannotator_submit_plan")).toBe(false); + // Wrong tool name case + expect(isPlannotatorSubmitDevicePath("xd://PLANNOTATOR_SUBMIT_PLAN")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://Plannotator_Submit_Plan")).toBe(false); + // Surrounding whitespace + expect(isPlannotatorSubmitDevicePath(" xd://plannotator_submit_plan")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://plannotator_submit_plan ")).toBe(false); + }); + + test("rejects other device URIs", () => { + expect(isPlannotatorSubmitDevicePath("xd://report_issue")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://plannotator_mark_done")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://eval")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://bash")).toBe(false); + expect(isPlannotatorSubmitDevicePath("xd://browse")).toBe(false); + }); + + test("rejects non-string and empty inputs", () => { + expect(isPlannotatorSubmitDevicePath(undefined)).toBe(false); + expect(isPlannotatorSubmitDevicePath(null)).toBe(false); + expect(isPlannotatorSubmitDevicePath(123)).toBe(false); + expect(isPlannotatorSubmitDevicePath({})).toBe(false); + expect(isPlannotatorSubmitDevicePath([])).toBe(false); + expect(isPlannotatorSubmitDevicePath(true)).toBe(false); + expect(isPlannotatorSubmitDevicePath("")).toBe(false); + }); + + test("isPlanWritePathAllowed still rejects device URIs as plan file paths", () => { + const cwd = "/r"; + expect(isPlanWritePathAllowed("xd://plannotator_submit_plan", cwd)).toBe(false); + expect(isPlanWritePathAllowed(PLAN_SUBMIT_DEVICE_URI, cwd)).toBe(false); + expect(isPlanWritePathAllowed("xd://report_issue", cwd)).toBe(false); + }); +}); diff --git a/apps/pi-extension/tool-scope.ts b/apps/pi-extension/tool-scope.ts index 2872cb98f..523e8d28f 100644 --- a/apps/pi-extension/tool-scope.ts +++ b/apps/pi-extension/tool-scope.ts @@ -3,6 +3,11 @@ import { extname, isAbsolute, relative, resolve } from "node:path"; export type Phase = "idle" | "planning" | "executing"; export const PLAN_SUBMIT_TOOL = "plannotator_submit_plan"; +export const PLAN_SUBMIT_DEVICE_URI = `xd://${PLAN_SUBMIT_TOOL}` as const; + +export function isPlannotatorSubmitDevicePath(input: unknown): boolean { + return typeof input === "string" && input === PLAN_SUBMIT_DEVICE_URI; +} export const PLAN_MARK_DONE_TOOL = "plannotator_mark_done"; const ALLOWED_PLAN_EXTENSIONS = new Set([".md", ".mdx"]);