@gotgenes/pi-permission-system 5.5.0 → 5.6.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.
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
 
3
3
  import { toRecord } from "../common";
4
+ import { emitDecisionEvent } from "../permission-events";
4
5
  import {
5
6
  formatMissingToolNameReason,
6
7
  formatUnknownToolReason,
@@ -9,12 +10,15 @@ import {
9
10
  checkRequestedToolRegistration,
10
11
  getToolNameFromValue,
11
12
  } from "../tool-registry";
12
- import { evaluateBashExternalDirectoryGate } from "./gates/bash-external-directory";
13
- import { evaluateExternalDirectoryGate } from "./gates/external-directory";
14
- import { evaluateSkillReadGate } from "./gates/skill-read";
15
- import { evaluateToolGate } from "./gates/tool";
13
+ import { describeBashExternalDirectoryGate } from "./gates/bash-external-directory";
14
+ import type { GateRunnerDeps } from "./gates/descriptor";
15
+ import { isGateBypass } from "./gates/descriptor";
16
+ import { describeExternalDirectoryGate } from "./gates/external-directory";
17
+ import { runGateCheck } from "./gates/runner";
18
+ import { describeSkillReadGate } from "./gates/skill-read";
19
+ import { describeToolGate } from "./gates/tool";
16
20
  import type { ToolCallContext } from "./gates/types";
17
- import type { HandlerDeps } from "./types";
21
+ import type { HandlerDeps, PromptPermissionDetails } from "./types";
18
22
 
19
23
  /**
20
24
  * Extract the tool input from an event, checking both `input` and `arguments`
@@ -39,7 +43,7 @@ export async function handleToolCall(
39
43
  event: unknown,
40
44
  ctx: ExtensionContext,
41
45
  ): Promise<{ block?: true; reason?: string }> {
42
- deps.runtime.runtimeContext = ctx;
46
+ deps.session.runtimeContext = ctx;
43
47
  deps.startForwardedPermissionPolling(ctx);
44
48
 
45
49
  const agentName = deps.resolveAgentName(ctx);
@@ -81,26 +85,123 @@ export async function handleToolCall(
81
85
  cwd: ctx.cwd,
82
86
  };
83
87
 
84
- // ── Skill-read gate ──────────────────────────────────────────────────────
85
- const skillResult = await evaluateSkillReadGate(tcc, deps);
86
- if (skillResult?.action === "block") {
87
- return { block: true, reason: skillResult.reason };
88
+ // ── Shared gate adapter closures ───────────────────────────────────────
89
+ const canConfirm = () => deps.canRequestPermissionConfirmation(ctx);
90
+ const promptPermission = (details: PromptPermissionDetails) =>
91
+ deps.promptPermission(ctx, details);
92
+ const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
93
+ emitDecisionEvent(deps.events, e);
94
+ const { writeReviewLog } = deps;
95
+ const checkPermission: GateRunnerDeps["checkPermission"] = (
96
+ surface,
97
+ input,
98
+ agent,
99
+ sessionRules,
100
+ ) =>
101
+ deps.session.permissionManager.checkPermission(
102
+ surface,
103
+ input,
104
+ agent,
105
+ sessionRules,
106
+ );
107
+ const getSessionRuleset = () => deps.session.sessionRules.getRuleset();
108
+ const approveSessionRule = (surface: string, pattern: string) =>
109
+ deps.session.sessionRules.approve(surface, pattern);
110
+
111
+ // ── Shared runner deps (built once, reused for all gates) ─────────────
112
+ const runnerDeps: GateRunnerDeps = {
113
+ checkPermission,
114
+ getSessionRuleset,
115
+ approveSessionRule,
116
+ writeReviewLog,
117
+ emitDecision,
118
+ canConfirm,
119
+ promptPermission,
120
+ };
121
+
122
+ // ── Skill-read gate (descriptor + runner) ────────────────────────────────
123
+ const skillDescriptor = describeSkillReadGate(
124
+ tcc,
125
+ () => deps.session.activeSkillEntries,
126
+ );
127
+ if (skillDescriptor) {
128
+ const skillResult = await runGateCheck(
129
+ skillDescriptor,
130
+ tcc.agentName,
131
+ tcc.toolCallId,
132
+ runnerDeps,
133
+ );
134
+ if (skillResult.action === "block") {
135
+ return { block: true, reason: skillResult.reason };
136
+ }
88
137
  }
89
138
 
90
- // ── External-directory gate (file tools) ─────────────────────────────────
91
- const extDirResult = await evaluateExternalDirectoryGate(tcc, deps);
92
- if (extDirResult?.action === "block") {
93
- return { block: true, reason: extDirResult.reason };
139
+ // ── External-directory gate (descriptor + runner) ─────────────────────────
140
+ const infraDirs = [
141
+ ...deps.piInfrastructureDirs,
142
+ ...deps.getPiInfrastructureReadPaths(),
143
+ ];
144
+ const extDirDesc = describeExternalDirectoryGate(tcc, infraDirs);
145
+ if (extDirDesc) {
146
+ if (isGateBypass(extDirDesc)) {
147
+ if (extDirDesc.log) {
148
+ writeReviewLog(extDirDesc.log.event, extDirDesc.log.details);
149
+ }
150
+ if (extDirDesc.decision) {
151
+ emitDecision(extDirDesc.decision);
152
+ }
153
+ } else {
154
+ const extDirResult = await runGateCheck(
155
+ extDirDesc,
156
+ tcc.agentName,
157
+ tcc.toolCallId,
158
+ runnerDeps,
159
+ );
160
+ if (extDirResult.action === "block") {
161
+ return { block: true, reason: extDirResult.reason };
162
+ }
163
+ }
94
164
  }
95
165
 
96
- // ── Bash external-directory gate ─────────────────────────────────────────
97
- const bashExtResult = await evaluateBashExternalDirectoryGate(tcc, deps);
98
- if (bashExtResult?.action === "block") {
99
- return { block: true, reason: bashExtResult.reason };
166
+ // ── Bash external-directory gate (descriptor + runner) ─────────────────────
167
+ const bashExtDesc = await describeBashExternalDirectoryGate(
168
+ tcc,
169
+ checkPermission,
170
+ getSessionRuleset,
171
+ );
172
+ if (bashExtDesc) {
173
+ if (isGateBypass(bashExtDesc)) {
174
+ if (bashExtDesc.log) {
175
+ writeReviewLog(bashExtDesc.log.event, bashExtDesc.log.details);
176
+ }
177
+ } else {
178
+ const bashExtResult = await runGateCheck(
179
+ bashExtDesc,
180
+ tcc.agentName,
181
+ tcc.toolCallId,
182
+ runnerDeps,
183
+ );
184
+ if (bashExtResult.action === "block") {
185
+ return { block: true, reason: bashExtResult.reason };
186
+ }
187
+ }
100
188
  }
101
189
 
102
- // ── Normal tool permission gate ──────────────────────────────────────────
103
- const toolResult = await evaluateToolGate(tcc, deps);
190
+ // ── Normal tool permission gate (descriptor + runner) ───────────────────────────
191
+ const toolCheck = checkPermission(
192
+ tcc.toolName,
193
+ tcc.input,
194
+ tcc.agentName ?? undefined,
195
+ getSessionRuleset(),
196
+ );
197
+ const toolDescriptor = describeToolGate(tcc, toolCheck);
198
+ toolDescriptor.preCheck = toolCheck;
199
+ const toolResult = await runGateCheck(
200
+ toolDescriptor,
201
+ tcc.agentName,
202
+ tcc.toolCallId,
203
+ runnerDeps,
204
+ );
104
205
  if (toolResult.action === "block") {
105
206
  return { block: true, reason: toolResult.reason };
106
207
  }
@@ -3,7 +3,7 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
3
3
  import type { PermissionPromptDecision } from "../permission-dialog";
4
4
  import type { PermissionEventBus } from "../permission-events";
5
5
  import type { PermissionManager } from "../permission-manager";
6
- import type { ExtensionRuntime } from "../runtime";
6
+ import type { SessionState } from "../runtime";
7
7
 
8
8
  export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
9
9
 
@@ -27,13 +27,26 @@ export interface PromptPermissionDetails {
27
27
  /**
28
28
  * Explicit dependency bag passed to each extracted event handler.
29
29
  *
30
- * Mutable state lives in `runtime`; handlers read and write `deps.runtime.*`
31
- * directly instead of going through getter/setter pairs.
30
+ * Mutable session state lives in `session`; handlers read and write
31
+ * `deps.session.*` directly. Logging, infrastructure paths, and the
32
+ * event bus are promoted to top-level fields so handlers and gate
33
+ * adapters never reach through nested objects for leaf operations.
32
34
  */
33
35
  export interface HandlerDeps {
34
- // ── Runtime context ────────────────────────────────────────────────────
35
- /** All mutable extension state and log-writing methods. */
36
- readonly runtime: ExtensionRuntime;
36
+ // ── Session state ─────────────────────────────────────────────────────
37
+ /** Mutable session state: permissionManager, sessionRules, cache keys. */
38
+ readonly session: SessionState;
39
+
40
+ // ── Logging (promoted from runtime) ───────────────────────────────────
41
+ writeDebugLog(event: string, details?: Record<string, unknown>): void;
42
+ writeReviewLog(event: string, details?: Record<string, unknown>): void;
43
+
44
+ // ── Immutable infrastructure paths ───────────────────────────────────
45
+ readonly piInfrastructureDirs: string[];
46
+ /** Returns config-derived infrastructure read paths (current at call time). */
47
+ getPiInfrastructureReadPaths(): string[];
48
+
49
+ // ── Event bus ────────────────────────────────────────────────────────
37
50
  /** Event bus for emitting permissions:decision broadcast events. */
38
51
  readonly events: PermissionEventBus;
39
52
 
@@ -54,7 +67,7 @@ export interface HandlerDeps {
54
67
  // ── Permission helpers ─────────────────────────────────────────────────
55
68
  /**
56
69
  * Resolve the active agent name from the session context or system prompt.
57
- * Updates runtime.lastKnownActiveAgentName as a side effect.
70
+ * Updates session.lastKnownActiveAgentName as a side effect.
58
71
  */
59
72
  resolveAgentName(ctx: ExtensionContext, systemPrompt?: string): string | null;
60
73
  /** Whether the current context can show an interactive permission prompt. */
package/src/index.ts CHANGED
@@ -78,7 +78,12 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
78
78
  });
79
79
 
80
80
  const deps: HandlerDeps = {
81
- runtime,
81
+ session: runtime,
82
+ writeDebugLog: (event, details) => runtime.writeDebugLog(event, details),
83
+ writeReviewLog: (event, details) => runtime.writeReviewLog(event, details),
84
+ piInfrastructureDirs: runtime.piInfrastructureDirs,
85
+ getPiInfrastructureReadPaths: () =>
86
+ runtime.config.piInfrastructureReadPaths ?? [],
82
87
  events: pi.events,
83
88
  createPermissionManagerForCwd: (cwd) =>
84
89
  createPermissionManagerForCwd(runtime.agentDir, cwd),
package/src/runtime.ts CHANGED
@@ -47,6 +47,21 @@ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
47
47
  import { syncPermissionSystemStatus } from "./status";
48
48
  import { isSubagentExecutionContext } from "./subagent-context";
49
49
 
50
+ /**
51
+ * Mutable session state — the subset of ExtensionRuntime that handlers
52
+ * read and write. Lifecycle handlers reset fields here on session
53
+ * start/shutdown; gate adapters read permissionManager and sessionRules.
54
+ */
55
+ export interface SessionState {
56
+ runtimeContext: ExtensionContext | null;
57
+ permissionManager: PermissionManager;
58
+ readonly sessionRules: SessionRules;
59
+ activeSkillEntries: SkillPromptEntry[];
60
+ lastKnownActiveAgentName: string | null;
61
+ lastActiveToolsCacheKey: string | null;
62
+ lastPromptStateCacheKey: string | null;
63
+ }
64
+
50
65
  /**
51
66
  * Runtime context object created once inside `piPermissionSystemExtension()`.
52
67
  *
@@ -58,7 +73,7 @@ import { isSubagentExecutionContext } from "./subagent-context";
58
73
  * Tests construct this via `createExtensionRuntime({ agentDir: tmpDir })`
59
74
  * without timing issues around `PI_CODING_AGENT_DIR`.
60
75
  */
61
- export interface ExtensionRuntime {
76
+ export interface ExtensionRuntime extends SessionState {
62
77
  // ── Immutable paths (derived from agentDir at construction) ───────────
63
78
  readonly agentDir: string;
64
79
  readonly sessionsDir: string;
@@ -74,16 +89,9 @@ export interface ExtensionRuntime {
74
89
  */
75
90
  readonly piInfrastructureDirs: string[];
76
91
 
77
- // ── Mutable state ──────────────────────────────────────────────────────
92
+ // ── Mutable state (beyond SessionState) ───────────────────────────────────
78
93
  config: PermissionSystemExtensionConfig;
79
- runtimeContext: ExtensionContext | null;
80
- permissionManager: PermissionManager;
81
- activeSkillEntries: SkillPromptEntry[];
82
- lastKnownActiveAgentName: string | null;
83
- lastActiveToolsCacheKey: string | null;
84
- lastPromptStateCacheKey: string | null;
85
94
  lastConfigWarning: string | null;
86
- readonly sessionRules: SessionRules;
87
95
 
88
96
  // ── Forwarding polling state ───────────────────────────────────────────
89
97
  permissionForwardingContext: ExtensionContext | null;
@@ -7,7 +7,7 @@ import {
7
7
  } from "../../src/handlers/before-agent-start";
8
8
  import type { HandlerDeps } from "../../src/handlers/types";
9
9
  import type { PermissionManager } from "../../src/permission-manager";
10
- import type { ExtensionRuntime } from "../../src/runtime";
10
+ import type { SessionState } from "../../src/runtime";
11
11
  import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
12
12
 
13
13
  // ── SDK stubs ──────────────────────────────────────────────────────────────
@@ -57,40 +57,30 @@ function makePm(
57
57
  } as unknown as PermissionManager;
58
58
  }
59
59
 
60
- function makeRuntime(
61
- overrides: Partial<ExtensionRuntime> = {},
62
- ): ExtensionRuntime {
60
+ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
63
61
  return {
64
- agentDir: "/test/agent",
65
- sessionsDir: "/test/agent/sessions",
66
- subagentSessionsDir: "/test/agent/subagent-sessions",
67
- forwardingDir: "/test/agent/sessions/permission-forwarding",
68
- globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
69
- config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
70
62
  runtimeContext: null,
71
63
  permissionManager: makePm() as unknown as PermissionManager,
72
64
  activeSkillEntries: [] as SkillPromptEntry[],
73
65
  lastKnownActiveAgentName: null,
74
66
  lastActiveToolsCacheKey: null,
75
67
  lastPromptStateCacheKey: null,
76
- lastConfigWarning: null,
77
68
  sessionRules: {
78
69
  approve: vi.fn(),
79
70
  getRuleset: vi.fn().mockReturnValue([]),
80
71
  clear: vi.fn(),
81
- } as unknown as ExtensionRuntime["sessionRules"],
82
- permissionForwardingContext: null,
83
- permissionForwardingTimer: null,
84
- isProcessingForwardedRequests: false,
85
- writeDebugLog: vi.fn(),
86
- writeReviewLog: vi.fn(),
72
+ } as unknown as SessionState["sessionRules"],
87
73
  ...overrides,
88
- } as ExtensionRuntime;
74
+ };
89
75
  }
90
76
 
91
77
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
92
78
  return {
93
- runtime: makeRuntime(),
79
+ session: makeSession(),
80
+ writeDebugLog: vi.fn(),
81
+ writeReviewLog: vi.fn(),
82
+ piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
83
+ getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
94
84
  createPermissionManagerForCwd: vi.fn().mockReturnValue(makePm()),
95
85
  refreshExtensionConfig: vi.fn(),
96
86
  notifyWarning: vi.fn(),
@@ -176,7 +166,7 @@ describe("handleBeforeAgentStart", () => {
176
166
  it("filters out denied tools from allowed list", async () => {
177
167
  const pm = makePm("deny");
178
168
  const deps = makeDeps({
179
- runtime: makeRuntime({
169
+ session: makeSession({
180
170
  permissionManager: pm as unknown as PermissionManager,
181
171
  }),
182
172
  getAllTools: vi
@@ -191,7 +181,7 @@ describe("handleBeforeAgentStart", () => {
191
181
  it("includes allowed and ask tools in the active list", async () => {
192
182
  const pm = makePm("allow");
193
183
  const deps = makeDeps({
194
- runtime: makeRuntime({
184
+ session: makeSession({
195
185
  permissionManager: pm as unknown as PermissionManager,
196
186
  }),
197
187
  getAllTools: vi
@@ -207,7 +197,7 @@ describe("handleBeforeAgentStart", () => {
207
197
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
208
198
  });
209
199
  await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
210
- expect(deps.runtime.lastActiveToolsCacheKey).not.toBeNull();
200
+ expect(deps.session.lastActiveToolsCacheKey).not.toBeNull();
211
201
  });
212
202
 
213
203
  it("skips setActiveTools when cache key is unchanged", async () => {
@@ -217,7 +207,7 @@ describe("handleBeforeAgentStart", () => {
217
207
  );
218
208
  const key = createActiveToolsCacheKey(["read"]);
219
209
  const deps = makeDeps({
220
- runtime: makeRuntime({ lastActiveToolsCacheKey: key }),
210
+ session: makeSession({ lastActiveToolsCacheKey: key }),
221
211
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
222
212
  });
223
213
  await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
@@ -238,7 +228,7 @@ describe("handleBeforeAgentStart", () => {
238
228
  );
239
229
  // The prompt was modified, so systemPrompt should be returned
240
230
  expect(result).toHaveProperty("systemPrompt");
241
- expect(deps.runtime.lastPromptStateCacheKey).not.toBeNull();
231
+ expect(deps.session.lastPromptStateCacheKey).not.toBeNull();
242
232
  });
243
233
 
244
234
  it("returns empty object when systemPrompt is unchanged", async () => {
@@ -259,7 +249,7 @@ describe("handleBeforeAgentStart", () => {
259
249
  getAllTools: vi.fn().mockReturnValue([]),
260
250
  });
261
251
  await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
262
- expect(deps.runtime.activeSkillEntries).toEqual(expect.any(Array));
252
+ expect(deps.session.activeSkillEntries).toEqual(expect.any(Array));
263
253
  });
264
254
 
265
255
  it("returns empty object and skips prompt work when prompt cache key is unchanged", async () => {
@@ -277,7 +267,7 @@ describe("handleBeforeAgentStart", () => {
277
267
  allowedToolNames: allowedTools,
278
268
  });
279
269
  const deps = makeDeps({
280
- runtime: makeRuntime({
270
+ session: makeSession({
281
271
  permissionManager: pm as unknown as PermissionManager,
282
272
  lastPromptStateCacheKey: key,
283
273
  }),
@@ -286,6 +276,6 @@ describe("handleBeforeAgentStart", () => {
286
276
  const result = await handleBeforeAgentStart(deps, makeEvent("hello"), ctx);
287
277
  expect(result).toEqual({});
288
278
  // activeSkillEntries was not assigned by the handler (early return)
289
- expect(deps.runtime.activeSkillEntries).toEqual([]);
279
+ expect(deps.session.activeSkillEntries).toEqual([]);
290
280
  });
291
281
  });