@komarspn/pi-permission-system 16.0.2

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.
Files changed (203) hide show
  1. package/CHANGELOG.md +2234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +158 -0
  4. package/config/config.example.json +39 -0
  5. package/package.json +82 -0
  6. package/schemas/permissions.schema.json +158 -0
  7. package/src/active-agent.ts +72 -0
  8. package/src/async-cache.ts +21 -0
  9. package/src/bash-arity.ts +210 -0
  10. package/src/builtin-tool-input-formatters.ts +82 -0
  11. package/src/canonicalize-path.ts +30 -0
  12. package/src/common.ts +121 -0
  13. package/src/config-loader.ts +432 -0
  14. package/src/config-modal.ts +259 -0
  15. package/src/config-paths.ts +47 -0
  16. package/src/config-reporter.ts +34 -0
  17. package/src/config-store.ts +222 -0
  18. package/src/decision-audit.ts +75 -0
  19. package/src/decision-reporter.ts +41 -0
  20. package/src/denial-messages.ts +232 -0
  21. package/src/expand-home.ts +28 -0
  22. package/src/extension-config.ts +79 -0
  23. package/src/extension-paths.ts +66 -0
  24. package/src/forwarded-permissions/io.ts +404 -0
  25. package/src/forwarded-permissions/permission-forwarder.ts +580 -0
  26. package/src/forwarding-manager.ts +74 -0
  27. package/src/gate-prompter.ts +12 -0
  28. package/src/handlers/before-agent-start.ts +94 -0
  29. package/src/handlers/gates/bash-command.ts +75 -0
  30. package/src/handlers/gates/bash-external-directory.ts +127 -0
  31. package/src/handlers/gates/bash-path-extractor.ts +15 -0
  32. package/src/handlers/gates/bash-path.ts +152 -0
  33. package/src/handlers/gates/bash-program.ts +1143 -0
  34. package/src/handlers/gates/bash-token-classification.ts +105 -0
  35. package/src/handlers/gates/candidate-check.ts +32 -0
  36. package/src/handlers/gates/descriptor.ts +81 -0
  37. package/src/handlers/gates/external-directory-messages.ts +20 -0
  38. package/src/handlers/gates/external-directory.ts +133 -0
  39. package/src/handlers/gates/helpers.ts +76 -0
  40. package/src/handlers/gates/path.ts +91 -0
  41. package/src/handlers/gates/runner.ts +186 -0
  42. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  43. package/src/handlers/gates/skill-input.ts +46 -0
  44. package/src/handlers/gates/skill-read.ts +87 -0
  45. package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
  46. package/src/handlers/gates/tool.ts +102 -0
  47. package/src/handlers/gates/types.ts +13 -0
  48. package/src/handlers/index.ts +3 -0
  49. package/src/handlers/lifecycle.ts +95 -0
  50. package/src/handlers/permission-gate-handler.ts +190 -0
  51. package/src/handlers/tool-call-boundary.ts +91 -0
  52. package/src/index.ts +225 -0
  53. package/src/input-normalizer.ts +157 -0
  54. package/src/logging.ts +113 -0
  55. package/src/mcp-targets.ts +170 -0
  56. package/src/node-modules-discovery.ts +76 -0
  57. package/src/normalize.ts +43 -0
  58. package/src/path-utils.ts +355 -0
  59. package/src/pattern-suggest.ts +132 -0
  60. package/src/permission-dialog.ts +138 -0
  61. package/src/permission-event-rpc.ts +223 -0
  62. package/src/permission-events.ts +266 -0
  63. package/src/permission-forwarding.ts +188 -0
  64. package/src/permission-gate.ts +94 -0
  65. package/src/permission-manager.ts +392 -0
  66. package/src/permission-merge.ts +32 -0
  67. package/src/permission-prompter.ts +142 -0
  68. package/src/permission-prompts.ts +93 -0
  69. package/src/permission-resolver.ts +109 -0
  70. package/src/permission-session.ts +189 -0
  71. package/src/permission-ui-prompt.ts +127 -0
  72. package/src/permissions-service.ts +63 -0
  73. package/src/persistent-approval-recorder.ts +139 -0
  74. package/src/policy-loader.ts +350 -0
  75. package/src/prompting-gateway.ts +104 -0
  76. package/src/rule.ts +188 -0
  77. package/src/scope-merge.ts +72 -0
  78. package/src/service-lifecycle.ts +49 -0
  79. package/src/service.ts +163 -0
  80. package/src/session-approval-recorder.ts +6 -0
  81. package/src/session-approval.ts +43 -0
  82. package/src/session-logger.ts +91 -0
  83. package/src/session-rules.ts +79 -0
  84. package/src/skill-prompt-sanitizer.ts +292 -0
  85. package/src/status.ts +35 -0
  86. package/src/subagent-context.ts +104 -0
  87. package/src/subagent-lifecycle-events.ts +72 -0
  88. package/src/subagent-registry.ts +105 -0
  89. package/src/synthesize.ts +92 -0
  90. package/src/system-prompt-sanitizer.ts +274 -0
  91. package/src/tool-access-extractor-registry.ts +68 -0
  92. package/src/tool-input-formatter-registry.ts +67 -0
  93. package/src/tool-input-preview.ts +34 -0
  94. package/src/tool-input-prompt-formatters.ts +63 -0
  95. package/src/tool-preview-formatter.ts +207 -0
  96. package/src/tool-registry.ts +148 -0
  97. package/src/types.ts +64 -0
  98. package/src/wildcard-matcher.ts +120 -0
  99. package/src/yolo-mode.ts +30 -0
  100. package/test/active-agent.test.ts +155 -0
  101. package/test/async-cache.test.ts +48 -0
  102. package/test/bash-arity.test.ts +144 -0
  103. package/test/bash-external-directory.test.ts +956 -0
  104. package/test/builtin-tool-input-formatters.test.ts +109 -0
  105. package/test/canonicalize-path.test.ts +93 -0
  106. package/test/common.test.ts +287 -0
  107. package/test/composition-root.test.ts +603 -0
  108. package/test/config-loader.test.ts +740 -0
  109. package/test/config-modal.test.ts +320 -0
  110. package/test/config-paths.test.ts +83 -0
  111. package/test/config-pipeline.test.ts +90 -0
  112. package/test/config-reporter.test.ts +147 -0
  113. package/test/config-store.test.ts +466 -0
  114. package/test/decision-audit.test.ts +72 -0
  115. package/test/decision-reporter.test.ts +112 -0
  116. package/test/denial-messages.test.ts +656 -0
  117. package/test/detect-permissive-bash-fallback.test.ts +56 -0
  118. package/test/expand-home.test.ts +93 -0
  119. package/test/extension-config.test.ts +129 -0
  120. package/test/extension-paths.test.ts +108 -0
  121. package/test/forwarded-permissions/io.test.ts +251 -0
  122. package/test/forwarding-manager.test.ts +194 -0
  123. package/test/handlers/before-agent-start.test.ts +317 -0
  124. package/test/handlers/external-directory-integration.test.ts +623 -0
  125. package/test/handlers/external-directory-session-dedup.test.ts +430 -0
  126. package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
  127. package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
  128. package/test/handlers/gates/bash-command.test.ts +191 -0
  129. package/test/handlers/gates/bash-external-directory.test.ts +269 -0
  130. package/test/handlers/gates/bash-path.test.ts +337 -0
  131. package/test/handlers/gates/bash-program.test.ts +410 -0
  132. package/test/handlers/gates/bash-token-classification.test.ts +241 -0
  133. package/test/handlers/gates/candidate-check.test.ts +52 -0
  134. package/test/handlers/gates/external-directory-messages.test.ts +61 -0
  135. package/test/handlers/gates/external-directory.test.ts +259 -0
  136. package/test/handlers/gates/helpers.test.ts +177 -0
  137. package/test/handlers/gates/path.test.ts +294 -0
  138. package/test/handlers/gates/runner.test.ts +447 -0
  139. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  140. package/test/handlers/gates/skill-input.test.ts +131 -0
  141. package/test/handlers/gates/skill-read.test.ts +158 -0
  142. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
  143. package/test/handlers/gates/tool.test.ts +223 -0
  144. package/test/handlers/input-events.test.ts +168 -0
  145. package/test/handlers/input.test.ts +199 -0
  146. package/test/handlers/lifecycle.test.ts +221 -0
  147. package/test/handlers/tool-call-boundary.test.ts +145 -0
  148. package/test/handlers/tool-call-events.test.ts +277 -0
  149. package/test/handlers/tool-call.test.ts +395 -0
  150. package/test/handlers/validate-requested-tool.test.ts +92 -0
  151. package/test/helpers/gate-fixtures.ts +323 -0
  152. package/test/helpers/handler-fixtures.ts +335 -0
  153. package/test/helpers/make-fake-pi.ts +100 -0
  154. package/test/helpers/manager-harness.ts +112 -0
  155. package/test/helpers/session-fixtures.ts +204 -0
  156. package/test/input-normalizer.test.ts +367 -0
  157. package/test/logging.test.ts +51 -0
  158. package/test/mcp-targets.test.ts +233 -0
  159. package/test/node-modules-discovery.test.ts +97 -0
  160. package/test/normalize.test.ts +247 -0
  161. package/test/path-utils.test.ts +650 -0
  162. package/test/pattern-suggest.test.ts +248 -0
  163. package/test/permission-dialog.test.ts +241 -0
  164. package/test/permission-event-rpc.test.ts +541 -0
  165. package/test/permission-events.test.ts +402 -0
  166. package/test/permission-forwarder.test.ts +369 -0
  167. package/test/permission-forwarding.test.ts +315 -0
  168. package/test/permission-gate.test.ts +305 -0
  169. package/test/permission-manager-unified.test.ts +3368 -0
  170. package/test/permission-merge.test.ts +61 -0
  171. package/test/permission-prompter.test.ts +518 -0
  172. package/test/permission-prompts.test.ts +363 -0
  173. package/test/permission-resolver.test.ts +265 -0
  174. package/test/permission-session.test.ts +363 -0
  175. package/test/permission-ui-prompt.test.ts +146 -0
  176. package/test/permissions-service.test.ts +177 -0
  177. package/test/persistent-approval-recorder.test.ts +133 -0
  178. package/test/pi-infrastructure-read.test.ts +369 -0
  179. package/test/policy-loader.test.ts +561 -0
  180. package/test/prompting-gateway.test.ts +230 -0
  181. package/test/rule.test.ts +604 -0
  182. package/test/scope-merge.test.ts +116 -0
  183. package/test/service-lifecycle.test.ts +163 -0
  184. package/test/service.test.ts +308 -0
  185. package/test/session-approval.test.ts +75 -0
  186. package/test/session-logger.test.ts +200 -0
  187. package/test/session-rules.test.ts +304 -0
  188. package/test/session-start.test.ts +112 -0
  189. package/test/skill-prompt-sanitizer.test.ts +374 -0
  190. package/test/status.test.ts +10 -0
  191. package/test/subagent-context.test.ts +326 -0
  192. package/test/subagent-lifecycle-events.test.ts +132 -0
  193. package/test/subagent-registry.test.ts +145 -0
  194. package/test/synthesize.test.ts +300 -0
  195. package/test/system-prompt-sanitizer.test.ts +382 -0
  196. package/test/tool-access-extractor-registry.test.ts +77 -0
  197. package/test/tool-input-formatter-registry.test.ts +75 -0
  198. package/test/tool-input-preview.test.ts +129 -0
  199. package/test/tool-input-prompt-formatters.test.ts +115 -0
  200. package/test/tool-preview-formatter.test.ts +458 -0
  201. package/test/tool-registry.test.ts +197 -0
  202. package/test/wildcard-matcher.test.ts +424 -0
  203. package/test/yolo-mode.test.ts +188 -0
@@ -0,0 +1,93 @@
1
+ import { getNonEmptyString, toRecord } from "./common";
2
+ import { matchQualifier } from "./denial-messages";
3
+ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
4
+ import type { ToolPreviewFormatter } from "./tool-preview-formatter";
5
+ import type { PermissionCheckResult } from "./types";
6
+
7
+ // NOTE: formatDenyReason, formatUserDeniedReason, and
8
+ // formatPermissionHardStopHint have been moved to denial-messages.ts.
9
+ // This module retains only pre-check messages and user-facing ask prompts.
10
+
11
+ export function formatMissingToolNameReason(): string {
12
+ return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
13
+ }
14
+
15
+ export function formatUnknownToolReason(
16
+ toolName: string,
17
+ availableToolNames: readonly string[],
18
+ ): string {
19
+ const preview = availableToolNames.slice(0, 10);
20
+ const suffix = availableToolNames.length > preview.length ? ", ..." : "";
21
+ const availableList =
22
+ preview.length > 0 ? `${preview.join(", ")}${suffix}` : "none";
23
+
24
+ const mcpHint =
25
+ toolName === "mcp"
26
+ ? ""
27
+ : ' If this was intended as an MCP server tool, call the registered \'mcp\' tool when available (for example: {"tool":"server:tool"}).';
28
+
29
+ return `Tool '${toolName}' is not registered in this runtime and was blocked before permission checks.${mcpHint} Registered tools: ${availableList}.`;
30
+ }
31
+
32
+ export function formatAskPrompt(
33
+ result: PermissionCheckResult,
34
+ agentName?: string,
35
+ input?: unknown,
36
+ formatter?: ToolPreviewFormatter,
37
+ ): string {
38
+ const subject = agentName ? `Agent '${agentName}'` : "Current agent";
39
+
40
+ if (result.toolName === "bash") {
41
+ const subCommand = result.command ?? "";
42
+ const qualifier = matchQualifier(
43
+ result.matchedPattern,
44
+ result.commandContext,
45
+ );
46
+ const qualifierInfo = qualifier ? ` ${qualifier}` : "";
47
+ const fullCommand = getNonEmptyString(toRecord(input).command);
48
+ const fullCommandInfo =
49
+ fullCommand && fullCommand !== subCommand
50
+ ? ` (full command: '${fullCommand}')`
51
+ : "";
52
+ return `${subject} requested bash command '${subCommand}'${qualifierInfo}${fullCommandInfo}. Allow this command?`;
53
+ }
54
+
55
+ if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
56
+ const patternInfo = result.matchedPattern
57
+ ? ` (matched '${result.matchedPattern}')`
58
+ : "";
59
+ const mcpPreview = formatter
60
+ ? formatter.formatToolInputForPrompt("mcp", input)
61
+ : "";
62
+ const previewSuffix = mcpPreview ? ` ${mcpPreview}` : "";
63
+ return `${subject} requested MCP target '${result.target}'${patternInfo}${previewSuffix}. Allow this call?`;
64
+ }
65
+
66
+ const patternInfo = result.matchedPattern
67
+ ? ` (matched '${result.matchedPattern}')`
68
+ : "";
69
+ const inputPreview = formatter
70
+ ? formatter.formatToolInputForPrompt(result.toolName, input)
71
+ : "";
72
+ const inputSuffix = inputPreview ? ` ${inputPreview}` : "";
73
+ return `${subject} requested tool '${result.toolName}'${patternInfo}${inputSuffix}. Allow this call?`;
74
+ }
75
+
76
+ export function formatSkillAskPrompt(
77
+ skillName: string,
78
+ agentName?: string,
79
+ ): string {
80
+ const subject = agentName ? `Agent '${agentName}'` : "Current agent";
81
+ return `${subject} requested skill '${skillName}'. Allow loading this skill?`;
82
+ }
83
+
84
+ export function formatSkillPathAskPrompt(
85
+ skill: SkillPromptEntry,
86
+ readPath: string,
87
+ agentName?: string,
88
+ ): string {
89
+ const subject = agentName ? `Agent '${agentName}'` : "Current agent";
90
+ return `${subject} requested access to skill '${skill.name}' via '${readPath}'. Allow this read?`;
91
+ }
92
+
93
+ // formatSkillPathDenyReason has been moved to denial-messages.ts.
@@ -0,0 +1,109 @@
1
+ import type { ScopedPermissionManager } from "./permission-manager";
2
+ import type { Rule } from "./rule";
3
+ import type { SessionRules } from "./session-rules";
4
+ import type { PermissionCheckResult, PermissionState } from "./types";
5
+
6
+ /**
7
+ * Resolves the effective permission for a surface/input, applying the current
8
+ * session rules internally.
9
+ *
10
+ * Collapses the `checkPermission` + `getSessionRuleset` relay that every gate
11
+ * previously threaded by hand: the ruleset was only ever fetched to be passed
12
+ * straight back into `checkPermission`, so the two are one operation.
13
+ */
14
+ export interface ScopedPermissionResolver {
15
+ resolve(
16
+ surface: string,
17
+ input: unknown,
18
+ agentName?: string,
19
+ ): PermissionCheckResult;
20
+ /**
21
+ * Resolve a path-shaped surface against a caller-supplied set of equivalent
22
+ * policy values, applying the current session rules. Used by the bash path
23
+ * gate (`path`) and the external-directory gates (`external_directory`),
24
+ * which compute equivalent path aliases per token. `surface` defaults to
25
+ * `path`.
26
+ */
27
+ resolvePathPolicy(
28
+ values: readonly string[],
29
+ agentName?: string,
30
+ surface?: string,
31
+ ): PermissionCheckResult;
32
+ }
33
+
34
+ /**
35
+ * Concrete collaborator that owns the resolution surface.
36
+ *
37
+ * Holds a `ScopedPermissionManager` and a `SessionRules` store, composing
38
+ * them so callers never thread the session ruleset by hand.
39
+ *
40
+ * Constructor deps:
41
+ * - `permissionManager` — the narrow session-scoped permission-checking interface
42
+ * - `sessionRules` — narrowed to `getRuleset` (ISP: the resolver only reads, never records)
43
+ */
44
+ export class PermissionResolver implements ScopedPermissionResolver {
45
+ constructor(
46
+ private readonly permissionManager: ScopedPermissionManager,
47
+ private readonly sessionRules: Pick<SessionRules, "getRuleset">,
48
+ ) {}
49
+
50
+ /**
51
+ * Resolve the effective permission for a surface/input, applying the current
52
+ * session rules. Composes `checkPermission` with `getRuleset()` so callers
53
+ * never thread the ruleset by hand.
54
+ */
55
+ resolve(
56
+ surface: string,
57
+ input: unknown,
58
+ agentName?: string,
59
+ ): PermissionCheckResult {
60
+ return this.checkPermission(
61
+ surface,
62
+ input,
63
+ agentName,
64
+ this.sessionRules.getRuleset(),
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Resolve a path-shaped surface (`path` or `external_directory`) for
70
+ * precomputed policy values, composing the current session ruleset so callers
71
+ * never thread it by hand. `surface` defaults to `path`; the external-directory
72
+ * gates pass `external_directory` so a path's typed and symlink-resolved
73
+ * aliases match against the `external_directory` rules.
74
+ */
75
+ resolvePathPolicy(
76
+ values: readonly string[],
77
+ agentName?: string,
78
+ surface = "path",
79
+ ): PermissionCheckResult {
80
+ return this.permissionManager.checkPathPolicy(
81
+ values,
82
+ agentName,
83
+ this.sessionRules.getRuleset(),
84
+ surface,
85
+ );
86
+ }
87
+
88
+ checkPermission(
89
+ surface: string,
90
+ input: unknown,
91
+ agentName?: string,
92
+ sessionRules?: Rule[],
93
+ ): PermissionCheckResult {
94
+ return this.permissionManager.checkPermission(
95
+ surface,
96
+ input,
97
+ agentName,
98
+ sessionRules,
99
+ );
100
+ }
101
+
102
+ getToolPermission(toolName: string, agentName?: string): PermissionState {
103
+ return this.permissionManager.getToolPermission(toolName, agentName);
104
+ }
105
+
106
+ getConfigIssues(agentName?: string): string[] {
107
+ return this.permissionManager.getConfigIssues(agentName);
108
+ }
109
+ }
@@ -0,0 +1,189 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ getActiveAgentName,
4
+ getActiveAgentNameFromSystemPrompt,
5
+ } from "./active-agent";
6
+
7
+ import type { SessionConfigStore } from "./config-store";
8
+ import type { PermissionSystemExtensionConfig } from "./extension-config";
9
+ import type { ExtensionPaths } from "./extension-paths";
10
+ import type { ForwardingController } from "./forwarding-manager";
11
+ import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipeline";
12
+ import type { ScopedPermissionManager } from "./permission-manager";
13
+ import type { PromptingGatewayLifecycle } from "./prompting-gateway";
14
+
15
+ import type { SessionRules } from "./session-rules";
16
+ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
17
+ import {
18
+ resolveToolPreviewLimits,
19
+ type ToolPreviewFormatterOptions,
20
+ } from "./tool-preview-formatter";
21
+
22
+ /**
23
+ * Encapsulates all mutable session state and exposes operations instead of
24
+ * fields.
25
+ *
26
+ * Replaces the `SessionState` interface + scattered handler field mutations
27
+ * with a single class that owns the `PermissionManager`, `SessionRules`,
28
+ * cache keys, skill entries, and runtime context.
29
+ *
30
+ * Constructor deps:
31
+ * - `ExtensionPaths` — immutable path constants
32
+ * - `ForwardingController` — polling lifecycle
33
+ * - `SessionConfigStore` — owns extension config; provides refresh, log, read
34
+ * - `PromptingGatewayLifecycle` — prompting lifecycle forwarded via activate/deactivate
35
+ */
36
+ export class PermissionSession implements ToolCallGateInputs {
37
+ private context: ExtensionContext | null = null;
38
+ private skillEntries: SkillPromptEntry[] = [];
39
+ private knownAgentName: string | null = null;
40
+
41
+ constructor(
42
+ private readonly paths: ExtensionPaths,
43
+ private readonly forwarding: ForwardingController,
44
+ private readonly permissionManager: ScopedPermissionManager,
45
+ private readonly sessionRules: SessionRules,
46
+ private readonly configStore: SessionConfigStore,
47
+ private readonly gateway: PromptingGatewayLifecycle,
48
+ ) {}
49
+
50
+ // ── Context lifecycle ──────────────────────────────────────────────────
51
+
52
+ /** Store the current extension context, start forwarding, and activate the gateway. */
53
+ activate(ctx: ExtensionContext): void {
54
+ this.context = ctx;
55
+ this.forwarding.start(ctx);
56
+ this.gateway.activate(ctx);
57
+ }
58
+
59
+ /** Clear the context, stop forwarding, and deactivate the gateway. */
60
+ deactivate(): void {
61
+ this.context = null;
62
+ this.forwarding.stop();
63
+ this.gateway.deactivate();
64
+ }
65
+
66
+ /** Return the current runtime context, or null if not activated. */
67
+ getRuntimeContext(): ExtensionContext | null {
68
+ return this.context;
69
+ }
70
+
71
+ // ── UI notifications ────────────────────────────────────────────────────
72
+
73
+ /** Surface a warning message to the user via the active UI context, if any. */
74
+ notify(message: string): void {
75
+ this.context?.ui.notify(message, "warning");
76
+ }
77
+
78
+ // ── Session lifecycle ────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Reset all mutable state for a new session.
82
+ *
83
+ * Configures the injected PermissionManager for `ctx.cwd`, clears skill
84
+ * entries, and activates the new context.
85
+ */
86
+ resetForNewSession(ctx: ExtensionContext): void {
87
+ this.permissionManager.configureForCwd(ctx.cwd);
88
+ this.skillEntries = [];
89
+ this.activate(ctx);
90
+ }
91
+
92
+ /**
93
+ * Shut down the session: clear rules, skill entries, and deactivate
94
+ * context + forwarding.
95
+ */
96
+ shutdown(): void {
97
+ this.sessionRules.clear();
98
+ this.skillEntries = [];
99
+ this.deactivate();
100
+ }
101
+
102
+ /**
103
+ * Reload permission manager and clear skill entries for the current context.
104
+ * Used on config reload (e.g. `resources_discover` with reason "reload").
105
+ */
106
+ reload(): void {
107
+ this.permissionManager.configureForCwd(this.context?.cwd);
108
+ this.skillEntries = [];
109
+ }
110
+
111
+ // ── Skill entries ──────────────────────────────────────────────────────
112
+
113
+ getActiveSkillEntries(): SkillPromptEntry[] {
114
+ return this.skillEntries;
115
+ }
116
+
117
+ setActiveSkillEntries(entries: SkillPromptEntry[]): void {
118
+ this.skillEntries = entries;
119
+ }
120
+
121
+ // ── Agent name ─────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Resolve the active agent name from the session context, system prompt,
125
+ * or last known name. Updates lastKnownActiveAgentName as a side effect.
126
+ */
127
+ resolveAgentName(
128
+ ctx: ExtensionContext,
129
+ systemPrompt?: string,
130
+ ): string | null {
131
+ const fromSession = getActiveAgentName(ctx);
132
+ if (fromSession) {
133
+ this.knownAgentName = fromSession;
134
+ return fromSession;
135
+ }
136
+ const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
137
+ if (fromSystemPrompt) {
138
+ this.knownAgentName = fromSystemPrompt;
139
+ return fromSystemPrompt;
140
+ }
141
+ return this.knownAgentName;
142
+ }
143
+
144
+ // Read by the `index.ts` config-modal adapter closure:
145
+ // `permissionManager.getComposedConfigRules(session.lastKnownActiveAgentName ?? undefined)`.
146
+ get lastKnownActiveAgentName(): string | null {
147
+ return this.knownAgentName;
148
+ }
149
+
150
+ // ── Config ─────────────────────────────────────────────────────────────
151
+
152
+ /** Reload merged config from disk; optionally update the stored runtime context. */
153
+ refreshConfig(ctx?: ExtensionContext): void {
154
+ this.configStore.refresh(ctx);
155
+ }
156
+
157
+ /** Write the resolved config path set to the review and debug logs. */
158
+ logResolvedConfigPaths(): void {
159
+ this.configStore.logResolvedPaths(this.context?.cwd);
160
+ }
161
+
162
+ /** Read current extension config. */
163
+ get config(): PermissionSystemExtensionConfig {
164
+ return this.configStore.current();
165
+ }
166
+
167
+ // ── Infrastructure paths ───────────────────────────────────────────────
168
+
169
+ /**
170
+ * Combined infrastructure read directories: static paths from
171
+ * `ExtensionPaths` plus config-derived paths.
172
+ */
173
+ getInfrastructureReadDirs(): string[] {
174
+ return [
175
+ ...this.paths.piInfrastructureDirs,
176
+ ...(this.config.piInfrastructureReadPaths ?? []),
177
+ ];
178
+ }
179
+
180
+ /**
181
+ * Resolved tool-preview formatter options from the current config.
182
+ *
183
+ * Replaces the handler's `resolveToolPreviewLimits(session.config)` reach
184
+ * so the pipeline reads a clean value rather than pulling raw config.
185
+ */
186
+ getToolPreviewLimits(): ToolPreviewFormatterOptions {
187
+ return resolveToolPreviewLimits(this.config);
188
+ }
189
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Centralized construction for `permissions:ui_prompt` payloads.
3
+ *
4
+ * Every emit site builds its event through one of these functions, so the
5
+ * public contract's shape — including the normalized `surface`/`value`
6
+ * projection — lives in exactly one place and cannot drift by source.
7
+ *
8
+ * This module is a leaf: it owns narrow input types that each call site's
9
+ * domain object satisfies structurally, so it imports nothing from the
10
+ * prompter, RPC, or forwarding modules (no import cycles, correct layering).
11
+ */
12
+
13
+ import type {
14
+ PermissionUiPromptEvent,
15
+ PermissionUiPromptSource,
16
+ } from "./permission-events";
17
+
18
+ /** Input for a direct (non-forwarded) tool or skill prompt. */
19
+ export interface DirectPromptInput {
20
+ requestId: string;
21
+ source: "tool_call" | "skill_input" | "skill_read";
22
+ agentName: string | null;
23
+ message: string;
24
+ toolName?: string;
25
+ skillName?: string;
26
+ path?: string;
27
+ command?: string;
28
+ target?: string;
29
+ }
30
+
31
+ /** Input for a `permissions:rpc:prompt` forwarded UI prompt. */
32
+ export interface RpcPromptInput {
33
+ requestId: string;
34
+ surface?: string | null;
35
+ value?: string | null;
36
+ agentName?: string | null;
37
+ message: string;
38
+ }
39
+
40
+ /** Input for a file-forwarded subagent prompt shown by the parent UI. */
41
+ export interface ForwardedPromptInput {
42
+ requestId: string;
43
+ message: string;
44
+ requesterAgentName: string | null;
45
+ requesterSessionId: string | null;
46
+ /** Original prompt origin, when the forwarded request carries it. */
47
+ source?: PermissionUiPromptSource | null;
48
+ /** Original normalized surface, when the forwarded request carries it. */
49
+ surface?: string | null;
50
+ /** Original normalized value, when the forwarded request carries it. */
51
+ value?: string | null;
52
+ }
53
+
54
+ /** Normalized display surface for a direct prompt. */
55
+ function directSurface(input: DirectPromptInput): string | null {
56
+ if (input.source === "skill_input" || input.source === "skill_read") {
57
+ return "skill";
58
+ }
59
+ return input.toolName ?? null;
60
+ }
61
+
62
+ /** Normalized display value for a direct prompt. */
63
+ function directValue(input: DirectPromptInput): string | null {
64
+ return (
65
+ input.command ??
66
+ input.path ??
67
+ input.target ??
68
+ input.skillName ??
69
+ input.toolName ??
70
+ null
71
+ );
72
+ }
73
+
74
+ /** Build the UI prompt event for a direct tool/skill prompt. */
75
+ export function buildDirectUiPrompt(
76
+ input: DirectPromptInput,
77
+ ): PermissionUiPromptEvent {
78
+ return {
79
+ requestId: input.requestId,
80
+ source: input.source,
81
+ surface: directSurface(input),
82
+ value: directValue(input),
83
+ agentName: input.agentName,
84
+ message: input.message,
85
+ forwarding: null,
86
+ };
87
+ }
88
+
89
+ /** Build the UI prompt event for an RPC-forwarded prompt. */
90
+ export function buildRpcUiPrompt(
91
+ input: RpcPromptInput,
92
+ ): PermissionUiPromptEvent {
93
+ return {
94
+ requestId: input.requestId,
95
+ source: "rpc_prompt",
96
+ surface: input.surface ?? null,
97
+ value: input.value ?? null,
98
+ agentName: input.agentName ?? null,
99
+ message: input.message,
100
+ forwarding: null,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Build the UI prompt event for a file-forwarded subagent prompt.
106
+ *
107
+ * `source` defaults to `"tool_call"` (the dominant forwarded origin) when the
108
+ * persisted request predates carrying it — a parent on a newer version may read
109
+ * a request written by an older child during an upgrade. The consumer still
110
+ * receives the notify-now signal, message, and forwarding context.
111
+ */
112
+ export function buildForwardedUiPrompt(
113
+ input: ForwardedPromptInput,
114
+ ): PermissionUiPromptEvent {
115
+ return {
116
+ requestId: input.requestId,
117
+ source: input.source ?? "tool_call",
118
+ surface: input.surface ?? null,
119
+ value: input.value ?? null,
120
+ agentName: input.requesterAgentName,
121
+ message: input.message,
122
+ forwarding: {
123
+ requesterAgentName: input.requesterAgentName,
124
+ requesterSessionId: input.requesterSessionId,
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,63 @@
1
+ import { buildInputForSurface } from "./input-normalizer";
2
+ import type { ScopedPermissionManager } from "./permission-manager";
3
+ import type { PermissionsService } from "./service";
4
+ import type { SessionRules } from "./session-rules";
5
+ import type {
6
+ ToolAccessExtractor,
7
+ ToolAccessExtractorRegistrar,
8
+ } from "./tool-access-extractor-registry";
9
+ import type {
10
+ ToolInputFormatter,
11
+ ToolInputFormatterRegistrar,
12
+ } from "./tool-input-formatter-registry";
13
+
14
+ /**
15
+ * In-process implementation of the cross-extension {@link PermissionsService}.
16
+ *
17
+ * Constructed once in the composition root and backed by the single shared
18
+ * `PermissionManager` and `SessionRules` instances that `PermissionSession`
19
+ * also uses — so service queries and gate-path approvals see the same state.
20
+ */
21
+ export class LocalPermissionsService implements PermissionsService {
22
+ constructor(
23
+ private readonly permissionManager: ScopedPermissionManager,
24
+ private readonly sessionRules: Pick<SessionRules, "getRuleset">,
25
+ private readonly formatterRegistry: ToolInputFormatterRegistrar,
26
+ private readonly accessExtractorRegistry: ToolAccessExtractorRegistrar,
27
+ ) {}
28
+
29
+ checkPermission(
30
+ surface: string,
31
+ value?: string,
32
+ agentName?: string,
33
+ ): ReturnType<PermissionsService["checkPermission"]> {
34
+ const input = buildInputForSurface(surface, value);
35
+ return this.permissionManager.checkPermission(
36
+ surface,
37
+ input,
38
+ agentName,
39
+ this.sessionRules.getRuleset(),
40
+ );
41
+ }
42
+
43
+ getToolPermission(
44
+ toolName: string,
45
+ agentName?: string,
46
+ ): ReturnType<PermissionsService["getToolPermission"]> {
47
+ return this.permissionManager.getToolPermission(toolName, agentName);
48
+ }
49
+
50
+ registerToolInputFormatter(
51
+ toolName: string,
52
+ formatter: ToolInputFormatter,
53
+ ): ReturnType<PermissionsService["registerToolInputFormatter"]> {
54
+ return this.formatterRegistry.register(toolName, formatter);
55
+ }
56
+
57
+ registerToolAccessExtractor(
58
+ toolName: string,
59
+ extractor: ToolAccessExtractor,
60
+ ): ReturnType<PermissionsService["registerToolAccessExtractor"]> {
61
+ return this.accessExtractorRegistry.register(toolName, extractor);
62
+ }
63
+ }