@gotgenes/pi-permission-system 15.0.0 → 15.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [15.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v15.0.0...pi-permission-system-v15.0.1) (2026-06-20)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **permission-system:** bind session approval for current-directory files ([#438](https://github.com/gotgenes/pi-packages/issues/438)) ([083a8e8](https://github.com/gotgenes/pi-packages/commit/083a8e8d9c2a4f6c49af158677d8669b4f099d9f))
14
+
8
15
  ## [15.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v14.0.1...pi-permission-system-v15.0.0) (2026-06-20)
9
16
 
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "15.0.0",
3
+ "version": "15.0.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -40,9 +40,14 @@ export function describeBashPathGate(
40
40
  if (candidates.length === 0) return null;
41
41
  const tokens = candidates.map(({ token }) => token);
42
42
 
43
- // Tokens whose resolved state needs a check (deny/ask), paired with the
44
- // token that produced them so the descriptor can derive its pattern.
45
- const uncovered: Array<{ token: string; check: PermissionCheckResult }> = [];
43
+ // Tokens whose resolved state needs a check (deny/ask), paired with the raw
44
+ // token (prompt/decision display) and its policy values (the first of which
45
+ // is the canonical absolute path the approval pattern is derived from).
46
+ const uncovered: Array<{
47
+ token: string;
48
+ policyValues: readonly string[];
49
+ check: PermissionCheckResult;
50
+ }> = [];
46
51
  let allSessionCovered = true;
47
52
 
48
53
  for (const { token, policyValues } of candidates) {
@@ -64,11 +69,11 @@ export function describeBashPathGate(
64
69
  }
65
70
 
66
71
  if (check.state === "deny") {
67
- uncovered.push({ token, check });
72
+ uncovered.push({ token, policyValues, check });
68
73
  break; // Short-circuit on deny.
69
74
  }
70
75
  if (check.state === "ask") {
71
- uncovered.push({ token, check });
76
+ uncovered.push({ token, policyValues, check });
72
77
  }
73
78
  }
74
79
 
@@ -93,14 +98,19 @@ export function describeBashPathGate(
93
98
 
94
99
  // Pick the most restrictive (deny > ask > allow, first-wins) uncovered token.
95
100
  const worstCheck = pickMostRestrictive(uncovered.map(({ check }) => check));
96
- const worstToken = worstCheck
97
- ? (uncovered.find(({ check }) => check === worstCheck)?.token ?? null)
98
- : null;
101
+ const worstEntry = worstCheck
102
+ ? uncovered.find(({ check }) => check === worstCheck)
103
+ : undefined;
104
+ const worstToken = worstEntry?.token ?? null;
99
105
 
100
106
  // All tokens evaluate to allow — no restriction.
101
- if (!worstCheck || !worstToken) return null;
107
+ if (!worstCheck || !worstToken || !worstEntry) return null;
102
108
 
103
- const pattern = deriveApprovalPattern(worstToken);
109
+ // Derive the pattern from the canonical absolute policy value (the cd-aware
110
+ // resolved path), so it matches the values a later call produces. Falls back
111
+ // to the raw token only when no base was resolvable (no cwd / unknown cd).
112
+ const approvalBase = worstEntry.policyValues[0] ?? worstToken;
113
+ const pattern = deriveApprovalPattern(approvalBase);
104
114
  const askMessage = formatPathAskPrompt(
105
115
  tcc.toolName,
106
116
  worstToken,
@@ -1,4 +1,4 @@
1
- import { getToolInputPath } from "#src/path-utils";
1
+ import { getToolInputPath, normalizePathForComparison } from "#src/path-utils";
2
2
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
3
  import { SessionApproval } from "#src/session-approval";
4
4
  import { deriveApprovalPattern } from "#src/session-rules";
@@ -35,7 +35,12 @@ export function describePathGate(
35
35
  // "path" key should not trigger path-level prompts (#58).
36
36
  if (check.matchedPattern === undefined) return null;
37
37
 
38
- const pattern = deriveApprovalPattern(filePath);
38
+ // Resolve to the canonical (cwd-anchored, absolute) path so the approval
39
+ // pattern matches the policy values a later call produces.
40
+ const approvalPath = tcc.cwd
41
+ ? normalizePathForComparison(filePath, tcc.cwd)
42
+ : filePath;
43
+ const pattern = deriveApprovalPattern(approvalPath);
39
44
 
40
45
  const descriptor: GateDescriptor = {
41
46
  surface: "path",
@@ -1,4 +1,8 @@
1
- import { getPathBearingToolPath, PATH_BEARING_TOOLS } from "#src/path-utils";
1
+ import {
2
+ getPathBearingToolPath,
3
+ normalizePathForComparison,
4
+ PATH_BEARING_TOOLS,
5
+ } from "#src/path-utils";
2
6
  import { suggestSessionPattern } from "#src/pattern-suggest";
3
7
  import { formatAskPrompt } from "#src/permission-prompts";
4
8
  import { SessionApproval } from "#src/session-approval";
@@ -12,7 +16,9 @@ import type { ToolCallContext } from "./types";
12
16
  * Derive the value used for session-approval pattern suggestions.
13
17
  *
14
18
  * Bash → command string; MCP → qualified target;
15
- * path-bearing tools → file path; others catch-all wildcard.
19
+ * path-bearing tools → the file path resolved to its canonical (cwd-anchored,
20
+ * absolute) form so the suggested pattern matches the policy values a later
21
+ * call produces; others → catch-all wildcard.
16
22
  */
17
23
  function deriveSuggestionValue(
18
24
  tcc: ToolCallContext,
@@ -20,7 +26,9 @@ function deriveSuggestionValue(
20
26
  ): string {
21
27
  if (tcc.toolName === "bash") return check.command ?? "";
22
28
  if (tcc.toolName === "mcp") return check.target ?? "mcp";
23
- return getPathBearingToolPath(tcc.toolName, tcc.input) ?? "*";
29
+ const path = getPathBearingToolPath(tcc.toolName, tcc.input);
30
+ if (path === null) return "*";
31
+ return tcc.cwd ? normalizePathForComparison(path, tcc.cwd) : path;
24
32
  }
25
33
 
26
34
  /**
@@ -90,6 +90,10 @@ function buildLabel(pattern: string, surface: string): string {
90
90
  *
91
91
  * Returns a `SessionApprovalSuggestion` with the surface, the wildcard pattern
92
92
  * to store in `SessionRules`, and a human-readable dialog label.
93
+ *
94
+ * `value` is expected to be the canonical (cwd-resolved, absolute) path for
95
+ * path surfaces — callers resolve it before suggesting, so the derived pattern
96
+ * matches the policy values a later tool call produces.
93
97
  */
94
98
  export function suggestSessionPattern(
95
99
  surface: string,
@@ -58,6 +58,11 @@ export class SessionRules implements SessionApprovalRecorder {
58
58
  *
59
59
  * For paths that already end with a separator (directories), the separator
60
60
  * is treated as the directory boundary and `*` is appended directly.
61
+ *
62
+ * The path is expected to be the canonical (cwd-resolved, absolute) form used
63
+ * for policy matching, so the derived pattern matches the same policy values a
64
+ * later tool call produces. Callers that hold a working directory resolve the
65
+ * path to that form first; the function itself stays free of cwd state.
61
66
  */
62
67
  export function deriveApprovalPattern(normalizedPath: string): string {
63
68
  // If the path already ends with a separator, it's a directory — glob its contents.
@@ -258,6 +258,25 @@ describe("describeBashPathGate", () => {
258
258
  undefined,
259
259
  );
260
260
  });
261
+
262
+ it("binds a current-directory token's session approval to the cwd subtree", async () => {
263
+ const resolver = makeResolver(
264
+ makeCheckResult({ state: "ask", matchedPattern: "*" }),
265
+ );
266
+ const result = (await describeGate(
267
+ makeTcc({
268
+ input: { command: "cat .env" },
269
+ cwd: "/test/project",
270
+ }),
271
+ resolver,
272
+ )) as GateDescriptor;
273
+
274
+ expect(result.decision.value).toBe(".env");
275
+ expect(result.sessionApproval?.surface).toBe("path");
276
+ expect(result.sessionApproval?.representativePattern).toBe(
277
+ "/test/project/*",
278
+ );
279
+ });
261
280
  });
262
281
 
263
282
  // Home-relative path characterization (#350) ──────────────────────────────
@@ -116,6 +116,20 @@ describe("describePathGate", () => {
116
116
  expect(result.sessionApproval?.representativePattern).toBeDefined();
117
117
  });
118
118
 
119
+ it("binds a current-directory file's session approval to the cwd subtree", () => {
120
+ const resolver = makeResolver(
121
+ makeCheckResult({ state: "ask", matchedPattern: "*" }),
122
+ );
123
+ const result = describePathGate(
124
+ makeTcc({ input: { path: "index.html" }, cwd: "/test/project" }),
125
+ resolver,
126
+ ) as GateDescriptor;
127
+ expect(result.sessionApproval?.surface).toBe("path");
128
+ expect(result.sessionApproval?.representativePattern).toBe(
129
+ "/test/project/*",
130
+ );
131
+ });
132
+
119
133
  it("descriptor denialContext references the file path and tool name", () => {
120
134
  const resolver = makeResolver(
121
135
  makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
@@ -146,6 +146,40 @@ describe("describeToolGate", () => {
146
146
  expect(desc.sessionApproval?.representativePattern).toBeDefined();
147
147
  });
148
148
 
149
+ it("binds a current-directory file's session approval to the cwd subtree", () => {
150
+ const check = makeCheckResult("ask", { toolName: "edit" });
151
+ const desc = describeToolGate(
152
+ makeTcc({
153
+ toolName: "edit",
154
+ input: { path: "index.html" },
155
+ cwd: "/test/project",
156
+ }),
157
+ check,
158
+ makeFormatter(),
159
+ );
160
+ expect(desc.sessionApproval?.surface).toBe("edit");
161
+ expect(desc.sessionApproval?.representativePattern).toBe("/test/project/*");
162
+ });
163
+
164
+ it("resolves a sub-directory file's session approval to an absolute pattern", () => {
165
+ // Resolve-at-gate canonicalizes every path (not just the cwd-root case),
166
+ // so sub-directory approvals are absolute too — the deliberate tradeoff
167
+ // that keeps the pattern aligned with the policy values it is matched against.
168
+ const check = makeCheckResult("ask", { toolName: "edit" });
169
+ const desc = describeToolGate(
170
+ makeTcc({
171
+ toolName: "edit",
172
+ input: { path: "src/foo.ts" },
173
+ cwd: "/test/project",
174
+ }),
175
+ check,
176
+ makeFormatter(),
177
+ );
178
+ expect(desc.sessionApproval?.representativePattern).toBe(
179
+ "/test/project/src/*",
180
+ );
181
+ });
182
+
149
183
  it("populates promptDetails with correct fields", () => {
150
184
  const check = makeCheckResult("ask");
151
185
  const desc = describeToolGate(
@@ -286,4 +286,19 @@ describe("deriveApprovalPattern", () => {
286
286
  ).action,
287
287
  ).toBe("ask");
288
288
  });
289
+
290
+ it("binds a current-directory file to the cwd subtree once resolved", () => {
291
+ // Callers resolve the path to its canonical absolute form before deriving;
292
+ // a current-directory file then yields the cwd glob and excludes siblings.
293
+ const pattern = deriveApprovalPattern("/test/project/index.html");
294
+ expect(pattern).toBe("/test/project/*");
295
+ const session = new SessionRules();
296
+ session.approve("edit", pattern);
297
+ expect(
298
+ evaluate("edit", "/test/project/index.html", session.getRuleset()).action,
299
+ ).toBe("allow");
300
+ expect(evaluate("edit", "/etc/passwd", session.getRuleset()).action).toBe(
301
+ "ask",
302
+ );
303
+ });
289
304
  });