@gotgenes/pi-permission-system 5.5.1 → 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,27 +1,27 @@
1
1
  import { toRecord } from "../../common";
2
2
  import { normalizePathForComparison } from "../../external-directory";
3
- import { applyPermissionGate } from "../../permission-gate";
4
3
  import {
5
4
  formatSkillPathAskPrompt,
6
5
  formatSkillPathDenyReason,
7
6
  } from "../../permission-prompts";
7
+ import type { SkillPromptEntry } from "../../skill-prompt-sanitizer";
8
8
  import { findSkillPathMatch } from "../../skill-prompt-sanitizer";
9
- import { deriveResolution } from "./helpers";
10
- import type { GateOutcome, SkillReadGateDeps, ToolCallContext } from "./types";
9
+ import type { GateDescriptor } from "./descriptor";
10
+ import type { ToolCallContext } from "./types";
11
11
 
12
12
  /**
13
- * Evaluate the skill-read permission gate.
13
+ * Build a pure descriptor for the skill-read permission gate.
14
14
  *
15
15
  * Returns `null` when the gate does not apply (tool is not `read`, no active
16
16
  * skill entries, or the read path does not match any skill).
17
+ * Returns a GateDescriptor with preResolved state from the matched skill entry.
17
18
  */
18
- export async function evaluateSkillReadGate(
19
+ export function describeSkillReadGate(
19
20
  tcc: ToolCallContext,
20
- deps: SkillReadGateDeps,
21
- ): Promise<GateOutcome | null> {
22
- const activeSkillEntries = deps.getActiveSkillEntries();
21
+ getActiveSkillEntries: () => SkillPromptEntry[],
22
+ ): GateDescriptor | null {
23
+ const activeSkillEntries = getActiveSkillEntries();
23
24
 
24
- // Only applies to read tool calls with active skill entries
25
25
  if (tcc.toolName !== "read" || activeSkillEntries.length === 0) {
26
26
  return null;
27
27
  }
@@ -47,29 +47,10 @@ export async function evaluateSkillReadGate(
47
47
  path,
48
48
  tcc.agentName ?? undefined,
49
49
  );
50
- const skillReadCanConfirm = deps.canConfirm();
51
- const skillReadGate = await applyPermissionGate({
52
- state: matchedSkill.state,
53
- canConfirm: skillReadCanConfirm,
54
- promptForApproval: () =>
55
- deps.promptPermission({
56
- requestId: tcc.toolCallId,
57
- source: "skill_read",
58
- agentName: tcc.agentName,
59
- message: skillReadMessage,
60
- toolCallId: tcc.toolCallId,
61
- toolName: tcc.toolName,
62
- skillName: matchedSkill.name,
63
- path,
64
- }),
65
- writeLog: deps.writeReviewLog,
66
- logContext: {
67
- source: "skill_read",
68
- skillName: matchedSkill.name,
69
- agentName: tcc.agentName,
70
- path,
71
- message: skillReadMessage,
72
- },
50
+
51
+ return {
52
+ surface: "skill",
53
+ input: { name: matchedSkill.name },
73
54
  messages: {
74
55
  denyReason: formatSkillPathDenyReason(
75
56
  matchedSkill,
@@ -84,26 +65,28 @@ export async function evaluateSkillReadGate(
84
65
  return `User denied access to skill '${matchedSkill.name}'.${denialReason}`;
85
66
  },
86
67
  },
87
- });
88
-
89
- deps.emitDecision({
90
- surface: "skill",
91
- value: matchedSkill.name,
92
- result: skillReadGate.action === "allow" ? "allow" : "deny",
93
- resolution: deriveResolution(
94
- matchedSkill.state,
95
- skillReadGate.action,
96
- false,
97
- skillReadCanConfirm,
98
- ),
99
- origin: null,
100
- agentName: tcc.agentName ?? null,
101
- matchedPattern: null,
102
- });
103
-
104
- if (skillReadGate.action === "block") {
105
- return { action: "block", reason: skillReadGate.reason };
106
- }
107
-
108
- 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
+ };
109
92
  }
@@ -1,53 +1,26 @@
1
1
  import { PATH_BEARING_TOOLS } from "../../external-directory";
2
2
  import { suggestSessionPattern } from "../../pattern-suggest";
3
- import { applyPermissionGate } from "../../permission-gate";
4
3
  import {
5
4
  formatAskPrompt,
6
5
  formatDenyReason,
7
6
  formatUserDeniedReason,
8
7
  } from "../../permission-prompts";
9
8
  import { getPermissionLogContext } from "../../tool-input-preview";
10
- import { deriveDecisionValue, deriveResolution } from "./helpers";
11
- import type { GateOutcome, ToolCallContext, ToolGateDeps } 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";
12
13
 
13
14
  /**
14
- * Evaluate the normal tool permission gate.
15
+ * Build a pure descriptor for the normal tool permission gate.
15
16
  *
16
- * 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.
17
19
  */
18
- export async function evaluateToolGate(
20
+ export function describeToolGate(
19
21
  tcc: ToolCallContext,
20
- deps: ToolGateDeps,
21
- ): Promise<GateOutcome> {
22
- const check = deps.checkPermission(
23
- tcc.toolName,
24
- tcc.input,
25
- tcc.agentName ?? undefined,
26
- deps.getSessionRuleset(),
27
- );
28
-
29
- // Session-hit: already approved by a session rule — skip the gate entirely.
30
- if (check.source === "session") {
31
- deps.writeReviewLog("permission_request.session_approved", {
32
- source: "tool_call",
33
- toolCallId: tcc.toolCallId,
34
- toolName: tcc.toolName,
35
- agentName: tcc.agentName,
36
- resolution: "session_approved",
37
- sessionApprovalPattern: check.matchedPattern,
38
- });
39
- deps.emitDecision({
40
- surface: tcc.toolName,
41
- value: deriveDecisionValue(tcc.toolName, check),
42
- result: "allow",
43
- resolution: "session_approved",
44
- origin: check.origin ?? null,
45
- agentName: tcc.agentName ?? null,
46
- matchedPattern: check.matchedPattern ?? null,
47
- });
48
- return { action: "allow" };
49
- }
50
-
22
+ check: PermissionCheckResult,
23
+ ): GateDescriptor {
51
24
  const permissionLogContext = getPermissionLogContext(
52
25
  check,
53
26
  tcc.input,
@@ -69,85 +42,50 @@ export async function evaluateToolGate(
69
42
  typeof (tcc.input as Record<string, unknown>)?.command === "string"
70
43
  ? ((tcc.input as Record<string, unknown>).command as string)
71
44
  : null;
72
- const toolUnavailableReason = inputCommand
45
+ const unavailableReason = inputCommand
73
46
  ? `Running bash command '${inputCommand}' requires approval, but no interactive UI is available.`
74
47
  : tcc.toolName === "mcp"
75
48
  ? "Using tool 'mcp' requires approval, but no interactive UI is available."
76
49
  : `Using tool '${tcc.toolName}' requires approval, but no interactive UI is available.`;
77
50
 
78
- const toolAskMessage = formatAskPrompt(
51
+ const askMessage = formatAskPrompt(
79
52
  check,
80
53
  tcc.agentName ?? undefined,
81
54
  tcc.input,
82
55
  );
83
- const toolCanConfirm = deps.canConfirm();
84
- let toolDecisionAutoApproved = false;
85
- const toolGate = await applyPermissionGate({
86
- state: check.state,
87
- 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
+ },
88
66
  sessionApproval: {
89
67
  surface: suggestion.surface,
90
68
  pattern: suggestion.pattern,
91
69
  },
92
- promptForApproval: async () => {
93
- const decision = await deps.promptPermission({
94
- requestId: tcc.toolCallId,
95
- source: "tool_call",
96
- agentName: tcc.agentName,
97
- message: toolAskMessage,
98
- toolCallId: tcc.toolCallId,
99
- toolName: tcc.toolName,
100
- sessionLabel: suggestion.label,
101
- ...permissionLogContext,
102
- });
103
- toolDecisionAutoApproved = decision.autoApproved === true;
104
- 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,
105
78
  },
106
- writeLog: deps.writeReviewLog,
107
79
  logContext: {
108
80
  source: "tool_call",
109
81
  toolCallId: tcc.toolCallId,
110
82
  toolName: tcc.toolName,
111
- agentName: tcc.agentName,
112
- message: toolAskMessage,
83
+ message: askMessage,
113
84
  ...permissionLogContext,
114
85
  },
115
- messages: {
116
- denyReason: formatDenyReason(check, tcc.agentName ?? undefined),
117
- unavailableReason: toolUnavailableReason,
118
- userDeniedReason: (decision) =>
119
- formatUserDeniedReason(check, decision.denialReason),
86
+ decision: {
87
+ surface: tcc.toolName,
88
+ value: deriveDecisionValue(tcc.toolName, check),
120
89
  },
121
- });
122
-
123
- const toolGateHasSession =
124
- toolGate.action === "allow" && toolGate.sessionApproval !== undefined;
125
- deps.emitDecision({
126
- surface: tcc.toolName,
127
- value: deriveDecisionValue(tcc.toolName, check),
128
- result: toolGate.action === "allow" ? "allow" : "deny",
129
- resolution: deriveResolution(
130
- check.state,
131
- toolGate.action,
132
- toolGateHasSession,
133
- toolCanConfirm,
134
- toolDecisionAutoApproved,
135
- ),
136
- origin: check.origin ?? null,
137
- agentName: tcc.agentName ?? null,
138
- matchedPattern: check.matchedPattern ?? null,
139
- });
140
-
141
- if (toolGate.action === "block") {
142
- return { action: "block", reason: toolGate.reason };
143
- }
144
-
145
- if (toolGate.sessionApproval) {
146
- deps.approveSessionRule(
147
- toolGate.sessionApproval.surface,
148
- toolGate.sessionApproval.pattern,
149
- );
150
- }
151
-
152
- return { action: "allow" };
90
+ };
153
91
  }
@@ -1,12 +1,3 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
-
3
- import type { PermissionPromptDecision } from "../../permission-dialog";
4
- import type { PermissionDecisionEvent } from "../../permission-events";
5
- import type { Rule } from "../../rule";
6
- import type { SkillPromptEntry } from "../../skill-prompt-sanitizer";
7
- import type { PermissionCheckResult } from "../../types";
8
- import type { PromptPermissionDetails } from "../types";
9
-
10
1
  /** Outcome of a single permission gate evaluation. */
11
2
  export type GateOutcome =
12
3
  | { action: "allow" }
@@ -20,71 +11,3 @@ export interface ToolCallContext {
20
11
  toolCallId: string;
21
12
  cwd: string | undefined;
22
13
  }
23
-
24
- // ── Per-gate narrow dependency interfaces ──────────────────────────────────
25
-
26
- /** Narrow deps for evaluateToolGate — every field is a leaf method. */
27
- export interface ToolGateDeps {
28
- checkPermission(
29
- surface: string,
30
- input: unknown,
31
- agentName?: string,
32
- sessionRules?: Rule[],
33
- ): PermissionCheckResult;
34
- getSessionRuleset(): Rule[];
35
- approveSessionRule(surface: string, pattern: string): void;
36
- writeReviewLog(event: string, details: Record<string, unknown>): void;
37
- emitDecision(event: PermissionDecisionEvent): void;
38
- canConfirm(): boolean;
39
- promptPermission(
40
- details: PromptPermissionDetails,
41
- ): Promise<PermissionPromptDecision>;
42
- }
43
-
44
- /** Narrow deps for evaluateExternalDirectoryGate. */
45
- export interface ExternalDirectoryGateDeps {
46
- checkPermission(
47
- surface: string,
48
- input: unknown,
49
- agentName?: string,
50
- sessionRules?: Rule[],
51
- ): PermissionCheckResult;
52
- getSessionRuleset(): Rule[];
53
- approveSessionRule(surface: string, pattern: string): void;
54
- writeReviewLog(event: string, details: Record<string, unknown>): void;
55
- emitDecision(event: PermissionDecisionEvent): void;
56
- canConfirm(): boolean;
57
- promptPermission(
58
- details: PromptPermissionDetails,
59
- ): Promise<PermissionPromptDecision>;
60
- /** Resolved infrastructure dirs (static + config-based). */
61
- getInfrastructureDirs(): string[];
62
- }
63
-
64
- /** Narrow deps for evaluateBashExternalDirectoryGate. */
65
- export interface BashExternalDirectoryGateDeps {
66
- checkPermission(
67
- surface: string,
68
- input: unknown,
69
- agentName?: string,
70
- sessionRules?: Rule[],
71
- ): PermissionCheckResult;
72
- getSessionRuleset(): Rule[];
73
- approveSessionRule(surface: string, pattern: string): void;
74
- writeReviewLog(event: string, details: Record<string, unknown>): void;
75
- canConfirm(): boolean;
76
- promptPermission(
77
- details: PromptPermissionDetails,
78
- ): Promise<PermissionPromptDecision>;
79
- }
80
-
81
- /** Narrow deps for evaluateSkillReadGate. */
82
- export interface SkillReadGateDeps {
83
- getActiveSkillEntries(): SkillPromptEntry[];
84
- writeReviewLog(event: string, details: Record<string, unknown>): void;
85
- emitDecision(event: PermissionDecisionEvent): void;
86
- canConfirm(): boolean;
87
- promptPermission(
88
- details: PromptPermissionDetails,
89
- ): Promise<PermissionPromptDecision>;
90
- }