Diff
1diff --git a/src/core/audit.ts b/src/core/audit.ts
2index 46cfc5e9e18a308cb56f2bd1e3707638e23e2840..b1b8aa4778ca5a575b4f7a4760552105bdb5553b 100644
3--- a/src/core/audit.ts
4+++ b/src/core/audit.ts
5@@ -1,5 +1,6 @@
6 import { appendFile, mkdir } from "node:fs/promises";
7 import { dirname } from "node:path";
8+import { POLICY_VERSION } from "./rules";
9 import type { DecisionAudit } from "./types";
10
11 export function createJsonlDecisionAudit(file: string): DecisionAudit {
12@@ -7,7 +8,10 @@ export function createJsonlDecisionAudit(file: string): DecisionAudit {
13 async record(entry): Promise<void> {
14 try {
15 await mkdir(dirname(file), { recursive: true });
16- await appendFile(file, `${JSON.stringify({ recordedAt: new Date().toISOString(), ...entry })}\n`);
17+ await appendFile(
18+ file,
19+ `${JSON.stringify({ recordedAt: new Date().toISOString(), ...entry, policyVersion: POLICY_VERSION })}\n`,
20+ );
21 } catch {}
22 },
23 };
24diff --git a/src/pi/static-permissions.ts b/src/pi/static-permissions.ts
25index 99a94b5fe993b3fb16746f13e490af9e2042c738..99129d4b6a2503af433d15a091674e99e25bd44b 100644
26--- a/src/pi/static-permissions.ts
27+++ b/src/pi/static-permissions.ts
28@@ -1,6 +1,6 @@
29 import { realpath } from "node:fs/promises";
30 import { homedir } from "node:os";
31-import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
32+import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
33 import type { ExternalDirectories, PermissionAction, PiPolicyEngineConfig } from "./config";
34 import type { Decision, DecisionCategory } from "../core/types";
35
36@@ -77,6 +77,13 @@ export async function checkStaticPermission(
37 if (PATH_TOOLS.has(toolType)) {
38 const path = typeof input.path === "string" ? input.path : ".";
39 const absolutePath = await canonicalPath(path, cwd);
40+ if (["edit", "write"].includes(toolType) && basename(absolutePath) === "policy-engine.json") {
41+ return {
42+ decision: "ask",
43+ reason: "Policy engine configuration changes require confirmation",
44+ category: "dangerous",
45+ };
46+ }
47 const workspacePath = await canonicalPath(cwd, cwd);
48 if (!isInside(absolutePath, workspacePath)) {
49 if (!matchesExternalDirectory(config.externalDirectories, absolutePath)) return undefined;
50diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
51index 1a9179c31fd983f4506641a47c7e3bc339698a59..44799577c4f9fa4fc7a056705db74146a845edbc 100644
52--- a/test/core/deterministic.test.ts
53+++ b/test/core/deterministic.test.ts
54@@ -68,6 +68,7 @@ test("allows recognized deterministic command grammars", () => {
55 "cut -d: -f1,3",
56 "golangci-lint run --fix",
57 "rg --hidden --glob '*.ts' policy src",
58+ 'rg -l "stt|transcri" --type go -i',
59 "rg --files --hidden",
60 ]) {
61 expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" });
62diff --git a/test/pi/static-permissions.test.ts b/test/pi/static-permissions.test.ts
63index 3b8b9373d9fb46e295bc74a9313dc227f8fde745..64782cc574959938c3c50dc7295740f235c98d49 100644
64--- a/test/pi/static-permissions.test.ts
65+++ b/test/pi/static-permissions.test.ts
66@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
67 import { tmpdir } from "node:os";
68 import { join } from "node:path";
69 import { checkStaticPermission, matchesExternalDirectory } from "../../src/pi/static-permissions";
70+import type { PiPolicyEngineConfig } from "../../src/pi/config";
71
72 test("explicit deny wins before external path handling", async () => {
73 const cwd = mkdtempSync(join(tmpdir(), "policy-engine-cwd-"));
74@@ -19,6 +20,26 @@ test("explicit deny wins before external path handling", async () => {
75 }
76 });
77
78+test("asks before changing a policy engine configuration", async () => {
79+ const cwd = mkdtempSync(join(tmpdir(), "policy-engine-cwd-"));
80+ try {
81+ const config: PiPolicyEngineConfig = {
82+ reviewer: { kind: "none" },
83+ tools: { edit: "allow", write: "allow" },
84+ externalDirectories: [],
85+ };
86+ for (const toolType of ["edit", "write"]) {
87+ await expect(
88+ checkStaticPermission(toolType, { path: ".pi/policy-engine.json" }, cwd, config),
89+ ).resolves.toMatchObject({
90+ decision: "ask",
91+ });
92+ }
93+ } finally {
94+ rmSync(cwd, { recursive: true, force: true });
95+ }
96+});
97+
98 test("matches external directories by path components", async () => {
99 const root = mkdtempSync(join(tmpdir(), "policy-engine-external-"));
100 const cwd = join(root, "cwd");