@gotgenes/pi-permission-system 20.7.3 → 20.9.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 +35 -0
- package/README.md +3 -0
- package/config/config.example.json +2 -0
- package/dist/public.d.ts +212 -75
- package/docs/configuration.md +45 -9
- package/package.json +1 -1
- package/schemas/permissions.schema.json +10 -0
- package/src/access-intent/input-normalizer.ts +34 -1
- package/src/authority/approval-escalator.ts +53 -23
- package/src/authority/authorizer-chain.ts +60 -0
- package/src/authority/authorizer-registry.ts +69 -0
- package/src/authority/authorizer-selection.ts +54 -8
- package/src/authority/authorizer.ts +31 -4
- package/src/authority/delegation-envelope.ts +52 -0
- package/src/authority/denying-authorizer.ts +2 -2
- package/src/authority/forwarded-request-server.ts +22 -32
- package/src/authority/forwarder-context.ts +7 -0
- package/src/authority/forwarding-io.ts +61 -0
- package/src/authority/local-user-authorizer.ts +2 -2
- package/src/authority/permission-forwarding.ts +46 -0
- package/src/authority/permission-prompter.ts +16 -5
- package/src/config-loader.ts +5 -4
- package/src/config-schema.ts +7 -0
- package/src/extension-config.ts +5 -0
- package/src/handlers/gates/bash-external-directory.ts +6 -0
- package/src/handlers/gates/bash-path.ts +2 -0
- package/src/handlers/gates/external-directory.ts +2 -0
- package/src/handlers/gates/helpers.ts +33 -0
- package/src/handlers/gates/path.ts +2 -0
- package/src/handlers/gates/skill-input.ts +2 -0
- package/src/handlers/gates/skill-read.ts +2 -0
- package/src/handlers/gates/tool.ts +19 -6
- package/src/index.ts +26 -13
- package/src/permission-resolver.ts +17 -2
- package/src/permissions-service.ts +10 -0
- package/src/service.ts +52 -14
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ 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.9.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.8.0...pi-permission-system-v20.9.0) (2026-07-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** add authorizerChain config field ([#599](https://github.com/gotgenes/pi-packages/issues/599)) ([6c0bb72](https://github.com/gotgenes/pi-packages/commit/6c0bb72680d864b70e8fac5bb5c480b9a96751f8))
|
|
14
|
+
* **pi-permission-system:** add registerAuthorizer cross-extension seam ([#599](https://github.com/gotgenes/pi-packages/issues/599)) ([ea60900](https://github.com/gotgenes/pi-packages/commit/ea60900439d68ccc028ba75c3e432945e0ac3b72))
|
|
15
|
+
* **pi-permission-system:** cap link verdicts with the delegation envelope ([#599](https://github.com/gotgenes/pi-packages/issues/599)) ([28733fc](https://github.com/gotgenes/pi-packages/commit/28733fca298f1fea1f7a810b61728f9c96bf225f))
|
|
16
|
+
* **pi-permission-system:** inject a session-scoped PermissionQuery into chain links ([#599](https://github.com/gotgenes/pi-packages/issues/599)) ([29452ff](https://github.com/gotgenes/pi-packages/commit/29452ff8cbec6abc431a318b7937843ecefb724d))
|
|
17
|
+
* **pi-permission-system:** resolve the configured authorizer chain ([#599](https://github.com/gotgenes/pi-packages/issues/599)) ([fb366d9](https://github.com/gotgenes/pi-packages/commit/fb366d9aed47ab439ffc033e4967a4285d37fe8a))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
* **pi-permission-system:** document registerAuthorizer + authorizerChain and mark Phase 12 Step 5 complete ([#599](https://github.com/gotgenes/pi-packages/issues/599)) ([1d6b228](https://github.com/gotgenes/pi-packages/commit/1d6b22889296b3277a118cc8168486664c691305))
|
|
23
|
+
|
|
24
|
+
## [20.8.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.7.3...pi-permission-system-v20.8.0) (2026-07-18)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* **pi-permission-system:** accept pre-fixed path-values intents for forwarded serving ([ab60874](https://github.com/gotgenes/pi-packages/commit/ab6087464c7719c01bd7f2a15fd4c56a1a7e9f93))
|
|
30
|
+
* **pi-permission-system:** declare ForwardedAccessIntent wire schema with tolerant read ([#596](https://github.com/gotgenes/pi-packages/issues/596)) ([66ddbef](https://github.com/gotgenes/pi-packages/commit/66ddbef74e3fa6cd41d17358a0b817ae91d49ed1))
|
|
31
|
+
* **pi-permission-system:** emit access-intent facts from the bash path gates ([#596](https://github.com/gotgenes/pi-packages/issues/596)) ([1a0e6de](https://github.com/gotgenes/pi-packages/commit/1a0e6de3fdf411ab576d0c9aa5571ff23cb5f179))
|
|
32
|
+
* **pi-permission-system:** emit access-intent facts from the per-tool gate ([#596](https://github.com/gotgenes/pi-packages/issues/596)) ([2e17256](https://github.com/gotgenes/pi-packages/commit/2e1725695442b10073ea627ce9699ef363e4c77c))
|
|
33
|
+
* **pi-permission-system:** emit access-intent facts from the skill gates ([#596](https://github.com/gotgenes/pi-packages/issues/596)) ([5a20033](https://github.com/gotgenes/pi-packages/commit/5a20033fca9f33dee576c5d39c0c04e468d2d031))
|
|
34
|
+
* **pi-permission-system:** emit access-intent facts from the tool path gates ([#596](https://github.com/gotgenes/pi-packages/issues/596)) ([93a3398](https://github.com/gotgenes/pi-packages/commit/93a3398245b06fe6cb80f841d2a2ca5dc8172de5))
|
|
35
|
+
* **pi-permission-system:** serialize the child-fixed access intent onto the forwarded request ([#596](https://github.com/gotgenes/pi-packages/issues/596)) ([5234614](https://github.com/gotgenes/pi-packages/commit/52346148c41a097719558175582b57d12e7832d0))
|
|
36
|
+
* **pi-permission-system:** serving resolves the forwarded access intent at gate parity ([#597](https://github.com/gotgenes/pi-packages/issues/597)) ([a8fe815](https://github.com/gotgenes/pi-packages/commit/a8fe815bd70e3243553b7cac6841ec270e20bf23))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
* **pi-permission-system:** fix stale ServingPolicy doc comment ([#597](https://github.com/gotgenes/pi-packages/issues/597)) ([64b1b8e](https://github.com/gotgenes/pi-packages/commit/64b1b8e13a8d22ee4f088bef7d54024a81d9a437))
|
|
42
|
+
|
|
8
43
|
## [20.7.3](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.7.2...pi-permission-system-v20.7.3) (2026-07-15)
|
|
9
44
|
|
|
10
45
|
|
package/README.md
CHANGED
|
@@ -112,6 +112,9 @@ Within a surface map like `bash` or `mcp`, **last matching rule wins** — put b
|
|
|
112
112
|
|
|
113
113
|
The optional `shellTools` field records which non-`bash` tools carry shell semantics (e.g. an `exec_command` tool that replaces native `bash`), so they are gated at full parity with native `bash` — see [docs/configuration.md](docs/configuration.md#shelltools--gating-aliased-shell-tools).
|
|
114
114
|
|
|
115
|
+
The optional `authorizerChain` field names registered case-by-case decision links (e.g. a light model judge) to consult when a request lands on `ask`, ahead of the interactive prompt.
|
|
116
|
+
A downstream extension registers a link via `getPermissionsService().registerAuthorizer(name, authorize)`; it decides nothing until you name it here (opt-in), config order fixes the chain order, and the chain owner caps any link's `allow` on `external_directory`/`path` to keep it within your policy — see [docs/configuration.md](docs/configuration.md#authorizer-chain--case-by-case-decision-links).
|
|
117
|
+
|
|
115
118
|
For the full reference — all surfaces, runtime knobs, per-agent overrides, merge semantics, and common recipes — see [docs/configuration.md](docs/configuration.md).
|
|
116
119
|
|
|
117
120
|
## Upgrading
|
package/dist/public.d.ts
CHANGED
|
@@ -1,65 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Registry for custom tool access-intent extractors.
|
|
5
|
-
*
|
|
6
|
-
* Lets sibling extensions declare the filesystem path a tool will access when
|
|
7
|
-
* the tool's input shape is not the default `input.path` convention, so the
|
|
8
|
-
* cross-cutting `path` and `external_directory` gates can see it.
|
|
9
|
-
* One extractor per tool name; duplicate registration throws.
|
|
10
|
-
*/
|
|
11
|
-
/** Returns the filesystem path this tool will access, or `undefined` to decline. */
|
|
12
|
-
type ToolAccessExtractor = (input: Record<string, unknown>) => string | undefined;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Registry for custom tool-input preview formatters.
|
|
16
|
-
*
|
|
17
|
-
* Allows extensions to register a formatter for a specific tool name so
|
|
18
|
-
* permission prompts can show a human-readable summary instead of raw JSON.
|
|
19
|
-
* One formatter per tool name; duplicate registration throws.
|
|
20
|
-
*/
|
|
21
|
-
/** A custom preview formatter for one tool's input. Returns `undefined` to decline. */
|
|
22
|
-
type ToolInputFormatter = (input: Record<string, unknown>) => string | undefined;
|
|
23
|
-
|
|
24
|
-
declare const permissionStateSchema: z.ZodUnion<readonly [z.ZodLiteral<"allow">, z.ZodLiteral<"deny">, z.ZodLiteral<"ask">]>;
|
|
25
|
-
/** A permission decision. */
|
|
26
|
-
type PermissionState = z.infer<typeof permissionStateSchema>;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Provenance of a rule — which source contributed it.
|
|
30
|
-
*
|
|
31
|
-
* Config scopes: "global", "project", "agent", "project-agent".
|
|
32
|
-
* Synthesized: "builtin" (universal default / evaluate() fallback),
|
|
33
|
-
* "baseline" (conditional MCP metadata auto-allow).
|
|
34
|
-
* Runtime: "session" (session approvals).
|
|
35
|
-
* Rewrite: "yolo" (composition-stage ask→allow rewrite under yolo mode).
|
|
36
|
-
*/
|
|
37
|
-
type RuleOrigin = "global" | "project" | "agent" | "project-agent" | "builtin" | "baseline" | "session" | "yolo";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Execution context of a bash command nested inside a substitution or subshell.
|
|
41
|
-
* Absent for current-shell (top-level) commands.
|
|
42
|
-
*/
|
|
43
|
-
type BashCommandContext = "command_substitution" | "process_substitution" | "subshell";
|
|
44
|
-
interface PermissionCheckResult {
|
|
45
|
-
toolName: string;
|
|
46
|
-
state: PermissionState;
|
|
47
|
-
/** Custom denial reason from a deny-with-reason pattern, when present. */
|
|
48
|
-
reason?: string;
|
|
49
|
-
matchedPattern?: string;
|
|
50
|
-
command?: string;
|
|
51
|
-
target?: string;
|
|
52
|
-
source: "tool" | "bash" | "mcp" | "skill" | "special" | "default" | "session";
|
|
53
|
-
/** Which source contributed the winning rule. */
|
|
54
|
-
origin: RuleOrigin;
|
|
55
|
-
/**
|
|
56
|
-
* Execution context of the offending nested command, when the winning bash
|
|
57
|
-
* unit came from a substitution or subshell. Absent for current-shell
|
|
58
|
-
* (top-level) commands.
|
|
59
|
-
*/
|
|
60
|
-
commandContext?: BashCommandContext;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
3
|
/** Emitted at `session_start`, after the service is published. */
|
|
64
4
|
declare const PERMISSIONS_READY_CHANNEL = "permissions:ready";
|
|
65
5
|
/** Emitted when a permission request is committed to the active UI prompt path. */
|
|
@@ -135,6 +75,175 @@ interface PermissionDecisionEvent {
|
|
|
135
75
|
matchedPattern: string | null;
|
|
136
76
|
}
|
|
137
77
|
|
|
78
|
+
declare const permissionStateSchema: z.ZodUnion<readonly [z.ZodLiteral<"allow">, z.ZodLiteral<"deny">, z.ZodLiteral<"ask">]>;
|
|
79
|
+
/** A permission decision. */
|
|
80
|
+
type PermissionState = z.infer<typeof permissionStateSchema>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Provenance of a rule — which source contributed it.
|
|
84
|
+
*
|
|
85
|
+
* Config scopes: "global", "project", "agent", "project-agent".
|
|
86
|
+
* Synthesized: "builtin" (universal default / evaluate() fallback),
|
|
87
|
+
* "baseline" (conditional MCP metadata auto-allow).
|
|
88
|
+
* Runtime: "session" (session approvals).
|
|
89
|
+
* Rewrite: "yolo" (composition-stage ask→allow rewrite under yolo mode).
|
|
90
|
+
*/
|
|
91
|
+
type RuleOrigin = "global" | "project" | "agent" | "project-agent" | "builtin" | "baseline" | "session" | "yolo";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Execution context of a bash command nested inside a substitution or subshell.
|
|
95
|
+
* Absent for current-shell (top-level) commands.
|
|
96
|
+
*/
|
|
97
|
+
type BashCommandContext = "command_substitution" | "process_substitution" | "subshell";
|
|
98
|
+
interface PermissionCheckResult {
|
|
99
|
+
toolName: string;
|
|
100
|
+
state: PermissionState;
|
|
101
|
+
/** Custom denial reason from a deny-with-reason pattern, when present. */
|
|
102
|
+
reason?: string;
|
|
103
|
+
matchedPattern?: string;
|
|
104
|
+
command?: string;
|
|
105
|
+
target?: string;
|
|
106
|
+
source: "tool" | "bash" | "mcp" | "skill" | "special" | "default" | "session";
|
|
107
|
+
/** Which source contributed the winning rule. */
|
|
108
|
+
origin: RuleOrigin;
|
|
109
|
+
/**
|
|
110
|
+
* Execution context of the offending nested command, when the winning bash
|
|
111
|
+
* unit came from a substitution or subshell. Absent for current-shell
|
|
112
|
+
* (top-level) commands.
|
|
113
|
+
*/
|
|
114
|
+
commandContext?: BashCommandContext;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The child's session-approval suggestion, relayed to the serving node so a
|
|
119
|
+
* human who grants "the whole session" records the same pattern the child
|
|
120
|
+
* would have recorded locally.
|
|
121
|
+
*
|
|
122
|
+
* A plain data shape (not the `SessionApproval` value object) so it serializes
|
|
123
|
+
* onto the forwarded request; the serving node rebuilds a `SessionApproval`
|
|
124
|
+
* from it via `SessionApproval.multiple`.
|
|
125
|
+
*/
|
|
126
|
+
interface ForwardedSessionApproval {
|
|
127
|
+
surface: string;
|
|
128
|
+
patterns: readonly string[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The child-fixed facts a gate emits: the surface it evaluated and the match
|
|
132
|
+
* set it computed. `requesterCwd` and `principal` are stamped at the escalation
|
|
133
|
+
* edge (`ParentAuthorizer`), so a gate carries only what it alone can produce.
|
|
134
|
+
*
|
|
135
|
+
* Strings only — an `AccessPath` never crosses onto the wire
|
|
136
|
+
* (`docs/decisions/0002-path-values-string-boundary.md`).
|
|
137
|
+
*/
|
|
138
|
+
interface ForwardedAccessFacts {
|
|
139
|
+
/** Gate surface: `"path"`, `"external_directory"`, `"bash"`, a tool name, or a skill name. */
|
|
140
|
+
surface: string;
|
|
141
|
+
/**
|
|
142
|
+
* The child-fixed match set. Path surface: `AccessPath.matchValues()`
|
|
143
|
+
* (absolute ∪ cwd-relative ∪ canonical), computed at the child. Non-path
|
|
144
|
+
* surface: the already-portable single value as a one-element array.
|
|
145
|
+
*/
|
|
146
|
+
matchValues: string[];
|
|
147
|
+
/** `AccessPath.boundaryValue()` (canonical) for a path surface; `null` for a non-path surface. */
|
|
148
|
+
boundaryValue: string | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
152
|
+
/**
|
|
153
|
+
* Provenance of a forwarded ask: who is really asking, one hop below.
|
|
154
|
+
*
|
|
155
|
+
* Present on {@link PromptPermissionDetails} only when the ask was forwarded
|
|
156
|
+
* from a subagent. Structurally identical to the event's `ForwardedPromptContext`
|
|
157
|
+
* so the details flow straight into `buildUiPrompt`, but declared here to keep
|
|
158
|
+
* the prompter layer free of an events-module import.
|
|
159
|
+
*/
|
|
160
|
+
interface ForwardedAskProvenance {
|
|
161
|
+
requesterAgentName: string | null;
|
|
162
|
+
requesterSessionId: string | null;
|
|
163
|
+
}
|
|
164
|
+
/** Details passed when prompting the user for a permission decision. */
|
|
165
|
+
interface PromptPermissionDetails {
|
|
166
|
+
requestId: string;
|
|
167
|
+
source: PermissionReviewSource;
|
|
168
|
+
agentName: string | null;
|
|
169
|
+
message: string;
|
|
170
|
+
toolCallId?: string;
|
|
171
|
+
toolName?: string;
|
|
172
|
+
skillName?: string;
|
|
173
|
+
path?: string;
|
|
174
|
+
command?: string;
|
|
175
|
+
target?: string;
|
|
176
|
+
toolInputPreview?: string;
|
|
177
|
+
/** Override label for the "for this session" dialog option. */
|
|
178
|
+
sessionLabel?: string;
|
|
179
|
+
/** Explicit display-surface override (a forwarded ask carries the child's original). */
|
|
180
|
+
surface?: string | null;
|
|
181
|
+
/** Explicit display-value override (a forwarded ask carries the child's original). */
|
|
182
|
+
value?: string | null;
|
|
183
|
+
/** Present iff this ask was forwarded from a subagent; drives the non-degraded broadcast + "(Subagent)" title. */
|
|
184
|
+
forwarding?: ForwardedAskProvenance;
|
|
185
|
+
/**
|
|
186
|
+
* The session-approval suggestion for this ask. On the child's escalation it
|
|
187
|
+
* rides into the forwarded request; on the serving node it lets the dialog
|
|
188
|
+
* offer a whole-session grant scope. Absent when the gate computed no
|
|
189
|
+
* suggestion.
|
|
190
|
+
*/
|
|
191
|
+
sessionApproval?: ForwardedSessionApproval;
|
|
192
|
+
/**
|
|
193
|
+
* The child-fixed access facts the raising gate computed (surface + match
|
|
194
|
+
* set). Rides through the runner to the escalation edge, which completes
|
|
195
|
+
* them into a `ForwardedAccessIntent` by stamping `requesterCwd` and
|
|
196
|
+
* `principal`. Absent for a serving-node local prompt reconstructed from a
|
|
197
|
+
* forwarded request.
|
|
198
|
+
*/
|
|
199
|
+
accessIntent?: ForwardedAccessFacts;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A non-terminal chain link's ruling on an `ask`: decide (`allow`/`deny`) or
|
|
204
|
+
* pass the ask on to the next link (`defer`). A `deny` carries an optional
|
|
205
|
+
* teaching `reason` the invoking model sees, so it can self-correct.
|
|
206
|
+
*/
|
|
207
|
+
type AuthorizerVerdict = {
|
|
208
|
+
kind: "allow";
|
|
209
|
+
} | {
|
|
210
|
+
kind: "deny";
|
|
211
|
+
reason?: string;
|
|
212
|
+
} | {
|
|
213
|
+
kind: "defer";
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* A non-terminal link in the live-authority chain: reviews an `ask` and may
|
|
217
|
+
* decide it or defer to the next link (ADR 0007). The chain injects a narrow,
|
|
218
|
+
* session-scoped {@link PermissionQuery} at `authorize` time (§3), so a link
|
|
219
|
+
* queries the deterministic engine at gate parity rather than reaching for the
|
|
220
|
+
* cross-extension service via `Symbol.for()`.
|
|
221
|
+
*/
|
|
222
|
+
interface Authorizer {
|
|
223
|
+
authorize(details: PromptPermissionDetails, query: PermissionQuery): Promise<AuthorizerVerdict>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Registry for custom tool access-intent extractors.
|
|
228
|
+
*
|
|
229
|
+
* Lets sibling extensions declare the filesystem path a tool will access when
|
|
230
|
+
* the tool's input shape is not the default `input.path` convention, so the
|
|
231
|
+
* cross-cutting `path` and `external_directory` gates can see it.
|
|
232
|
+
* One extractor per tool name; duplicate registration throws.
|
|
233
|
+
*/
|
|
234
|
+
/** Returns the filesystem path this tool will access, or `undefined` to decline. */
|
|
235
|
+
type ToolAccessExtractor = (input: Record<string, unknown>) => string | undefined;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Registry for custom tool-input preview formatters.
|
|
239
|
+
*
|
|
240
|
+
* Allows extensions to register a formatter for a specific tool name so
|
|
241
|
+
* permission prompts can show a human-readable summary instead of raw JSON.
|
|
242
|
+
* One formatter per tool name; duplicate registration throws.
|
|
243
|
+
*/
|
|
244
|
+
/** A custom preview formatter for one tool's input. Returns `undefined` to decline. */
|
|
245
|
+
type ToolInputFormatter = (input: Record<string, unknown>) => string | undefined;
|
|
246
|
+
|
|
138
247
|
/**
|
|
139
248
|
* Cross-extension service accessor backed by `Symbol.for()` on `globalThis`.
|
|
140
249
|
*
|
|
@@ -149,13 +258,12 @@ interface PermissionDecisionEvent {
|
|
|
149
258
|
*/
|
|
150
259
|
|
|
151
260
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* rules internally.
|
|
261
|
+
* The narrow, read-only projection of {@link PermissionsService}: answer a
|
|
262
|
+
* policy query for a surface, and report a tool-level state. This is the
|
|
263
|
+
* capability an Authorizer chain link is handed (ISP) — it never sees the
|
|
264
|
+
* registration surface.
|
|
157
265
|
*/
|
|
158
|
-
interface
|
|
266
|
+
interface PermissionQuery {
|
|
159
267
|
/**
|
|
160
268
|
* Query the permission policy for a surface and value.
|
|
161
269
|
*
|
|
@@ -168,6 +276,27 @@ interface PermissionsService {
|
|
|
168
276
|
* @returns Full check result including state, matched pattern, and origin.
|
|
169
277
|
*/
|
|
170
278
|
checkPermission(surface: string, value?: string, agentName?: string): PermissionCheckResult;
|
|
279
|
+
/**
|
|
280
|
+
* Query the tool-level permission state for pre-filtering tools before
|
|
281
|
+
* creating a child session.
|
|
282
|
+
*
|
|
283
|
+
* Returns `"deny"` | `"allow"` | `"ask"` based on the composed policy.
|
|
284
|
+
* Does not consider command-level rules (e.g. per-bash-command patterns) —
|
|
285
|
+
* use `checkPermission` for runtime invocation gates.
|
|
286
|
+
*
|
|
287
|
+
* @param toolName - Tool name (e.g. `"bash"`, `"read"`, `"my-extension:tool"`).
|
|
288
|
+
* @param agentName - Optional agent name for per-agent policy resolution.
|
|
289
|
+
*/
|
|
290
|
+
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Public interface exposed to other extensions via `getPermissionsService()`.
|
|
294
|
+
*
|
|
295
|
+
* `checkPermission` takes a surface + optional value + optional agent name,
|
|
296
|
+
* and delegates to `PermissionManager.checkPermission()` with current session
|
|
297
|
+
* rules internally.
|
|
298
|
+
*/
|
|
299
|
+
interface PermissionsService extends PermissionQuery {
|
|
171
300
|
/**
|
|
172
301
|
* Register a custom preview formatter for a specific tool name.
|
|
173
302
|
*
|
|
@@ -203,17 +332,25 @@ interface PermissionsService {
|
|
|
203
332
|
*/
|
|
204
333
|
registerToolAccessExtractor(toolName: string, extractor: ToolAccessExtractor): () => void;
|
|
205
334
|
/**
|
|
206
|
-
*
|
|
207
|
-
* creating a child session.
|
|
335
|
+
* Register a named live-authority chain link (ADR 0007 §4).
|
|
208
336
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
337
|
+
* A link reviews an `ask` and returns `allow` / `deny` (with an optional
|
|
338
|
+
* teaching `reason`) / `defer`. It is handed a narrow, session-scoped
|
|
339
|
+
* {@link PermissionQuery} at `authorize` time so it can query the
|
|
340
|
+
* deterministic engine at gate parity. Register from a `permissions:ready`
|
|
341
|
+
* handler so registration is robust to load order and survives `/reload`.
|
|
212
342
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
343
|
+
* Registration alone grants **no authority**: the link decides nothing until
|
|
344
|
+
* the operator names it in the `authorizerChain` config (opt-in activation),
|
|
345
|
+
* and the chain owner caps every verdict with the bounded-delegation
|
|
346
|
+
* checkpoint (an `allow` on an excluded surface downgrades to `defer`). Only
|
|
347
|
+
* one link may be registered per name — a second call for the same name
|
|
348
|
+
* throws. The returned disposer unregisters the link.
|
|
349
|
+
*
|
|
350
|
+
* @param name - Operator-facing link name referenced from `authorizerChain`.
|
|
351
|
+
* @param authorize - The link's decision callback (`(details, query) => verdict`).
|
|
215
352
|
*/
|
|
216
|
-
|
|
353
|
+
registerAuthorizer(name: string, authorize: Authorizer["authorize"]): () => void;
|
|
217
354
|
}
|
|
218
355
|
/**
|
|
219
356
|
* Store a `PermissionsService` on `globalThis` so other extensions can
|
|
@@ -246,4 +383,4 @@ declare function getPermissionsService(): PermissionsService | undefined;
|
|
|
246
383
|
declare function unpublishPermissionsService(service: PermissionsService): void;
|
|
247
384
|
|
|
248
385
|
export { PERMISSIONS_DECISION_CHANNEL, PERMISSIONS_READY_CHANNEL, PERMISSIONS_UI_PROMPT_CHANNEL, getPermissionsService, publishPermissionsService, unpublishPermissionsService };
|
|
249
|
-
export type { ForwardedPromptContext, PermissionCheckResult, PermissionDecisionEvent, PermissionState, PermissionUiPromptEvent, PermissionUiPromptSource, PermissionsReadyEvent, PermissionsService, ToolInputFormatter };
|
|
386
|
+
export type { Authorizer, AuthorizerVerdict, ForwardedPromptContext, PermissionCheckResult, PermissionDecisionEvent, PermissionQuery, PermissionState, PermissionUiPromptEvent, PermissionUiPromptSource, PermissionsReadyEvent, PermissionsService, PromptPermissionDetails, ToolInputFormatter };
|
package/docs/configuration.md
CHANGED
|
@@ -52,6 +52,9 @@ Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`, `doublePressToConf
|
|
|
52
52
|
"exec_command": { "commandArgument": "cmd", "workdirArgument": "workdir" }
|
|
53
53
|
},
|
|
54
54
|
|
|
55
|
+
// Ordered names of registered live-authority chain links (empty = none)
|
|
56
|
+
"authorizerChain": [],
|
|
57
|
+
|
|
55
58
|
// Flat permission policy
|
|
56
59
|
"permission": {
|
|
57
60
|
"*": "ask", // universal fallback
|
|
@@ -81,15 +84,16 @@ Scalar fields (`debugLog`, `permissionReviewLog`, `yoloMode`, `doublePressToConf
|
|
|
81
84
|
|
|
82
85
|
## Runtime Knobs
|
|
83
86
|
|
|
84
|
-
| Key | Default | Description
|
|
85
|
-
| --------------------------- | ------- |
|
|
86
|
-
| `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl`
|
|
87
|
-
| `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl`
|
|
88
|
-
| `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled
|
|
89
|
-
| `doublePressToConfirm` | `true` | Requires a confirming second press of a decision hotkey in the inline TUI dialog (see below). TUI sessions only; set to `false` for single-press.
|
|
90
|
-
| `toolInputPreviewMaxLength` | `200` | Max characters of inline JSON shown in permission prompts for tool inputs. Omit to use the default. Set to a large value to disable truncation.
|
|
91
|
-
| `toolTextSummaryMaxLength` | `80` | Max characters of inline pattern/path summaries (grep patterns, find globs, ls paths) in permission prompts. Omit to use the default.
|
|
92
|
-
| `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate. Supports `~`/`$HOME` expansion and wildcard patterns (`*`, `?`).
|
|
87
|
+
| Key | Default | Description |
|
|
88
|
+
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
|
|
90
|
+
| `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl` |
|
|
91
|
+
| `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled |
|
|
92
|
+
| `doublePressToConfirm` | `true` | Requires a confirming second press of a decision hotkey in the inline TUI dialog (see below). TUI sessions only; set to `false` for single-press. |
|
|
93
|
+
| `toolInputPreviewMaxLength` | `200` | Max characters of inline JSON shown in permission prompts for tool inputs. Omit to use the default. Set to a large value to disable truncation. |
|
|
94
|
+
| `toolTextSummaryMaxLength` | `80` | Max characters of inline pattern/path summaries (grep patterns, find globs, ls paths) in permission prompts. Omit to use the default. |
|
|
95
|
+
| `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate. Supports `~`/`$HOME` expansion and wildcard patterns (`*`, `?`). |
|
|
96
|
+
| `authorizerChain` | `[]` | Ordered names of registered live-authority chain links to consult before the terminal authorizer (see [Authorizer chain](#authorizer-chain--case-by-case-decision-links)). |
|
|
93
97
|
|
|
94
98
|
Both logs write to `~/.pi/agent/extensions/pi-permission-system/logs/`.
|
|
95
99
|
No debug output is printed to the terminal.
|
|
@@ -159,6 +163,38 @@ To change a specific tool's mapping, set that tool's key at the project scope (t
|
|
|
159
163
|
`shellTools` only ever *tightens* enforcement and is inert when the named tool is not registered in the current session.
|
|
160
164
|
Opting a project out of a shell-aliasing extension is a package-disable concern, not a `shellTools` edit.
|
|
161
165
|
|
|
166
|
+
### Authorizer chain — case-by-case decision links
|
|
167
|
+
|
|
168
|
+
The deterministic policy above decides `allow` / `deny` / `ask` for every request.
|
|
169
|
+
When a request lands on `ask`, the **authorizer chain** decides who answers it.
|
|
170
|
+
By default that is you (an interactive prompt), the subagent-forwarding path, or a headless deny.
|
|
171
|
+
A downstream extension can register a **link** — a reviewer that sees the `ask` and returns `allow`, `deny` (with an optional teaching reason), or `defer` to the next link — and the chain ends at the default terminal that always decides.
|
|
172
|
+
The canonical use case is a light model judge that reviews asks case by case (e.g. auto-denying an errant typo-path with a corrective reason).
|
|
173
|
+
|
|
174
|
+
`authorizerChain` is the ordered list of link names to consult, ahead of the terminal:
|
|
175
|
+
|
|
176
|
+
```jsonc
|
|
177
|
+
{
|
|
178
|
+
"authorizerChain": ["model-judge"]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Three invariants govern the chain:
|
|
183
|
+
|
|
184
|
+
1. **Config order wins, never registration order.**
|
|
185
|
+
The order in `authorizerChain` — not the order extensions happen to register in — fixes the security-relevant chain order.
|
|
186
|
+
2. **A missing link is skipped fail-safe.**
|
|
187
|
+
A name with no registered link is skipped with a logged warning; the `ask` still reaches the terminal.
|
|
188
|
+
Absence of a judge means *more* prompting, never less.
|
|
189
|
+
3. **Registration alone grants no authority.**
|
|
190
|
+
Installing a judge extension gives it nothing; a link decides nothing until you name it here (opt-in activation).
|
|
191
|
+
|
|
192
|
+
The chain owner caps every link with a **bounded-delegation checkpoint**: a link's `allow` on an excluded surface (`external_directory` or the `path` surface) is downgraded to `defer`, so a buggy or over-eager judge can never approve access outside your policy.
|
|
193
|
+
Deny and defer are never capped.
|
|
194
|
+
|
|
195
|
+
Extension authors: register a link from a `permissions:ready` handler via `getPermissionsService().registerAuthorizer(name, authorize)`; the callback receives the ask details and a narrow, session-scoped `PermissionQuery` (`checkPermission` / `getToolPermission`) so it can consult the deterministic engine at gate parity.
|
|
196
|
+
Registration returns a disposer, and only one link may hold a given name.
|
|
197
|
+
|
|
162
198
|
---
|
|
163
199
|
|
|
164
200
|
## Policy Reference
|
package/package.json
CHANGED
|
@@ -55,6 +55,16 @@
|
|
|
55
55
|
"minLength": 1
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
|
+
"authorizerChain": {
|
|
59
|
+
"description": "Ordered names of registered live-authority chain links to consult before the terminal authorizer. Config order (not registration order) fixes the chain order; an unregistered name is skipped fail-safe (more prompting, never less); a link decides nothing until it is named here.",
|
|
60
|
+
"markdownDescription": "Ordered names of registered **live-authority chain links** (e.g. a model judge) to consult before the terminal authorizer (the human, or the subagent-forwarding / headless-deny fallback).\n\nA link reviews an `ask` and returns `allow` / `deny` (with an optional teaching reason) / `defer` to the next link. Three invariants govern the chain:\n\n- **Config order wins.** The order here — not the order extensions register in — fixes the security-relevant chain order.\n- **Fail-safe skip.** A name with no registered link is skipped with a warning; the `ask` still reaches the terminal (more prompting, never less).\n- **Opt-in activation.** Installing a judge extension grants it no authority; a link decides nothing until you name it here.\n\nThe chain owner caps every verdict with a bounded-delegation checkpoint: a link's `allow` on an excluded surface (`external_directory` or `path`) is downgraded to `defer`, so a link cannot exceed your policy.\n\nDefaults to an empty list (no links).",
|
|
61
|
+
"default": [],
|
|
62
|
+
"type": "array",
|
|
63
|
+
"items": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"minLength": 1
|
|
66
|
+
}
|
|
67
|
+
},
|
|
58
68
|
"permission": {
|
|
59
69
|
"type": "object",
|
|
60
70
|
"propertyNames": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stripBashCommentLines } from "#src/bash-arity";
|
|
2
2
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
3
3
|
import { getNonEmptyString, toRecord } from "#src/value-guards";
|
|
4
|
-
import type { AccessIntent } from "./access-intent";
|
|
4
|
+
import type { AccessIntent, ResolvedAccessIntent } from "./access-intent";
|
|
5
5
|
import { createMcpPermissionTargets } from "./mcp-targets";
|
|
6
6
|
import { PATH_SURFACES } from "./path-surfaces";
|
|
7
7
|
import { classifyToolKind } from "./tool-kind";
|
|
@@ -41,6 +41,39 @@ export function buildAccessIntentForSurface(
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Build a {@link ResolvedAccessIntent} directly from a forwarded request's
|
|
46
|
+
* child-fixed match values (ADR 0008 §2), for the forwarded-serving wire
|
|
47
|
+
* (#597).
|
|
48
|
+
*
|
|
49
|
+
* Unlike {@link buildAccessIntentForSurface}, this never touches a
|
|
50
|
+
* `PathNormalizer` and never rebuilds an `AccessPath` — a path-shaped surface
|
|
51
|
+
* gets a `path-values` intent carrying `matchValues` as-is (the values the
|
|
52
|
+
* child already fixed), and every other surface gets a `tool` intent built
|
|
53
|
+
* from its single portable value. `agentName` is always the requester's
|
|
54
|
+
* `principal.agentName` (ADR 0008 §3, agent-scoped serving).
|
|
55
|
+
*/
|
|
56
|
+
export function buildResolvedIntentFromMatchValues(
|
|
57
|
+
surface: string,
|
|
58
|
+
matchValues: readonly string[],
|
|
59
|
+
agentName: string,
|
|
60
|
+
): ResolvedAccessIntent {
|
|
61
|
+
if (PATH_SURFACES.has(surface)) {
|
|
62
|
+
return {
|
|
63
|
+
kind: "path-values",
|
|
64
|
+
surface,
|
|
65
|
+
values: [...matchValues],
|
|
66
|
+
agentName,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
kind: "tool",
|
|
71
|
+
surface,
|
|
72
|
+
input: buildInputForSurface(surface, matchValues[0]),
|
|
73
|
+
agentName,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
/**
|
|
45
78
|
* Construct a surface-appropriate input object from a raw value string for the
|
|
46
79
|
* `tool`-intent branch of {@link buildAccessIntentForSurface} (the non-path
|