@gotgenes/pi-permission-system 3.8.0 → 3.10.0
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 +37 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/defaults.ts +60 -0
- package/src/handlers/lifecycle.ts +1 -1
- package/src/handlers/tool-call.ts +16 -12
- package/src/index.ts +1 -4
- package/src/normalize.ts +70 -0
- package/src/permission-manager.ts +127 -254
- package/src/rule.ts +7 -23
- package/src/runtime.ts +3 -3
- package/src/session-rules.ts +54 -0
- package/src/types.ts +13 -18
- package/tests/defaults.test.ts +105 -0
- package/tests/handlers/before-agent-start.test.ts +4 -5
- package/tests/handlers/input.test.ts +3 -4
- package/tests/handlers/lifecycle.test.ts +7 -8
- package/tests/handlers/tool-call.test.ts +27 -19
- package/tests/normalize.test.ts +121 -0
- package/tests/permission-system.test.ts +11 -39
- package/tests/rule.test.ts +24 -42
- package/tests/runtime.test.ts +5 -4
- package/tests/session-rules.test.ts +225 -0
- package/tests/session-start.test.ts +2 -2
- package/src/bash-filter.ts +0 -51
- package/src/session-approval-cache.ts +0 -81
- package/tests/bash-filter.test.ts +0 -142
- package/tests/session-approval-cache.test.ts +0 -131
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ 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
|
+
## [3.10.0](https://github.com/gotgenes/pi-permission-system/compare/v3.9.0...v3.10.0) (2026-05-04)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* migrate tool_call external_directory to SessionRules ([42c2bd9](https://github.com/gotgenes/pi-permission-system/commit/42c2bd91dbc35c6e4343133fb907f43a6a2550bf))
|
|
14
|
+
* remove SessionApprovalCache ([9d5a5be](https://github.com/gotgenes/pi-permission-system/commit/9d5a5be8251491a66b2826183ca22cbd5a232374))
|
|
15
|
+
* replace SessionApprovalCache with SessionRules in runtime ([4cec9c5](https://github.com/gotgenes/pi-permission-system/commit/4cec9c553779afa8a5fb62bf2ffbd35a43af3e23))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* plan replace SessionApprovalCache with session Ruleset ([#57](https://github.com/gotgenes/pi-permission-system/issues/57)) ([ed1cefe](https://github.com/gotgenes/pi-permission-system/commit/ed1cefec2fd81542084460eb02cd3706b7093c07))
|
|
21
|
+
* **retro:** add retro notes for issue [#56](https://github.com/gotgenes/pi-permission-system/issues/56) ([f97f65c](https://github.com/gotgenes/pi-permission-system/commit/f97f65c448bd907866042bf9804378f441ae7c36))
|
|
22
|
+
* update session approval references ([#57](https://github.com/gotgenes/pi-permission-system/issues/57)) ([40e5e89](https://github.com/gotgenes/pi-permission-system/commit/40e5e89bf29b404b36fedaa48c896391d30574f6))
|
|
23
|
+
|
|
24
|
+
## [3.9.0](https://github.com/gotgenes/pi-permission-system/compare/v3.8.0...v3.9.0) (2026-05-03)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* add normalizeConfig and defaults modules ([84f9c3e](https://github.com/gotgenes/pi-permission-system/commit/84f9c3ef1c665e8d55b694ecf8bbec2dff41b093))
|
|
30
|
+
* evaluate() accepts optional defaultAction parameter ([69dde81](https://github.com/gotgenes/pi-permission-system/commit/69dde81d05722065307b04102a8af6935df0e17c))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Bug Fixes
|
|
34
|
+
|
|
35
|
+
* remove unused imports flagged by biome ([62704a3](https://github.com/gotgenes/pi-permission-system/commit/62704a3b5c833af74a3bd27942ffc4247c92c12c))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Documentation
|
|
39
|
+
|
|
40
|
+
* mark [#42](https://github.com/gotgenes/pi-permission-system/issues/42) and [#43](https://github.com/gotgenes/pi-permission-system/issues/43) complete in target architecture ([04430f2](https://github.com/gotgenes/pi-permission-system/commit/04430f2ea000e9607541262a71ad2be633dc7bb6))
|
|
41
|
+
* mark [#56](https://github.com/gotgenes/pi-permission-system/issues/56) complete in target architecture ([2fe95c5](https://github.com/gotgenes/pi-permission-system/commit/2fe95c577ec97e78c1390db91020294ba25662e0))
|
|
42
|
+
* plan unify Rule type and normalize config into flat Ruleset ([#56](https://github.com/gotgenes/pi-permission-system/issues/56)) ([61e8c48](https://github.com/gotgenes/pi-permission-system/commit/61e8c4800f39a173b69f2773e8b2f09fa9c7318b))
|
|
43
|
+
* **retro:** add retro notes for issue [#43](https://github.com/gotgenes/pi-permission-system/issues/43) ([bd6aea6](https://github.com/gotgenes/pi-permission-system/commit/bd6aea6ed2e1dfdad1ef610f9abb8319d87460cd))
|
|
44
|
+
|
|
8
45
|
## [3.8.0](https://github.com/gotgenes/pi-permission-system/compare/v3.7.0...v3.8.0) (2026-05-03)
|
|
9
46
|
|
|
10
47
|
|
package/README.md
CHANGED
|
@@ -529,7 +529,7 @@ This makes it easy to verify which files the extension actually loaded:
|
|
|
529
529
|
index.ts → Root Pi entrypoint shim
|
|
530
530
|
src/
|
|
531
531
|
├── index.ts → Extension bootstrap, permission checks, readable prompts, review logging, reload handling, and subagent forwarding
|
|
532
|
-
├── session-
|
|
532
|
+
├── session-rules.ts → Ephemeral session-scoped approval rules (Ruleset-based, external-directory access)
|
|
533
533
|
├── config-loader.ts → Unified config loader, merger, and legacy-path detection
|
|
534
534
|
├── config-paths.ts → Path derivation for global, project, and legacy config locations
|
|
535
535
|
├── config-reporter.ts → Resolved config path reporting for diagnostic logs
|
package/package.json
CHANGED
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PermissionDefaultPolicy, PermissionState } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Hardcoded fallback — every surface defaults to "ask" (least privilege). */
|
|
4
|
+
export const DEFAULT_POLICY: PermissionDefaultPolicy = {
|
|
5
|
+
tools: "ask",
|
|
6
|
+
bash: "ask",
|
|
7
|
+
mcp: "ask",
|
|
8
|
+
skills: "ask",
|
|
9
|
+
special: "ask",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Map a surface name used in evaluate() to the corresponding
|
|
14
|
+
* defaultPolicy key. Surfaces not listed here fall through to
|
|
15
|
+
* either "tools" or "special" via getSurfaceDefault().
|
|
16
|
+
*/
|
|
17
|
+
const SURFACE_TO_DEFAULT_KEY: Record<string, keyof PermissionDefaultPolicy> = {
|
|
18
|
+
bash: "bash",
|
|
19
|
+
mcp: "mcp",
|
|
20
|
+
skill: "skills",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the default action for a surface, consulting merged defaults.
|
|
25
|
+
*
|
|
26
|
+
* - "bash", "mcp", "skill" → dedicated defaultPolicy key
|
|
27
|
+
* - special-key surfaces (e.g. "external_directory") → defaults.special
|
|
28
|
+
* - everything else (tool-name surfaces) → defaults.tools
|
|
29
|
+
*/
|
|
30
|
+
export function getSurfaceDefault(
|
|
31
|
+
surface: string,
|
|
32
|
+
defaults: PermissionDefaultPolicy,
|
|
33
|
+
specialKeys: ReadonlySet<string>,
|
|
34
|
+
): PermissionState {
|
|
35
|
+
const key = SURFACE_TO_DEFAULT_KEY[surface];
|
|
36
|
+
if (key) return defaults[key];
|
|
37
|
+
if (specialKeys.has(surface)) return defaults.special;
|
|
38
|
+
return defaults.tools;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Merge zero or more partial default policies on top of DEFAULT_POLICY.
|
|
43
|
+
* Later partials override earlier ones (shallow spread per key).
|
|
44
|
+
*/
|
|
45
|
+
export function mergeDefaults(
|
|
46
|
+
...partials: ReadonlyArray<Partial<PermissionDefaultPolicy> | undefined>
|
|
47
|
+
): PermissionDefaultPolicy {
|
|
48
|
+
const merged: PermissionDefaultPolicy = { ...DEFAULT_POLICY };
|
|
49
|
+
|
|
50
|
+
for (const partial of partials) {
|
|
51
|
+
if (!partial) continue;
|
|
52
|
+
if (partial.tools) merged.tools = partial.tools;
|
|
53
|
+
if (partial.bash) merged.bash = partial.bash;
|
|
54
|
+
if (partial.mcp) merged.mcp = partial.mcp;
|
|
55
|
+
if (partial.skills) merged.skills = partial.skills;
|
|
56
|
+
if (partial.special) merged.special = partial.special;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
@@ -76,6 +76,6 @@ export async function handleSessionShutdown(deps: HandlerDeps): Promise<void> {
|
|
|
76
76
|
deps.runtime.activeSkillEntries = [];
|
|
77
77
|
deps.runtime.lastActiveToolsCacheKey = null;
|
|
78
78
|
deps.runtime.lastPromptStateCacheKey = null;
|
|
79
|
-
deps.runtime.
|
|
79
|
+
deps.runtime.sessionRules.clear();
|
|
80
80
|
deps.stopForwardedPermissionPolling();
|
|
81
81
|
}
|
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
formatUnknownToolReason,
|
|
30
30
|
formatUserDeniedReason,
|
|
31
31
|
} from "../permission-prompts";
|
|
32
|
-
import {
|
|
32
|
+
import { evaluate } from "../rule";
|
|
33
|
+
import { deriveApprovalPattern } from "../session-rules";
|
|
33
34
|
import { findSkillPathMatch } from "../skill-prompt-sanitizer";
|
|
34
35
|
import { getPermissionLogContext } from "../tool-input-preview";
|
|
35
36
|
import {
|
|
@@ -169,12 +170,15 @@ export async function handleToolCall(
|
|
|
169
170
|
externalDirectoryPath,
|
|
170
171
|
ctx.cwd,
|
|
171
172
|
);
|
|
172
|
-
const
|
|
173
|
+
const sessionRuleset = deps.runtime.sessionRules.getRuleset();
|
|
174
|
+
const sessionMatch = evaluate(
|
|
173
175
|
"external_directory",
|
|
174
176
|
normalizedExtPath,
|
|
177
|
+
sessionRuleset,
|
|
175
178
|
);
|
|
179
|
+
const isSessionApproved = sessionRuleset.includes(sessionMatch);
|
|
176
180
|
|
|
177
|
-
if (
|
|
181
|
+
if (isSessionApproved) {
|
|
178
182
|
deps.runtime.writeReviewLog("permission_request.session_approved", {
|
|
179
183
|
source: "tool_call",
|
|
180
184
|
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
@@ -182,7 +186,7 @@ export async function handleToolCall(
|
|
|
182
186
|
agentName,
|
|
183
187
|
path: externalDirectoryPath,
|
|
184
188
|
resolution: "session_approved",
|
|
185
|
-
|
|
189
|
+
sessionApprovalPattern: sessionMatch.pattern,
|
|
186
190
|
});
|
|
187
191
|
// Fall through to normal permission check
|
|
188
192
|
} else {
|
|
@@ -245,8 +249,8 @@ export async function handleToolCall(
|
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
if (extDirDecision?.state === "approved_for_session") {
|
|
248
|
-
const
|
|
249
|
-
deps.runtime.
|
|
252
|
+
const pattern = deriveApprovalPattern(normalizedExtPath);
|
|
253
|
+
deps.runtime.sessionRules.approve("external_directory", pattern);
|
|
250
254
|
}
|
|
251
255
|
}
|
|
252
256
|
// Fall through to normal permission check
|
|
@@ -261,9 +265,12 @@ export async function handleToolCall(
|
|
|
261
265
|
ctx.cwd,
|
|
262
266
|
);
|
|
263
267
|
if (externalPaths.length > 0) {
|
|
268
|
+
const bashSessionRuleset = deps.runtime.sessionRules.getRuleset();
|
|
264
269
|
const uncoveredPaths = externalPaths.filter(
|
|
265
270
|
(p) =>
|
|
266
|
-
!
|
|
271
|
+
!bashSessionRuleset.includes(
|
|
272
|
+
evaluate("external_directory", p, bashSessionRuleset),
|
|
273
|
+
),
|
|
267
274
|
);
|
|
268
275
|
|
|
269
276
|
if (uncoveredPaths.length === 0) {
|
|
@@ -339,11 +346,8 @@ export async function handleToolCall(
|
|
|
339
346
|
|
|
340
347
|
if (bashExtDecision?.state === "approved_for_session") {
|
|
341
348
|
for (const extPath of uncoveredPaths) {
|
|
342
|
-
const
|
|
343
|
-
deps.runtime.
|
|
344
|
-
"external_directory",
|
|
345
|
-
prefix,
|
|
346
|
-
);
|
|
349
|
+
const pattern = deriveApprovalPattern(extPath);
|
|
350
|
+
deps.runtime.sessionRules.approve("external_directory", pattern);
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
353
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,10 +11,7 @@ import {
|
|
|
11
11
|
handleSessionStart,
|
|
12
12
|
handleToolCall,
|
|
13
13
|
} from "./handlers";
|
|
14
|
-
import {
|
|
15
|
-
type PermissionPromptDecision,
|
|
16
|
-
requestPermissionDecisionFromUi,
|
|
17
|
-
} from "./permission-dialog";
|
|
14
|
+
import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
18
15
|
import {
|
|
19
16
|
createExtensionRuntime,
|
|
20
17
|
createPermissionManagerForCwd,
|
package/src/normalize.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Rule, Ruleset } from "./rule";
|
|
2
|
+
import type { PermissionState } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Subset of UnifiedPermissionConfig covering only policy fields.
|
|
6
|
+
* Used as the input shape for normalizeConfig().
|
|
7
|
+
*/
|
|
8
|
+
export interface NormalizableConfig {
|
|
9
|
+
tools?: Record<string, PermissionState>;
|
|
10
|
+
bash?: Record<string, PermissionState>;
|
|
11
|
+
mcp?: Record<string, PermissionState>;
|
|
12
|
+
skills?: Record<string, PermissionState>;
|
|
13
|
+
special?: Record<string, PermissionState>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Keys in the `tools` map that serve as fallback defaults for their
|
|
18
|
+
* respective pattern-based surfaces rather than as tool-level rules.
|
|
19
|
+
*
|
|
20
|
+
* `tools.bash` sets the bash default (fallback when no bash pattern matches).
|
|
21
|
+
* `tools.mcp` sets the tool-level MCP fallback.
|
|
22
|
+
*
|
|
23
|
+
* These are NOT normalized into the Ruleset — they are extracted by the
|
|
24
|
+
* caller and handled as separate fallbacks to preserve the semantic that
|
|
25
|
+
* specific bash/mcp patterns always have priority.
|
|
26
|
+
*/
|
|
27
|
+
export const TOOL_SURFACE_OVERRIDE_KEYS: ReadonlySet<string> = new Set([
|
|
28
|
+
"bash",
|
|
29
|
+
"mcp",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert the on-disk config shape into a flat Ruleset.
|
|
34
|
+
*
|
|
35
|
+
* Ordering within a scope:
|
|
36
|
+
* 1. tools entries (tool-name-as-surface, pattern "*") — excluding bash/mcp
|
|
37
|
+
* 2. bash entries (surface "bash", pattern = command glob)
|
|
38
|
+
* 3. mcp entries (surface "mcp", pattern = target glob)
|
|
39
|
+
* 4. skills entries (surface "skill", pattern = skill glob)
|
|
40
|
+
* 5. special entries (surface "special", pattern = key name)
|
|
41
|
+
*
|
|
42
|
+
* `tools.bash` and `tools.mcp` are excluded — see TOOL_SURFACE_OVERRIDE_KEYS.
|
|
43
|
+
* `defaultPolicy` is NOT included — handled separately by the caller.
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeConfig(config: NormalizableConfig): Ruleset {
|
|
46
|
+
const rules: Rule[] = [];
|
|
47
|
+
|
|
48
|
+
for (const [name, action] of Object.entries(config.tools ?? {})) {
|
|
49
|
+
if (TOOL_SURFACE_OVERRIDE_KEYS.has(name)) continue;
|
|
50
|
+
rules.push({ surface: name, pattern: "*", action });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const [pattern, action] of Object.entries(config.bash ?? {})) {
|
|
54
|
+
rules.push({ surface: "bash", pattern, action });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [pattern, action] of Object.entries(config.mcp ?? {})) {
|
|
58
|
+
rules.push({ surface: "mcp", pattern, action });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const [pattern, action] of Object.entries(config.skills ?? {})) {
|
|
62
|
+
rules.push({ surface: "skill", pattern, action });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [name, action] of Object.entries(config.special ?? {})) {
|
|
66
|
+
rules.push({ surface: "special", pattern: name, action });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return rules;
|
|
70
|
+
}
|