@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 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
 
@@ -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.2",
3
+ "version": "20.7.3",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/service.ts"
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
  }