@gotgenes/pi-permission-system 20.7.2 → 20.7.3
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 +12 -0
- package/dist/public.d.ts +249 -0
- package/docs/cross-extension-api.md +3 -0
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.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
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** ship consumable public type declarations ([#592](https://github.com/gotgenes/pi-packages/issues/592)) ([542e094](https://github.com/gotgenes/pi-packages/commit/542e094b9650e8f13bd9dad3864007f3ce2c0cc2))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* **pi-permission-system:** document the bundled public type declaration ([070875d](https://github.com/gotgenes/pi-packages/commit/070875d654efde50e8867f8fd4aeba857a26c4fb))
|
|
19
|
+
|
|
8
20
|
## [20.7.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.7.1...pi-permission-system-v20.7.2) (2026-07-14)
|
|
9
21
|
|
|
10
22
|
|
package/dist/public.d.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
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
|
+
/** Emitted at `session_start`, after the service is published. */
|
|
64
|
+
declare const PERMISSIONS_READY_CHANNEL = "permissions:ready";
|
|
65
|
+
/** Emitted when a permission request is committed to the active UI prompt path. */
|
|
66
|
+
declare const PERMISSIONS_UI_PROMPT_CHANNEL = "permissions:ui_prompt";
|
|
67
|
+
/** Emitted after every permission gate resolution. */
|
|
68
|
+
declare const PERMISSIONS_DECISION_CHANNEL = "permissions:decision";
|
|
69
|
+
/**
|
|
70
|
+
* Payload emitted on `permissions:ready`.
|
|
71
|
+
*
|
|
72
|
+
* Intentionally empty: the channel is a readiness signal. There is no
|
|
73
|
+
* `protocolVersion` — the published types plus package semver define the
|
|
74
|
+
* broadcast contract.
|
|
75
|
+
*/
|
|
76
|
+
type PermissionsReadyEvent = Record<string, never>;
|
|
77
|
+
/**
|
|
78
|
+
* Origin of a UI prompt.
|
|
79
|
+
*
|
|
80
|
+
* Forwarding is orthogonal to origin: a forwarded subagent prompt keeps its
|
|
81
|
+
* original source and is identified by a non-null `forwarding` field, not by a
|
|
82
|
+
* dedicated source value.
|
|
83
|
+
*/
|
|
84
|
+
type PermissionUiPromptSource = "tool_call" | "skill_input" | "skill_read";
|
|
85
|
+
/** Forwarding context, present only when a prompt was forwarded from a non-UI subagent. */
|
|
86
|
+
interface ForwardedPromptContext {
|
|
87
|
+
/** Requesting subagent's display name, when known. */
|
|
88
|
+
requesterAgentName: string | null;
|
|
89
|
+
/** Requesting subagent's session id, when known. */
|
|
90
|
+
requesterSessionId: string | null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Payload emitted on `permissions:ui_prompt`, immediately before the active
|
|
94
|
+
* user-facing permission UI is shown.
|
|
95
|
+
*
|
|
96
|
+
* Lean by design: `surface`/`value` are the normalized display projection a
|
|
97
|
+
* notification consumer reads; `source` is the origin; `forwarding` is non-null
|
|
98
|
+
* only for forwarded subagent prompts. There is no `protocolVersion` — the
|
|
99
|
+
* published types plus package semver define the broadcast contract, and
|
|
100
|
+
* consumers should read defensively.
|
|
101
|
+
*/
|
|
102
|
+
interface PermissionUiPromptEvent {
|
|
103
|
+
/** Unique ID for the permission request being prompted. */
|
|
104
|
+
requestId: string;
|
|
105
|
+
/** Prompt origin. */
|
|
106
|
+
source: PermissionUiPromptSource;
|
|
107
|
+
/** Normalized display surface (e.g. "bash", "skill"), when known. */
|
|
108
|
+
surface: string | null;
|
|
109
|
+
/** Normalized display value (command, path, skill name, etc.), when known. */
|
|
110
|
+
value: string | null;
|
|
111
|
+
/** Agent name (when known). */
|
|
112
|
+
agentName: string | null;
|
|
113
|
+
/** Message displayed to the user. */
|
|
114
|
+
message: string;
|
|
115
|
+
/** Forwarding context, or null for a direct prompt. */
|
|
116
|
+
forwarding: ForwardedPromptContext | null;
|
|
117
|
+
}
|
|
118
|
+
/** How a permission decision was reached. */
|
|
119
|
+
type PermissionDecisionResolution = "policy_allow" | "policy_deny" | "session_approved" | "infrastructure_auto_allowed" | "user_approved" | "user_approved_for_session" | "user_denied" | "auto_approved" | "confirmation_unavailable";
|
|
120
|
+
/** Payload emitted on `permissions:decision`. */
|
|
121
|
+
interface PermissionDecisionEvent {
|
|
122
|
+
/** Permission surface: "bash", "read", "mcp", "skill", "external_directory", etc. */
|
|
123
|
+
surface: string;
|
|
124
|
+
/** The value that was evaluated (command, tool name, skill name, path). */
|
|
125
|
+
value: string;
|
|
126
|
+
/** Final decision. */
|
|
127
|
+
result: "allow" | "deny";
|
|
128
|
+
/** How the decision was reached. */
|
|
129
|
+
resolution: PermissionDecisionResolution;
|
|
130
|
+
/** Which config scope contributed the winning rule (when available). */
|
|
131
|
+
origin: string | null;
|
|
132
|
+
/** Agent name (when known). */
|
|
133
|
+
agentName: string | null;
|
|
134
|
+
/** Matched pattern from the winning rule (when available). */
|
|
135
|
+
matchedPattern: string | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Cross-extension service accessor backed by `Symbol.for()` on `globalThis`.
|
|
140
|
+
*
|
|
141
|
+
* `Symbol.for()` is process-global by spec, so it survives jiti's per-extension
|
|
142
|
+
* module isolation (`moduleCache: false`). A consumer doing
|
|
143
|
+
* `import("@gotgenes/pi-permission-system")` gets a fresh module copy, but
|
|
144
|
+
* `getPermissionsService()` reads from the same `globalThis` slot the provider
|
|
145
|
+
* wrote to — enabling direct, synchronous, type-safe function calls.
|
|
146
|
+
*
|
|
147
|
+
* Best practice: call `getPermissionsService()` per use rather than caching the
|
|
148
|
+
* reference — this ensures resilience across `/reload` and load-order edge cases.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Public interface exposed to other extensions via `getPermissionsService()`.
|
|
153
|
+
*
|
|
154
|
+
* `checkPermission` takes a surface + optional value + optional agent name,
|
|
155
|
+
* and delegates to `PermissionManager.checkPermission()` with current session
|
|
156
|
+
* rules internally.
|
|
157
|
+
*/
|
|
158
|
+
interface PermissionsService {
|
|
159
|
+
/**
|
|
160
|
+
* Query the permission policy for a surface and value.
|
|
161
|
+
*
|
|
162
|
+
* @param surface - Permission surface: "bash", "read", "mcp", "skill",
|
|
163
|
+
* "external_directory", etc.
|
|
164
|
+
* @param value - The value to evaluate: command string, tool name, skill
|
|
165
|
+
* name, or path. Omit or pass `undefined` for a
|
|
166
|
+
* surface-level query.
|
|
167
|
+
* @param agentName - Optional agent name for per-agent policy resolution.
|
|
168
|
+
* @returns Full check result including state, matched pattern, and origin.
|
|
169
|
+
*/
|
|
170
|
+
checkPermission(surface: string, value?: string, agentName?: string): PermissionCheckResult;
|
|
171
|
+
/**
|
|
172
|
+
* Register a custom preview formatter for a specific tool name.
|
|
173
|
+
*
|
|
174
|
+
* The formatter is consulted first inside `ToolPreviewFormatter.formatToolInputForPrompt`;
|
|
175
|
+
* returning `undefined` falls through to the built-in switch (and ultimately
|
|
176
|
+
* the JSON default).
|
|
177
|
+
*
|
|
178
|
+
* Only one formatter may be registered per tool name — a second call for the
|
|
179
|
+
* same name throws. The returned disposer unregisters the formatter.
|
|
180
|
+
*
|
|
181
|
+
* @param toolName - Exact tool name to register for (e.g. `"mcp"`, `"my-server:run"`).
|
|
182
|
+
* @param formatter - Receives the raw `input` record; return a string to use
|
|
183
|
+
* as the prompt preview, or `undefined` to decline.
|
|
184
|
+
*/
|
|
185
|
+
registerToolInputFormatter(toolName: string, formatter: ToolInputFormatter): () => void;
|
|
186
|
+
/**
|
|
187
|
+
* Register a custom access-intent extractor for a specific tool name.
|
|
188
|
+
*
|
|
189
|
+
* The extractor declares the filesystem path a tool will access so the
|
|
190
|
+
* cross-cutting `path` and `external_directory` gates can see it. Use it for
|
|
191
|
+
* tools whose path lives under a non-standard key — built-in file tools and
|
|
192
|
+
* any tool exposing `input.path` (plus MCP via `input.arguments.path`) are
|
|
193
|
+
* already covered by convention without registration.
|
|
194
|
+
*
|
|
195
|
+
* The extractor receives the raw `input` record and returns the path string,
|
|
196
|
+
* or `undefined` to decline. Only one extractor may be registered per tool
|
|
197
|
+
* name — a second call for the same name throws. The returned disposer
|
|
198
|
+
* unregisters the extractor.
|
|
199
|
+
*
|
|
200
|
+
* @param toolName - Exact tool name to register for (e.g. `"ffgrep"`).
|
|
201
|
+
* @param extractor - Receives the raw `input` record; return the path string,
|
|
202
|
+
* or `undefined` to decline.
|
|
203
|
+
*/
|
|
204
|
+
registerToolAccessExtractor(toolName: string, extractor: ToolAccessExtractor): () => void;
|
|
205
|
+
/**
|
|
206
|
+
* Query the tool-level permission state for pre-filtering tools before
|
|
207
|
+
* creating a child session.
|
|
208
|
+
*
|
|
209
|
+
* Returns `"deny"` | `"allow"` | `"ask"` based on the composed policy.
|
|
210
|
+
* Does not consider command-level rules (e.g. per-bash-command patterns) —
|
|
211
|
+
* use `checkPermission` for runtime invocation gates.
|
|
212
|
+
*
|
|
213
|
+
* @param toolName - Tool name (e.g. `"bash"`, `"read"`, `"my-extension:tool"`).
|
|
214
|
+
* @param agentName - Optional agent name for per-agent policy resolution.
|
|
215
|
+
*/
|
|
216
|
+
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Store a `PermissionsService` on `globalThis` so other extensions can
|
|
220
|
+
* retrieve it via `getPermissionsService()`.
|
|
221
|
+
*
|
|
222
|
+
* Called at `session_start` by the top-level (parent) instance only — an
|
|
223
|
+
* in-process subagent child skips publishing so it cannot clobber the parent's
|
|
224
|
+
* service. Overwrites any previously published service, which keeps `/reload`
|
|
225
|
+
* working: a reloaded parent re-publishes its fresh service.
|
|
226
|
+
*/
|
|
227
|
+
declare function publishPermissionsService(service: PermissionsService): void;
|
|
228
|
+
/**
|
|
229
|
+
* Retrieve the published `PermissionsService`, or `undefined` if the
|
|
230
|
+
* permission-system extension has not loaded (or has been unloaded).
|
|
231
|
+
*/
|
|
232
|
+
declare function getPermissionsService(): PermissionsService | undefined;
|
|
233
|
+
/**
|
|
234
|
+
* Remove `service` from `globalThis`, but only when the current slot still
|
|
235
|
+
* holds it (identity compare-and-delete).
|
|
236
|
+
*
|
|
237
|
+
* Called during `session_shutdown` to avoid stale references after the
|
|
238
|
+
* extension is torn down. Scoping the delete to the publishing instance keeps
|
|
239
|
+
* two cases correct:
|
|
240
|
+
*
|
|
241
|
+
* - An in-process subagent child never published the parent's service, so its
|
|
242
|
+
* shutdown is a no-op and the parent's slot survives.
|
|
243
|
+
* - A superseded `/reload` generation no longer owns the slot, so its late
|
|
244
|
+
* shutdown cannot wipe the new generation's freshly published service.
|
|
245
|
+
*/
|
|
246
|
+
declare function unpublishPermissionsService(service: PermissionsService): void;
|
|
247
|
+
|
|
248
|
+
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 };
|
|
@@ -39,6 +39,9 @@ Consumers call `getPermissionsService()` to retrieve it — even though their `i
|
|
|
39
39
|
An in-process subagent child does not publish its own service; inside a child, `getPermissionsService()` resolves the parent's service.
|
|
40
40
|
A consumer reacting to the `permissions:ready` broadcast (also emitted at `session_start`, after the publish) can resolve the service immediately.
|
|
41
41
|
|
|
42
|
+
All types below are directly importable and type-check with `tsc` out of the box.
|
|
43
|
+
`@gotgenes/pi-permission-system`'s published `exports` resolve `import type { … }` to a self-contained, bundled declaration file with no internal module references, so a downstream `tsconfig.json` needs no special path configuration.
|
|
44
|
+
|
|
42
45
|
### API
|
|
43
46
|
|
|
44
47
|
The `PermissionsService` interface:
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-permission-system",
|
|
3
|
-
"version": "20.7.
|
|
3
|
+
"version": "20.7.3",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
".":
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/public.d.ts",
|
|
9
|
+
"default": "./src/service.ts"
|
|
10
|
+
}
|
|
8
11
|
},
|
|
9
12
|
"imports": {
|
|
10
13
|
"#src/*": "./src/*",
|
|
@@ -12,6 +15,7 @@
|
|
|
12
15
|
},
|
|
13
16
|
"files": [
|
|
14
17
|
"src",
|
|
18
|
+
"dist",
|
|
15
19
|
"config/config.example.json",
|
|
16
20
|
"schemas/permissions.schema.json",
|
|
17
21
|
"docs/*.md",
|
|
@@ -67,6 +71,8 @@
|
|
|
67
71
|
"@earendil-works/pi-coding-agent": "0.79.1",
|
|
68
72
|
"@earendil-works/pi-tui": "0.79.1",
|
|
69
73
|
"@types/node": "^22.15.3",
|
|
74
|
+
"rollup": "^4.62.2",
|
|
75
|
+
"rollup-plugin-dts": "^6.4.1",
|
|
70
76
|
"rumdl": "^0.2.10",
|
|
71
77
|
"typescript": "^6.0.3",
|
|
72
78
|
"vitest": "^4.1.8"
|
|
@@ -78,9 +84,11 @@
|
|
|
78
84
|
},
|
|
79
85
|
"scripts": {
|
|
80
86
|
"check": "tsc --noEmit",
|
|
87
|
+
"build:types": "rollup -c rollup.dts.config.mjs",
|
|
81
88
|
"gen:schema": "node --experimental-strip-types scripts/generate-permissions-schema.ts && biome format --write schemas/permissions.schema.json",
|
|
82
89
|
"test": "vitest run",
|
|
83
90
|
"test:watch": "vitest",
|
|
91
|
+
"verify:public-types": "bash scripts/verify-public-types.sh",
|
|
84
92
|
"lint:md": "rumdl check *.md docs/**/*.md",
|
|
85
93
|
"lint": "biome check . && eslint . && pnpm run lint:md"
|
|
86
94
|
}
|