/** * KHAELOR * File: tests/permissions/capabilities.test.ts * Description: Per-tool capability mapping goldens — path resolution tricks, bash derivation, process actions. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { unwrap } from "../../src/shared/index.js"; import { mapToolCapabilities } from "../../src/permissions/capabilities.js"; import type { CapabilityMappingContext, CapabilityRequest, } from "../../src/permissions/capabilities.js"; const CTX: CapabilityMappingContext = { projectRoot: "/proj", cwd: "/proj", home: "/Users/x" }; function map(tool: string, input: unknown, ctx: CapabilityMappingContext = CTX): CapabilityRequest[] { return unwrap(mapToolCapabilities(tool, input, ctx)); } describe("read / grep / glob → file.read", () => { it("read maps to file.read with the resolved path", () => { const [req] = map("read", { file_path: "src/a.ts" }); expect(req).toMatchObject({ capability: "file.read", subject: "/proj/src/a.ts" }); }); it("reads outside the project stay file.read (pattern rules can still gate them)", () => { const [req] = map("read", { file_path: "/etc/hosts" }); expect(req?.capability).toBe("file.read"); expect(req?.subject).toBe("/etc/hosts"); }); it("grep and glob default their subject to the cwd", () => { expect(map("grep", {})[0]).toMatchObject({ capability: "file.read", subject: "/proj" }); expect(map("glob", { path: "src" })[0]).toMatchObject({ capability: "file.read", subject: "/proj/src", }); }); }); describe("write / edit → file.write.* with full path resolution", () => { it("write under the project root is file.write.project", () => { const [req] = map("write", { file_path: "src/new.ts" }); expect(req?.capability).toBe("file.write.project"); expect(req?.subject).toBe("/proj/src/new.ts"); }); it("../ traversal flips to file.write.outsideProject", () => { const [req] = map("edit", { file_path: "../outside.txt" }); expect(req?.capability).toBe("file.write.outsideProject"); expect(req?.subject).toBe("/outside.txt"); expect(req?.riskNotes).toContain("Path is outside the project root."); }); it("./x/../../etc/hosts is resolved BEFORE classification (spec §1 example)", () => { const [req] = map("write", { file_path: "./x/../../etc/hosts", }, { ...CTX, cwd: "/proj" }); expect(req?.capability).toBe("file.write.outsideProject"); expect(req?.subject).toBe("/etc/hosts"); }); it("symlinks are canonicalized via the injected resolver", () => { const ctx: CapabilityMappingContext = { ...CTX, resolvePath: (p) => (p === "/proj/link.txt" ? "/etc/target" : p), }; const [req] = map("write", { file_path: "link.txt" }, ctx); expect(req?.capability).toBe("file.write.outsideProject"); expect(req?.subject).toBe("/etc/target"); }); it("a prefix-sharing sibling directory is NOT inside the project", () => { const [req] = map("write", { file_path: "/proj-evil/x.ts" }); expect(req?.capability).toBe("file.write.outsideProject"); }); }); describe("bash → process.execute plus derived requests", () => { it("a simple command yields one process.execute request with suggestions", () => { const reqs = map("bash", { command: "npm install" }); expect(reqs).toHaveLength(1); expect(reqs[0]).toMatchObject({ capability: "process.execute", subject: "npm install", // collapsed whitespace alwaysPatterns: ["npm install"], }); }); it("derives network.access for net tools", () => { const reqs = map("bash", { command: "curl https://api.example.com" }); expect(reqs.map((r) => r.capability)).toEqual(["process.execute", "network.access"]); }); it("derives git.modify for mutating git commands", () => { const reqs = map("bash", { command: "git push origin main" }); expect(reqs.map((r) => r.capability)).toEqual(["process.execute", "git.modify"]); expect(reqs[0]?.alwaysPatterns).toEqual(["git push *"]); }); it("derives file.write.outsideProject from filesystem verbs", () => { const reqs = map("bash", { command: "tee /etc/hosts" }); const outside = reqs.find((r) => r.capability === "file.write.outsideProject"); expect(outside?.subject).toBe("/etc/hosts"); }); it("compound commands yield one process.execute request per part, no suggestions", () => { const reqs = map("bash", { command: "git status && git diff" }); const exec = reqs.filter((r) => r.capability === "process.execute"); expect(exec.map((r) => r.subject)).toEqual(["git status", "git diff"]); expect(exec.every((r) => r.alwaysPatterns.length === 0)).toBe(true); }); it("a redirect adds an exact-only whole-command request plus the outside write", () => { const reqs = map("bash", { command: "cat README.md > /tmp/out" }); const whole = reqs.find((r) => r.exactOnly === true); expect(whole?.subject).toBe("cat README.md > /tmp/out"); const outside = reqs.find((r) => r.capability === "file.write.outsideProject"); expect(outside?.subject).toBe("/tmp/out"); }); it("obfuscated commands yield one exact-only request with the substitution note", () => { const reqs = map("bash", { command: "echo $(whoami)" }); expect(reqs).toHaveLength(1); expect(reqs[0]).toMatchObject({ capability: "process.execute", exactOnly: true }); expect(reqs[0]?.alwaysPatterns).toEqual([]); }); it("obfuscated commands with net-tool names conservatively carry network.access", () => { const reqs = map("bash", { command: "curl $(cat url.txt)" }); expect(reqs.map((r) => r.capability)).toEqual(["process.execute", "network.access"]); }); }); describe("process tool", () => { it("start maps to process.background plus derived analysis", () => { const reqs = map("process", { action: "start", command: "npm run dev" }); expect(reqs).toHaveLength(1); expect(reqs[0]).toMatchObject({ capability: "process.background", subject: "npm run dev", alwaysPatterns: ["npm run dev", "npm run *"], }); }); it("start derives network.access too", () => { const reqs = map("process", { action: "start", command: "ssh -N -L 8080:x:80 host" }); expect(reqs.map((r) => r.capability)).toEqual(["process.background", "network.access"]); }); it("list / read / stop produce NO requests (pure observation)", () => { expect(map("process", { action: "list" })).toEqual([]); expect(map("process", { action: "read", id: "p1" })).toEqual([]); expect(map("process", { action: "stop", id: "p1" })).toEqual([]); }); it("write maps to process.background with a stdin: subject", () => { const ctx: CapabilityMappingContext = { ...CTX, processCommandLookup: (id) => (id === "p1" ? "npm run dev" : undefined), }; const [req] = map("process", { action: "write", id: "p1", input: "y\n" }, ctx); expect(req).toMatchObject({ capability: "process.background", subject: "stdin:npm run dev" }); }); it("unknown actions fail closed", () => { expect(mapToolCapabilities("process", { action: "hack" }, CTX).ok).toBe(false); }); }); describe("fail-closed mapping errors", () => { it("unknown tools are a design error, never a silent bypass", () => { expect(mapToolCapabilities("teleport", {}, CTX).ok).toBe(false); }); it("missing required fields fail", () => { expect(mapToolCapabilities("read", {}, CTX).ok).toBe(false); expect(mapToolCapabilities("bash", { command: "" }, CTX).ok).toBe(false); expect(mapToolCapabilities("write", null, CTX).ok).toBe(false); }); });