@gotgenes/pi-permission-system 9.0.0 → 9.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,20 @@ 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
+ ## [9.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v9.0.0...pi-permission-system-v9.0.1) (2026-06-01)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * enumerate top-level bash commands in BashProgram ([cdb41e1](https://github.com/gotgenes/pi-packages/commit/cdb41e1ed03ad2219f5ba0a3ec79130bd39f3686))
14
+ * evaluate each bash sub-command with most-restrictive precedence ([85e48b2](https://github.com/gotgenes/pi-packages/commit/85e48b258dad84756a05fe11615d6d5de68a8659))
15
+ * gate bash command chains per sub-command ([#301](https://github.com/gotgenes/pi-packages/issues/301)) ([3f80097](https://github.com/gotgenes/pi-packages/commit/3f800977a909b2efc2a21ffefe933804b1c0eafd))
16
+
17
+
18
+ ### Documentation
19
+
20
+ * document per-sub-command bash chain evaluation ([#301](https://github.com/gotgenes/pi-packages/issues/301)) ([e195a70](https://github.com/gotgenes/pi-packages/commit/e195a706d192f3acaeb232c6ed580890ac3c0652))
21
+
8
22
  ## [9.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v8.3.2...pi-permission-system-v9.0.0) (2026-06-01)
9
23
 
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "9.0.0",
3
+ "version": "9.0.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,55 @@
1
+ import { BashProgram } from "#src/handlers/gates/bash-program";
2
+ import { pickMostRestrictive } from "#src/handlers/gates/candidate-check";
3
+ import type { Rule } from "#src/rule";
4
+ import type { PermissionCheckResult } from "#src/types";
5
+
6
+ /** Function type for checkPermission used by the resolver. */
7
+ type CheckPermissionFn = (
8
+ surface: string,
9
+ input: unknown,
10
+ agentName?: string,
11
+ sessionRules?: Rule[],
12
+ ) => PermissionCheckResult;
13
+
14
+ /** Decompose a bash command into its top-level simple-commands. */
15
+ async function decomposeTopLevelCommands(command: string): Promise<string[]> {
16
+ return (await BashProgram.parse(command)).topLevelCommands();
17
+ }
18
+
19
+ /**
20
+ * Resolve the bash command-pattern decision for a (possibly chained) command.
21
+ *
22
+ * A bash invocation may be a shell program with several commands joined by
23
+ * `&&`, `||`, `;`, `|`, `&`, or newlines. Matching the whole string against the
24
+ * bash patterns lets a denied command ride through on an allowed leading one
25
+ * (issue #301). Instead, decompose the command into its top-level simple-commands
26
+ * and evaluate each on the `bash` surface, then select the most restrictive
27
+ * result (`deny > ask > allow`).
28
+ *
29
+ * The selected result carries the offending sub-command in `command` and its
30
+ * rule in `matchedPattern`, so the prompt, session-approval suggestion, and
31
+ * decision event scope to that command.
32
+ *
33
+ * When decomposition yields no top-level commands (an empty command, a comment,
34
+ * or a bare compound statement), the whole command is evaluated as before, so
35
+ * the surface is never weaker than the previous behavior.
36
+ *
37
+ * `checkPermission` stays synchronous and single-command; only the decomposition
38
+ * is async (tree-sitter). `decompose` is injectable for testing.
39
+ */
40
+ export async function resolveBashCommandCheck(
41
+ command: string,
42
+ agentName: string | undefined,
43
+ sessionRules: Rule[],
44
+ checkPermission: CheckPermissionFn,
45
+ decompose: (command: string) => Promise<string[]> = decomposeTopLevelCommands,
46
+ ): Promise<PermissionCheckResult> {
47
+ const units = await decompose(command);
48
+ const results = units.map((unit) =>
49
+ checkPermission("bash", { command: unit }, agentName, sessionRules),
50
+ );
51
+ return (
52
+ pickMostRestrictive(results) ??
53
+ checkPermission("bash", { command }, agentName, sessionRules)
54
+ );
55
+ }
@@ -4,6 +4,7 @@ import { SessionApproval } from "#src/session-approval";
4
4
  import { deriveApprovalPattern } from "#src/session-rules";
5
5
  import type { PermissionCheckResult } from "#src/types";
6
6
  import { extractExternalPathsFromBashCommand } from "./bash-path-extractor";
7
+ import { pickMostRestrictive } from "./candidate-check";
7
8
  import type { GateResult } from "./descriptor";
8
9
  import { formatBashExternalDirectoryAskPrompt } from "./external-directory-messages";
9
10
  import type { ToolCallContext } from "./types";
@@ -85,7 +86,7 @@ export async function describeBashExternalDirectoryGate(
85
86
  // This ensures a config-level "deny" rule is not downgraded to "ask" by the
86
87
  // generic "*" catch-all that the old path-less checkPermission call returned.
87
88
  const worstCheck =
88
- uncoveredEntries.find(({ check }) => check.state === "deny")?.check ??
89
+ pickMostRestrictive(uncoveredEntries.map(({ check }) => check)) ??
89
90
  uncoveredEntries[0].check;
90
91
 
91
92
  const bashExtMessage = formatBashExternalDirectoryAskPrompt(