@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 +7 -0
- package/package.json +1 -1
- package/src/handlers/gates/bash-path.ts +20 -10
- package/src/handlers/gates/path.ts +7 -2
- package/src/handlers/gates/tool.ts +11 -3
- package/src/pattern-suggest.ts +4 -0
- package/src/session-rules.ts +5 -0
- package/test/handlers/gates/bash-path.test.ts +19 -0
- package/test/handlers/gates/path.test.ts +14 -0
- package/test/handlers/gates/tool.test.ts +34 -0
- package/test/session-rules.test.ts +15 -0
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
|
@@ -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
|
|
45
|
-
|
|
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
|
|
97
|
-
?
|
|
98
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
/**
|
package/src/pattern-suggest.ts
CHANGED
|
@@ -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,
|
package/src/session-rules.ts
CHANGED
|
@@ -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
|
});
|