spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: tests/permissions/capabilities.test.ts4 * Description: Per-tool capability mapping goldens — path resolution tricks, bash derivation, process actions.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { unwrap } from "../../src/shared/index.js";12import { mapToolCapabilities } from "../../src/permissions/capabilities.js";13import type {14 CapabilityMappingContext,15 CapabilityRequest,16} from "../../src/permissions/capabilities.js";1718const CTX: CapabilityMappingContext = { projectRoot: "/proj", cwd: "/proj", home: "/Users/x" };1920function map(tool: string, input: unknown, ctx: CapabilityMappingContext = CTX): CapabilityRequest[] {21 return unwrap(mapToolCapabilities(tool, input, ctx));22}2324describe("read / grep / glob → file.read", () => {25 it("read maps to file.read with the resolved path", () => {26 const [req] = map("read", { file_path: "src/a.ts" });27 expect(req).toMatchObject({ capability: "file.read", subject: "/proj/src/a.ts" });28 });2930 it("reads outside the project stay file.read (pattern rules can still gate them)", () => {31 const [req] = map("read", { file_path: "/etc/hosts" });32 expect(req?.capability).toBe("file.read");33 expect(req?.subject).toBe("/etc/hosts");34 });3536 it("grep and glob default their subject to the cwd", () => {37 expect(map("grep", {})[0]).toMatchObject({ capability: "file.read", subject: "/proj" });38 expect(map("glob", { path: "src" })[0]).toMatchObject({39 capability: "file.read",40 subject: "/proj/src",41 });42 });43});4445describe("write / edit → file.write.* with full path resolution", () => {46 it("write under the project root is file.write.project", () => {47 const [req] = map("write", { file_path: "src/new.ts" });48 expect(req?.capability).toBe("file.write.project");49 expect(req?.subject).toBe("/proj/src/new.ts");50 });5152 it("../ traversal flips to file.write.outsideProject", () => {53 const [req] = map("edit", { file_path: "../outside.txt" });54 expect(req?.capability).toBe("file.write.outsideProject");55 expect(req?.subject).toBe("/outside.txt");56 expect(req?.riskNotes).toContain("Path is outside the project root.");57 });5859 it("./x/../../etc/hosts is resolved BEFORE classification (spec §1 example)", () => {60 const [req] = map("write", { file_path: "./x/../../etc/hosts", }, { ...CTX, cwd: "/proj" });61 expect(req?.capability).toBe("file.write.outsideProject");62 expect(req?.subject).toBe("/etc/hosts");63 });6465 it("symlinks are canonicalized via the injected resolver", () => {66 const ctx: CapabilityMappingContext = {67 ...CTX,68 resolvePath: (p) => (p === "/proj/link.txt" ? "/etc/target" : p),69 };70 const [req] = map("write", { file_path: "link.txt" }, ctx);71 expect(req?.capability).toBe("file.write.outsideProject");72 expect(req?.subject).toBe("/etc/target");73 });7475 it("a prefix-sharing sibling directory is NOT inside the project", () => {76 const [req] = map("write", { file_path: "/proj-evil/x.ts" });77 expect(req?.capability).toBe("file.write.outsideProject");78 });79});8081describe("bash → process.execute plus derived requests", () => {82 it("a simple command yields one process.execute request with suggestions", () => {83 const reqs = map("bash", { command: "npm install" });84 expect(reqs).toHaveLength(1);85 expect(reqs[0]).toMatchObject({86 capability: "process.execute",87 subject: "npm install", // collapsed whitespace88 alwaysPatterns: ["npm install"],89 });90 });9192 it("derives network.access for net tools", () => {93 const reqs = map("bash", { command: "curl https://api.example.com" });94 expect(reqs.map((r) => r.capability)).toEqual(["process.execute", "network.access"]);95 });9697 it("derives git.modify for mutating git commands", () => {98 const reqs = map("bash", { command: "git push origin main" });99 expect(reqs.map((r) => r.capability)).toEqual(["process.execute", "git.modify"]);100 expect(reqs[0]?.alwaysPatterns).toEqual(["git push *"]);101 });102103 it("derives file.write.outsideProject from filesystem verbs", () => {104 const reqs = map("bash", { command: "tee /etc/hosts" });105 const outside = reqs.find((r) => r.capability === "file.write.outsideProject");106 expect(outside?.subject).toBe("/etc/hosts");107 });108109 it("compound commands yield one process.execute request per part, no suggestions", () => {110 const reqs = map("bash", { command: "git status && git diff" });111 const exec = reqs.filter((r) => r.capability === "process.execute");112 expect(exec.map((r) => r.subject)).toEqual(["git status", "git diff"]);113 expect(exec.every((r) => r.alwaysPatterns.length === 0)).toBe(true);114 });115116 it("a redirect adds an exact-only whole-command request plus the outside write", () => {117 const reqs = map("bash", { command: "cat README.md > /tmp/out" });118 const whole = reqs.find((r) => r.exactOnly === true);119 expect(whole?.subject).toBe("cat README.md > /tmp/out");120 const outside = reqs.find((r) => r.capability === "file.write.outsideProject");121 expect(outside?.subject).toBe("/tmp/out");122 });123124 it("obfuscated commands yield one exact-only request with the substitution note", () => {125 const reqs = map("bash", { command: "echo $(whoami)" });126 expect(reqs).toHaveLength(1);127 expect(reqs[0]).toMatchObject({ capability: "process.execute", exactOnly: true });128 expect(reqs[0]?.alwaysPatterns).toEqual([]);129 });130131 it("obfuscated commands with net-tool names conservatively carry network.access", () => {132 const reqs = map("bash", { command: "curl $(cat url.txt)" });133 expect(reqs.map((r) => r.capability)).toEqual(["process.execute", "network.access"]);134 });135});136137describe("process tool", () => {138 it("start maps to process.background plus derived analysis", () => {139 const reqs = map("process", { action: "start", command: "npm run dev" });140 expect(reqs).toHaveLength(1);141 expect(reqs[0]).toMatchObject({142 capability: "process.background",143 subject: "npm run dev",144 alwaysPatterns: ["npm run dev", "npm run *"],145 });146 });147148 it("start derives network.access too", () => {149 const reqs = map("process", { action: "start", command: "ssh -N -L 8080:x:80 host" });150 expect(reqs.map((r) => r.capability)).toEqual(["process.background", "network.access"]);151 });152153 it("list / read / stop produce NO requests (pure observation)", () => {154 expect(map("process", { action: "list" })).toEqual([]);155 expect(map("process", { action: "read", id: "p1" })).toEqual([]);156 expect(map("process", { action: "stop", id: "p1" })).toEqual([]);157 });158159 it("write maps to process.background with a stdin: subject", () => {160 const ctx: CapabilityMappingContext = {161 ...CTX,162 processCommandLookup: (id) => (id === "p1" ? "npm run dev" : undefined),163 };164 const [req] = map("process", { action: "write", id: "p1", input: "y\n" }, ctx);165 expect(req).toMatchObject({ capability: "process.background", subject: "stdin:npm run dev" });166 });167168 it("unknown actions fail closed", () => {169 expect(mapToolCapabilities("process", { action: "hack" }, CTX).ok).toBe(false);170 });171});172173describe("fail-closed mapping errors", () => {174 it("unknown tools are a design error, never a silent bypass", () => {175 expect(mapToolCapabilities("teleport", {}, CTX).ok).toBe(false);176 });177178 it("missing required fields fail", () => {179 expect(mapToolCapabilities("read", {}, CTX).ok).toBe(false);180 expect(mapToolCapabilities("bash", { command: "" }, CTX).ok).toBe(false);181 expect(mapToolCapabilities("write", null, CTX).ok).toBe(false);182 });183});184