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/rules.test.ts4 * Description: Evaluator tests — wildcard matching, last-match-wins, layer precedence, hardline floor, normalization, defaults.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import type { CapabilityRequest } from "../../src/permissions/capabilities.js";12import {13 DEFAULT_RULES,14 HARDLINE_RULE,15 combineDecisions,16 evaluate,17 mergeRuleLayers,18 normalizePermissionsSection,19 tagRules,20 wildcardMatch,21} from "../../src/permissions/rules.js";22import type { PermissionRule } from "../../src/permissions/rules.js";23import { unwrap } from "../../src/shared/index.js";2425function req(capability: CapabilityRequest["capability"], subject: string, extra?: Partial<CapabilityRequest>): CapabilityRequest {26 return {27 capability,28 subject,29 display: subject,30 alwaysPatterns: [],31 riskNotes: [],32 ...extra,33 };34}3536describe("wildcardMatch", () => {37 it("* matches any run of characters, including / in paths", () => {38 expect(wildcardMatch("*", "anything at all")).toBe(true);39 expect(wildcardMatch("file.write.*", "file.write.project")).toBe(true);40 expect(wildcardMatch("git push *", "git push origin main")).toBe(true);41 expect(wildcardMatch("/Users/x/notes/*", "/Users/x/notes/a/b.md")).toBe(true);42 expect(wildcardMatch("*.env", "/proj/.env")).toBe(true);43 expect(wildcardMatch("*/.ssh/*", "/Users/x/.ssh/id_rsa")).toBe(true);44 });4546 it("a pattern without * must match exactly, case-sensitively", () => {47 expect(wildcardMatch("git status", "git status")).toBe(true);48 expect(wildcardMatch("git status", "git status --short")).toBe(false);49 expect(wildcardMatch("Git status", "git status")).toBe(false);50 });5152 it("escapes regex specials in patterns", () => {53 expect(wildcardMatch("npm run build (prod)*", "npm run build (prod) --x")).toBe(true);54 expect(wildcardMatch("a.b", "aXb")).toBe(false);55 });56});5758describe("evaluate — last match wins (§4.3)", () => {59 const request = req("process.execute", "git push origin main");6061 it("the LAST matching rule wins", () => {62 const allowThenDeny: PermissionRule[] = [63 { capability: "process.execute", pattern: "*", action: "allow" },64 { capability: "process.execute", pattern: "git push *", action: "deny" },65 ];66 expect(evaluate(allowThenDeny, request).action).toBe("deny");67 expect(evaluate([...allowThenDeny].reverse(), request).action).toBe("allow");68 });6970 it("both fields must match", () => {71 const rules: PermissionRule[] = [72 { capability: "network.access", pattern: "git push *", action: "deny" },73 ];74 expect(evaluate(rules, request).action).toBe("ask"); // capability mismatch → unmatched75 });7677 it("unmatched requests default to ask (safe default)", () => {78 expect(evaluate([], request)).toEqual({ action: "ask" });79 });8081 it("missing pattern defaults to *", () => {82 const rules: PermissionRule[] = [{ capability: "process.execute", action: "allow" }];83 expect(evaluate(rules, request).action).toBe("allow");84 });8586 it("returns the matched rule for provenance", () => {87 const rule: PermissionRule = {88 capability: "process.execute",89 pattern: "git push *",90 action: "allow",91 source: "project",92 };93 expect(evaluate([rule], request).rule).toBe(rule);94 });9596 it("command subjects are matched with collapsed whitespace", () => {97 const rules: PermissionRule[] = [98 { capability: "process.execute", pattern: "npm install", action: "allow" },99 ];100 expect(evaluate(rules, req("process.execute", "npm install")).action).toBe("allow");101 });102103 it("exact-only requests: allow rules need an exact pattern, deny still matches wildcards", () => {104 const subject = "cat README.md > /tmp/out";105 const exactReq = req("process.execute", subject, { exactOnly: true });106 const wildcardAllow: PermissionRule[] = [107 { capability: "process.execute", pattern: "cat *", action: "allow" },108 ];109 expect(evaluate(wildcardAllow, exactReq).action).toBe("ask"); // cat * > file asks (§4.5)110 const exactAllow: PermissionRule[] = [111 { capability: "process.execute", pattern: subject, action: "allow" },112 ];113 expect(evaluate(exactAllow, exactReq).action).toBe("allow");114 const wildcardDeny: PermissionRule[] = [115 { capability: "process.execute", pattern: "cat *", action: "deny" },116 ];117 expect(evaluate(wildcardDeny, exactReq).action).toBe("deny");118 });119});120121describe("hardline floor — beneath the rule system, unoverridable (§4.2)", () => {122 const allowEverything: PermissionRule[] = [{ capability: "*", pattern: "*", action: "allow" }];123124 it("no allow rule can override the floor", () => {125 const decision = evaluate(allowEverything, req("process.execute", "rm -rf /"));126 expect(decision.action).toBe("deny");127 expect(decision.rule).toBe(HARDLINE_RULE);128 });129130 it("fires on de-obfuscated variants", () => {131 expect(evaluate(allowEverything, req("process.execute", 'rm -rf "/"')).action).toBe("deny");132 expect(evaluate(allowEverything, req("process.background", "shutdown -h now")).action).toBe(133 "deny",134 );135 });136137 it("path capabilities are not routed through the command floor", () => {138 const decision = evaluate(allowEverything, req("file.read", "/proj/rm -rf slash"));139 expect(decision.action).toBe("allow");140 });141});142143describe("layering — defaults < user < project < session (§4.2)", () => {144 it("later layers win by position", () => {145 const defaults: PermissionRule[] = [146 { capability: "process.execute", pattern: "*", action: "ask", source: "default" },147 ];148 const user: PermissionRule[] = [149 { capability: "process.execute", pattern: "npm test", action: "deny", source: "user" },150 ];151 const project: PermissionRule[] = [152 { capability: "process.execute", pattern: "npm test", action: "allow", source: "project" },153 ];154 const session: PermissionRule[] = [155 { capability: "process.execute", pattern: "npm test", action: "deny", source: "session" },156 ];157 const request = req("process.execute", "npm test");158 expect(evaluate(mergeRuleLayers(defaults, user), request).action).toBe("deny");159 expect(evaluate(mergeRuleLayers(defaults, user, project), request).action).toBe("allow");160 expect(evaluate(mergeRuleLayers(defaults, user, project, session), request).action).toBe("deny");161 });162163 it("skips undefined layers and tags provenance", () => {164 const merged = mergeRuleLayers(undefined, tagRules([{ capability: "*", action: "ask" }], "user"));165 expect(merged).toEqual([{ capability: "*", action: "ask", source: "user" }]);166 });167});168169describe("normalizePermissionsSection (§4.1)", () => {170 it("expands shorthand and nested forms in source key order, then appends rules", () => {171 const rules = unwrap(172 normalizePermissionsSection(173 {174 "file.read": "allow",175 "process.execute": { "git status": "allow", "*": "ask" },176 rules: [177 { capability: "file.write.outsideProject", pattern: "/Users/x/notes/*", action: "allow" },178 ],179 },180 "project",181 ),182 );183 expect(rules).toEqual([184 { capability: "file.read", action: "allow", source: "project" },185 { capability: "process.execute", pattern: "git status", action: "allow", source: "project" },186 { capability: "process.execute", pattern: "*", action: "ask", source: "project" },187 {188 capability: "file.write.outsideProject",189 pattern: "/Users/x/notes/*",190 action: "allow",191 source: "project",192 },193 ]);194 });195196 it("rejects invalid shapes", () => {197 expect(normalizePermissionsSection(null, "user").ok).toBe(false);198 expect(normalizePermissionsSection({ "file.read": "maybe" }, "user").ok).toBe(false);199 expect(normalizePermissionsSection({ "file.read": 3 }, "user").ok).toBe(false);200 expect(normalizePermissionsSection({ rules: [{ action: "allow" }] }, "user").ok).toBe(false);201 expect(normalizePermissionsSection({ rules: [{ capability: "x", action: "later" }] }, "user").ok).toBe(false);202 });203});204205describe("shipped defaults (§4.5)", () => {206 const rules = [...DEFAULT_RULES];207208 it("ordinary project work is friction-free", () => {209 expect(evaluate(rules, req("file.read", "/proj/src/a.ts")).action).toBe("allow");210 expect(evaluate(rules, req("file.write.project", "/proj/src/a.ts")).action).toBe("allow");211 expect(evaluate(rules, req("process.execute", "git status --short")).action).toBe("allow");212 expect(evaluate(rules, req("process.execute", "ls -la")).action).toBe("allow");213 expect(evaluate(rules, req("process.background", "stdin:npm run dev")).action).toBe("allow");214 });215216 it("secrets-shaped reads and broad actions ask", () => {217 expect(evaluate(rules, req("file.read", "/proj/.env")).action).toBe("ask");218 expect(evaluate(rules, req("file.read", "/proj/.env.local")).action).toBe("ask");219 expect(evaluate(rules, req("file.read", "/proj/.env.example")).action).toBe("allow"); // last match wins220 expect(evaluate(rules, req("file.read", "/Users/x/.ssh/id_rsa")).action).toBe("ask");221 expect(evaluate(rules, req("file.write.outsideProject", "/etc/hosts")).action).toBe("ask");222 expect(evaluate(rules, req("process.execute", "npm install")).action).toBe("ask");223 expect(evaluate(rules, req("network.access", "curl https://x")).action).toBe("ask");224 expect(evaluate(rules, req("git.modify", "git push origin main")).action).toBe("ask");225 expect(evaluate(rules, req("process.background", "npm run dev")).action).toBe("ask");226 });227});228229describe("combineDecisions — deny > ask > allow (§4.4)", () => {230 it("combines correctly", () => {231 expect(combineDecisions([{ action: "allow" }, { action: "allow" }])).toBe("allow");232 expect(combineDecisions([{ action: "allow" }, { action: "ask" }])).toBe("ask");233 expect(combineDecisions([{ action: "ask" }, { action: "deny" }])).toBe("deny");234 expect(combineDecisions([])).toBe("allow"); // no requests = pure observation235 });236});237