@gotgenes/pi-permission-system 39.0.1 → 39.0.2
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,17 @@ 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
|
+
## [39.0.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v39.0.1...pi-permission-system-v39.0.2) (2026-10-03)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** stop logging config- and default-allowed bash external paths as session approvals ([1e597c0](https://github.com/gotgenes/pi-packages/commit/1e597c033c58843f298d9fd0f01cf227dc649e60)), closes [#1011](https://github.com/gotgenes/pi-packages/issues/1011)
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* **pi-permission-system:** say what external_directory does when the key is absent ([7319277](https://github.com/gotgenes/pi-packages/commit/7319277d338665225d59af21c43b463e94bda6e6)), closes [#1011](https://github.com/gotgenes/pi-packages/issues/1011)
|
|
18
|
+
|
|
8
19
|
## [39.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v39.0.0...pi-permission-system-v39.0.1) (2026-10-03)
|
|
9
20
|
|
|
10
21
|
|
package/docs/configuration.md
CHANGED
|
@@ -789,6 +789,9 @@ Use a pattern map to allow specific directories without opening all external acc
|
|
|
789
789
|
For example, `read: "allow"` can permit ordinary reads while `external_directory: "ask"` still requires confirmation before reading `../outside.txt` or an absolute path outside `ctx.cwd`.
|
|
790
790
|
Optional-path search tools (`find`, `grep`, `ls`) skip this check when no `path` is provided.
|
|
791
791
|
|
|
792
|
+
When no `external_directory` key is present, the universal fallback (`permission["*"]`) applies.
|
|
793
|
+
So `"*": "allow"` lets every outside-CWD access through with no prompt and, like any policy allow, no review-log entry; the examples on this page set `"*": "ask"` inside `external_directory` so the boundary asks regardless of the fallback.
|
|
794
|
+
|
|
792
795
|
#### Allow an outside-CWD cache directory
|
|
793
796
|
|
|
794
797
|
When an agent keeps reading a local cache outside the working tree — `~/.cargo/registry`, `~/.npm`, `~/go/pkg/mod` — and you want to stop confirming it every time, allow that directory on the `external_directory` surface:
|
package/package.json
CHANGED
|
@@ -14,7 +14,10 @@ import type { ToolCallContext } from "./types";
|
|
|
14
14
|
* Reads the external paths from the injected `BashProgram` and checks whether
|
|
15
15
|
* any reference directories outside the working directory. Returns `null` when the gate
|
|
16
16
|
* does not apply (not a shell invocation, no command, or no external paths found).
|
|
17
|
-
* Returns
|
|
17
|
+
* Returns `null` when every path is allowed by a config rule or the universal
|
|
18
|
+
* fallback — like any policy allow, it writes no review entry. Returns a
|
|
19
|
+
* `GateBypass` recording a session approval when every path is allowed and at
|
|
20
|
+
* least one was covered by a session grant.
|
|
18
21
|
* Returns a `GateDescriptor` with multi-pattern sessionApproval for uncovered paths.
|
|
19
22
|
*
|
|
20
23
|
* Each path is resolved on the narrowest `external_directory`-family surface
|
|
@@ -42,23 +45,29 @@ export function describeBashExternalDirectoryGate(
|
|
|
42
45
|
|
|
43
46
|
// Resolve every external path on the external_directory surface and keep the
|
|
44
47
|
// ones not already allowed (config-level allows suppress the prompt just as
|
|
45
|
-
// session-level allows do)
|
|
46
|
-
// matching and the
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// session-level allows do), noting which allowed ones a session grant
|
|
49
|
+
// covered; the shared helper single-sources the #418 alias matching and the
|
|
50
|
+
// worst-uncovered selection.
|
|
51
|
+
const {
|
|
52
|
+
uncovered: uncoveredEntries,
|
|
53
|
+
sessionCovered,
|
|
54
|
+
worstCheck,
|
|
55
|
+
} = selectUncoveredExternalPaths(
|
|
56
|
+
externalAccesses,
|
|
57
|
+
resolver,
|
|
58
|
+
tcc.agentName ?? undefined,
|
|
59
|
+
);
|
|
53
60
|
const uncoveredPaths = uncoveredEntries.map(({ path }) => path.value());
|
|
54
61
|
|
|
55
62
|
if (uncoveredPaths.length === 0) {
|
|
63
|
+
// No session grant decided any path: policy alone allowed the command.
|
|
64
|
+
if (sessionCovered.length === 0) return null;
|
|
56
65
|
return {
|
|
57
66
|
action: "allow",
|
|
58
|
-
// A whole-command bypass covers every
|
|
59
|
-
// may have matched a different session pattern -- so the surface is
|
|
60
|
-
// value and the pattern is not. The entry's `externalPaths` lists
|
|
61
|
-
//
|
|
67
|
+
// A whole-command bypass covers every session-granted path at once, and
|
|
68
|
+
// each may have matched a different session pattern -- so the surface is
|
|
69
|
+
// one value and the pattern is not. The entry's `externalPaths` lists the
|
|
70
|
+
// paths a grant covered, never the ones policy allowed.
|
|
62
71
|
decidedBy: {
|
|
63
72
|
kind: "session_approval",
|
|
64
73
|
surface: "external_directory",
|
|
@@ -72,7 +81,7 @@ export function describeBashExternalDirectoryGate(
|
|
|
72
81
|
toolName: tcc.toolName,
|
|
73
82
|
agentName: tcc.agentName,
|
|
74
83
|
command,
|
|
75
|
-
externalPaths:
|
|
84
|
+
externalPaths: sessionCovered.map((path) => path.value()),
|
|
76
85
|
resolution: "session_approved",
|
|
77
86
|
},
|
|
78
87
|
},
|
|
@@ -16,9 +16,18 @@ export interface UncoveredExternalPath {
|
|
|
16
16
|
check: PermissionCheckResult;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* How a set of external paths resolved: the uncovered ones, the worst among
|
|
21
|
+
* them, and which allowed paths a session grant covered.
|
|
22
|
+
*/
|
|
23
|
+
export interface ExternalPathCoverage {
|
|
21
24
|
uncovered: UncoveredExternalPath[];
|
|
25
|
+
/**
|
|
26
|
+
* Allowed paths whose winning rule was a session grant. A path allowed by a
|
|
27
|
+
* config rule or the universal fallback is in neither list, so a caller can
|
|
28
|
+
* stamp `session_approval` only where a grant actually decided.
|
|
29
|
+
*/
|
|
30
|
+
sessionCovered: AccessPath[];
|
|
22
31
|
/** Worst check among uncovered paths; `undefined` only when none are uncovered. */
|
|
23
32
|
worstCheck: PermissionCheckResult | undefined;
|
|
24
33
|
}
|
|
@@ -60,14 +69,16 @@ export function resolveExternalDirectoryPolicy(
|
|
|
60
69
|
* not "allow" are collected (filtering on state, not source, so config-level
|
|
61
70
|
* allow rules suppress the prompt just as session-level allow rules do), and
|
|
62
71
|
* the most restrictive uncovered check is returned so a config "deny" is not
|
|
63
|
-
* downgraded to the catch-all "ask".
|
|
72
|
+
* downgraded to the catch-all "ask". Allowed entries a session rule decided
|
|
73
|
+
* are reported apart, as the provenance a whole-command bypass records.
|
|
64
74
|
*/
|
|
65
75
|
export function selectUncoveredExternalPaths(
|
|
66
76
|
accesses: readonly BashExternalPath[],
|
|
67
77
|
resolver: ScopedPermissionResolver,
|
|
68
78
|
agentName: string | undefined,
|
|
69
|
-
):
|
|
79
|
+
): ExternalPathCoverage {
|
|
70
80
|
const uncovered: UncoveredExternalPath[] = [];
|
|
81
|
+
const sessionCovered: AccessPath[] = [];
|
|
71
82
|
for (const { path, effect } of accesses) {
|
|
72
83
|
const surface = capabilitySurfaceForEffect(
|
|
73
84
|
"external_directory",
|
|
@@ -81,10 +92,13 @@ export function selectUncoveredExternalPaths(
|
|
|
81
92
|
);
|
|
82
93
|
if (check.state !== "allow") {
|
|
83
94
|
uncovered.push({ path, surface, effect, check });
|
|
95
|
+
} else if (check.source === "session") {
|
|
96
|
+
sessionCovered.push(path);
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
return {
|
|
87
100
|
uncovered,
|
|
101
|
+
sessionCovered,
|
|
88
102
|
worstCheck: pickMostRestrictive(uncovered.map(({ check }) => check)),
|
|
89
103
|
};
|
|
90
104
|
}
|