@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.
@@ -0,0 +1,144 @@
1
+ import type { PermissionPromptDecision } from "../../permission-dialog";
2
+ import { applyPermissionGate } from "../../permission-gate";
3
+ import type { PermissionCheckResult } from "../../types";
4
+ import type { GateDescriptor, GateRunnerDeps } from "./descriptor";
5
+ import { deriveResolution } from "./helpers";
6
+ import type { GateOutcome } from "./types";
7
+
8
+ /**
9
+ * Execute the full check→log→emit→approve cycle for a gate descriptor.
10
+ *
11
+ * This is the single site for:
12
+ * - Permission checking (or using pre-resolved state)
13
+ * - Session-hit fast path
14
+ * - Interactive prompt orchestration
15
+ * - Decision event emission
16
+ * - Session-rule recording
17
+ *
18
+ * Gate functions produce descriptors; this runner executes them.
19
+ */
20
+ export async function runGateCheck(
21
+ descriptor: GateDescriptor,
22
+ agentName: string | null,
23
+ toolCallId: string,
24
+ deps: GateRunnerDeps,
25
+ ): Promise<GateOutcome> {
26
+ // 1. Resolve permission state — pre-check, pre-resolved, or via checkPermission
27
+ let check: PermissionCheckResult;
28
+ if (descriptor.preCheck) {
29
+ check = descriptor.preCheck;
30
+ } else if (descriptor.preResolved) {
31
+ check = {
32
+ state: descriptor.preResolved.state,
33
+ toolName: descriptor.surface,
34
+ source: "tool",
35
+ origin: "builtin",
36
+ };
37
+ } else {
38
+ check = deps.checkPermission(
39
+ descriptor.surface,
40
+ descriptor.input,
41
+ agentName ?? undefined,
42
+ deps.getSessionRuleset(),
43
+ );
44
+ }
45
+
46
+ // 2. Session-hit fast path
47
+ if (check.source === "session") {
48
+ deps.writeReviewLog("permission_request.session_approved", {
49
+ ...descriptor.logContext,
50
+ agentName,
51
+ resolution: "session_approved",
52
+ sessionApprovalPattern: check.matchedPattern,
53
+ });
54
+ deps.emitDecision({
55
+ surface: descriptor.decision.surface,
56
+ value: descriptor.decision.value,
57
+ result: "allow",
58
+ resolution: "session_approved",
59
+ origin: check.origin ?? null,
60
+ agentName: agentName ?? null,
61
+ matchedPattern: check.matchedPattern ?? null,
62
+ });
63
+ return { action: "allow" };
64
+ }
65
+
66
+ // 3. Apply the deny/ask/allow gate
67
+ const canConfirm = deps.canConfirm();
68
+
69
+ // Resolve the first pattern for applyPermissionGate's sessionApproval param
70
+ const singleSessionApproval = descriptor.sessionApproval
71
+ ? "pattern" in descriptor.sessionApproval
72
+ ? {
73
+ surface: descriptor.sessionApproval.surface,
74
+ pattern: descriptor.sessionApproval.pattern,
75
+ }
76
+ : descriptor.sessionApproval.patterns.length > 0
77
+ ? {
78
+ surface: descriptor.sessionApproval.surface,
79
+ pattern: descriptor.sessionApproval.patterns[0],
80
+ }
81
+ : undefined
82
+ : undefined;
83
+
84
+ let autoApproved = false;
85
+ const gateResult = await applyPermissionGate({
86
+ state: check.state,
87
+ canConfirm,
88
+ sessionApproval: singleSessionApproval,
89
+ promptForApproval: async () => {
90
+ const decision = await deps.promptPermission({
91
+ requestId: toolCallId,
92
+ ...descriptor.promptDetails,
93
+ });
94
+ autoApproved = decision.autoApproved === true;
95
+ return decision;
96
+ },
97
+ writeLog: deps.writeReviewLog,
98
+ logContext: { ...descriptor.logContext, agentName },
99
+ messages: descriptor.messages,
100
+ });
101
+
102
+ // 4. Determine whether session approval was granted
103
+ const hasSessionApproval =
104
+ gateResult.action === "allow" && gateResult.sessionApproval !== undefined;
105
+
106
+ // 5. Emit decision event
107
+ deps.emitDecision({
108
+ surface: descriptor.decision.surface,
109
+ value: descriptor.decision.value,
110
+ result: gateResult.action === "allow" ? "allow" : "deny",
111
+ resolution: deriveResolution(
112
+ check.state,
113
+ gateResult.action,
114
+ hasSessionApproval,
115
+ canConfirm,
116
+ autoApproved,
117
+ ),
118
+ origin: check.origin ?? null,
119
+ agentName: agentName ?? null,
120
+ matchedPattern: check.matchedPattern ?? null,
121
+ });
122
+
123
+ // 6. Record session approval(s)
124
+ if (gateResult.action === "allow" && hasSessionApproval) {
125
+ if (descriptor.sessionApproval) {
126
+ if ("patterns" in descriptor.sessionApproval) {
127
+ for (const pattern of descriptor.sessionApproval.patterns) {
128
+ deps.approveSessionRule(descriptor.sessionApproval.surface, pattern);
129
+ }
130
+ } else {
131
+ deps.approveSessionRule(
132
+ descriptor.sessionApproval.surface,
133
+ descriptor.sessionApproval.pattern,
134
+ );
135
+ }
136
+ }
137
+ }
138
+
139
+ if (gateResult.action === "block") {
140
+ return { action: "block", reason: gateResult.reason };
141
+ }
142
+
143
+ return { action: "allow" };
144
+ }
@@ -1,28 +1,28 @@
1
1
  import { toRecord } from "../../common";
2
2
  import { normalizePathForComparison } from "../../external-directory";
3
- import { emitDecisionEvent } from "../../permission-events";
4
- import { applyPermissionGate } from "../../permission-gate";
5
3
  import {
6
4
  formatSkillPathAskPrompt,
7
5
  formatSkillPathDenyReason,
8
6
  } from "../../permission-prompts";
7
+ import type { SkillPromptEntry } from "../../skill-prompt-sanitizer";
9
8
  import { findSkillPathMatch } from "../../skill-prompt-sanitizer";
10
- import type { HandlerDeps } from "../types";
11
- import { deriveResolution } from "./helpers";
12
- import type { GateOutcome, ToolCallContext } from "./types";
9
+ import type { GateDescriptor } from "./descriptor";
10
+ import type { ToolCallContext } from "./types";
13
11
 
14
12
  /**
15
- * Evaluate the skill-read permission gate.
13
+ * Build a pure descriptor for the skill-read permission gate.
16
14
  *
17
15
  * Returns `null` when the gate does not apply (tool is not `read`, no active
18
16
  * skill entries, or the read path does not match any skill).
17
+ * Returns a GateDescriptor with preResolved state from the matched skill entry.
19
18
  */
20
- export async function evaluateSkillReadGate(
19
+ export function describeSkillReadGate(
21
20
  tcc: ToolCallContext,
22
- deps: HandlerDeps,
23
- ): Promise<GateOutcome | null> {
24
- // Only applies to read tool calls with active skill entries
25
- if (tcc.toolName !== "read" || deps.runtime.activeSkillEntries.length === 0) {
21
+ getActiveSkillEntries: () => SkillPromptEntry[],
22
+ ): GateDescriptor | null {
23
+ const activeSkillEntries = getActiveSkillEntries();
24
+
25
+ if (tcc.toolName !== "read" || activeSkillEntries.length === 0) {
26
26
  return null;
27
27
  }
28
28
 
@@ -35,7 +35,7 @@ export async function evaluateSkillReadGate(
35
35
  const normalizedReadPath = normalizePathForComparison(path, tcc.cwd);
36
36
  const matchedSkill = findSkillPathMatch(
37
37
  normalizedReadPath,
38
- deps.runtime.activeSkillEntries,
38
+ activeSkillEntries,
39
39
  );
40
40
 
41
41
  if (!matchedSkill) {
@@ -47,31 +47,10 @@ export async function evaluateSkillReadGate(
47
47
  path,
48
48
  tcc.agentName ?? undefined,
49
49
  );
50
- const skillReadCanConfirm = deps.canRequestPermissionConfirmation(
51
- deps.runtime.runtimeContext!,
52
- );
53
- const skillReadGate = await applyPermissionGate({
54
- state: matchedSkill.state,
55
- canConfirm: skillReadCanConfirm,
56
- promptForApproval: () =>
57
- deps.promptPermission(deps.runtime.runtimeContext!, {
58
- requestId: tcc.toolCallId,
59
- source: "skill_read",
60
- agentName: tcc.agentName,
61
- message: skillReadMessage,
62
- toolCallId: tcc.toolCallId,
63
- toolName: tcc.toolName,
64
- skillName: matchedSkill.name,
65
- path,
66
- }),
67
- writeLog: deps.runtime.writeReviewLog,
68
- logContext: {
69
- source: "skill_read",
70
- skillName: matchedSkill.name,
71
- agentName: tcc.agentName,
72
- path,
73
- message: skillReadMessage,
74
- },
50
+
51
+ return {
52
+ surface: "skill",
53
+ input: { name: matchedSkill.name },
75
54
  messages: {
76
55
  denyReason: formatSkillPathDenyReason(
77
56
  matchedSkill,
@@ -86,26 +65,28 @@ export async function evaluateSkillReadGate(
86
65
  return `User denied access to skill '${matchedSkill.name}'.${denialReason}`;
87
66
  },
88
67
  },
89
- });
90
-
91
- emitDecisionEvent(deps.events, {
92
- surface: "skill",
93
- value: matchedSkill.name,
94
- result: skillReadGate.action === "allow" ? "allow" : "deny",
95
- resolution: deriveResolution(
96
- matchedSkill.state,
97
- skillReadGate.action,
98
- false,
99
- skillReadCanConfirm,
100
- ),
101
- origin: null,
102
- agentName: tcc.agentName ?? null,
103
- matchedPattern: null,
104
- });
105
-
106
- if (skillReadGate.action === "block") {
107
- return { action: "block", reason: skillReadGate.reason };
108
- }
109
-
110
- return { action: "allow" };
68
+ promptDetails: {
69
+ source: "skill_read",
70
+ agentName: tcc.agentName,
71
+ message: skillReadMessage,
72
+ toolCallId: tcc.toolCallId,
73
+ toolName: tcc.toolName,
74
+ skillName: matchedSkill.name,
75
+ path,
76
+ },
77
+ logContext: {
78
+ source: "skill_read",
79
+ skillName: matchedSkill.name,
80
+ agentName: tcc.agentName,
81
+ path,
82
+ message: skillReadMessage,
83
+ },
84
+ decision: {
85
+ surface: "skill",
86
+ value: matchedSkill.name,
87
+ },
88
+ preResolved: {
89
+ state: matchedSkill.state,
90
+ },
91
+ };
111
92
  }
@@ -1,55 +1,26 @@
1
1
  import { PATH_BEARING_TOOLS } from "../../external-directory";
2
2
  import { suggestSessionPattern } from "../../pattern-suggest";
3
- import { emitDecisionEvent } from "../../permission-events";
4
- import { applyPermissionGate } from "../../permission-gate";
5
3
  import {
6
4
  formatAskPrompt,
7
5
  formatDenyReason,
8
6
  formatUserDeniedReason,
9
7
  } from "../../permission-prompts";
10
8
  import { getPermissionLogContext } from "../../tool-input-preview";
11
- import type { HandlerDeps } from "../types";
12
- import { deriveDecisionValue, deriveResolution } from "./helpers";
13
- import type { GateOutcome, ToolCallContext } from "./types";
9
+ import type { PermissionCheckResult } from "../../types";
10
+ import type { GateDescriptor } from "./descriptor";
11
+ import { deriveDecisionValue } from "./helpers";
12
+ import type { ToolCallContext } from "./types";
14
13
 
15
14
  /**
16
- * Evaluate the normal tool permission gate.
15
+ * Build a pure descriptor for the normal tool permission gate.
17
16
  *
18
- * Unlike the other gates this one always applies — it never returns `null`.
17
+ * Takes a pre-computed PermissionCheckResult (from checkPermission) and
18
+ * returns a GateDescriptor that the runner can execute. No side effects.
19
19
  */
20
- export async function evaluateToolGate(
20
+ export function describeToolGate(
21
21
  tcc: ToolCallContext,
22
- deps: HandlerDeps,
23
- ): Promise<GateOutcome> {
24
- const check = deps.runtime.permissionManager.checkPermission(
25
- tcc.toolName,
26
- tcc.input,
27
- tcc.agentName ?? undefined,
28
- deps.runtime.sessionRules.getRuleset(),
29
- );
30
-
31
- // Session-hit: already approved by a session rule — skip the gate entirely.
32
- if (check.source === "session") {
33
- deps.runtime.writeReviewLog("permission_request.session_approved", {
34
- source: "tool_call",
35
- toolCallId: tcc.toolCallId,
36
- toolName: tcc.toolName,
37
- agentName: tcc.agentName,
38
- resolution: "session_approved",
39
- sessionApprovalPattern: check.matchedPattern,
40
- });
41
- emitDecisionEvent(deps.events, {
42
- surface: tcc.toolName,
43
- value: deriveDecisionValue(tcc.toolName, check),
44
- result: "allow",
45
- resolution: "session_approved",
46
- origin: check.origin ?? null,
47
- agentName: tcc.agentName ?? null,
48
- matchedPattern: check.matchedPattern ?? null,
49
- });
50
- return { action: "allow" };
51
- }
52
-
22
+ check: PermissionCheckResult,
23
+ ): GateDescriptor {
53
24
  const permissionLogContext = getPermissionLogContext(
54
25
  check,
55
26
  tcc.input,
@@ -71,90 +42,50 @@ export async function evaluateToolGate(
71
42
  typeof (tcc.input as Record<string, unknown>)?.command === "string"
72
43
  ? ((tcc.input as Record<string, unknown>).command as string)
73
44
  : null;
74
- const toolUnavailableReason = inputCommand
45
+ const unavailableReason = inputCommand
75
46
  ? `Running bash command '${inputCommand}' requires approval, but no interactive UI is available.`
76
47
  : tcc.toolName === "mcp"
77
48
  ? "Using tool 'mcp' requires approval, but no interactive UI is available."
78
49
  : `Using tool '${tcc.toolName}' requires approval, but no interactive UI is available.`;
79
50
 
80
- const toolAskMessage = formatAskPrompt(
51
+ const askMessage = formatAskPrompt(
81
52
  check,
82
53
  tcc.agentName ?? undefined,
83
54
  tcc.input,
84
55
  );
85
- const toolCanConfirm = deps.canRequestPermissionConfirmation(
86
- deps.runtime.runtimeContext!,
87
- );
88
- let toolDecisionAutoApproved = false;
89
- const toolGate = await applyPermissionGate({
90
- state: check.state,
91
- canConfirm: toolCanConfirm,
56
+
57
+ return {
58
+ surface: tcc.toolName,
59
+ input: tcc.input,
60
+ messages: {
61
+ denyReason: formatDenyReason(check, tcc.agentName ?? undefined),
62
+ unavailableReason,
63
+ userDeniedReason: (decision) =>
64
+ formatUserDeniedReason(check, decision.denialReason),
65
+ },
92
66
  sessionApproval: {
93
67
  surface: suggestion.surface,
94
68
  pattern: suggestion.pattern,
95
69
  },
96
- promptForApproval: async () => {
97
- const decision = await deps.promptPermission(
98
- deps.runtime.runtimeContext!,
99
- {
100
- requestId: tcc.toolCallId,
101
- source: "tool_call",
102
- agentName: tcc.agentName,
103
- message: toolAskMessage,
104
- toolCallId: tcc.toolCallId,
105
- toolName: tcc.toolName,
106
- sessionLabel: suggestion.label,
107
- ...permissionLogContext,
108
- },
109
- );
110
- toolDecisionAutoApproved = decision.autoApproved === true;
111
- return decision;
70
+ promptDetails: {
71
+ source: "tool_call",
72
+ agentName: tcc.agentName,
73
+ message: askMessage,
74
+ toolCallId: tcc.toolCallId,
75
+ toolName: tcc.toolName,
76
+ sessionLabel: suggestion.label,
77
+ ...permissionLogContext,
112
78
  },
113
- writeLog: deps.runtime.writeReviewLog,
114
79
  logContext: {
115
80
  source: "tool_call",
116
81
  toolCallId: tcc.toolCallId,
117
82
  toolName: tcc.toolName,
118
- agentName: tcc.agentName,
119
- message: toolAskMessage,
83
+ message: askMessage,
120
84
  ...permissionLogContext,
121
85
  },
122
- messages: {
123
- denyReason: formatDenyReason(check, tcc.agentName ?? undefined),
124
- unavailableReason: toolUnavailableReason,
125
- userDeniedReason: (decision) =>
126
- formatUserDeniedReason(check, decision.denialReason),
86
+ decision: {
87
+ surface: tcc.toolName,
88
+ value: deriveDecisionValue(tcc.toolName, check),
127
89
  },
128
- });
129
-
130
- const toolGateHasSession =
131
- toolGate.action === "allow" && toolGate.sessionApproval !== undefined;
132
- emitDecisionEvent(deps.events, {
133
- surface: tcc.toolName,
134
- value: deriveDecisionValue(tcc.toolName, check),
135
- result: toolGate.action === "allow" ? "allow" : "deny",
136
- resolution: deriveResolution(
137
- check.state,
138
- toolGate.action,
139
- toolGateHasSession,
140
- toolCanConfirm,
141
- toolDecisionAutoApproved,
142
- ),
143
- origin: check.origin ?? null,
144
- agentName: tcc.agentName ?? null,
145
- matchedPattern: check.matchedPattern ?? null,
146
- });
147
-
148
- if (toolGate.action === "block") {
149
- return { action: "block", reason: toolGate.reason };
150
- }
151
-
152
- if (toolGate.sessionApproval) {
153
- deps.runtime.sessionRules.approve(
154
- toolGate.sessionApproval.surface,
155
- toolGate.sessionApproval.pattern,
156
- );
157
- }
158
-
159
- return { action: "allow" };
90
+ };
160
91
  }
@@ -1,5 +1,3 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
-
3
1
  /** Outcome of a single permission gate evaluation. */
4
2
  export type GateOutcome =
5
3
  | { action: "allow" }
@@ -40,7 +40,7 @@ export async function handleInput(
40
40
  event: InputPayload,
41
41
  ctx: ExtensionContext,
42
42
  ): Promise<InputEventResult> {
43
- deps.runtime.runtimeContext = ctx;
43
+ deps.session.runtimeContext = ctx;
44
44
  deps.startForwardedPermissionPolling(ctx);
45
45
 
46
46
  const skillName = extractSkillNameFromInput(event.text);
@@ -49,7 +49,7 @@ export async function handleInput(
49
49
  }
50
50
 
51
51
  const agentName = deps.resolveAgentName(ctx);
52
- const check = deps.runtime.permissionManager.checkPermission(
52
+ const check = deps.session.permissionManager.checkPermission(
53
53
  "skill",
54
54
  { name: skillName },
55
55
  agentName ?? undefined,
@@ -82,7 +82,7 @@ export async function handleInput(
82
82
  skillInputAutoApproved = decision.autoApproved === true;
83
83
  return decision;
84
84
  },
85
- writeLog: deps.runtime.writeReviewLog,
85
+ writeLog: deps.writeReviewLog,
86
86
  logContext: {
87
87
  source: "skill_input",
88
88
  skillName,
@@ -19,25 +19,25 @@ export async function handleSessionStart(
19
19
  event: SessionStartPayload,
20
20
  ctx: ExtensionContext,
21
21
  ): Promise<void> {
22
- deps.runtime.runtimeContext = ctx;
22
+ deps.session.runtimeContext = ctx;
23
23
  deps.refreshExtensionConfig(ctx);
24
- deps.runtime.permissionManager = deps.createPermissionManagerForCwd(ctx.cwd);
25
- deps.runtime.activeSkillEntries = [];
26
- deps.runtime.lastActiveToolsCacheKey = null;
27
- deps.runtime.lastPromptStateCacheKey = null;
28
- deps.runtime.lastKnownActiveAgentName = getActiveAgentName(ctx);
24
+ deps.session.permissionManager = deps.createPermissionManagerForCwd(ctx.cwd);
25
+ deps.session.activeSkillEntries = [];
26
+ deps.session.lastActiveToolsCacheKey = null;
27
+ deps.session.lastPromptStateCacheKey = null;
28
+ deps.session.lastKnownActiveAgentName = getActiveAgentName(ctx);
29
29
  deps.startForwardedPermissionPolling(ctx);
30
30
  deps.logResolvedConfigPaths();
31
31
 
32
- const agentName = deps.runtime.lastKnownActiveAgentName;
32
+ const agentName = deps.session.lastKnownActiveAgentName;
33
33
  const policyIssues =
34
- deps.runtime.permissionManager.getConfigIssues(agentName);
34
+ deps.session.permissionManager.getConfigIssues(agentName);
35
35
  for (const issue of policyIssues) {
36
36
  deps.notifyWarning(issue);
37
37
  }
38
38
 
39
39
  if (event.reason === "reload") {
40
- deps.runtime.writeDebugLog("lifecycle.reload", {
40
+ deps.writeDebugLog("lifecycle.reload", {
41
41
  triggeredBy: "session_start",
42
42
  reason: event.reason,
43
43
  cwd: ctx.cwd,
@@ -53,14 +53,14 @@ export async function handleResourcesDiscover(
53
53
  return;
54
54
  }
55
55
 
56
- const { runtimeContext } = deps.runtime;
57
- deps.runtime.permissionManager = deps.createPermissionManagerForCwd(
56
+ const { runtimeContext } = deps.session;
57
+ deps.session.permissionManager = deps.createPermissionManagerForCwd(
58
58
  runtimeContext?.cwd,
59
59
  );
60
- deps.runtime.activeSkillEntries = [];
61
- deps.runtime.lastActiveToolsCacheKey = null;
62
- deps.runtime.lastPromptStateCacheKey = null;
63
- deps.runtime.writeDebugLog("lifecycle.reload", {
60
+ deps.session.activeSkillEntries = [];
61
+ deps.session.lastActiveToolsCacheKey = null;
62
+ deps.session.lastPromptStateCacheKey = null;
63
+ deps.writeDebugLog("lifecycle.reload", {
64
64
  triggeredBy: "resources_discover",
65
65
  reason: event.reason,
66
66
  cwd: runtimeContext?.cwd ?? null,
@@ -68,15 +68,15 @@ export async function handleResourcesDiscover(
68
68
  }
69
69
 
70
70
  export async function handleSessionShutdown(deps: HandlerDeps): Promise<void> {
71
- const { runtimeContext } = deps.runtime;
71
+ const { runtimeContext } = deps.session;
72
72
  if (runtimeContext) {
73
73
  runtimeContext.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
74
74
  }
75
- deps.runtime.runtimeContext = null;
76
- deps.runtime.activeSkillEntries = [];
77
- deps.runtime.lastActiveToolsCacheKey = null;
78
- deps.runtime.lastPromptStateCacheKey = null;
79
- deps.runtime.sessionRules.clear();
75
+ deps.session.runtimeContext = null;
76
+ deps.session.activeSkillEntries = [];
77
+ deps.session.lastActiveToolsCacheKey = null;
78
+ deps.session.lastPromptStateCacheKey = null;
79
+ deps.session.sessionRules.clear();
80
80
  deps.stopForwardedPermissionPolling();
81
81
  deps.stopPermissionRpcHandlers();
82
82
  }