@gotgenes/pi-permission-system 7.3.0 → 7.3.2

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,25 @@ 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
+ ## [7.3.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.3.1...pi-permission-system-v7.3.2) (2026-05-27)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * replace \n with <br/> in Mermaid node labels ([3312a45](https://github.com/gotgenes/pi-packages/commit/3312a4559100cf9ae923f67819653b5a99fceb12))
14
+
15
+ ## [7.3.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.3.0...pi-permission-system-v7.3.1) (2026-05-26)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * resolve pre-existing lint errors in pi-autoformat and pi-permission-system ([68fd516](https://github.com/gotgenes/pi-packages/commit/68fd516e33ddbb9a5e37ef19e949ee9ecdc37252))
21
+
22
+
23
+ ### Documentation
24
+
25
+ * update subagent integration docs for native permission bridge ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([0bd456b](https://github.com/gotgenes/pi-packages/commit/0bd456befa8ea6918e74f4393d844868795edc77))
26
+
8
27
  ## [7.3.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.2.0...pi-permission-system-v7.3.0) (2026-05-25)
9
28
 
10
29
 
package/README.md CHANGED
@@ -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
+ - **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
23
24
 
24
25
  ## Install
25
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "7.3.0",
3
+ "version": "7.3.2",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync } from "node:fs";
1
+ import { mkdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
@@ -50,11 +50,10 @@ export class AgentPrepHandler {
50
50
  event: BeforeAgentStartPayload,
51
51
  ctx: ExtensionContext,
52
52
  ): Promise<BeforeAgentStartEventResult> {
53
- const { session } = this;
54
- session.activate(ctx);
55
- session.refreshConfig(ctx);
53
+ this.session.activate(ctx);
54
+ this.session.refreshConfig(ctx);
56
55
 
57
- const agentName = session.resolveAgentName(ctx, event.systemPrompt);
56
+ const agentName = this.session.resolveAgentName(ctx, event.systemPrompt);
58
57
  const allTools = this.toolRegistry.getAll();
59
58
  const allowedTools: string[] = [];
60
59
 
@@ -65,7 +64,7 @@ export class AgentPrepHandler {
65
64
  }
66
65
  if (
67
66
  shouldExposeTool(toolName, agentName, (t, a) =>
68
- session.getToolPermission(t, a),
67
+ this.session.getToolPermission(t, a),
69
68
  )
70
69
  ) {
71
70
  allowedTools.push(toolName);
@@ -73,24 +72,24 @@ export class AgentPrepHandler {
73
72
  }
74
73
 
75
74
  const activeToolsCacheKey = createActiveToolsCacheKey(allowedTools);
76
- if (session.shouldUpdateActiveTools(activeToolsCacheKey)) {
75
+ if (this.session.shouldUpdateActiveTools(activeToolsCacheKey)) {
77
76
  this.toolRegistry.setActive(allowedTools);
78
- session.commitActiveToolsCacheKey(activeToolsCacheKey);
77
+ this.session.commitActiveToolsCacheKey(activeToolsCacheKey);
79
78
  }
80
79
 
81
80
  const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
82
81
  agentName,
83
82
  cwd: ctx.cwd,
84
- permissionStamp: session.getPolicyCacheStamp(agentName ?? undefined),
83
+ permissionStamp: this.session.getPolicyCacheStamp(agentName ?? undefined),
85
84
  systemPrompt: event.systemPrompt,
86
85
  allowedToolNames: allowedTools,
87
86
  });
88
87
 
89
- if (!session.shouldUpdatePromptState(promptStateCacheKey)) {
88
+ if (!this.session.shouldUpdatePromptState(promptStateCacheKey)) {
90
89
  return {};
91
90
  }
92
91
 
93
- session.commitPromptStateCacheKey(promptStateCacheKey);
92
+ this.session.commitPromptStateCacheKey(promptStateCacheKey);
94
93
 
95
94
  const toolPromptResult = sanitizeAvailableToolsSection(
96
95
  event.systemPrompt,
@@ -98,11 +97,11 @@ export class AgentPrepHandler {
98
97
  );
99
98
  const skillPromptResult = resolveSkillPromptEntries(
100
99
  toolPromptResult.prompt,
101
- session,
100
+ this.session,
102
101
  agentName,
103
102
  ctx.cwd,
104
103
  );
105
- session.setActiveSkillEntries(skillPromptResult.entries);
104
+ this.session.setActiveSkillEntries(skillPromptResult.entries);
106
105
 
107
106
  if (skillPromptResult.prompt !== event.systemPrompt) {
108
107
  return { systemPrompt: skillPromptResult.prompt };
@@ -48,14 +48,6 @@ function getParser(): Promise<TSParser> {
48
48
  return parserPromise;
49
49
  }
50
50
 
51
- /**
52
- * Reset the cached parser promise. Only used by tests to avoid
53
- * cross-test pollution or to inject a mock parser.
54
- */
55
- function resetParserForTesting(): void {
56
- parserPromise = null;
57
- }
58
-
59
51
  // ── AST walker ─────────────────────────────────────────────────────────────
60
52
 
61
53
  /**
@@ -30,19 +30,18 @@ export class SessionLifecycleHandler {
30
30
  event: SessionStartPayload,
31
31
  ctx: ExtensionContext,
32
32
  ): Promise<void> {
33
- const { session } = this;
34
- session.refreshConfig(ctx);
35
- session.resetForNewSession(ctx);
36
- session.logResolvedConfigPaths();
33
+ this.session.refreshConfig(ctx);
34
+ this.session.resetForNewSession(ctx);
35
+ this.session.logResolvedConfigPaths();
37
36
 
38
- const agentName = session.resolveAgentName(ctx);
39
- const policyIssues = session.getConfigIssues(agentName ?? undefined);
37
+ const agentName = this.session.resolveAgentName(ctx);
38
+ const policyIssues = this.session.getConfigIssues(agentName ?? undefined);
40
39
  for (const issue of policyIssues) {
41
- session.logger.warn(issue);
40
+ this.session.logger.warn(issue);
42
41
  }
43
42
 
44
43
  if (event.reason === "reload") {
45
- session.logger.debug("lifecycle.reload", {
44
+ this.session.logger.debug("lifecycle.reload", {
46
45
  triggeredBy: "session_start",
47
46
  reason: event.reason,
48
47
  cwd: ctx.cwd,
@@ -56,23 +55,21 @@ export class SessionLifecycleHandler {
56
55
  return Promise.resolve();
57
56
  }
58
57
 
59
- const { session } = this;
60
- session.reload();
61
- session.logger.debug("lifecycle.reload", {
58
+ this.session.reload();
59
+ this.session.logger.debug("lifecycle.reload", {
62
60
  triggeredBy: "resources_discover",
63
61
  reason: event.reason,
64
- cwd: session.getRuntimeContext()?.cwd ?? null,
62
+ cwd: this.session.getRuntimeContext()?.cwd ?? null,
65
63
  });
66
64
  return Promise.resolve();
67
65
  }
68
66
 
69
67
  handleSessionShutdown(): Promise<void> {
70
- const { session } = this;
71
- const ctx = session.getRuntimeContext();
68
+ const ctx = this.session.getRuntimeContext();
72
69
  if (ctx) {
73
70
  ctx.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
74
71
  }
75
- session.shutdown();
72
+ this.session.shutdown();
76
73
  this.cleanupRpc();
77
74
  return Promise.resolve();
78
75
  }
@@ -56,10 +56,9 @@ export class PermissionGateHandler {
56
56
  event: unknown,
57
57
  ctx: ExtensionContext,
58
58
  ): Promise<{ block?: true; reason?: string }> {
59
- const { session } = this;
60
- session.activate(ctx);
59
+ this.session.activate(ctx);
61
60
 
62
- const agentName = session.resolveAgentName(ctx);
61
+ const agentName = this.session.resolveAgentName(ctx);
63
62
  const toolName = getToolNameFromValue(event);
64
63
 
65
64
  if (!toolName) {
@@ -99,22 +98,22 @@ export class PermissionGateHandler {
99
98
  };
100
99
 
101
100
  // ── Shared gate adapter closures ─────────────────────────────────────
102
- const canConfirm = () => session.canPrompt(ctx);
101
+ const canConfirm = () => this.session.canPrompt(ctx);
103
102
  const promptPermission = (details: PromptPermissionDetails) =>
104
- session.prompt(ctx, details);
103
+ this.session.prompt(ctx, details);
105
104
  const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
106
105
  emitDecisionEvent(this.events, e);
107
106
  // eslint-disable-next-line @typescript-eslint/unbound-method -- logger.review is a plain function closure; no this-binding issue
108
- const writeReviewLog = session.logger.review;
107
+ const writeReviewLog = this.session.logger.review;
109
108
  const checkPermission: GateRunnerDeps["checkPermission"] = (
110
109
  surface,
111
110
  input,
112
111
  agent,
113
112
  sessionRules,
114
- ) => session.checkPermission(surface, input, agent, sessionRules);
115
- const getSessionRuleset = () => session.getSessionRuleset();
113
+ ) => this.session.checkPermission(surface, input, agent, sessionRules);
114
+ const getSessionRuleset = () => this.session.getSessionRuleset();
116
115
  const approveSessionRule = (surface: string, pattern: string) =>
117
- session.approveSessionRule(surface, pattern);
116
+ this.session.approveSessionRule(surface, pattern);
118
117
 
119
118
  // ── Shared runner deps (built once, reused for all gates) ────────────
120
119
  const runnerDeps: GateRunnerDeps = {
@@ -129,7 +128,7 @@ export class PermissionGateHandler {
129
128
 
130
129
  // ── Skill-read gate (descriptor + runner) ───────────────────────────────
131
130
  const skillDescriptor = describeSkillReadGate(tcc, () =>
132
- session.getActiveSkillEntries(),
131
+ this.session.getActiveSkillEntries(),
133
132
  );
134
133
  if (skillDescriptor) {
135
134
  const skillResult = await runGateCheck(
@@ -165,8 +164,8 @@ export class PermissionGateHandler {
165
164
 
166
165
  // ── External-directory gate (descriptor + runner) ────────────────────────
167
166
  const infraDirs = [
168
- ...session.getInfrastructureDirs(),
169
- ...session.getInfrastructureReadPaths(),
167
+ ...this.session.getInfrastructureDirs(),
168
+ ...this.session.getInfrastructureReadPaths(),
170
169
  ];
171
170
  const extDirDesc = describeExternalDirectoryGate(tcc, infraDirs);
172
171
  if (extDirDesc) {
@@ -264,16 +263,15 @@ export class PermissionGateHandler {
264
263
  event: InputPayload,
265
264
  ctx: ExtensionContext,
266
265
  ): Promise<InputEventResult> {
267
- const { session } = this;
268
- session.activate(ctx);
266
+ this.session.activate(ctx);
269
267
 
270
268
  const skillName = extractSkillNameFromInput(event.text);
271
269
  if (!skillName) {
272
270
  return { action: "continue" };
273
271
  }
274
272
 
275
- const agentName = session.resolveAgentName(ctx);
276
- const check = session.checkPermission(
273
+ const agentName = this.session.resolveAgentName(ctx);
274
+ const check = this.session.checkPermission(
277
275
  "skill",
278
276
  { name: skillName },
279
277
  agentName ?? undefined,
@@ -290,14 +288,14 @@ export class PermissionGateHandler {
290
288
  skillName,
291
289
  agentName ?? undefined,
292
290
  );
293
- const skillInputCanConfirm = session.canPrompt(ctx);
291
+ const skillInputCanConfirm = this.session.canPrompt(ctx);
294
292
  let skillInputAutoApproved = false;
295
293
  const skillInputGate = await applyPermissionGate({
296
294
  state: check.state,
297
295
  canConfirm: skillInputCanConfirm,
298
296
  promptForApproval: async () => {
299
- const decision = await session.prompt(ctx, {
300
- requestId: session.createPermissionRequestId("skill-input"),
297
+ const decision = await this.session.prompt(ctx, {
298
+ requestId: this.session.createPermissionRequestId("skill-input"),
301
299
  source: "skill_input",
302
300
  agentName,
303
301
  message: skillInputMessage,
@@ -307,7 +305,7 @@ export class PermissionGateHandler {
307
305
  return decision;
308
306
  },
309
307
  // eslint-disable-next-line @typescript-eslint/unbound-method -- logger.review is a plain function closure; no this-binding issue
310
- writeLog: session.logger.review,
308
+ writeLog: this.session.logger.review,
311
309
  logContext: {
312
310
  source: "skill_input",
313
311
  skillName,
@@ -25,12 +25,6 @@ const APPROVE_OPTION = "Yes";
25
25
  const APPROVE_FOR_SESSION_OPTION = "Yes, for this session";
26
26
  const DENY_OPTION = "No";
27
27
  const DENY_WITH_REASON_OPTION = "No, provide reason";
28
- const PERMISSION_DECISION_OPTIONS = [
29
- APPROVE_OPTION,
30
- APPROVE_FOR_SESSION_OPTION,
31
- DENY_OPTION,
32
- DENY_WITH_REASON_OPTION,
33
- ] as const;
34
28
 
35
29
  export function normalizePermissionDenialReason(
36
30
  value: unknown,
@@ -120,7 +120,7 @@ export class PermissionManager {
120
120
  // existing patterns from lower scopes keep their earlier origin.
121
121
  if (!origins.has(surface)) origins.set(surface, new Map());
122
122
  for (const pattern of Object.keys(value)) {
123
- origins.get(surface)!.set(pattern, scopeName);
123
+ origins.get(surface)?.set(pattern, scopeName);
124
124
  }
125
125
  } else {
126
126
  // Full replacement: this scope takes over the entire surface entry.
@@ -1,4 +1,4 @@
1
- import type { FlatPermissionConfig, PermissionState } from "./types";
1
+ import type { FlatPermissionConfig } from "./types";
2
2
 
3
3
  /**
4
4
  * Deep-shallow merge two flat permission configs.
@@ -92,33 +92,6 @@ function parseSkillEntries(sectionBody: string): ParsedSkillPromptEntry[] {
92
92
  return entries;
93
93
  }
94
94
 
95
- function parseSkillPromptSection(prompt: string): SkillPromptSection | null {
96
- const start = prompt.indexOf(AVAILABLE_SKILLS_OPEN_TAG);
97
- if (start === -1) {
98
- return null;
99
- }
100
-
101
- const closeStart = prompt.indexOf(
102
- AVAILABLE_SKILLS_CLOSE_TAG,
103
- start + AVAILABLE_SKILLS_OPEN_TAG.length,
104
- );
105
- if (closeStart === -1) {
106
- return null;
107
- }
108
-
109
- const end = closeStart + AVAILABLE_SKILLS_CLOSE_TAG.length;
110
- const sectionBody = prompt.slice(
111
- start + AVAILABLE_SKILLS_OPEN_TAG.length,
112
- closeStart,
113
- );
114
-
115
- return {
116
- start,
117
- end,
118
- entries: parseSkillEntries(sectionBody),
119
- };
120
- }
121
-
122
95
  export function parseAllSkillPromptSections(
123
96
  prompt: string,
124
97
  ): SkillPromptSection[] {
package/src/types.ts CHANGED
@@ -14,17 +14,6 @@ export type FlatPermissionConfig = Record<
14
14
  PermissionState | Record<string, PermissionState>
15
15
  >;
16
16
 
17
- type BuiltInToolName =
18
- | "bash"
19
- | "read"
20
- | "write"
21
- | "edit"
22
- | "grep"
23
- | "find"
24
- | "ls";
25
-
26
- type SpecialPermissionName = "external_directory";
27
-
28
17
  /**
29
18
  * Per-scope permission config shape after loading and validation.
30
19
  * Holds only the flat permission map — all policy is expressed there.
@@ -7,7 +7,6 @@ import {
7
7
  } from "#src/handlers/before-agent-start";
8
8
  import type { PermissionSession } from "#src/permission-session";
9
9
  import type { ToolRegistry } from "#src/tool-registry";
10
- import type { PermissionState } from "#src/types";
11
10
 
12
11
  // ── SDK stubs ──────────────────────────────────────────────────────────────
13
12
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
@@ -101,12 +101,15 @@ describe("describeBashExternalDirectoryGate", () => {
101
101
  it("uses config-level checkPermission for the policy state", async () => {
102
102
  const checkPermission = vi
103
103
  .fn()
104
- .mockImplementation((surface: string, input: Record<string, unknown>) => {
105
- // Path-specific check returns session for coverage filtering
106
- if (input.path) return makeCheckResult("allow", { source: "special" });
107
- // Config-level check (no path) returns deny
108
- return makeCheckResult("deny");
109
- });
104
+ .mockImplementation(
105
+ (_surface: string, input: Record<string, unknown>) => {
106
+ // Path-specific check returns session for coverage filtering
107
+ if (input.path)
108
+ return makeCheckResult("allow", { source: "special" });
109
+ // Config-level check (no path) returns deny
110
+ return makeCheckResult("deny");
111
+ },
112
+ );
110
113
  const result = await describeBashExternalDirectoryGate(
111
114
  makeTcc(),
112
115
  checkPermission,
@@ -172,12 +175,14 @@ describe("describeBashExternalDirectoryGate", () => {
172
175
  it("only includes uncovered paths when some are session-covered", async () => {
173
176
  const checkPermission = vi
174
177
  .fn()
175
- .mockImplementation((surface: string, input: Record<string, unknown>) => {
176
- if (input.path === "/outside/a.ts") {
177
- return makeCheckResult("allow", { source: "session" });
178
- }
179
- return makeCheckResult("ask");
180
- });
178
+ .mockImplementation(
179
+ (_surface: string, input: Record<string, unknown>) => {
180
+ if (input.path === "/outside/a.ts") {
181
+ return makeCheckResult("allow", { source: "session" });
182
+ }
183
+ return makeCheckResult("ask");
184
+ },
185
+ );
181
186
  const result = await describeBashExternalDirectoryGate(
182
187
  makeTcc({ input: { command: "diff /outside/a.ts /outside/b.ts" } }),
183
188
  checkPermission,
@@ -1,6 +1,4 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
-
3
- import type { GateDescriptor } from "#src/handlers/gates/descriptor";
4
2
  import { describeSkillReadGate } from "#src/handlers/gates/skill-read";
5
3
  import type { ToolCallContext } from "#src/handlers/gates/types";
6
4
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
@@ -9,7 +9,6 @@ import type { PermissionDecisionEvent } from "#src/permission-events";
9
9
  import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
10
10
  import type { PermissionSession } from "#src/permission-session";
11
11
  import type { ToolRegistry } from "#src/tool-registry";
12
- import type { PermissionState } from "#src/types";
13
12
 
14
13
  // ── helpers ────────────────────────────────────────────────────────────────
15
14
 
@@ -7,7 +7,6 @@ import {
7
7
  } from "#src/handlers/permission-gate-handler";
8
8
  import type { PermissionSession } from "#src/permission-session";
9
9
  import type { ToolRegistry } from "#src/tool-registry";
10
- import type { PermissionState } from "#src/types";
11
10
 
12
11
  // ── helpers ────────────────────────────────────────────────────────────────
13
12
 
@@ -10,7 +10,7 @@ import type { PermissionDecisionEvent } from "#src/permission-events";
10
10
  import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
11
11
  import type { PermissionSession } from "#src/permission-session";
12
12
  import type { ToolRegistry } from "#src/tool-registry";
13
- import type { PermissionCheckResult, PermissionState } from "#src/types";
13
+ import type { PermissionCheckResult } from "#src/types";
14
14
 
15
15
  // ── helpers ────────────────────────────────────────────────────────────────
16
16
 
@@ -7,7 +7,7 @@ import {
7
7
  } from "#src/handlers/permission-gate-handler";
8
8
  import type { PermissionSession } from "#src/permission-session";
9
9
  import type { ToolRegistry } from "#src/tool-registry";
10
- import type { PermissionCheckResult, PermissionState } from "#src/types";
10
+ import type { PermissionCheckResult } from "#src/types";
11
11
 
12
12
  // ── SDK stubs ──────────────────────────────────────────────────────────────
13
13
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
@@ -264,7 +264,7 @@ describe("piPermissionSystemExtension ready event wiring", () => {
264
264
  mkdirSync(join(baseDir, "agents"), { recursive: true });
265
265
  writeFileSync(
266
266
  globalConfigPath,
267
- JSON.stringify({ permission: { "*": "ask" } }) + "\n",
267
+ `${JSON.stringify({ permission: { "*": "ask" } })}\n`,
268
268
  "utf8",
269
269
  );
270
270
  process.env.PI_CODING_AGENT_DIR = baseDir;
@@ -38,7 +38,6 @@ import {
38
38
  } from "#src/permission-session";
39
39
  import type { SessionLogger } from "#src/session-logger";
40
40
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
41
- import type { PermissionCheckResult } from "#src/types";
42
41
 
43
42
  function makeSkillEntry(
44
43
  name: string,