@gotgenes/pi-permission-system 9.2.0 → 10.1.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +12 -11
  3. package/package.json +1 -1
  4. package/src/agent-prep-session.ts +28 -0
  5. package/src/decision-reporter.ts +41 -0
  6. package/src/denial-messages.ts +11 -0
  7. package/src/forwarded-permissions/io.ts +29 -0
  8. package/src/forwarded-permissions/permission-forwarder.ts +549 -0
  9. package/src/forwarding-manager.ts +3 -7
  10. package/src/gate-handler-session.ts +13 -0
  11. package/src/gate-prompter.ts +14 -0
  12. package/src/handlers/before-agent-start.ts +2 -3
  13. package/src/handlers/gates/bash-command.ts +4 -18
  14. package/src/handlers/gates/bash-external-directory.ts +3 -15
  15. package/src/handlers/gates/bash-path.ts +3 -16
  16. package/src/handlers/gates/descriptor.ts +0 -28
  17. package/src/handlers/gates/path.ts +3 -15
  18. package/src/handlers/gates/runner.ts +142 -105
  19. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  20. package/src/handlers/gates/skill-input.ts +44 -0
  21. package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
  22. package/src/handlers/lifecycle.ts +9 -9
  23. package/src/handlers/permission-gate-handler.ts +34 -238
  24. package/src/index.ts +50 -68
  25. package/src/mcp-targets.ts +56 -46
  26. package/src/permission-event-rpc.ts +7 -0
  27. package/src/permission-events.ts +89 -8
  28. package/src/permission-forwarding.ts +23 -0
  29. package/src/permission-prompter.ts +27 -56
  30. package/src/permission-resolver.ts +17 -0
  31. package/src/permission-session.ts +77 -9
  32. package/src/permission-ui-prompt.ts +127 -0
  33. package/src/permissions-service.ts +53 -0
  34. package/src/service-lifecycle.ts +49 -0
  35. package/src/service.ts +17 -0
  36. package/src/session-approval-recorder.ts +6 -0
  37. package/src/session-lifecycle-session.ts +24 -0
  38. package/src/tool-input-preview.ts +0 -62
  39. package/src/tool-input-prompt-formatters.ts +63 -0
  40. package/src/tool-preview-formatter.ts +6 -4
  41. package/test/composition-root.test.ts +5 -0
  42. package/test/decision-reporter.test.ts +112 -0
  43. package/test/denial-messages.test.ts +62 -0
  44. package/test/forwarding-manager.test.ts +26 -44
  45. package/test/handlers/before-agent-start.test.ts +45 -21
  46. package/test/handlers/external-directory-integration.test.ts +86 -22
  47. package/test/handlers/external-directory-session-dedup.test.ts +102 -55
  48. package/test/handlers/gates/bash-command.test.ts +49 -90
  49. package/test/handlers/gates/bash-external-directory.test.ts +54 -95
  50. package/test/handlers/gates/bash-path.test.ts +63 -148
  51. package/test/handlers/gates/path.test.ts +38 -105
  52. package/test/handlers/gates/runner.test.ts +150 -93
  53. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  54. package/test/handlers/gates/skill-input.test.ts +128 -0
  55. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
  56. package/test/handlers/input.test.ts +1 -2
  57. package/test/handlers/lifecycle.test.ts +49 -33
  58. package/test/handlers/tool-call-events.test.ts +1 -1
  59. package/test/helpers/gate-fixtures.ts +147 -16
  60. package/test/helpers/handler-fixtures.ts +143 -27
  61. package/test/mcp-targets.test.ts +55 -0
  62. package/test/permission-event-rpc.test.ts +39 -0
  63. package/test/permission-events.test.ts +78 -10
  64. package/test/permission-forwarder.test.ts +295 -0
  65. package/test/permission-prompter.test.ts +147 -38
  66. package/test/permission-session.test.ts +160 -27
  67. package/test/permission-ui-prompt.test.ts +146 -0
  68. package/test/permissions-service.test.ts +151 -0
  69. package/test/runtime.test.ts +0 -4
  70. package/test/service-lifecycle.test.ts +162 -0
  71. package/test/tool-input-preview.test.ts +0 -111
  72. package/test/tool-input-prompt-formatters.test.ts +115 -0
  73. package/src/forwarded-permissions/polling.ts +0 -379
package/CHANGELOG.md CHANGED
@@ -5,6 +5,58 @@ 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
+ ## [10.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.0.0...pi-permission-system-v10.1.0) (2026-06-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * add DecisionReporter and GateDecisionReporter ([530211d](https://github.com/gotgenes/pi-packages/commit/530211da9158a012e86f37311e326f4e2b571c55))
14
+ * add GatePrompter and SessionApprovalRecorder session roles ([2f761e4](https://github.com/gotgenes/pi-packages/commit/2f761e44fd07c98fe98147275f75fc170163b06d))
15
+ * add GateRunner class consolidating gate dispatch ([a390558](https://github.com/gotgenes/pi-packages/commit/a390558f19723fa911924b2d1878d4d874d6966d))
16
+ * add getToolPreviewLimits and getInfrastructureReadDirs to PermissionSession ([#327](https://github.com/gotgenes/pi-packages/issues/327)) ([a0bf166](https://github.com/gotgenes/pi-packages/commit/a0bf1662119eeeb4b390c668c6993c7ff87194bf))
17
+ * add PermissionResolver.resolve to PermissionSession ([c922bbd](https://github.com/gotgenes/pi-packages/commit/c922bbddcfb47a2ec88d349f3a797b633bd58f45))
18
+ * add skill_input denial context ([#326](https://github.com/gotgenes/pi-packages/issues/326)) ([71e9d28](https://github.com/gotgenes/pi-packages/commit/71e9d28c5f6c8a09d2bfa9fe21cca6c55948898b))
19
+ * introduce SkillInputGatePipeline collaborator ([#329](https://github.com/gotgenes/pi-packages/issues/329)) ([4ddd5af](https://github.com/gotgenes/pi-packages/commit/4ddd5af1c476ab3c3eb29ce456a33b273c386ca0))
20
+ * introduce ToolCallGatePipeline collaborator ([#327](https://github.com/gotgenes/pi-packages/issues/327)) ([3a87727](https://github.com/gotgenes/pi-packages/commit/3a877274091bfc2db3998ca915f76ecbdd2ac1e7))
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * drop vestigial events field; document makeReporter in package skill ([9e0a8a7](https://github.com/gotgenes/pi-packages/commit/9e0a8a7d89b4c081d21465e02b7aa77c18ddd1b0))
26
+
27
+
28
+ ### Documentation
29
+
30
+ * document SkillInputGatePipeline in architecture and package skill ([#329](https://github.com/gotgenes/pi-packages/issues/329)) ([9193c86](https://github.com/gotgenes/pi-packages/commit/9193c86ecc2fa6b18da6a7c7e4a9f9efbbc807ed))
31
+ * record the composition-root collaborator extraction ([#320](https://github.com/gotgenes/pi-packages/issues/320)) ([dab8890](https://github.com/gotgenes/pi-packages/commit/dab8890df05e003bb9136924ab2d344c7fe69319))
32
+ * standardize and correct package READMEs ([4c270ad](https://github.com/gotgenes/pi-packages/commit/4c270adac97ca816fa1889a879d1d4fe19cdd464))
33
+
34
+ ## [10.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v9.2.0...pi-permission-system-v10.0.0) (2026-06-02)
35
+
36
+
37
+ ### ⚠ BREAKING CHANGES
38
+
39
+ * **pi-permission-system:** the permissions:ready event payload no longer includes protocolVersion. Consumers that read it must rely on package semver instead.
40
+
41
+ ### Features
42
+
43
+ * **pi-permission-manager:** broadcast permission prompts on permissions:prompt channel ([8540f3b](https://github.com/gotgenes/pi-packages/commit/8540f3b462b76a4789c4c17a75fadf254ae39feb))
44
+ * **pi-permission-system:** drop protocolVersion from permissions:ready ([6728a93](https://github.com/gotgenes/pi-packages/commit/6728a93af7edbc6953d20f448f1c3f54f9b7893f))
45
+ * **pi-permission-system:** harden prompt broadcasts ([067bafd](https://github.com/gotgenes/pi-packages/commit/067bafd80ef983fd8b9ab00914cf1cec9b6db915))
46
+ * **pi-permission-system:** make ready and decision broadcasts best-effort ([00a895f](https://github.com/gotgenes/pi-packages/commit/00a895f9377bcb7b598acbc6c95d7bc7cc83c515))
47
+ * **pi-permission-system:** preserve display fields for forwarded prompts ([9970912](https://github.com/gotgenes/pi-packages/commit/997091228736bbd4395d8bd16aeb9f4a4ae7e0b2))
48
+ * **pi-permission-system:** slim ui_prompt payload and centralize construction ([7a1ec56](https://github.com/gotgenes/pi-packages/commit/7a1ec56a827e90fe80a0f7de48e1222b5271700d))
49
+
50
+
51
+ ### Bug Fixes
52
+
53
+ * **pi-permission-system:** drop manual CHANGELOG Unreleased section ([f14e4f5](https://github.com/gotgenes/pi-packages/commit/f14e4f5d9b5ae1d6b207a913aefdd71980a46dd6))
54
+
55
+
56
+ ### Documentation
57
+
58
+ * **pi-permission-system:** document the lean ui_prompt contract ([0b3c11c](https://github.com/gotgenes/pi-packages/commit/0b3c11c57a7b718d2f73a184802f8c5dcb95fbe7))
59
+
8
60
  ## [9.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v9.1.0...pi-permission-system-v9.2.0) (2026-06-02)
9
61
 
10
62
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # @gotgenes/pi-permission-system
6
6
 
7
- [![npm version](https://img.shields.io/npm/v/@gotgenes/pi-permission-system?style=flat&logo=npm&logoColor=white)](https://www.npmjs.com/package/@gotgenes/pi-permission-system) [![CI](https://img.shields.io/github/actions/workflow/status/gotgenes/pi-permission-system/ci.yml?style=flat&logo=github&label=CI)](https://github.com/gotgenes/pi-permission-system/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Pi Package](https://img.shields.io/badge/Pi-Package-6366F1?style=flat)](https://pi.mariozechner.at/)
7
+ [![npm version](https://img.shields.io/npm/v/@gotgenes/pi-permission-system?style=flat&logo=npm&logoColor=white)](https://www.npmjs.com/package/@gotgenes/pi-permission-system) [![CI](https://img.shields.io/github/actions/workflow/status/gotgenes/pi-packages/ci.yml?style=flat&logo=github&label=CI)](https://github.com/gotgenes/pi-packages/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![pnpm](https://img.shields.io/badge/pnpm-%3E%3D11-F69220?style=flat&logo=pnpm&logoColor=white)](https://pnpm.io/) [![Pi Package](https://img.shields.io/badge/Pi-Package-6366F1?style=flat)](https://pi.mariozechner.at/)
8
8
 
9
9
  Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) coding agent that provides centralized, deterministic permission gates over tool, bash, MCP, skill, and special operations.
10
10
 
@@ -20,6 +20,7 @@ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) codin
20
20
  - **Protects sensitive file patterns** — cross-cutting `path` rules deny `.env`, `~/.ssh/*`, etc. across all tools and bash at once
21
21
  - **Guards external paths** — prompts before file tools or bash commands reach outside `cwd`
22
22
  - **Forwards prompts from subagents** — `ask` policies work even in non-UI execution contexts
23
+ - **Broadcasts UI prompt events** — `permissions:ui_prompt` fires only when the permission system is about to invoke the active user-facing permission UI
23
24
  - **Native [`@gotgenes/pi-subagents`](https://github.com/gotgenes/pi-subagents) integration** — in-process child sessions register with the permission system automatically, enabling per-agent policy enforcement and `ask`-state forwarding to the parent UI without configuration
24
25
 
25
26
  ## Install
@@ -89,16 +90,16 @@ For the full reference — all surfaces, runtime knobs, per-agent overrides, mer
89
90
 
90
91
  ## Documentation
91
92
 
92
- | Document | Contents |
93
- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
94
- | [docs/configuration.md](docs/configuration.md) | Full policy reference, runtime knobs, per-agent overrides, recipes |
95
- | [docs/session-approvals.md](docs/session-approvals.md) | Session-scoped rules, pattern suggestions, bash arity table |
96
- | [docs/cross-extension-api.md](docs/cross-extension-api.md) | Cross-extension service accessor, event bus integration, decision broadcasts |
97
- | [docs/subagent-integration.md](docs/subagent-integration.md) | Permission forwarding, coexistence with subagent extensions |
98
- | [docs/guides/permission-frontmatter-for-subagent-extensions.md](docs/guides/permission-frontmatter-for-subagent-extensions.md) | Convention guide for subagent extension authors |
99
- | [docs/opencode-compatibility.md](docs/opencode-compatibility.md) | OpenCode compatibility — shared concepts, divergences, porting guide |
100
- | [docs/troubleshooting.md](docs/troubleshooting.md) | Common issues, diagnostic logging, threat model |
101
- | [docs/migration/legacy-to-flat.md](docs/migration/legacy-to-flat.md) | Migration from pre-v2 config layout |
93
+ | Document | Contents |
94
+ | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
95
+ | [docs/configuration.md](docs/configuration.md) | Full policy reference, runtime knobs, per-agent overrides, recipes |
96
+ | [docs/session-approvals.md](docs/session-approvals.md) | Session-scoped rules, pattern suggestions, bash arity table |
97
+ | [docs/cross-extension-api.md](docs/cross-extension-api.md) | Cross-extension service accessor, event bus integration, prompt and decision broadcasts |
98
+ | [docs/subagent-integration.md](docs/subagent-integration.md) | Permission forwarding, coexistence with subagent extensions |
99
+ | [docs/guides/permission-frontmatter-for-subagent-extensions.md](docs/guides/permission-frontmatter-for-subagent-extensions.md) | Convention guide for subagent extension authors |
100
+ | [docs/opencode-compatibility.md](docs/opencode-compatibility.md) | OpenCode compatibility — shared concepts, divergences, porting guide |
101
+ | [docs/troubleshooting.md](docs/troubleshooting.md) | Common issues, diagnostic logging, threat model |
102
+ | [docs/migration/legacy-to-flat.md](docs/migration/legacy-to-flat.md) | Migration from pre-v2 config layout |
102
103
 
103
104
  ## Development
104
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "9.2.0",
3
+ "version": "10.1.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,28 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import type { GateHandlerSession } from "./gate-handler-session";
4
+ import type {
5
+ SkillPermissionChecker,
6
+ SkillPromptEntry,
7
+ } from "./skill-prompt-sanitizer";
8
+ import type { PermissionState } from "./types";
9
+
10
+ /**
11
+ * The session surface `AgentPrepHandler` invokes during `before_agent_start`:
12
+ * bind context + identify the agent (via {@link GateHandlerSession}), check
13
+ * skill permissions for prompt sanitization (via {@link SkillPermissionChecker}),
14
+ * refresh config, decide tool exposure, manage the active-tools / prompt-state
15
+ * cache keys, and store the resolved skill entries.
16
+ */
17
+ export interface AgentPrepSession
18
+ extends GateHandlerSession,
19
+ SkillPermissionChecker {
20
+ refreshConfig(ctx?: ExtensionContext): void;
21
+ getToolPermission(toolName: string, agentName?: string): PermissionState;
22
+ shouldUpdateActiveTools(cacheKey: string): boolean;
23
+ commitActiveToolsCacheKey(cacheKey: string): void;
24
+ getPolicyCacheStamp(agentName?: string): string;
25
+ shouldUpdatePromptState(cacheKey: string): boolean;
26
+ commitPromptStateCacheKey(cacheKey: string): void;
27
+ setActiveSkillEntries(entries: SkillPromptEntry[]): void;
28
+ }
@@ -0,0 +1,41 @@
1
+ import {
2
+ emitDecisionEvent,
3
+ type PermissionDecisionEvent,
4
+ type PermissionEventBus,
5
+ } from "./permission-events";
6
+ import type { SessionLogger } from "./session-logger";
7
+
8
+ /**
9
+ * Reports a permission gate's outcome to the review log and the decision
10
+ * channel. Groups the two side effects that always travel together:
11
+ * writing a structured review-log entry and broadcasting a decision event.
12
+ */
13
+ export interface DecisionReporter {
14
+ writeReviewLog(event: string, details: Record<string, unknown>): void;
15
+ emitDecision(event: PermissionDecisionEvent): void;
16
+ }
17
+
18
+ /**
19
+ * Owns the `SessionLogger` and the event bus so neither the handler nor
20
+ * the runner has to reach through the session to its logger or close over
21
+ * the event bus directly.
22
+ *
23
+ * Built once in `PermissionGateHandler`'s constructor; shared between
24
+ * `handleToolCall` (gate runner + bypass branch) and `handleInput`.
25
+ *
26
+ * Answers "who owns the event bus" — the reporter does, not the session.
27
+ */
28
+ export class GateDecisionReporter implements DecisionReporter {
29
+ constructor(
30
+ private readonly logger: SessionLogger,
31
+ private readonly events: PermissionEventBus,
32
+ ) {}
33
+
34
+ writeReviewLog(event: string, details: Record<string, unknown>): void {
35
+ this.logger.review(event, details);
36
+ }
37
+
38
+ emitDecision(event: PermissionDecisionEvent): void {
39
+ emitDecisionEvent(this.events, event);
40
+ }
41
+ }
@@ -45,6 +45,11 @@ export type DenialContext =
45
45
  skillName: string;
46
46
  readPath: string;
47
47
  agentName?: string;
48
+ }
49
+ | {
50
+ kind: "skill_input";
51
+ skillName: string;
52
+ agentName?: string;
48
53
  };
49
54
 
50
55
  // ── Public formatter API ───────────────────────────────────────────────────
@@ -91,6 +96,8 @@ function buildDenyBody(ctx: DenialContext): string {
91
96
  return `${subject(ctx.agentName)} is not permitted to access path '${ctx.pathValue}' via tool 'bash'.`;
92
97
  case "skill_read":
93
98
  return `${subject(ctx.agentName)} is not permitted to access skill '${ctx.skillName}' via '${ctx.readPath}'.`;
99
+ case "skill_input":
100
+ return `${subject(ctx.agentName)} is not permitted to access skill '${ctx.skillName}'.`;
94
101
  }
95
102
  }
96
103
 
@@ -184,6 +191,8 @@ function buildUnavailableBody(ctx: DenialContext): string {
184
191
  return `Bash command '${ctx.command}' accesses path '${ctx.pathValue}' which requires approval, but no interactive UI is available.`;
185
192
  case "skill_read":
186
193
  return `Accessing skill '${ctx.skillName}' requires approval, but no interactive UI is available.`;
194
+ case "skill_input":
195
+ return `Accessing skill '${ctx.skillName}' requires approval, but no interactive UI is available.`;
187
196
  }
188
197
  }
189
198
 
@@ -212,6 +221,8 @@ function buildUserDeniedBody(
212
221
  return `User denied path access for bash command '${ctx.command}' (path '${ctx.pathValue}').${reasonSuffix(denialReason)}`;
213
222
  case "skill_read":
214
223
  return `User denied access to skill '${ctx.skillName}'.${reasonSuffix(denialReason)}`;
224
+ case "skill_input":
225
+ return `User denied access to skill '${ctx.skillName}'.${reasonSuffix(denialReason)}`;
215
226
  }
216
227
  }
217
228
 
@@ -10,6 +10,7 @@ import {
10
10
  } from "node:fs";
11
11
 
12
12
  import { isPermissionDecisionState } from "#src/permission-dialog";
13
+ import type { PermissionUiPromptSource } from "#src/permission-events";
13
14
  import {
14
15
  createPermissionForwardingLocation,
15
16
  type ForwardedPermissionRequest,
@@ -17,6 +18,29 @@ import {
17
18
  type PermissionForwardingLocation,
18
19
  } from "#src/permission-forwarding";
19
20
 
21
+ /** Valid `permissions:ui_prompt` source values, for tolerant request reads. */
22
+ const UI_PROMPT_SOURCES = [
23
+ "tool_call",
24
+ "skill_input",
25
+ "skill_read",
26
+ "rpc_prompt",
27
+ ] as const satisfies readonly PermissionUiPromptSource[];
28
+
29
+ /** Narrow an unknown value to a valid prompt source, or `undefined`. */
30
+ function asUiPromptSource(
31
+ value: unknown,
32
+ ): PermissionUiPromptSource | undefined {
33
+ return UI_PROMPT_SOURCES.find((source) => source === value);
34
+ }
35
+
36
+ /** Narrow an unknown value to a nullable display string, or `undefined`. */
37
+ function asNullableDisplayString(value: unknown): string | null | undefined {
38
+ if (value === null || typeof value === "string") {
39
+ return value;
40
+ }
41
+ return undefined;
42
+ }
43
+
20
44
  type LogFn = (event: string, details: Record<string, unknown>) => void;
21
45
 
22
46
  export interface ForwardedPermissionLogger {
@@ -285,6 +309,11 @@ export function readForwardedPermissionRequest(
285
309
  targetSessionId: parsed.targetSessionId,
286
310
  requesterAgentName: parsed.requesterAgentName,
287
311
  message: parsed.message,
312
+ // Tolerant read: display fields are optional and may be absent (older
313
+ // child) or malformed; reconstruct only the well-formed ones.
314
+ source: asUiPromptSource(parsed.source),
315
+ surface: asNullableDisplayString(parsed.surface),
316
+ value: asNullableDisplayString(parsed.value),
288
317
  };
289
318
  } catch (error) {
290
319
  logPermissionForwardingWarning(