@gotgenes/pi-permission-system 5.7.0 → 5.8.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,20 @@ 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.8.0](https://github.com/gotgenes/pi-permission-system/compare/v5.7.0...v5.8.0) (2026-05-08)
9
+
10
+
11
+ ### Features
12
+
13
+ * add SessionLogger interface and createSessionLogger factory ([#127](https://github.com/gotgenes/pi-permission-system/issues/127)) ([8765ab8](https://github.com/gotgenes/pi-permission-system/commit/8765ab8cfe461324fc2a89c80486d3dde190d9d9))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * plan SessionLogger extraction ([#127](https://github.com/gotgenes/pi-permission-system/issues/127)) ([b13ac62](https://github.com/gotgenes/pi-permission-system/commit/b13ac62513d4b233ee4fc3f554324a54518f75ba))
19
+ * **retro:** add retro notes for issue [#126](https://github.com/gotgenes/pi-permission-system/issues/126) ([3d8a38a](https://github.com/gotgenes/pi-permission-system/commit/3d8a38a09f9dfd2570178c856aec260ebdba89b1))
20
+ * update architecture doc for SessionLogger ([#127](https://github.com/gotgenes/pi-permission-system/issues/127)) ([8fa4123](https://github.com/gotgenes/pi-permission-system/commit/8fa41237dc1b43cbe4487ba7d0acf75dc768ad9c))
21
+
8
22
  ## [5.7.0](https://github.com/gotgenes/pi-permission-system/compare/v5.6.3...v5.7.0) (2026-05-08)
9
23
 
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.7.0",
3
+ "version": "5.8.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -82,7 +82,7 @@ export async function handleInput(
82
82
  skillInputAutoApproved = decision.autoApproved === true;
83
83
  return decision;
84
84
  },
85
- writeLog: deps.writeReviewLog,
85
+ writeLog: deps.logger.review,
86
86
  logContext: {
87
87
  source: "skill_input",
88
88
  skillName,
@@ -33,11 +33,11 @@ export async function handleSessionStart(
33
33
  const policyIssues =
34
34
  deps.session.permissionManager.getConfigIssues(agentName);
35
35
  for (const issue of policyIssues) {
36
- deps.notifyWarning(issue);
36
+ deps.logger.warn(issue);
37
37
  }
38
38
 
39
39
  if (event.reason === "reload") {
40
- deps.writeDebugLog("lifecycle.reload", {
40
+ deps.logger.debug("lifecycle.reload", {
41
41
  triggeredBy: "session_start",
42
42
  reason: event.reason,
43
43
  cwd: ctx.cwd,
@@ -60,7 +60,7 @@ export async function handleResourcesDiscover(
60
60
  deps.session.activeSkillEntries = [];
61
61
  deps.session.lastActiveToolsCacheKey = null;
62
62
  deps.session.lastPromptStateCacheKey = null;
63
- deps.writeDebugLog("lifecycle.reload", {
63
+ deps.logger.debug("lifecycle.reload", {
64
64
  triggeredBy: "resources_discover",
65
65
  reason: event.reason,
66
66
  cwd: runtimeContext?.cwd ?? null,
@@ -91,7 +91,7 @@ export async function handleToolCall(
91
91
  deps.promptPermission(ctx, details);
92
92
  const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
93
93
  emitDecisionEvent(deps.events, e);
94
- const { writeReviewLog } = deps;
94
+ const { review: writeReviewLog } = deps.logger;
95
95
  const checkPermission: GateRunnerDeps["checkPermission"] = (
96
96
  surface,
97
97
  input,
@@ -4,6 +4,7 @@ import type { PermissionPromptDecision } from "../permission-dialog";
4
4
  import type { PermissionEventBus } from "../permission-events";
5
5
  import type { PermissionManager } from "../permission-manager";
6
6
  import type { SessionState } from "../runtime";
7
+ import type { SessionLogger } from "../session-logger";
7
8
 
8
9
  export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
9
10
 
@@ -37,9 +38,8 @@ export interface HandlerDeps {
37
38
  /** Mutable session state: permissionManager, sessionRules, cache keys. */
38
39
  readonly session: SessionState;
39
40
 
40
- // ── Logging (promoted from runtime) ───────────────────────────────────
41
- writeDebugLog(event: string, details?: Record<string, unknown>): void;
42
- writeReviewLog(event: string, details?: Record<string, unknown>): void;
41
+ // ── Logging ────────────────────────────────────────────────────────────
42
+ readonly logger: SessionLogger;
43
43
 
44
44
  // ── Immutable infrastructure paths ───────────────────────────────────
45
45
  readonly piInfrastructureDirs: readonly string[];
@@ -59,8 +59,6 @@ export interface HandlerDeps {
59
59
  // ── Config & lifecycle helpers ─────────────────────────────────────────
60
60
  /** Reload merged config from disk; optionally update the stored runtime context. */
61
61
  refreshExtensionConfig(ctx?: ExtensionContext): void;
62
- /** Show a warning notification to the user (no-op when no UI is available). */
63
- notifyWarning(message: string): void;
64
62
  /** Write the resolved config path set to the review and debug logs. */
65
63
  logResolvedConfigPaths(): void;
66
64
 
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  startForwardedPermissionPolling,
26
26
  stopForwardedPermissionPolling,
27
27
  } from "./runtime";
28
+ import { createSessionLogger } from "./session-logger";
28
29
  import { isSubagentExecutionContext } from "./subagent-context";
29
30
  import {
30
31
  canResolveAskPermissionRequest,
@@ -79,8 +80,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
79
80
 
80
81
  const deps: HandlerDeps = {
81
82
  session: runtime,
82
- writeDebugLog: (event, details) => runtime.writeDebugLog(event, details),
83
- writeReviewLog: (event, details) => runtime.writeReviewLog(event, details),
83
+ logger: createSessionLogger(runtime),
84
84
  piInfrastructureDirs: runtime.piInfrastructureDirs,
85
85
  getPiInfrastructureReadPaths: () =>
86
86
  runtime.config.piInfrastructureReadPaths ?? [],
@@ -88,8 +88,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
88
88
  createPermissionManagerForCwd: (cwd) =>
89
89
  createPermissionManagerForCwd(runtime.agentDir, cwd),
90
90
  refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
91
- notifyWarning: (message) =>
92
- runtime.runtimeContext?.ui.notify(message, "warning"),
93
91
  logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
94
92
  resolveAgentName: (ctx, systemPrompt) =>
95
93
  resolveAgentName(runtime, ctx, systemPrompt),
@@ -0,0 +1,29 @@
1
+ import type { ExtensionRuntime } from "./runtime";
2
+
3
+ /**
4
+ * Unified logging + notification surface for handler deps.
5
+ *
6
+ * Replaces three separate HandlerDeps fields (`writeDebugLog`,
7
+ * `writeReviewLog`, `notifyWarning`) with a single typed collaborator.
8
+ * This is an intermediate abstraction on the path to PermissionSession (#129).
9
+ */
10
+ export interface SessionLogger {
11
+ debug(event: string, details?: Record<string, unknown>): void;
12
+ review(event: string, details?: Record<string, unknown>): void;
13
+ warn(message: string): void;
14
+ }
15
+
16
+ /**
17
+ * Create a SessionLogger backed by an ExtensionRuntime.
18
+ *
19
+ * Captures `runtime` by reference so `warn` always reads the current
20
+ * `runtimeContext` at call time — matching the behavior of the inline
21
+ * closures it replaces in `src/index.ts`.
22
+ */
23
+ export function createSessionLogger(runtime: ExtensionRuntime): SessionLogger {
24
+ return {
25
+ debug: (event, details) => runtime.writeDebugLog(event, details),
26
+ review: (event, details) => runtime.writeReviewLog(event, details),
27
+ warn: (message) => runtime.runtimeContext?.ui.notify(message, "warning"),
28
+ };
29
+ }
@@ -77,13 +77,11 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
77
77
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
78
78
  return {
79
79
  session: makeSession(),
80
- writeDebugLog: vi.fn(),
81
- writeReviewLog: vi.fn(),
80
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
82
81
  piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
83
82
  getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
84
83
  createPermissionManagerForCwd: vi.fn().mockReturnValue(makePm()),
85
84
  refreshExtensionConfig: vi.fn(),
86
- notifyWarning: vi.fn(),
87
85
  logResolvedConfigPaths: vi.fn(),
88
86
  resolveAgentName: vi.fn().mockReturnValue(null),
89
87
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
@@ -68,14 +68,12 @@ function makeDeps(
68
68
  ): HandlerDeps {
69
69
  return {
70
70
  session: makeSession(state),
71
- writeDebugLog: vi.fn(),
72
- writeReviewLog: vi.fn(),
71
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
73
72
  piInfrastructureDirs: ["/test/agent"],
74
73
  getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
75
74
  events: makeEvents(),
76
75
  createPermissionManagerForCwd: vi.fn(),
77
76
  refreshExtensionConfig: vi.fn(),
78
- notifyWarning: vi.fn(),
79
77
  logResolvedConfigPaths: vi.fn(),
80
78
  resolveAgentName: vi.fn().mockReturnValue(null),
81
79
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
@@ -56,13 +56,11 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
56
56
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
57
57
  return {
58
58
  session: makeSession(),
59
- writeDebugLog: vi.fn(),
60
- writeReviewLog: vi.fn(),
59
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
61
60
  piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
62
61
  getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
63
62
  createPermissionManagerForCwd: vi.fn(),
64
63
  refreshExtensionConfig: vi.fn(),
65
- notifyWarning: vi.fn(),
66
64
  logResolvedConfigPaths: vi.fn(),
67
65
  resolveAgentName: vi.fn().mockReturnValue(null),
68
66
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
@@ -83,15 +83,13 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
83
83
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
84
84
  return {
85
85
  session: makeSession(),
86
- writeDebugLog: vi.fn(),
87
- writeReviewLog: vi.fn(),
86
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
88
87
  piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
89
88
  getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
90
89
  createPermissionManagerForCwd: vi
91
90
  .fn()
92
91
  .mockReturnValue(makePermissionManager()),
93
92
  refreshExtensionConfig: vi.fn(),
94
- notifyWarning: vi.fn(),
95
93
  logResolvedConfigPaths: vi.fn(),
96
94
  resolveAgentName: vi.fn().mockReturnValue(null),
97
95
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
@@ -189,21 +187,21 @@ describe("handleSessionStart", () => {
189
187
  createPermissionManagerForCwd: vi.fn().mockReturnValue(pm),
190
188
  });
191
189
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
192
- expect(deps.notifyWarning).toHaveBeenCalledWith("issue A");
193
- expect(deps.notifyWarning).toHaveBeenCalledWith("issue B");
190
+ expect(deps.logger.warn).toHaveBeenCalledWith("issue A");
191
+ expect(deps.logger.warn).toHaveBeenCalledWith("issue B");
194
192
  });
195
193
 
196
194
  it("does not call notifyWarning when there are no policy issues", async () => {
197
195
  const deps = makeDeps();
198
196
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
199
- expect(deps.notifyWarning).not.toHaveBeenCalled();
197
+ expect(deps.logger.warn).not.toHaveBeenCalled();
200
198
  });
201
199
 
202
200
  it("writes lifecycle.reload debug log when reason is reload", async () => {
203
201
  const ctx = makeCtx({ cwd: "/proj" });
204
202
  const deps = makeDeps();
205
203
  await handleSessionStart(deps, { reason: "reload" }, ctx);
206
- expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
204
+ expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
207
205
  triggeredBy: "session_start",
208
206
  reason: "reload",
209
207
  cwd: "/proj",
@@ -213,7 +211,7 @@ describe("handleSessionStart", () => {
213
211
  it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
214
212
  const deps = makeDeps();
215
213
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
216
- expect(deps.writeDebugLog).not.toHaveBeenCalled();
214
+ expect(deps.logger.debug).not.toHaveBeenCalled();
217
215
  });
218
216
  });
219
217
 
@@ -224,7 +222,7 @@ describe("handleResourcesDiscover", () => {
224
222
  const deps = makeDeps();
225
223
  await handleResourcesDiscover(deps, { reason: "startup" });
226
224
  expect(deps.createPermissionManagerForCwd).not.toHaveBeenCalled();
227
- expect(deps.writeDebugLog).not.toHaveBeenCalled();
225
+ expect(deps.logger.debug).not.toHaveBeenCalled();
228
226
  });
229
227
 
230
228
  it("creates and stores a new PM using runtimeContext.cwd on reload", async () => {
@@ -259,7 +257,7 @@ describe("handleResourcesDiscover", () => {
259
257
  const ctx = makeCtx({ cwd: "/proj" });
260
258
  const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
261
259
  await handleResourcesDiscover(deps, { reason: "reload" });
262
- expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
260
+ expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
263
261
  triggeredBy: "resources_discover",
264
262
  reason: "reload",
265
263
  cwd: "/proj",
@@ -269,7 +267,7 @@ describe("handleResourcesDiscover", () => {
269
267
  it("logs cwd as null when runtimeContext is null on reload", async () => {
270
268
  const deps = makeDeps();
271
269
  await handleResourcesDiscover(deps, { reason: "reload" });
272
- expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
270
+ expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
273
271
  triggeredBy: "resources_discover",
274
272
  reason: "reload",
275
273
  cwd: null,
@@ -89,14 +89,12 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
89
89
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
90
90
  return {
91
91
  session: makeSession(),
92
- writeDebugLog: vi.fn(),
93
- writeReviewLog: vi.fn(),
92
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
94
93
  piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
95
94
  getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
96
95
  events: makeEvents(),
97
96
  createPermissionManagerForCwd: vi.fn(),
98
97
  refreshExtensionConfig: vi.fn(),
99
- notifyWarning: vi.fn(),
100
98
  logResolvedConfigPaths: vi.fn(),
101
99
  resolveAgentName: vi.fn().mockReturnValue(null),
102
100
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
@@ -77,13 +77,11 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
77
77
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
78
78
  return {
79
79
  session: makeSession(),
80
- writeDebugLog: vi.fn(),
81
- writeReviewLog: vi.fn(),
80
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
82
81
  piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
83
82
  getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
84
83
  createPermissionManagerForCwd: vi.fn(),
85
84
  refreshExtensionConfig: vi.fn(),
86
- notifyWarning: vi.fn(),
87
85
  logResolvedConfigPaths: vi.fn(),
88
86
  resolveAgentName: vi.fn().mockReturnValue(null),
89
87
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { ExtensionRuntime } from "../src/runtime";
3
+ import { createSessionLogger } from "../src/session-logger";
4
+
5
+ // ── helpers ────────────────────────────────────────────────────────────────
6
+
7
+ function makeRuntime(
8
+ overrides: Partial<ExtensionRuntime> = {},
9
+ ): ExtensionRuntime {
10
+ return {
11
+ runtimeContext: null,
12
+ writeDebugLog: vi.fn(),
13
+ writeReviewLog: vi.fn(),
14
+ ...overrides,
15
+ } as unknown as ExtensionRuntime;
16
+ }
17
+
18
+ // ── createSessionLogger ────────────────────────────────────────────────────
19
+
20
+ describe("createSessionLogger", () => {
21
+ describe("debug", () => {
22
+ it("delegates to runtime.writeDebugLog with event and details", () => {
23
+ const runtime = makeRuntime();
24
+ const logger = createSessionLogger(runtime);
25
+
26
+ logger.debug("test.event", { key: "value" });
27
+
28
+ expect(runtime.writeDebugLog).toHaveBeenCalledWith("test.event", {
29
+ key: "value",
30
+ });
31
+ });
32
+
33
+ it("delegates to runtime.writeDebugLog with event and no details", () => {
34
+ const runtime = makeRuntime();
35
+ const logger = createSessionLogger(runtime);
36
+
37
+ logger.debug("test.event");
38
+
39
+ expect(runtime.writeDebugLog).toHaveBeenCalledWith(
40
+ "test.event",
41
+ undefined,
42
+ );
43
+ });
44
+ });
45
+
46
+ describe("review", () => {
47
+ it("delegates to runtime.writeReviewLog with event and details", () => {
48
+ const runtime = makeRuntime();
49
+ const logger = createSessionLogger(runtime);
50
+
51
+ logger.review("permission.granted", { agentName: "coder" });
52
+
53
+ expect(runtime.writeReviewLog).toHaveBeenCalledWith(
54
+ "permission.granted",
55
+ { agentName: "coder" },
56
+ );
57
+ });
58
+
59
+ it("delegates to runtime.writeReviewLog with event and no details", () => {
60
+ const runtime = makeRuntime();
61
+ const logger = createSessionLogger(runtime);
62
+
63
+ logger.review("permission.granted");
64
+
65
+ expect(runtime.writeReviewLog).toHaveBeenCalledWith(
66
+ "permission.granted",
67
+ undefined,
68
+ );
69
+ });
70
+ });
71
+
72
+ describe("warn", () => {
73
+ it("calls ui.notify with the message and 'warning' severity when runtimeContext is present", () => {
74
+ const notify = vi.fn();
75
+ const runtime = makeRuntime({
76
+ runtimeContext: {
77
+ ui: { notify, setStatus: vi.fn(), select: vi.fn(), input: vi.fn() },
78
+ } as unknown as ExtensionRuntime["runtimeContext"],
79
+ });
80
+ const logger = createSessionLogger(runtime);
81
+
82
+ logger.warn("Something went wrong");
83
+
84
+ expect(notify).toHaveBeenCalledWith("Something went wrong", "warning");
85
+ });
86
+
87
+ it("does not throw when runtimeContext is null", () => {
88
+ const runtime = makeRuntime({ runtimeContext: null });
89
+ const logger = createSessionLogger(runtime);
90
+
91
+ expect(() => logger.warn("no-op warning")).not.toThrow();
92
+ });
93
+
94
+ it("reads runtimeContext at call time, not at creation time", () => {
95
+ const runtime = makeRuntime({ runtimeContext: null });
96
+ const logger = createSessionLogger(runtime);
97
+
98
+ // runtimeContext is null at creation — warn should be a no-op now
99
+ logger.warn("early warning");
100
+
101
+ // Later runtimeContext is set
102
+ const notify = vi.fn();
103
+ runtime.runtimeContext = {
104
+ ui: { notify, setStatus: vi.fn(), select: vi.fn(), input: vi.fn() },
105
+ } as unknown as ExtensionRuntime["runtimeContext"];
106
+
107
+ logger.warn("late warning");
108
+
109
+ expect(notify).toHaveBeenCalledOnce();
110
+ expect(notify).toHaveBeenCalledWith("late warning", "warning");
111
+ });
112
+ });
113
+ });