@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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "39.0.1",
3
+ "version": "39.0.2",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 a `GateBypass` when all paths are allowed (by config or session rule).
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); the shared helper single-sources the #418 alias
46
- // matching and the worst-uncovered selection.
47
- const { uncovered: uncoveredEntries, worstCheck } =
48
- selectUncoveredExternalPaths(
49
- externalAccesses,
50
- resolver,
51
- tcc.agentName ?? undefined,
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 external path at once, and each
59
- // may have matched a different session pattern -- so the surface is one
60
- // value and the pattern is not. The entry's `externalPaths` lists what
61
- // was covered.
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: externalAccesses.map(({ path }) => path.value()),
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
- /** The uncovered external paths plus the most restrictive check among them. */
20
- export interface UncoveredExternalPaths {
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
- ): UncoveredExternalPaths {
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
  }