@gotgenes/pi-permission-system 29.2.0 → 29.3.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 +16 -0
- package/docs/configuration.md +2 -0
- package/package.json +6 -6
- package/schemas/permissions.schema.json +73 -1
- package/src/config-schema.ts +89 -43
- package/src/config-store.ts +0 -2
- package/src/normalize.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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
|
+
## [29.3.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v29.2.0...pi-permission-system-v29.3.0) (2026-09-02)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** name and document the well-known permission surfaces ([#808](https://github.com/gotgenes/pi-packages/issues/808)) ([95ef9ab](https://github.com/gotgenes/pi-packages/commit/95ef9ab9c00ee09d4dda820d43abc7fe7e5ff1b2)), closes [#808](https://github.com/gotgenes/pi-packages/issues/808)
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* **pi-permission-system:** document the named permission surfaces ([#808](https://github.com/gotgenes/pi-packages/issues/808)) ([03c6a52](https://github.com/gotgenes/pi-packages/commit/03c6a5228aa050ac940d851e6c219e62062acbd6))
|
|
18
|
+
|
|
19
|
+
### Miscellaneous Chores
|
|
20
|
+
|
|
21
|
+
* upgrade lint tooling and pin rumdl below the MD013 reflow regression ([0ee1ad8](https://github.com/gotgenes/pi-packages/commit/0ee1ad886815e6e50d99f66050fbbcf5a9f0319a)), closes [#866](https://github.com/gotgenes/pi-packages/issues/866)
|
|
22
|
+
* upgrade fallow to 3.22.0 ([1b2a562](https://github.com/gotgenes/pi-packages/commit/1b2a5620bf5d7f1014676b47e58df8b43ff00496)), closes [#866](https://github.com/gotgenes/pi-packages/issues/866)
|
|
23
|
+
|
|
8
24
|
## [29.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v29.1.0...pi-permission-system-v29.2.0) (2026-09-01)
|
|
9
25
|
|
|
10
26
|
|
package/docs/configuration.md
CHANGED
|
@@ -1204,4 +1204,6 @@ npx --yes ajv-cli@5 validate \
|
|
|
1204
1204
|
"$schema": "https://raw.githubusercontent.com/gotgenes/pi-packages/main/packages/pi-permission-system/schemas/permissions.schema.json"
|
|
1205
1205
|
```
|
|
1206
1206
|
|
|
1207
|
+
The well-known surface keys — `*`, `path`, `external_directory`, `bash`, `mcp`, `skill`, and the four directional keys — are named properties in the schema, so an editor completes them and shows each key's own documentation on hover; any other registered tool name still validates as a surface.
|
|
1208
|
+
|
|
1207
1209
|
The schema is generated from the extension's zod source of truth (`src/config-schema.ts`); regenerate it with `pnpm run gen:schema` after changing the config shape.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-permission-system",
|
|
3
|
-
"version": "29.
|
|
3
|
+
"version": "29.3.0",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -67,15 +67,15 @@
|
|
|
67
67
|
"@earendil-works/pi-tui": ">=0.79.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@biomejs/biome": "^2.
|
|
70
|
+
"@biomejs/biome": "^2.5.11",
|
|
71
71
|
"@earendil-works/pi-coding-agent": "0.79.1",
|
|
72
72
|
"@earendil-works/pi-tui": "0.79.1",
|
|
73
73
|
"@types/node": "^22.15.3",
|
|
74
|
-
"rollup": "^4.
|
|
75
|
-
"rollup-plugin-dts": "^6.
|
|
76
|
-
"rumdl": "
|
|
74
|
+
"rollup": "^4.63.1",
|
|
75
|
+
"rollup-plugin-dts": "^6.5.1",
|
|
76
|
+
"rumdl": "0.2.24",
|
|
77
77
|
"typescript": "^6.0.3",
|
|
78
|
-
"vitest": "^4.1.
|
|
78
|
+
"vitest": "^4.1.11"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"tree-sitter-bash": "^0.25.1",
|
|
@@ -102,6 +102,30 @@
|
|
|
102
102
|
"permission": {
|
|
103
103
|
"type": "object",
|
|
104
104
|
"properties": {
|
|
105
|
+
"*": {
|
|
106
|
+
"description": "Universal fallback — the action used when no surface-specific rule matches. Omitted, it defaults to ask.",
|
|
107
|
+
"markdownDescription": "Universal fallback — the action used when **no** surface-specific rule matches.\n\n`{ \"*\": \"ask\" }` is the least-privilege posture, and is what an omitted `\"*\"` means anyway. It replaces `defaultPolicy.tools` from the legacy config format.\n\nA surface-specific rule always beats it, whatever the key order in the file.",
|
|
108
|
+
"anyOf": [
|
|
109
|
+
{
|
|
110
|
+
"$ref": "#/$defs/permissionState"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"$ref": "#/$defs/permissionMap"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"path": {
|
|
118
|
+
"description": "Cross-cutting gate for file access by path pattern, across every path-aware tool. Sugar for both directions.",
|
|
119
|
+
"markdownDescription": "Cross-cutting gate that applies to **all** file access: Pi tools, bash commands, MCP calls (via `input.arguments.path`), and extension tools (via `input.path` or a registered access extractor).\n\nA `path` deny cannot be overridden by a per-tool allow. Use it to protect sensitive files (`.env`, `~/.ssh/*`) from every path-aware tool at once.\n\nThis bare key is **sugar**: it expands at load into `path_read` and `path_write`, its entries placed first, so an explicit directional entry always has the final say whatever the key order in the file.",
|
|
120
|
+
"anyOf": [
|
|
121
|
+
{
|
|
122
|
+
"$ref": "#/$defs/permissionState"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"$ref": "#/$defs/permissionMap"
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
105
129
|
"path_read": {
|
|
106
130
|
"description": "Cross-cutting gate for reading a file, by path pattern. The useful directional grant.",
|
|
107
131
|
"markdownDescription": "Cross-cutting gate for **reading** a file, matched by path pattern across all path-aware tools.\n\nThis is the directional key worth granting: `\"path_read\": { \"~/dev/*\": \"allow\" }` permits reads without permitting writes.\n\nA bare `\"path\"` key is sugar that expands into this key **and** `path_write`, with its entries placed first — so an explicit `path_read` entry always has the final say, whatever the key order in the file.",
|
|
@@ -126,6 +150,18 @@
|
|
|
126
150
|
}
|
|
127
151
|
]
|
|
128
152
|
},
|
|
153
|
+
"external_directory": {
|
|
154
|
+
"description": "Boundary gate for access outside the session working directory. Sugar for both directions.",
|
|
155
|
+
"markdownDescription": "Boundary gate for access **outside** the session working directory.\n\nGive it a pattern map to allow specific outside-CWD directories without opening all external access — e.g. `{ \"*\": \"ask\", \"~/.cargo/registry/*\": \"allow\" }` to silence repeated prompts on a local cache. The trailing `*` is greedy and crosses subdirectory boundaries; a bare `~/.cargo/registry` matches only the directory entry itself.\n\nBecause layers compose with most-restrictive-wins, a `path` allow cannot loosen an `external_directory: ask` boundary — allow outside-CWD directories here, not on `path`.\n\nThis bare key is **sugar**: it expands at load into `external_directory_read` and `external_directory_write`, its entries placed first.",
|
|
156
|
+
"anyOf": [
|
|
157
|
+
{
|
|
158
|
+
"$ref": "#/$defs/permissionState"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"$ref": "#/$defs/permissionMap"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
129
165
|
"external_directory_read": {
|
|
130
166
|
"description": "Boundary gate for reading outside the working directory. The relief most asks want.",
|
|
131
167
|
"markdownDescription": "Boundary gate for **reading** a path outside the session working directory.\n\nThe one-line grant for an external root: `\"external_directory_read\": { \"~/dev/*\": \"allow\" }` silences repeated read prompts on a directory outside the tree while a write to the same path still prompts. No parallel `path_read` entry is needed.",
|
|
@@ -149,6 +185,42 @@
|
|
|
149
185
|
"$ref": "#/$defs/permissionMap"
|
|
150
186
|
}
|
|
151
187
|
]
|
|
188
|
+
},
|
|
189
|
+
"bash": {
|
|
190
|
+
"description": "Shell command execution, matched per top-level command in a chain. Most restrictive wins.",
|
|
191
|
+
"markdownDescription": "Shell command execution, matched by **command pattern**.\n\nA chain (`&&`, `||`, `;`, `|`, newline) is split into its top-level commands and each is matched independently, most-restrictive-wins — so `cd /repo && npm install x` is denied when `npm *` is. A command nested in a substitution, process substitution, or subshell is matched too, since it really runs.\n\nA leading env-var assignment is stripped before matching (`AWS_PROFILE=prod aws …` matches `aws *`), and a pattern ending in ` *` (space + wildcard) also matches the bare command (`git *` matches `git`). A pattern containing a chain operator never matches — write one pattern per command.\n\nA shell wrapper (`bash -c`, `eval`, `sudo`, `xargs`) is floored from `allow` to `ask`, so an opaque payload cannot ride a permissive rule.",
|
|
192
|
+
"anyOf": [
|
|
193
|
+
{
|
|
194
|
+
"$ref": "#/$defs/permissionState"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"$ref": "#/$defs/permissionMap"
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
"mcp": {
|
|
202
|
+
"description": "Registered MCP proxy tools, matched against targets derived from the tool input.",
|
|
203
|
+
"markdownDescription": "Registered MCP proxy tools, matched against targets derived from the tool input: a baseline op (`mcp_status`, `mcp_list`, `mcp_search`, `mcp_describe`, `mcp_connect`), a server name (`myServer`), a server/tool combination (`myServer:search`, `myServer_search`), or the generic `mcp_call`.\n\nBaseline discovery targets auto-allow whenever any explicit `mcp` allow rule exists.",
|
|
204
|
+
"anyOf": [
|
|
205
|
+
{
|
|
206
|
+
"$ref": "#/$defs/permissionState"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"$ref": "#/$defs/permissionMap"
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
"skill": {
|
|
214
|
+
"description": "Skill invocation, matched by skill name. The surface is `skill`, not `skills`.",
|
|
215
|
+
"markdownDescription": "Skill invocation, matched by skill name — the surface is `skill`, not `skills`.\n\nWildcards behave as everywhere else: `{ \"*\": \"ask\", \"dangerous-*\": \"deny\", \"librarian\": \"allow\" }`.",
|
|
216
|
+
"anyOf": [
|
|
217
|
+
{
|
|
218
|
+
"$ref": "#/$defs/permissionState"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"$ref": "#/$defs/permissionMap"
|
|
222
|
+
}
|
|
223
|
+
]
|
|
152
224
|
}
|
|
153
225
|
},
|
|
154
226
|
"additionalProperties": {
|
|
@@ -162,7 +234,7 @@
|
|
|
162
234
|
]
|
|
163
235
|
},
|
|
164
236
|
"description": "Flat permission policy. Each key is a surface name; values are a PermissionState string (catch-all) or a pattern→action map.",
|
|
165
|
-
"markdownDescription": "Flat permission policy.\n\nEach top-level key is a surface
|
|
237
|
+
"markdownDescription": "Flat permission policy.\n\nEach top-level key is a surface: the `\"*\"` fallback, a well-known surface documented below, or any registered tool name.\n\nA **string** value is shorthand for `{ \"*\": action }` (a surface-level catch-all).\nAn **object** value maps wildcard patterns to actions — last matching pattern wins.\n\nFor built-in file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`), patterns are matched against the file path from `input.path`. For example, `\"read\": { \"*\": \"allow\", \"*.env\": \"deny\" }` allows reads but denies `.env` files.\n\nWhen Pi's current working directory is known, relative path inputs also match their cwd-normalized absolute form, so `src/App.jsx` can match both `src/*` and `/workspace/project/*`. Bash path tokens use the effective directory after literal `cd` commands for this matching; non-literal `cd \"$DIR\"` style commands remain conservative.\n\n**Merge order (lowest → highest precedence):** global → project → per-agent frontmatter.",
|
|
166
238
|
"examples": [
|
|
167
239
|
{
|
|
168
240
|
"*": "ask",
|
package/src/config-schema.ts
CHANGED
|
@@ -79,57 +79,103 @@ const surfaceValueSchema = z.union([
|
|
|
79
79
|
]);
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* The
|
|
83
|
-
* (ADR 0013 §3), named so editors offer autocomplete and hover documentation.
|
|
82
|
+
* The legal spellings of a directional surface key (ADR 0013 §3).
|
|
84
83
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* This is the loader's allowlist — {@link rejectUnusableSurfaceKeys} rejects a
|
|
85
|
+
* key shaped like a directional surface that is not one of these — and the
|
|
86
|
+
* schema documents exactly these four as named properties. Nothing structural
|
|
87
|
+
* holds the two halves together, so a test in `config-schema.test.ts` does.
|
|
88
88
|
*/
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
external_directory_write: {
|
|
112
|
-
description: "Boundary gate for writing outside the working directory.",
|
|
113
|
-
markdownDescription:
|
|
114
|
-
'Boundary gate for **writing** to a path outside the session working directory.\n\nA bare `"external_directory"` key is sugar that expands into this key and `external_directory_read`; write this one only to give the two directions different answers.',
|
|
115
|
-
},
|
|
116
|
-
};
|
|
89
|
+
const DIRECTIONAL_SURFACE_KEYS = [
|
|
90
|
+
"path_read",
|
|
91
|
+
"path_write",
|
|
92
|
+
"external_directory_read",
|
|
93
|
+
"external_directory_write",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* One documented, optional property for a well-known permission surface.
|
|
98
|
+
*
|
|
99
|
+
* A named property is what an editor completes and hovers; the enclosing
|
|
100
|
+
* object's `.catchall(...)` is what keeps every other tool name valid. Each
|
|
101
|
+
* surface's prose lives here rather than on the object, so a reader gets the
|
|
102
|
+
* key under their cursor and not all nine of its neighbours.
|
|
103
|
+
*/
|
|
104
|
+
function surfaceProperty(meta: {
|
|
105
|
+
description: string;
|
|
106
|
+
markdownDescription: string;
|
|
107
|
+
}) {
|
|
108
|
+
return surfaceValueSchema.optional().meta(meta);
|
|
109
|
+
}
|
|
117
110
|
|
|
118
111
|
const permissionSchema = z
|
|
119
|
-
.object(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
),
|
|
126
|
-
|
|
112
|
+
.object({
|
|
113
|
+
"*": surfaceProperty({
|
|
114
|
+
description:
|
|
115
|
+
"Universal fallback — the action used when no surface-specific rule matches. Omitted, it defaults to ask.",
|
|
116
|
+
markdownDescription:
|
|
117
|
+
'Universal fallback — the action used when **no** surface-specific rule matches.\n\n`{ "*": "ask" }` is the least-privilege posture, and is what an omitted `"*"` means anyway. It replaces `defaultPolicy.tools` from the legacy config format.\n\nA surface-specific rule always beats it, whatever the key order in the file.',
|
|
118
|
+
}),
|
|
119
|
+
path: surfaceProperty({
|
|
120
|
+
description:
|
|
121
|
+
"Cross-cutting gate for file access by path pattern, across every path-aware tool. Sugar for both directions.",
|
|
122
|
+
markdownDescription:
|
|
123
|
+
"Cross-cutting gate that applies to **all** file access: Pi tools, bash commands, MCP calls (via `input.arguments.path`), and extension tools (via `input.path` or a registered access extractor).\n\nA `path` deny cannot be overridden by a per-tool allow. Use it to protect sensitive files (`.env`, `~/.ssh/*`) from every path-aware tool at once.\n\nThis bare key is **sugar**: it expands at load into `path_read` and `path_write`, its entries placed first, so an explicit directional entry always has the final say whatever the key order in the file.",
|
|
124
|
+
}),
|
|
125
|
+
path_read: surfaceProperty({
|
|
126
|
+
description:
|
|
127
|
+
"Cross-cutting gate for reading a file, by path pattern. The useful directional grant.",
|
|
128
|
+
markdownDescription:
|
|
129
|
+
'Cross-cutting gate for **reading** a file, matched by path pattern across all path-aware tools.\n\nThis is the directional key worth granting: `"path_read": { "~/dev/*": "allow" }` permits reads without permitting writes.\n\nA bare `"path"` key is sugar that expands into this key **and** `path_write`, with its entries placed first — so an explicit `path_read` entry always has the final say, whatever the key order in the file.',
|
|
130
|
+
}),
|
|
131
|
+
path_write: surfaceProperty({
|
|
132
|
+
description:
|
|
133
|
+
"Cross-cutting gate for writing a file, by path pattern. Earns its keep as a restriction.",
|
|
134
|
+
markdownDescription:
|
|
135
|
+
'Cross-cutting gate for **writing** a file, matched by path pattern across all path-aware tools.\n\nThis key earns its keep as a *restriction* rather than a grant: `"path_write": { "*": "deny" }` is a coherent read-only-agent posture. A `"path_write": "allow"` on its own does not silence an `edit`, which also reads — grant `path_read` too, or use the bare `"path"` key.',
|
|
136
|
+
}),
|
|
137
|
+
external_directory: surfaceProperty({
|
|
138
|
+
description:
|
|
139
|
+
"Boundary gate for access outside the session working directory. Sugar for both directions.",
|
|
140
|
+
markdownDescription:
|
|
141
|
+
'Boundary gate for access **outside** the session working directory.\n\nGive it a pattern map to allow specific outside-CWD directories without opening all external access — e.g. `{ "*": "ask", "~/.cargo/registry/*": "allow" }` to silence repeated prompts on a local cache. The trailing `*` is greedy and crosses subdirectory boundaries; a bare `~/.cargo/registry` matches only the directory entry itself.\n\nBecause layers compose with most-restrictive-wins, a `path` allow cannot loosen an `external_directory: ask` boundary — allow outside-CWD directories here, not on `path`.\n\nThis bare key is **sugar**: it expands at load into `external_directory_read` and `external_directory_write`, its entries placed first.',
|
|
142
|
+
}),
|
|
143
|
+
external_directory_read: surfaceProperty({
|
|
144
|
+
description:
|
|
145
|
+
"Boundary gate for reading outside the working directory. The relief most asks want.",
|
|
146
|
+
markdownDescription:
|
|
147
|
+
'Boundary gate for **reading** a path outside the session working directory.\n\nThe one-line grant for an external root: `"external_directory_read": { "~/dev/*": "allow" }` silences repeated read prompts on a directory outside the tree while a write to the same path still prompts. No parallel `path_read` entry is needed.',
|
|
148
|
+
}),
|
|
149
|
+
external_directory_write: surfaceProperty({
|
|
150
|
+
description: "Boundary gate for writing outside the working directory.",
|
|
151
|
+
markdownDescription:
|
|
152
|
+
'Boundary gate for **writing** to a path outside the session working directory.\n\nA bare `"external_directory"` key is sugar that expands into this key and `external_directory_read`; write this one only to give the two directions different answers.',
|
|
153
|
+
}),
|
|
154
|
+
bash: surfaceProperty({
|
|
155
|
+
description:
|
|
156
|
+
"Shell command execution, matched per top-level command in a chain. Most restrictive wins.",
|
|
157
|
+
markdownDescription:
|
|
158
|
+
"Shell command execution, matched by **command pattern**.\n\nA chain (`&&`, `||`, `;`, `|`, newline) is split into its top-level commands and each is matched independently, most-restrictive-wins — so `cd /repo && npm install x` is denied when `npm *` is. A command nested in a substitution, process substitution, or subshell is matched too, since it really runs.\n\nA leading env-var assignment is stripped before matching (`AWS_PROFILE=prod aws …` matches `aws *`), and a pattern ending in ` *` (space + wildcard) also matches the bare command (`git *` matches `git`). A pattern containing a chain operator never matches — write one pattern per command.\n\nA shell wrapper (`bash -c`, `eval`, `sudo`, `xargs`) is floored from `allow` to `ask`, so an opaque payload cannot ride a permissive rule.",
|
|
159
|
+
}),
|
|
160
|
+
mcp: surfaceProperty({
|
|
161
|
+
description:
|
|
162
|
+
"Registered MCP proxy tools, matched against targets derived from the tool input.",
|
|
163
|
+
markdownDescription:
|
|
164
|
+
"Registered MCP proxy tools, matched against targets derived from the tool input: a baseline op (`mcp_status`, `mcp_list`, `mcp_search`, `mcp_describe`, `mcp_connect`), a server name (`myServer`), a server/tool combination (`myServer:search`, `myServer_search`), or the generic `mcp_call`.\n\nBaseline discovery targets auto-allow whenever any explicit `mcp` allow rule exists.",
|
|
165
|
+
}),
|
|
166
|
+
skill: surfaceProperty({
|
|
167
|
+
description:
|
|
168
|
+
"Skill invocation, matched by skill name. The surface is `skill`, not `skills`.",
|
|
169
|
+
markdownDescription:
|
|
170
|
+
'Skill invocation, matched by skill name — the surface is `skill`, not `skills`.\n\nWildcards behave as everywhere else: `{ "*": "ask", "dangerous-*": "deny", "librarian": "allow" }`.',
|
|
171
|
+
}),
|
|
172
|
+
})
|
|
127
173
|
.catchall(surfaceValueSchema)
|
|
128
174
|
.meta({
|
|
129
175
|
description:
|
|
130
176
|
"Flat permission policy. Each key is a surface name; values are a PermissionState string (catch-all) or a pattern→action map.",
|
|
131
177
|
markdownDescription:
|
|
132
|
-
'Flat permission policy.\n\nEach top-level key is a surface
|
|
178
|
+
'Flat permission policy.\n\nEach top-level key is a surface: the `"*"` fallback, a well-known surface documented below, or any registered tool name.\n\nA **string** value is shorthand for `{ "*": action }` (a surface-level catch-all).\nAn **object** value maps wildcard patterns to actions — last matching pattern wins.\n\nFor built-in file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`), patterns are matched against the file path from `input.path`. For example, `"read": { "*": "allow", "*.env": "deny" }` allows reads but denies `.env` files.\n\nWhen Pi\'s current working directory is known, relative path inputs also match their cwd-normalized absolute form, so `src/App.jsx` can match both `src/*` and `/workspace/project/*`. Bash path tokens use the effective directory after literal `cd` commands for this matching; non-literal `cd "$DIR"` style commands remain conservative.\n\n**Merge order (lowest → highest precedence):** global → project → per-agent frontmatter.',
|
|
133
179
|
examples: [
|
|
134
180
|
{
|
|
135
181
|
"*": "ask",
|
|
@@ -176,7 +222,7 @@ function rejectUnusableSurfaceKeys(
|
|
|
176
222
|
permission: Record<string, unknown>,
|
|
177
223
|
ctx: z.core.$RefinementCtx,
|
|
178
224
|
): void {
|
|
179
|
-
const legalDirectionalKeys =
|
|
225
|
+
const legalDirectionalKeys: readonly string[] = DIRECTIONAL_SURFACE_KEYS;
|
|
180
226
|
for (const key of Object.keys(permission)) {
|
|
181
227
|
if (key === "") {
|
|
182
228
|
ctx.addIssue({
|
package/src/config-store.ts
CHANGED
|
@@ -140,8 +140,6 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
|
|
|
140
140
|
*
|
|
141
141
|
* Equivalent to `saveExtensionConfig(runtime, next, ctx)`.
|
|
142
142
|
*/
|
|
143
|
-
// Called via the CommandConfigStore interface from config-modal.ts — fallow cannot trace through interfaces.
|
|
144
|
-
// fallow-ignore-next-line unused-class-member
|
|
145
143
|
save(
|
|
146
144
|
next: PermissionSystemExtensionConfig,
|
|
147
145
|
ctx: ExtensionCommandContext,
|
package/src/normalize.ts
CHANGED
|
@@ -35,6 +35,11 @@ export function expandDirectionalSugar(
|
|
|
35
35
|
const expanded: FlatPermissionConfig = {};
|
|
36
36
|
for (const [surface, value] of Object.entries(permission)) {
|
|
37
37
|
// A key present with an explicit `undefined` value carries no rules.
|
|
38
|
+
// `Object.entries` resolves to the catchall's non-optional value type, so
|
|
39
|
+
// the type cannot see this case even though a named optional surface
|
|
40
|
+
// admits it; dropping the guard expands `{ path: undefined }` into two
|
|
41
|
+
// empty directional surfaces. Pinned in `normalize.test.ts`.
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime-reachable; see above
|
|
38
43
|
if (value === undefined) continue;
|
|
39
44
|
const members = surfaceFamilyMembers(surface);
|
|
40
45
|
if (members === null) {
|