@gotgenes/pi-permission-system 5.6.3 → 5.7.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 CHANGED
@@ -5,6 +5,23 @@ 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
+ ## [5.7.0](https://github.com/gotgenes/pi-permission-system/compare/v5.6.3...v5.7.0) (2026-05-08)
9
+
10
+
11
+ ### Features
12
+
13
+ * extract ExtensionPaths value object ([#126](https://github.com/gotgenes/pi-permission-system/issues/126)) ([85bc347](https://github.com/gotgenes/pi-permission-system/commit/85bc347d3ed487210ffbed4c1c53616b5cf0d978))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * add handler decomposition plan ([#126](https://github.com/gotgenes/pi-permission-system/issues/126), [#127](https://github.com/gotgenes/pi-permission-system/issues/127), [#128](https://github.com/gotgenes/pi-permission-system/issues/128), [#129](https://github.com/gotgenes/pi-permission-system/issues/129), [#130](https://github.com/gotgenes/pi-permission-system/issues/130)) ([5a116a6](https://github.com/gotgenes/pi-permission-system/commit/5a116a6cf2f6ef29f5e6550821bb26b6e1c3a90f))
19
+ * add structural design heuristics, design-review skill, and plan-issue hook ([d8e3233](https://github.com/gotgenes/pi-permission-system/commit/d8e32330baa25fa5a5abaf75f8e442ce650fe5a9))
20
+ * extract code-style, testing, and markdown-conventions skills from AGENTS.md ([9d5ba7a](https://github.com/gotgenes/pi-permission-system/commit/9d5ba7a4a4a7a9e9fbdfe869fb42840238351b81))
21
+ * plan ExtensionPaths value object extraction ([#126](https://github.com/gotgenes/pi-permission-system/issues/126)) ([d76e6cc](https://github.com/gotgenes/pi-permission-system/commit/d76e6cc255dc124a7a914e8d178113e5b7c8bddd))
22
+ * rename target-architecture to architecture, strip progress indicators ([9776550](https://github.com/gotgenes/pi-permission-system/commit/9776550351f3b59f96bdad614cc5129a7be52a51))
23
+ * **retro:** add retro notes for issue [#110](https://github.com/gotgenes/pi-permission-system/issues/110) ([5597de3](https://github.com/gotgenes/pi-permission-system/commit/5597de3c9c64c3d672a1fc77ba7910e952545824))
24
+
8
25
  ## [5.6.3](https://github.com/gotgenes/pi-permission-system/compare/v5.6.2...v5.6.3) (2026-05-07)
9
26
 
10
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.6.3",
3
+ "version": "5.7.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,55 @@
1
+ import { join } from "node:path";
2
+ import { getGlobalLogsDir } from "./config-paths";
3
+ import { discoverGlobalNodeModulesRoot } from "./node-modules-discovery";
4
+
5
+ /**
6
+ * Immutable path constants derived from `agentDir` at construction time.
7
+ *
8
+ * Computed once at startup in `computeExtensionPaths()` and embedded into
9
+ * `ExtensionRuntime`. Later refactorings (#129 PermissionSession, #130
10
+ * handler classes) consume this as a single dep instead of individual fields.
11
+ */
12
+ export interface ExtensionPaths {
13
+ readonly agentDir: string;
14
+ readonly sessionsDir: string;
15
+ readonly subagentSessionsDir: string;
16
+ readonly forwardingDir: string;
17
+ readonly globalLogsDir: string;
18
+ /**
19
+ * Static Pi infrastructure directories used for external-directory
20
+ * read auto-allow. Computed once from `agentDir` and
21
+ * `discoverGlobalNodeModulesRoot()`. Config-based extras
22
+ * (`piInfrastructureReadPaths`) are read from `runtime.config` at
23
+ * call time in the handler so they pick up config reloads.
24
+ */
25
+ readonly piInfrastructureDirs: readonly string[];
26
+ }
27
+
28
+ /**
29
+ * Compute all immutable path constants from `agentDir`.
30
+ *
31
+ * Calls `discoverGlobalNodeModulesRoot()` internally so the result is
32
+ * self-contained. Call this once at extension startup, not at module scope.
33
+ */
34
+ export function computeExtensionPaths(agentDir: string): ExtensionPaths {
35
+ const sessionsDir = join(agentDir, "sessions");
36
+ const subagentSessionsDir = join(agentDir, "subagent-sessions");
37
+ const forwardingDir = join(sessionsDir, "permission-forwarding");
38
+ const globalLogsDir = getGlobalLogsDir(agentDir);
39
+
40
+ const globalNodeModulesRoot = discoverGlobalNodeModulesRoot();
41
+ const piInfrastructureDirs: string[] = [
42
+ agentDir,
43
+ join(agentDir, "git"),
44
+ ...(globalNodeModulesRoot ? [globalNodeModulesRoot] : []),
45
+ ];
46
+
47
+ return {
48
+ agentDir,
49
+ sessionsDir,
50
+ subagentSessionsDir,
51
+ forwardingDir,
52
+ globalLogsDir,
53
+ piInfrastructureDirs,
54
+ };
55
+ }
@@ -1,14 +1,14 @@
1
1
  import { getNonEmptyString, toRecord } from "../../common";
2
- import {
3
- extractExternalPathsFromBashCommand,
4
- formatBashExternalDirectoryAskPrompt,
5
- formatBashExternalDirectoryDenyReason,
6
- formatExternalDirectoryHardStopHint,
7
- } from "../../external-directory";
8
2
  import type { Rule } from "../../rule";
9
3
  import { deriveApprovalPattern } from "../../session-rules";
10
4
  import type { PermissionCheckResult } from "../../types";
5
+ import { extractExternalPathsFromBashCommand } from "./bash-path-extractor";
11
6
  import type { GateResult } from "./descriptor";
7
+ import {
8
+ formatBashExternalDirectoryAskPrompt,
9
+ formatBashExternalDirectoryDenyReason,
10
+ formatExternalDirectoryHardStopHint,
11
+ } from "./external-directory-messages";
12
12
  import type { ToolCallContext } from "./types";
13
13
 
14
14
  /** Function type for checkPermission used by the descriptor factory. */
@@ -4,7 +4,7 @@ import { basename } from "node:path";
4
4
  import {
5
5
  isPathOutsideWorkingDirectory,
6
6
  normalizePathForComparison,
7
- } from "./path-utils";
7
+ } from "../../path-utils";
8
8
 
9
9
  // ── tree-sitter-bash lazy parser ───────────────────────────────────────────
10
10
 
@@ -1,14 +1,16 @@
1
1
  import {
2
- formatExternalDirectoryAskPrompt,
3
- formatExternalDirectoryDenyReason,
4
- formatExternalDirectoryUserDeniedReason,
5
2
  getPathBearingToolPath,
6
3
  isPathOutsideWorkingDirectory,
7
4
  isPiInfrastructureRead,
8
- } from "../../external-directory";
9
- import { normalizePathForComparison } from "../../path-utils";
5
+ normalizePathForComparison,
6
+ } from "../../path-utils";
10
7
  import { deriveApprovalPattern } from "../../session-rules";
11
8
  import type { GateResult } from "./descriptor";
9
+ import {
10
+ formatExternalDirectoryAskPrompt,
11
+ formatExternalDirectoryDenyReason,
12
+ formatExternalDirectoryUserDeniedReason,
13
+ } from "./external-directory-messages";
12
14
  import type { ToolCallContext } from "./types";
13
15
 
14
16
  /**
@@ -1,4 +1,4 @@
1
- import { PATH_BEARING_TOOLS } from "../../external-directory";
1
+ import { PATH_BEARING_TOOLS } from "../../path-utils";
2
2
  import { suggestSessionPattern } from "../../pattern-suggest";
3
3
  import {
4
4
  formatAskPrompt,
@@ -42,7 +42,7 @@ export interface HandlerDeps {
42
42
  writeReviewLog(event: string, details?: Record<string, unknown>): void;
43
43
 
44
44
  // ── Immutable infrastructure paths ───────────────────────────────────
45
- readonly piInfrastructureDirs: string[];
45
+ readonly piInfrastructureDirs: readonly string[];
46
46
  /** Returns config-derived infrastructure read paths (current at call time). */
47
47
  getPiInfrastructureReadPaths(): string[];
48
48
 
package/src/runtime.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  type ExtensionContext,
12
12
  getAgentDir,
13
13
  } from "@mariozechner/pi-coding-agent";
14
+
14
15
  import {
15
16
  getActiveAgentName,
16
17
  getActiveAgentNameFromSystemPrompt,
@@ -19,7 +20,6 @@ import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
19
20
  import {
20
21
  DEBUG_LOG_FILENAME,
21
22
  getGlobalConfigPath,
22
- getGlobalLogsDir,
23
23
  getLegacyExtensionConfigPath,
24
24
  getLegacyGlobalPolicyPath,
25
25
  getLegacyProjectPolicyPath,
@@ -34,7 +34,10 @@ import {
34
34
  normalizePermissionSystemConfig,
35
35
  type PermissionSystemExtensionConfig,
36
36
  } from "./extension-config";
37
- import { discoverGlobalNodeModulesRoot } from "./external-directory";
37
+ import { computeExtensionPaths, type ExtensionPaths } from "./extension-paths";
38
+
39
+ export type { ExtensionPaths } from "./extension-paths";
40
+
38
41
  import {
39
42
  type PermissionForwardingDeps,
40
43
  processForwardedPermissionRequests,
@@ -73,22 +76,7 @@ export interface SessionState {
73
76
  * Tests construct this via `createExtensionRuntime({ agentDir: tmpDir })`
74
77
  * without timing issues around `PI_CODING_AGENT_DIR`.
75
78
  */
76
- export interface ExtensionRuntime extends SessionState {
77
- // ── Immutable paths (derived from agentDir at construction) ───────────
78
- readonly agentDir: string;
79
- readonly sessionsDir: string;
80
- readonly subagentSessionsDir: string;
81
- readonly forwardingDir: string;
82
- readonly globalLogsDir: string;
83
- /**
84
- * Static Pi infrastructure directories used for external-directory
85
- * read auto-allow. Computed once at construction from `agentDir` and
86
- * `discoverGlobalNodeModulesRoot()`. Config-based extras
87
- * (`piInfrastructureReadPaths`) are read from `runtime.config` at
88
- * call time in the handler so they pick up config reloads.
89
- */
90
- readonly piInfrastructureDirs: string[];
91
-
79
+ export interface ExtensionRuntime extends ExtensionPaths, SessionState {
92
80
  // ── Mutable state (beyond SessionState) ───────────────────────────────────
93
81
  config: PermissionSystemExtensionConfig;
94
82
  lastConfigWarning: string | null;
@@ -353,27 +341,12 @@ export function createExtensionRuntime(options?: {
353
341
  agentDir?: string;
354
342
  }): ExtensionRuntime {
355
343
  const agentDir = options?.agentDir ?? getAgentDir();
356
- const sessionsDir = join(agentDir, "sessions");
357
- const subagentSessionsDir = join(agentDir, "subagent-sessions");
358
- const forwardingDir = join(sessionsDir, "permission-forwarding");
359
- const globalLogsDir = getGlobalLogsDir(agentDir);
360
-
361
- const globalNodeModulesRoot = discoverGlobalNodeModulesRoot();
362
- const piInfrastructureDirs: string[] = [
363
- agentDir,
364
- join(agentDir, "git"),
365
- ...(globalNodeModulesRoot ? [globalNodeModulesRoot] : []),
366
- ];
344
+ const paths = computeExtensionPaths(agentDir);
367
345
 
368
346
  // Build a plain-object runtime first so the logger's `getConfig` closure
369
347
  // can reference `runtime.config` directly (always reads current value).
370
348
  const runtime: ExtensionRuntime = {
371
- agentDir,
372
- sessionsDir,
373
- subagentSessionsDir,
374
- forwardingDir,
375
- globalLogsDir,
376
- piInfrastructureDirs,
349
+ ...paths,
377
350
  config: { ...DEFAULT_EXTENSION_CONFIG },
378
351
  runtimeContext: null,
379
352
  permissionManager: createPermissionManagerForCwd(agentDir, undefined),
@@ -395,10 +368,10 @@ export function createExtensionRuntime(options?: {
395
368
  const logger = createPermissionSystemLogger({
396
369
  // Reads runtime.config at call time — always current.
397
370
  getConfig: () => runtime.config,
398
- debugLogPath: join(globalLogsDir, DEBUG_LOG_FILENAME),
399
- reviewLogPath: join(globalLogsDir, REVIEW_LOG_FILENAME),
371
+ debugLogPath: join(paths.globalLogsDir, DEBUG_LOG_FILENAME),
372
+ reviewLogPath: join(paths.globalLogsDir, REVIEW_LOG_FILENAME),
400
373
  ensureLogsDirectory: () =>
401
- ensurePermissionSystemLogsDirectory(globalLogsDir),
374
+ ensurePermissionSystemLogsDirectory(paths.globalLogsDir),
402
375
  });
403
376
 
404
377
  const reportLoggingWarning = (message: string): void => {
@@ -9,11 +9,11 @@ vi.mock("node:os", () => {
9
9
  };
10
10
  });
11
11
 
12
+ import { extractExternalPathsFromBashCommand } from "../src/handlers/gates/bash-path-extractor";
12
13
  import {
13
- extractExternalPathsFromBashCommand,
14
14
  formatBashExternalDirectoryAskPrompt,
15
15
  formatBashExternalDirectoryDenyReason,
16
- } from "../src/external-directory";
16
+ } from "../src/handlers/gates/external-directory-messages";
17
17
 
18
18
  afterEach(() => {
19
19
  vi.restoreAllMocks();
@@ -0,0 +1,89 @@
1
+ import { join } from "node:path";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const { mockDiscoverGlobalNodeModulesRoot } = vi.hoisted(() => ({
5
+ mockDiscoverGlobalNodeModulesRoot: vi.fn<() => string | null>(),
6
+ }));
7
+
8
+ vi.mock("../src/node-modules-discovery", () => ({
9
+ discoverGlobalNodeModulesRoot: mockDiscoverGlobalNodeModulesRoot,
10
+ }));
11
+
12
+ import { getGlobalLogsDir } from "../src/config-paths";
13
+ import { computeExtensionPaths } from "../src/extension-paths";
14
+
15
+ describe("computeExtensionPaths", () => {
16
+ beforeEach(() => {
17
+ mockDiscoverGlobalNodeModulesRoot.mockReset();
18
+ mockDiscoverGlobalNodeModulesRoot.mockReturnValue(
19
+ "/mock/global/node_modules",
20
+ );
21
+ });
22
+
23
+ it("sets agentDir from argument", () => {
24
+ const paths = computeExtensionPaths("/test/agent");
25
+ expect(paths.agentDir).toBe("/test/agent");
26
+ });
27
+
28
+ it("derives sessionsDir as agentDir/sessions", () => {
29
+ const paths = computeExtensionPaths("/test/agent");
30
+ expect(paths.sessionsDir).toBe("/test/agent/sessions");
31
+ });
32
+
33
+ it("derives subagentSessionsDir as agentDir/subagent-sessions", () => {
34
+ const paths = computeExtensionPaths("/test/agent");
35
+ expect(paths.subagentSessionsDir).toBe("/test/agent/subagent-sessions");
36
+ });
37
+
38
+ it("derives forwardingDir as sessionsDir/permission-forwarding", () => {
39
+ const paths = computeExtensionPaths("/test/agent");
40
+ expect(paths.forwardingDir).toBe(
41
+ join("/test/agent/sessions", "permission-forwarding"),
42
+ );
43
+ });
44
+
45
+ it("derives globalLogsDir via getGlobalLogsDir(agentDir)", () => {
46
+ const paths = computeExtensionPaths("/test/agent");
47
+ expect(paths.globalLogsDir).toBe(getGlobalLogsDir("/test/agent"));
48
+ });
49
+
50
+ it("includes agentDir in piInfrastructureDirs", () => {
51
+ const paths = computeExtensionPaths("/test/agent");
52
+ expect(paths.piInfrastructureDirs).toContain("/test/agent");
53
+ });
54
+
55
+ it("includes agentDir/git in piInfrastructureDirs", () => {
56
+ const paths = computeExtensionPaths("/test/agent");
57
+ expect(paths.piInfrastructureDirs).toContain("/test/agent/git");
58
+ });
59
+
60
+ it("includes discovered global node_modules root in piInfrastructureDirs", () => {
61
+ const paths = computeExtensionPaths("/test/agent");
62
+ expect(paths.piInfrastructureDirs).toContain("/mock/global/node_modules");
63
+ });
64
+
65
+ it("omits global node_modules from piInfrastructureDirs when discovery returns null", () => {
66
+ mockDiscoverGlobalNodeModulesRoot.mockReturnValue(null);
67
+ const paths = computeExtensionPaths("/test/agent");
68
+ expect(paths.piInfrastructureDirs).toHaveLength(2);
69
+ expect(paths.piInfrastructureDirs).toContain("/test/agent");
70
+ expect(paths.piInfrastructureDirs).toContain("/test/agent/git");
71
+ });
72
+
73
+ it("all entries in piInfrastructureDirs are strings (no null)", () => {
74
+ mockDiscoverGlobalNodeModulesRoot.mockReturnValue(null);
75
+ const paths = computeExtensionPaths("/test/agent");
76
+ for (const dir of paths.piInfrastructureDirs) {
77
+ expect(typeof dir).toBe("string");
78
+ }
79
+ });
80
+
81
+ it("two calls with different agentDirs produce independent results", () => {
82
+ const a = computeExtensionPaths("/agent/a");
83
+ const b = computeExtensionPaths("/agent/b");
84
+ expect(a.agentDir).toBe("/agent/a");
85
+ expect(b.agentDir).toBe("/agent/b");
86
+ expect(a.sessionsDir).toBe("/agent/a/sessions");
87
+ expect(b.sessionsDir).toBe("/agent/b/sessions");
88
+ });
89
+ });
@@ -7,7 +7,7 @@ import {
7
7
  formatExternalDirectoryDenyReason,
8
8
  formatExternalDirectoryHardStopHint,
9
9
  formatExternalDirectoryUserDeniedReason,
10
- } from "../src/external-directory-messages";
10
+ } from "../../../src/handlers/gates/external-directory-messages";
11
11
 
12
12
  describe("formatExternalDirectoryHardStopHint", () => {
13
13
  test("returns the hard stop instruction string", () => {
@@ -14,10 +14,8 @@ vi.mock("node:child_process", () => ({
14
14
  default: { spawnSync: mockSpawnSync },
15
15
  }));
16
16
 
17
- import {
18
- discoverGlobalNodeModulesRoot,
19
- isPiInfrastructureRead,
20
- } from "../src/external-directory";
17
+ import { discoverGlobalNodeModulesRoot } from "../src/node-modules-discovery";
18
+ import { isPiInfrastructureRead } from "../src/path-utils";
21
19
 
22
20
  // ── discoverGlobalNodeModulesRoot ──────────────────────────────────────────
23
21
 
@@ -67,7 +67,7 @@ vi.mock("../src/subagent-context", () => ({
67
67
  isSubagentExecutionContext: vi.fn().mockReturnValue(false),
68
68
  }));
69
69
 
70
- vi.mock("../src/external-directory", () => ({
70
+ vi.mock("../src/node-modules-discovery", () => ({
71
71
  discoverGlobalNodeModulesRoot: mockDiscoverGlobalNodeModulesRoot,
72
72
  }));
73
73
 
@@ -1,24 +0,0 @@
1
- export {
2
- extractExternalPathsFromBashCommand,
3
- resetParserForTesting,
4
- } from "./bash-path-extractor";
5
- export {
6
- formatBashExternalDirectoryAskPrompt,
7
- formatBashExternalDirectoryDenyReason,
8
- formatExternalDirectoryAskPrompt,
9
- formatExternalDirectoryDenyReason,
10
- formatExternalDirectoryHardStopHint,
11
- formatExternalDirectoryUserDeniedReason,
12
- } from "./external-directory-messages";
13
- export { discoverGlobalNodeModulesRoot } from "./node-modules-discovery";
14
- export {
15
- getPathBearingToolPath,
16
- isPathOutsideWorkingDirectory,
17
- isPathWithinDirectory,
18
- isPiInfrastructureRead,
19
- isSafeSystemPath,
20
- normalizePathForComparison,
21
- PATH_BEARING_TOOLS,
22
- READ_ONLY_PATH_BEARING_TOOLS,
23
- SAFE_SYSTEM_PATHS,
24
- } from "./path-utils";