@gotgenes/pi-permission-system 20.4.1 → 20.5.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 CHANGED
@@ -5,6 +5,31 @@ 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
+ ## [20.5.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.4.2...pi-permission-system-v20.5.0) (2026-07-13)
9
+
10
+
11
+ ### Features
12
+
13
+ * **pi-permission-system:** add shellTools config schema ([cd4f851](https://github.com/gotgenes/pi-packages/commit/cd4f851ab4ddccbc2409f664432e3cf256997817))
14
+ * **pi-permission-system:** carry shellTools through config merge ([635d66b](https://github.com/gotgenes/pi-packages/commit/635d66b56d753f00192269f04cd7dceba1264f8d))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **pi-permission-system:** treat bare / as a filesystem-root path candidate ([#583](https://github.com/gotgenes/pi-packages/issues/583)) ([dfa4080](https://github.com/gotgenes/pi-packages/commit/dfa408026a6f74f63c4537f9e6904acabc4f260a))
20
+
21
+
22
+ ### Documentation
23
+
24
+ * **pi-permission-system:** document shellTools config ([1b8e3c3](https://github.com/gotgenes/pi-packages/commit/1b8e3c3a5779ac6cae9686c10da1b8b3a0f36309))
25
+
26
+ ## [20.4.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.4.1...pi-permission-system-v20.4.2) (2026-07-13)
27
+
28
+
29
+ ### Documentation
30
+
31
+ * **pi-permission-system:** add read-only bash command allowlist recipe ([#521](https://github.com/gotgenes/pi-packages/issues/521)) ([6e9710f](https://github.com/gotgenes/pi-packages/commit/6e9710fb4564d99721a8e077839d567af326f3cc))
32
+
8
33
  ## [20.4.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.4.0...pi-permission-system-v20.4.1) (2026-07-12)
9
34
 
10
35
 
package/README.md CHANGED
@@ -109,6 +109,8 @@ Project overrides global; per-agent YAML frontmatter overrides both.
109
109
 
110
110
  Within a surface map like `bash` or `mcp`, **last matching rule wins** — put broad catch-alls first and specific overrides after.
111
111
 
112
+ The optional `shellTools` field records which non-`bash` tools carry shell semantics (e.g. an `exec_command` tool that replaces native `bash`), so they can be gated through the same enforcement — see [docs/configuration.md](docs/configuration.md#shelltools--gating-aliased-shell-tools).
113
+
112
114
  For the full reference — all surfaces, runtime knobs, per-agent overrides, merge semantics, and common recipes — see [docs/configuration.md](docs/configuration.md).
113
115
 
114
116
  ## Upgrading
@@ -10,6 +10,10 @@
10
10
 
11
11
  "piInfrastructureReadPaths": [],
12
12
 
13
+ "shellTools": {
14
+ "exec_command": { "commandArgument": "cmd", "workdirArgument": "workdir" }
15
+ },
16
+
13
17
  "permission": {
14
18
  "*": "ask",
15
19
  "path": {
@@ -46,6 +46,11 @@ Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`) use simple replace
46
46
  "toolTextSummaryMaxLength": 120,
47
47
  "piInfrastructureReadPaths": [],
48
48
 
49
+ // Non-bash tools that carry shell semantics
50
+ "shellTools": {
51
+ "exec_command": { "commandArgument": "cmd", "workdirArgument": "workdir" }
52
+ },
53
+
49
54
  // Flat permission policy
50
55
  "permission": {
51
56
  "*": "ask", // universal fallback
@@ -104,6 +109,37 @@ Example — allow reads from a Homebrew-managed Pi install at any version:
104
109
  }
105
110
  ```
106
111
 
112
+ ### `shellTools` — gating aliased shell tools
113
+
114
+ The native `bash` tool goes through the full bash enforcement stack: command decomposition, wrapper flooring, path and external-directory token gates, and `bash:` rules.
115
+ Some extensions replace `bash` with a differently-named tool — for example [`@howaboua/pi-codex-conversion`](https://github.com/IgorWarzocha/howaboua-pi-stuff) registers `exec_command`, which carries the shell command in a `cmd` argument and an optional working directory in `workdir`.
116
+ Without a hint, the permission system cannot tell that such a tool is really a shell, so it gates it as a generic extension tool and the bash rules never apply.
117
+
118
+ `shellTools` records that hint.
119
+ Each key is a tool name; its value maps the tool's input arguments (the keys of the tool call's `arguments` object):
120
+
121
+ ```jsonc
122
+ {
123
+ "shellTools": {
124
+ "exec_command": { "commandArgument": "cmd", "workdirArgument": "workdir" }
125
+ }
126
+ }
127
+ ```
128
+
129
+ | Field | Required | Description |
130
+ | ----------------- | -------- | ------------------------------------------------------------------------- |
131
+ | `commandArgument` | yes | The tool's input argument holding the shell command string (e.g. `cmd`). |
132
+ | `workdirArgument` | no | The tool's input argument holding the working directory (e.g. `workdir`). |
133
+
134
+ Merge semantics: `shellTools` **shallow-merges by tool name** across global → project.
135
+ A project entry overrides a specific tool's mapping on a key collision but never drops a global entry — so adding a project-scoped alias cannot silently remove enforcement for a tool the global config already covers.
136
+ To change a specific tool's mapping, set that tool's key at the project scope (the alias object is replaced wholesale, not deep-merged).
137
+
138
+ `shellTools` only ever *tightens* enforcement and is inert when the named tool is not registered in the current session.
139
+ Opting a project out of a shell-aliasing extension is a package-disable concern, not a `shellTools` edit.
140
+
141
+ > **Note:** `shellTools` records the alias; the enforcement wiring that consumes it is tracked in [#574](https://github.com/gotgenes/pi-packages/issues/574).
142
+
107
143
  ---
108
144
 
109
145
  ## Policy Reference
@@ -639,6 +675,107 @@ Avoid arrays, multi-line scalars, and YAML anchors.
639
675
  }
640
676
  ```
641
677
 
678
+ ### Read-Only Bash Command Allowlist
679
+
680
+ The [Read-Only Mode](#read-only-mode) recipe above gates *tools*; this one gates the *bash* surface.
681
+ It allows a curated set of commands whose only effect is to read or report — none can create or modify a file, register, or system state by itself — while every other command falls through to `ask`.
682
+
683
+ ```jsonc
684
+ {
685
+ "permission": {
686
+ "*": "ask",
687
+ "write": "deny",
688
+ "edit": "deny",
689
+ "path": {
690
+ "*": "allow",
691
+ "*.env": "deny",
692
+ "*.env.*": "deny",
693
+ "~/.ssh/*": "deny"
694
+ },
695
+ "bash": {
696
+ "*": "ask",
697
+
698
+ // File inspection
699
+ "cat *": "allow",
700
+ "head *": "allow",
701
+ "tail *": "allow",
702
+ "less *": "allow",
703
+ "more *": "allow",
704
+
705
+ // Listing and metadata
706
+ "ls *": "allow",
707
+ "tree *": "allow",
708
+ "stat *": "allow",
709
+ "file *": "allow",
710
+ "wc *": "allow",
711
+ "du *": "allow",
712
+ "df *": "allow",
713
+
714
+ // Search (find/fd with -exec are auto-floored to ask)
715
+ "grep *": "allow",
716
+ "egrep *": "allow",
717
+ "fgrep *": "allow",
718
+ "rg *": "allow",
719
+ "find *": "allow",
720
+ "fd *": "allow",
721
+
722
+ // Comparison and hashing
723
+ "diff *": "allow",
724
+ "cmp *": "allow",
725
+ "comm *": "allow",
726
+ "md5sum *": "allow",
727
+ "sha1sum *": "allow",
728
+ "sha256sum *": "allow",
729
+ "cksum *": "allow",
730
+
731
+ // System info
732
+ "pwd": "allow",
733
+ "whoami": "allow",
734
+ "id": "allow",
735
+ "hostname": "allow",
736
+ "uname *": "allow",
737
+ "date": "allow",
738
+ "uptime": "allow",
739
+ "ps *": "allow",
740
+ "printenv *": "allow",
741
+ "which *": "allow",
742
+ "type *": "allow",
743
+
744
+ // Git read-only subcommands (never a broad "git *")
745
+ "git status": "allow",
746
+ "git diff *": "allow",
747
+ "git log *": "allow",
748
+ "git show *": "allow",
749
+ "git blame *": "allow",
750
+ "git ls-files *": "allow",
751
+ "git branch": "allow",
752
+ "git remote -v": "allow"
753
+ }
754
+ }
755
+ }
756
+ ```
757
+
758
+ Four existing behaviors keep this allowlist safe — you do not have to enumerate the destructive commands to block them:
759
+
760
+ 1. **Redirects are gated by the `path` surface, not `bash`.**
761
+ Allowing `cat *` allows the `cat` command, not a redirect it carries: `cat secret > out.txt` writes `out.txt` through the `path`/`external_directory` gate.
762
+ That is why this recipe ships with `write` and `edit` denied and a `path` deny block for sensitive files.
763
+ Keep the `path` surface locked down for anything you would not want an allowed read command to overwrite via `>`.
764
+ 2. **`find`/`fd` with an exec flag are floored to `ask`.**
765
+ A bare `find *` search is read-only, so it is safe to allow; the moment an exec flag appears (`find -exec`/`-execdir`/`-ok`/`-okdir`, `fd -x`/`-X`), the [indirection-wrapper floor](#fail-closed-behavior) clamps the decision back to `ask`.
766
+ So `find . -type f -exec rm {} +` still prompts even under `find *: allow`.
767
+ 3. **Chained commands resolve most-restrictive.**
768
+ `find . -name '*.log' && rm -f found.log` decomposes into `find …` and `rm …`; `rm` matches only `"*": "ask"`, and the most restrictive result governs the whole invocation, so the chain prompts.
769
+ 4. **Wrappers cannot ride the allowlist.**
770
+ `sudo grep …`, `env X=1 cat …`, `sh -c "…"`, and `eval "…"` are floored to `ask` (the [wrapper floors](#fail-closed-behavior)), so an allowed command cannot be smuggled past through a wrapper.
771
+
772
+ `git` is enumerated by read subcommand rather than a broad `git *`, because `git` has mutating subcommands (`commit`, `push`, `branch -D`, `remote add`, `config <key> <value>`).
773
+ Exact patterns like `git status` and `git branch` match only their literal form, so `git branch -D feature` falls through to `"*": "ask"`.
774
+ The `*`-suffixed git patterns (`git diff *`, `git log *`) are safe because those subcommands are read-only regardless of their arguments.
775
+
776
+ Commands that can originate a write are deliberately omitted: `echo` and `printf` are the usual content source for a `>` redirect, `tee` writes its input to a file, and `sort -o`, `sed -i`, and in-place `awk` redirects modify files directly.
777
+ Add them only if you understand that pairing them with `write: deny` and a strict `path` surface is what keeps them from writing.
778
+
642
779
  ### MCP Discovery Only
643
780
 
644
781
  ```jsonc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "20.4.1",
3
+ "version": "20.5.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -101,6 +101,42 @@
101
101
  }
102
102
  }
103
103
  ]
104
+ },
105
+ "shellTools": {
106
+ "type": "object",
107
+ "propertyNames": {
108
+ "type": "string",
109
+ "minLength": 1,
110
+ "description": "A non-bash tool name that carries shell semantics."
111
+ },
112
+ "additionalProperties": {
113
+ "type": "object",
114
+ "properties": {
115
+ "commandArgument": {
116
+ "type": "string",
117
+ "minLength": 1,
118
+ "description": "The name of the tool's input argument holding the shell command string (e.g. 'cmd')."
119
+ },
120
+ "workdirArgument": {
121
+ "description": "Optional name of the tool's input argument holding the working directory (e.g. 'workdir').",
122
+ "type": "string",
123
+ "minLength": 1
124
+ }
125
+ },
126
+ "required": ["commandArgument"],
127
+ "additionalProperties": false,
128
+ "description": "Maps one shell-aliased tool to the input arguments holding its command and (optionally) its working directory."
129
+ },
130
+ "description": "Maps non-bash tool names that carry shell semantics to the input arguments holding their command and working directory.",
131
+ "markdownDescription": "Records which non-`bash` tools carry shell semantics, mapping each tool name to the input argument holding its command (and optionally its working directory).\n\nUse this when an extension replaces the native `bash` tool under a different name — e.g. `@howaboua/pi-codex-conversion` registers `exec_command` with a `cmd` argument and an optional `workdir`. Recording the alias lets the permission system gate that tool through the same bash enforcement stack as native `bash` (command decomposition, wrapper flooring, path/external-directory token gates, and `bash:` rules).\n\nExample:\n\n```json\n\"shellTools\": {\n \"exec_command\": { \"commandArgument\": \"cmd\", \"workdirArgument\": \"workdir\" }\n}\n```\n\n**Merge order:** shallow-merge by tool name across global → project. A project entry overrides a specific tool's mapping on key collision but never drops a global entry.",
132
+ "examples": [
133
+ {
134
+ "exec_command": {
135
+ "commandArgument": "cmd",
136
+ "workdirArgument": "workdir"
137
+ }
138
+ }
139
+ ]
104
140
  }
105
141
  },
106
142
  "additionalProperties": false,
@@ -9,7 +9,7 @@
9
9
  * which matches an active, specific (non-`*`) `path` deny/ask rule (#509).
10
10
  *
11
11
  * All three classifiers share the private `rejectNonPathToken` predicate that
12
- * captures the seven rejection cases common to them (the production clone this
12
+ * captures the six rejection cases common to them (the production clone this
13
13
  * module was extracted to eliminate).
14
14
  *
15
15
  * Both `classifyTokenAsPathCandidate` and `classifyTokenAsRuleCandidate` recognize
@@ -143,8 +143,11 @@ const REGEX_METACHAR_PATTERN = /\.\*|\.\+|\\\||\\\(|\\\)|\[.*?\]|\^\//;
143
143
  * filesystem path, regardless of which classifier is asking.
144
144
  *
145
145
  * Rejects: empty tokens, flags (leading `-`), env assignments (`FOO=/bar`),
146
- * URLs, `@scope/package` patterns, bare-slash tokens, and regex metacharacter
147
- * sequences.
146
+ * URLs, `@scope/package` patterns, and regex metacharacter sequences.
147
+ *
148
+ * A bare `/` (or `//`, `///`) is NOT rejected: it denotes the filesystem root,
149
+ * a deliberate external-directory access (`find /`, `ls /`), so it must reach
150
+ * the path surfaces like any other absolute token (#583).
148
151
  */
149
152
  function rejectNonPathToken(token: string): boolean {
150
153
  if (!token) return true;
@@ -163,10 +166,6 @@ function rejectNonPathToken(token: string): boolean {
163
166
  // since it looks like an absolute-rooted path, not an npm scope.
164
167
  if (token.startsWith("@") && !token.startsWith("@/")) return true;
165
168
 
166
- // Bare-slash tokens (/, //, ///) resolve to filesystem root and are never
167
- // meaningful path arguments in practice.
168
- if (/^\/+$/.test(token)) return true;
169
-
170
169
  if (REGEX_METACHAR_PATTERN.test(token)) return true;
171
170
 
172
171
  return false;
@@ -1,10 +1,10 @@
1
- import type { AccessIntent } from "./access-intent/access-intent";
2
- import { classifyToolKind } from "./access-intent/tool-kind";
3
- import { stripBashCommentLines } from "./bash-arity";
1
+ import { stripBashCommentLines } from "#src/bash-arity";
2
+ import type { PathNormalizer } from "#src/path-normalizer";
3
+ import { getNonEmptyString, toRecord } from "#src/value-guards";
4
+ import type { AccessIntent } from "./access-intent";
4
5
  import { createMcpPermissionTargets } from "./mcp-targets";
5
- import type { PathNormalizer } from "./path-normalizer";
6
6
  import { PATH_SURFACES } from "./path-surfaces";
7
- import { getNonEmptyString, toRecord } from "./value-guards";
7
+ import { classifyToolKind } from "./tool-kind";
8
8
 
9
9
  /**
10
10
  * Build the {@link AccessIntent} an external policy query (the `Symbol.for()`
@@ -1,4 +1,4 @@
1
- import { getNonEmptyString, toRecord } from "./value-guards";
1
+ import { getNonEmptyString, toRecord } from "#src/value-guards";
2
2
 
3
3
  /**
4
4
  * An ordered accumulator that owns the uniqueness invariant.
@@ -1,6 +1,6 @@
1
- import { classifyToolKind } from "./access-intent/tool-kind";
2
- import type { ToolAccessExtractorLookup } from "./tool-access-extractor-registry";
3
- import { getNonEmptyString, toRecord } from "./value-guards";
1
+ import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
2
+ import { getNonEmptyString, toRecord } from "#src/value-guards";
3
+ import { classifyToolKind } from "./tool-kind";
4
4
 
5
5
  export function getPathBearingToolPath(
6
6
  toolName: string,
@@ -1,4 +1,4 @@
1
- import { PATH_BEARING_TOOLS } from "#src/path-surfaces";
1
+ import { PATH_BEARING_TOOLS } from "./path-surfaces";
2
2
 
3
3
  /**
4
4
  * What a tool invocation accesses — decided once from the tool name at the
@@ -9,6 +9,7 @@ import {
9
9
  getProjectConfigPath,
10
10
  } from "./config-paths";
11
11
  import {
12
+ type ShellToolsConfig,
12
13
  type UnifiedPermissionConfig,
13
14
  unifiedConfigSchema,
14
15
  } from "./config-schema";
@@ -20,7 +21,7 @@ import { isDenyWithReason, isPermissionState } from "./types";
20
21
  // the single source of truth) and re-exported so existing importers keep their
21
22
  // import path. All fields are optional so partial configs merge before
22
23
  // defaults are applied downstream.
23
- export type { UnifiedPermissionConfig };
24
+ export type { ShellToolsConfig, UnifiedPermissionConfig };
24
25
 
25
26
  export interface UnifiedConfigLoadResult {
26
27
  config: UnifiedPermissionConfig;
@@ -228,6 +229,19 @@ export function mergeUnifiedConfigs(
228
229
  merged.piInfrastructureReadPaths = piInfrastructureReadPaths;
229
230
  }
230
231
 
232
+ // shellTools: shallow-merge by tool name so a project entry overrides a
233
+ // colliding tool's alias but never drops a global entry (a dropped alias is
234
+ // a silent enforcement regression).
235
+ const baseShell = base.shellTools;
236
+ const overrideShell = override.shellTools;
237
+ if (baseShell && overrideShell) {
238
+ merged.shellTools = { ...baseShell, ...overrideShell };
239
+ } else if (baseShell) {
240
+ merged.shellTools = baseShell;
241
+ } else if (overrideShell) {
242
+ merged.shellTools = overrideShell;
243
+ }
244
+
231
245
  // Permission: deep-shallow merge
232
246
  const basePerm = base.permission;
233
247
  const overridePerm = override.permission;
@@ -110,6 +110,41 @@ const permissionSchema = z
110
110
  ],
111
111
  });
112
112
 
113
+ const shellToolAliasSchema = z
114
+ .strictObject({
115
+ commandArgument: z.string().min(1).meta({
116
+ description:
117
+ "The name of the tool's input argument holding the shell command string (e.g. 'cmd').",
118
+ }),
119
+ workdirArgument: z.string().min(1).optional().meta({
120
+ description:
121
+ "Optional name of the tool's input argument holding the working directory (e.g. 'workdir').",
122
+ }),
123
+ })
124
+ .meta({
125
+ description:
126
+ "Maps one shell-aliased tool to the input arguments holding its command and (optionally) its working directory.",
127
+ });
128
+
129
+ const shellToolsSchema = z
130
+ .record(
131
+ z.string().min(1).meta({
132
+ description: "A non-bash tool name that carries shell semantics.",
133
+ }),
134
+ shellToolAliasSchema,
135
+ )
136
+ .meta({
137
+ description:
138
+ "Maps non-bash tool names that carry shell semantics to the input arguments holding their command and working directory.",
139
+ markdownDescription:
140
+ 'Records which non-`bash` tools carry shell semantics, mapping each tool name to the input argument holding its command (and optionally its working directory).\n\nUse this when an extension replaces the native `bash` tool under a different name — e.g. `@howaboua/pi-codex-conversion` registers `exec_command` with a `cmd` argument and an optional `workdir`. Recording the alias lets the permission system gate that tool through the same bash enforcement stack as native `bash` (command decomposition, wrapper flooring, path/external-directory token gates, and `bash:` rules).\n\nExample:\n\n```json\n"shellTools": {\n "exec_command": { "commandArgument": "cmd", "workdirArgument": "workdir" }\n}\n```\n\n**Merge order:** shallow-merge by tool name across global → project. A project entry overrides a specific tool\'s mapping on key collision but never drops a global entry.',
141
+ examples: [
142
+ {
143
+ exec_command: { commandArgument: "cmd", workdirArgument: "workdir" },
144
+ },
145
+ ],
146
+ });
147
+
113
148
  /**
114
149
  * The on-disk config file shape.
115
150
  *
@@ -165,6 +200,7 @@ export const unifiedConfigSchema = z
165
200
  default: [],
166
201
  }),
167
202
  permission: permissionSchema.optional(),
203
+ shellTools: shellToolsSchema.optional(),
168
204
  })
169
205
  .meta({
170
206
  title: "PI Permission System Configuration",
@@ -186,6 +222,9 @@ export type PatternValue = z.infer<typeof patternValueSchema>;
186
222
  /** The on-disk permission shape inside the `"permission"` key. */
187
223
  export type FlatPermissionConfig = z.infer<typeof permissionSchema>;
188
224
 
225
+ /** The `shellTools` map: tool name → shell-alias argument mapping. */
226
+ export type ShellToolsConfig = z.infer<typeof shellToolsSchema>;
227
+
189
228
  /** The raw config file shape after validation (all fields optional). */
190
229
  export type UnifiedPermissionConfig = z.infer<typeof unifiedConfigSchema>;
191
230
 
@@ -2,7 +2,10 @@ import { mkdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- import type { UnifiedPermissionConfig } from "./config-loader";
5
+ import type {
6
+ ShellToolsConfig,
7
+ UnifiedPermissionConfig,
8
+ } from "./config-loader";
6
9
 
7
10
  export const EXTENSION_ID = "pi-permission-system";
8
11
 
@@ -16,6 +19,8 @@ export interface PermissionSystemExtensionConfig {
16
19
  toolInputPreviewMaxLength?: number;
17
20
  /** Max length of inline pattern/path summaries (grep/find/ls) in permission prompts. Defaults to 80. */
18
21
  toolTextSummaryMaxLength?: number;
22
+ /** Non-bash tools that carry shell semantics, keyed by tool name. */
23
+ shellTools?: ShellToolsConfig;
19
24
  }
20
25
 
21
26
  export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
@@ -63,6 +68,9 @@ export function normalizePermissionSystemConfig(
63
68
  if (raw.toolTextSummaryMaxLength !== undefined) {
64
69
  result.toolTextSummaryMaxLength = raw.toolTextSummaryMaxLength;
65
70
  }
71
+ if (raw.shellTools !== undefined) {
72
+ result.shellTools = raw.shellTools;
73
+ }
66
74
  return result;
67
75
  }
68
76
 
@@ -1,9 +1,9 @@
1
+ import { getToolInputPath } from "#src/access-intent/tool-input-path";
1
2
  import type { PathNormalizer } from "#src/path-normalizer";
2
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
4
  import { SessionApproval } from "#src/session-approval";
4
5
  import { deriveApprovalPattern } from "#src/session-rules";
5
6
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
6
- import { getToolInputPath } from "#src/tool-input-path";
7
7
  import type { GateResult } from "./descriptor";
8
8
  import { formatExternalDirectoryAskPrompt } from "./external-directory-messages";
9
9
  import { resolveExternalDirectoryPolicy } from "./external-directory-policy";
@@ -1,9 +1,9 @@
1
+ import { getToolInputPath } from "#src/access-intent/tool-input-path";
1
2
  import type { PathNormalizer } from "#src/path-normalizer";
2
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
4
  import { SessionApproval } from "#src/session-approval";
4
5
  import { deriveApprovalPattern } from "#src/session-rules";
5
6
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
6
- import { getToolInputPath } from "#src/tool-input-path";
7
7
  import type { GateDescriptor, GateResult } from "./descriptor";
8
8
  import type { ToolCallContext } from "./types";
9
9
 
@@ -1,12 +1,12 @@
1
1
  import type { AccessPath } from "#src/access-intent/access-path";
2
2
  import { BashProgram } from "#src/access-intent/bash/program";
3
+ import { getPathBearingToolPath } from "#src/access-intent/tool-input-path";
3
4
  import { classifyToolKind } from "#src/access-intent/tool-kind";
4
5
  import type { PathNormalizer } from "#src/path-normalizer";
5
6
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
6
7
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
7
8
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
8
9
  import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
9
- import { getPathBearingToolPath } from "#src/tool-input-path";
10
10
  import {
11
11
  ToolPreviewFormatter,
12
12
  type ToolPreviewFormatterOptions,
@@ -1,10 +1,10 @@
1
1
  import type { AccessPath } from "#src/access-intent/access-path";
2
+ import { PATH_BEARING_TOOLS } from "#src/access-intent/path-surfaces";
3
+ import { getPathBearingToolPath } from "#src/access-intent/tool-input-path";
2
4
  import { classifyToolKind } from "#src/access-intent/tool-kind";
3
- import { PATH_BEARING_TOOLS } from "#src/path-surfaces";
4
5
  import { suggestSessionPattern } from "#src/pattern-suggest";
5
6
  import { formatAskPrompt } from "#src/permission-prompts";
6
7
  import { SessionApproval } from "#src/session-approval";
7
- import { getPathBearingToolPath } from "#src/tool-input-path";
8
8
  import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
9
9
  import type { PermissionCheckResult } from "#src/types";
10
10
  import type { GateDescriptor } from "./descriptor";
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { getAgentDir, getPackageDir } from "@earendil-works/pi-coding-agent";
3
3
  import { warmBashParser } from "./access-intent/bash/parser";
4
+ import { buildAccessIntentForSurface } from "./access-intent/input-normalizer";
4
5
  import { AuthorizerSelection } from "./authority/authorizer-selection";
5
6
  import {
6
7
  ForwardedRequestServer,
@@ -29,7 +30,6 @@ import { GateRunner } from "./handlers/gates/runner";
29
30
  import { SkillInputGatePipeline } from "./handlers/gates/skill-input-gate-pipeline";
30
31
  import { ToolCallGatePipeline } from "./handlers/gates/tool-call-gate-pipeline";
31
32
  import { createFailClosedToolCall } from "./handlers/tool-call-boundary";
32
- import { buildAccessIntentForSurface } from "./input-normalizer";
33
33
  import { pathFlavorForPlatform } from "./path/path-flavor";
34
34
  import { PermissionManager } from "./permission-manager";
35
35
  import { PermissionResolver } from "./permission-resolver";
@@ -1,8 +1,7 @@
1
1
  import { join } from "node:path";
2
-
2
+ import { READ_ONLY_PATH_BEARING_TOOLS } from "#src/access-intent/path-surfaces";
3
3
  import { expandHomePath } from "#src/expand-home";
4
4
  import type { PathFlavor } from "#src/path/path-flavor";
5
- import { READ_ONLY_PATH_BEARING_TOOLS } from "#src/path-surfaces";
6
5
  import { wildcardMatch } from "#src/wildcard-matcher";
7
6
 
8
7
  function containsGlobChars(value: string): boolean {
@@ -1,5 +1,5 @@
1
+ import { PATH_BEARING_TOOLS } from "./access-intent/path-surfaces";
1
2
  import { prefix, stripBashCommentLines } from "./bash-arity";
2
- import { PATH_BEARING_TOOLS } from "./path-surfaces";
3
3
  import { deriveApprovalPattern } from "./session-rules";
4
4
 
5
5
  /** The suggestion returned for a "Yes, for this session" dialog option. */
@@ -1,15 +1,15 @@
1
1
  import { join } from "node:path";
2
2
  import type { ResolvedAccessIntent } from "./access-intent/access-intent";
3
+ import { normalizeInput } from "./access-intent/input-normalizer";
4
+ import { PATH_SURFACES } from "./access-intent/path-surfaces";
3
5
  import { classifyToolKind } from "./access-intent/tool-kind";
4
6
  import {
5
7
  getGlobalConfigPath,
6
8
  getProjectAgentsDir,
7
9
  getProjectConfigPath,
8
10
  } from "./config-paths";
9
- import { normalizeInput } from "./input-normalizer";
10
11
  import { normalizeFlatConfig } from "./normalize";
11
12
  import { type PathFlavor, posixPathFlavor } from "./path/path-flavor";
12
- import { PATH_SURFACES } from "./path-surfaces";
13
13
  import {
14
14
  FilePolicyLoader,
15
15
  type PolicyLoader,
@@ -1,6 +1,6 @@
1
1
  import type { AccessIntent } from "./access-intent/access-intent";
2
+ import { buildAccessIntentForSurface } from "./access-intent/input-normalizer";
2
3
  import { resolveBashAdvisoryCheck } from "./bash-advisory-check";
3
- import { buildAccessIntentForSurface } from "./input-normalizer";
4
4
  import type { PathNormalizer } from "./path-normalizer";
5
5
  import type { PermissionsService } from "./service";
6
6
  import type {
package/src/rule.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { PathFlavor } from "#src/path/path-flavor";
2
2
 
3
- import { PATH_SURFACES } from "./path-surfaces";
3
+ import { PATH_SURFACES } from "./access-intent/path-surfaces";
4
4
  import type { PermissionState } from "./types";
5
5
  import { type WildcardMatchOptions, wildcardMatch } from "./wildcard-matcher";
6
6