@gotgenes/pi-permission-system 31.0.2 → 31.1.1

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 (138) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/public.d.ts +15 -15
  3. package/docs/configuration.md +6 -2
  4. package/docs/cross-extension-api.md +1 -1
  5. package/docs/session-approvals.md +2 -2
  6. package/package.json +1 -1
  7. package/src/access-intent/access-intent.ts +1 -1
  8. package/src/access-intent/bash/bash-path-resolver.ts +11 -14
  9. package/src/access-intent/bash/command-enumeration.ts +6 -12
  10. package/src/access-intent/bash/msys-bash-tokens.ts +1 -1
  11. package/src/access-intent/bash/nested-execution.ts +1 -1
  12. package/src/access-intent/bash/node-text.ts +2 -2
  13. package/src/access-intent/bash/parser.ts +1 -1
  14. package/src/access-intent/bash/program.ts +4 -7
  15. package/src/access-intent/bash/redirect-analysis.ts +2 -2
  16. package/src/access-intent/bash/shell-variable-expansion.ts +1 -1
  17. package/src/access-intent/bash/sync-commands.ts +2 -5
  18. package/src/access-intent/bash/token-collection.ts +6 -9
  19. package/src/access-intent/bash/wrapper-analysis.ts +1 -1
  20. package/src/access-intent/input-normalizer.ts +2 -2
  21. package/src/access-intent/path-normalization.ts +1 -1
  22. package/src/access-intent/path-surfaces.ts +1 -1
  23. package/src/access-intent/tool-input-path.ts +1 -1
  24. package/src/access-intent/tool-kind.ts +1 -1
  25. package/src/authority/approval-escalator.ts +13 -13
  26. package/src/authority/authorizer-chain.ts +1 -1
  27. package/src/authority/authorizer-registry.ts +1 -1
  28. package/src/authority/authorizer-selection.ts +1 -1
  29. package/src/authority/authorizer.ts +9 -9
  30. package/src/authority/decision-resolution.ts +2 -5
  31. package/src/authority/denying-authorizer.ts +1 -1
  32. package/src/authority/forwarded-request-server.ts +21 -24
  33. package/src/authority/forwarder-context.ts +2 -2
  34. package/src/authority/forwarding-io.ts +14 -12
  35. package/src/authority/forwarding-liveness.ts +5 -8
  36. package/src/authority/forwarding-manager.ts +1 -1
  37. package/src/authority/inherited-registrations.ts +2 -2
  38. package/src/authority/local-user-authorizer.ts +13 -13
  39. package/src/authority/permission-dialog.ts +2 -2
  40. package/src/authority/permission-forwarding.ts +6 -3
  41. package/src/authority/permission-prompt-component.ts +12 -15
  42. package/src/authority/permission-prompt-decision.ts +2 -2
  43. package/src/authority/permission-prompter.ts +8 -8
  44. package/src/authority/subagent-context.ts +3 -3
  45. package/src/authority/subagent-detection.ts +3 -3
  46. package/src/{config-loader.ts → config/config-loader.ts} +3 -3
  47. package/src/{config-modal.ts → config/config-modal.ts} +1 -2
  48. package/src/{config-reporter.ts → config/config-reporter.ts} +1 -1
  49. package/src/{config-store.ts → config/config-store.ts} +1 -2
  50. package/src/{extension-config.ts → config/extension-config.ts} +4 -4
  51. package/src/{extension-paths.ts → config/extension-paths.ts} +1 -1
  52. package/src/{policy-loader.ts → config/policy-loader.ts} +2 -2
  53. package/src/{skill-prompt-sanitizer.ts → exposure/skill-prompt-sanitizer.ts} +2 -2
  54. package/src/{tool-registry.ts → exposure/tool-registry.ts} +1 -1
  55. package/src/exposure/tool-surface-baseline.ts +101 -0
  56. package/src/handlers/before-agent-start.ts +48 -21
  57. package/src/handlers/gates/bash-command.ts +2 -2
  58. package/src/handlers/gates/bash-external-directory.ts +3 -3
  59. package/src/handlers/gates/bash-path-extractor.ts +1 -1
  60. package/src/handlers/gates/bash-path.ts +4 -4
  61. package/src/handlers/gates/descriptor.ts +2 -2
  62. package/src/handlers/gates/external-directory-policy.ts +2 -2
  63. package/src/handlers/gates/external-directory.ts +4 -4
  64. package/src/handlers/gates/helpers.ts +1 -1
  65. package/src/handlers/gates/path.ts +4 -4
  66. package/src/handlers/gates/runner.ts +4 -4
  67. package/src/handlers/gates/skill-read.ts +3 -3
  68. package/src/handlers/gates/tool-call-gate-pipeline.ts +7 -7
  69. package/src/handlers/gates/tool.ts +3 -3
  70. package/src/handlers/lifecycle.ts +6 -7
  71. package/src/handlers/permission-gate-handler.ts +11 -11
  72. package/src/handlers/session-turn-prep.ts +1 -1
  73. package/src/handlers/tool-call-boundary.ts +3 -3
  74. package/src/index.ts +40 -39
  75. package/src/{decision-reporter.ts → logging/decision-reporter.ts} +1 -1
  76. package/src/{logging.ts → logging/logging.ts} +1 -1
  77. package/src/{session-logger.ts → logging/session-logger.ts} +14 -5
  78. package/src/path/approval-pattern.ts +1 -1
  79. package/src/path/canonicalize-path.ts +1 -1
  80. package/src/path/path-containment.ts +2 -2
  81. package/src/path/path-flavor.ts +1 -1
  82. package/src/{path-normalizer.ts → path/path-normalizer.ts} +6 -8
  83. package/src/path/pi-infrastructure-read.ts +3 -3
  84. package/src/{normalize.ts → policy/normalize.ts} +2 -2
  85. package/src/{permission-gate.ts → policy/permission-gate.ts} +1 -1
  86. package/src/{permission-manager.ts → policy/permission-manager.ts} +21 -15
  87. package/src/{permission-merge.ts → policy/permission-merge.ts} +1 -1
  88. package/src/{permission-resolver.ts → policy/permission-resolver.ts} +5 -5
  89. package/src/{rule.ts → policy/rule.ts} +3 -4
  90. package/src/{scope-merge.ts → policy/scope-merge.ts} +3 -3
  91. package/src/{synthesize.ts → policy/synthesize.ts} +1 -1
  92. package/src/{wildcard-matcher.ts → policy/wildcard-matcher.ts} +1 -1
  93. package/src/presentation/agent-renderer.ts +5 -5
  94. package/src/presentation/dialog-renderer.ts +3 -3
  95. package/src/presentation/fact-vocabulary.ts +1 -4
  96. package/src/presentation/forwarded-ask-payload.ts +1 -4
  97. package/src/presentation/path-ask-payload.ts +2 -5
  98. package/src/{pattern-suggest.ts → presentation/pattern-suggest.ts} +6 -3
  99. package/src/{permission-prompts.ts → presentation/permission-prompts.ts} +1 -1
  100. package/src/presentation/review-log-renderer.ts +1 -1
  101. package/src/presentation/skill-ask-payload.ts +3 -3
  102. package/src/presentation/tool-ask-payload.ts +3 -6
  103. package/src/{bash-advisory-check.ts → service/bash-advisory-check.ts} +1 -1
  104. package/src/{permissions-service.ts → service/permissions-service.ts} +10 -10
  105. package/src/{service-lifecycle.ts → service/service-lifecycle.ts} +5 -5
  106. package/src/service.ts +18 -18
  107. package/src/{permission-session.ts → session/permission-session.ts} +39 -15
  108. package/src/{session-approval.ts → session/session-approval.ts} +2 -2
  109. package/src/{session-rules.ts → session/session-rules.ts} +1 -1
  110. package/src/{builtin-tool-input-formatters.ts → tool-input/builtin-tool-input-formatters.ts} +1 -1
  111. package/src/{tool-input-preview.ts → tool-input/tool-input-preview.ts} +2 -2
  112. package/src/{tool-input-prompt-formatters.ts → tool-input/tool-input-prompt-formatters.ts} +1 -1
  113. package/src/{tool-preview-formatter.ts → tool-input/tool-preview-formatter.ts} +3 -3
  114. package/src/types.ts +2 -2
  115. /package/src/{async-cache.ts → access-intent/bash/async-cache.ts} +0 -0
  116. /package/src/{bash-arity.ts → access-intent/bash/bash-arity.ts} +0 -0
  117. /package/src/{config-paths.ts → config/config-paths.ts} +0 -0
  118. /package/src/{config-schema.ts → config/config-schema.ts} +0 -0
  119. /package/src/{status.ts → config/status.ts} +0 -0
  120. /package/src/{yaml-frontmatter.ts → config/yaml-frontmatter.ts} +0 -0
  121. /package/src/{system-prompt-sanitizer.ts → exposure/system-prompt-sanitizer.ts} +0 -0
  122. /package/src/{decision-audit.ts → logging/decision-audit.ts} +0 -0
  123. /package/src/{json-safe-stringify.ts → logging/json-safe-stringify.ts} +0 -0
  124. /package/src/{log-field-cap.ts → logging/log-field-cap.ts} +0 -0
  125. /package/src/{log-file-permissions.ts → logging/log-file-permissions.ts} +0 -0
  126. /package/src/{log-redaction.ts → logging/log-redaction.ts} +0 -0
  127. /package/src/{expand-home.ts → path/expand-home.ts} +0 -0
  128. /package/src/{node-modules-discovery.ts → path/node-modules-discovery.ts} +0 -0
  129. /package/src/{safe-system-paths.ts → path/safe-system-paths.ts} +0 -0
  130. /package/src/{restrictiveness.ts → policy/restrictiveness.ts} +0 -0
  131. /package/src/{permission-events.ts → service/permission-events.ts} +0 -0
  132. /package/src/{permission-ui-prompt.ts → service/permission-ui-prompt.ts} +0 -0
  133. /package/src/{active-agent.ts → session/active-agent.ts} +0 -0
  134. /package/src/{approval-grant.ts → session/approval-grant.ts} +0 -0
  135. /package/src/{session-approval-recorder.ts → session/session-approval-recorder.ts} +0 -0
  136. /package/src/{session-identity.ts → session/session-identity.ts} +0 -0
  137. /package/src/{tool-access-extractor-registry.ts → tool-input/tool-access-extractor-registry.ts} +0 -0
  138. /package/src/{tool-input-formatter-registry.ts → tool-input/tool-input-formatter-registry.ts} +0 -0
@@ -18,23 +18,20 @@
18
18
 
19
19
  import { readdirSync, readFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
+ import type { DebugReviewLogger } from "#src/logging/session-logger";
21
22
  import {
22
23
  ensureDirectoryExists,
23
24
  isErrnoCode,
24
25
  logPermissionForwardingError,
25
26
  safeDeleteFile,
26
27
  writeJsonFileAtomic,
27
- } from "#src/authority/forwarding-io";
28
- import type { PermissionForwardingTarget } from "#src/authority/permission-forwarding";
28
+ } from "./forwarding-io";
29
+ import type { PermissionForwardingTarget } from "./permission-forwarding";
29
30
  import {
30
31
  encodeSessionIdForPath,
31
32
  PERMISSION_FORWARDING_POLL_INTERVAL_MS,
32
- } from "#src/authority/permission-forwarding";
33
- import type {
34
- ServingAnnouncer,
35
- ServingLookup,
36
- } from "#src/authority/serving-registry";
37
- import type { DebugReviewLogger } from "#src/session-logger";
33
+ } from "./permission-forwarding";
34
+ import type { ServingAnnouncer, ServingLookup } from "./serving-registry";
38
35
 
39
36
  /**
40
37
  * How often a serving session rewrites its heartbeat — four poll ticks.
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { DebugReviewLogger } from "#src/session-logger";
2
+ import type { DebugReviewLogger } from "#src/logging/session-logger";
3
3
  import type { InboxProcessor } from "./forwarded-request-server";
4
4
  import { getSessionId } from "./forwarder-context";
5
5
  import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
@@ -36,11 +36,11 @@ import type { PermissionsService } from "#src/service";
36
36
  import type {
37
37
  ResolvedToolAccessExtractor,
38
38
  ToolAccessExtractorLookup,
39
- } from "#src/tool-access-extractor-registry";
39
+ } from "#src/tool-input/tool-access-extractor-registry";
40
40
  import type {
41
41
  ToolInputFormatter,
42
42
  ToolInputFormatterLookup,
43
- } from "#src/tool-input-formatter-registry";
43
+ } from "#src/tool-input/tool-input-formatter-registry";
44
44
 
45
45
  /** This node's own session id, or `null` when the host exposes none. */
46
46
  export interface NodeIdentity {
@@ -1,25 +1,25 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { provenDirectionOf } from "#src/approval-grant";
3
- import type {
4
- PermissionPromptDecision,
5
- RequestPermissionOptions,
6
- } from "#src/authority/permission-dialog";
7
- import type {
8
- PermissionPromptUi,
9
- PromptPreferences,
10
- requestPermissionDecision,
11
- } from "#src/authority/permission-prompt-component";
12
2
  import {
13
3
  buildDirectionalSessionLabels,
14
4
  buildForwardedScopeLabels,
15
5
  describeGrantTarget,
16
- } from "#src/pattern-suggest";
6
+ } from "#src/presentation/pattern-suggest";
17
7
  import {
18
8
  emitUiPromptEvent,
19
9
  type PermissionEventBus,
20
- } from "#src/permission-events";
21
- import { buildUiPrompt } from "#src/permission-ui-prompt";
10
+ } from "#src/service/permission-events";
11
+ import { buildUiPrompt } from "#src/service/permission-ui-prompt";
12
+ import { provenDirectionOf } from "#src/session/approval-grant";
22
13
  import type { TerminalAuthorizer } from "./authorizer";
14
+ import type {
15
+ PermissionPromptDecision,
16
+ RequestPermissionOptions,
17
+ } from "./permission-dialog";
18
+ import type {
19
+ PermissionPromptUi,
20
+ PromptPreferences,
21
+ requestPermissionDecision,
22
+ } from "./permission-prompt-component";
23
23
  import type { PromptPermissionDetails } from "./permission-prompter";
24
24
 
25
25
  /** Dependencies required by {@link LocalUserAuthorizer}. */
@@ -1,5 +1,5 @@
1
- import type { SessionGrantWidth } from "#src/approval-grant";
2
- import type { DecisionSource } from "#src/authority/decision-source";
1
+ import type { SessionGrantWidth } from "#src/session/approval-grant";
2
+ import type { DecisionSource } from "./decision-source";
3
3
 
4
4
  export type PermissionDecisionState =
5
5
  | "approved"
@@ -1,8 +1,11 @@
1
1
  import { join } from "node:path";
2
- import type { ApprovalGrant, SessionGrantWidth } from "#src/approval-grant";
3
- import type { DecisionSource } from "#src/authority/decision-source";
4
- import type { PermissionUiPromptSource } from "#src/permission-events";
5
2
  import type { PromptPayload } from "#src/presentation/prompt-payload";
3
+ import type { PermissionUiPromptSource } from "#src/service/permission-events";
4
+ import type {
5
+ ApprovalGrant,
6
+ SessionGrantWidth,
7
+ } from "#src/session/approval-grant";
8
+ import type { DecisionSource } from "./decision-source";
6
9
  import type { PermissionDecisionState } from "./permission-dialog";
7
10
  import type { SubagentSessionRegistry } from "./subagent-registry";
8
11
 
@@ -4,17 +4,22 @@ import type {
4
4
  KeybindingsManager,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { type Component, Input, matchesKey } from "@earendil-works/pi-tui";
7
- import { collapsePastedNewlines } from "#src/authority/bracketed-paste";
8
- import type {
9
- DecisionSource,
10
- UserDecisionSurface,
11
- } from "#src/authority/decision-source";
7
+ import {
8
+ completeViewBudget,
9
+ type DialogView,
10
+ type RenderBudget,
11
+ renderPromptDialog,
12
+ } from "#src/presentation/dialog-renderer";
13
+ import { fitLinesToWidth } from "#src/presentation/line-fitting";
14
+ import type { PromptPayload } from "#src/presentation/prompt-payload";
15
+ import { collapsePastedNewlines } from "./bracketed-paste";
16
+ import type { DecisionSource, UserDecisionSurface } from "./decision-source";
12
17
  import {
13
18
  type PermissionPromptDecision,
14
19
  type RequestPermissionOptions,
15
20
  requestPermissionDecisionFromUi,
16
21
  type UnattributedDecision,
17
- } from "#src/authority/permission-dialog";
22
+ } from "./permission-dialog";
18
23
  import {
19
24
  initialPromptState,
20
25
  type PromptEvent,
@@ -23,15 +28,7 @@ import {
23
28
  type PromptViewState,
24
29
  reducePrompt,
25
30
  visibleOptionKeys,
26
- } from "#src/authority/permission-prompt-decision";
27
- import {
28
- completeViewBudget,
29
- type DialogView,
30
- type RenderBudget,
31
- renderPromptDialog,
32
- } from "#src/presentation/dialog-renderer";
33
- import { fitLinesToWidth } from "#src/presentation/line-fitting";
34
- import type { PromptPayload } from "#src/presentation/prompt-payload";
31
+ } from "./permission-prompt-decision";
35
32
 
36
33
  /**
37
34
  * Inline `ctx.ui.custom` permission dialog for TUI sessions.
@@ -1,10 +1,10 @@
1
- import type { SessionGrantWidth } from "#src/approval-grant";
1
+ import type { SessionGrantWidth } from "#src/session/approval-grant";
2
2
  import {
3
3
  createDeniedPermissionDecision,
4
4
  normalizePermissionDenialReason,
5
5
  type RequestPermissionOptions,
6
6
  type UnattributedDecision,
7
- } from "#src/authority/permission-dialog";
7
+ } from "./permission-dialog";
8
8
 
9
9
  /**
10
10
  * Pure decision model for the inline keybind permission dialog.
@@ -1,14 +1,14 @@
1
- import type { SessionGrantWidth } from "#src/approval-grant";
2
- import type { DecisionSource } from "#src/authority/decision-source";
3
- import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
4
- import type {
5
- ForwardedAccessFacts,
6
- ForwardedSessionApproval,
7
- } from "#src/authority/permission-forwarding";
1
+ import type { ReviewLogger } from "#src/logging/session-logger";
8
2
  import type { PromptPayload } from "#src/presentation/prompt-payload";
9
3
  import { renderReviewLogFacts } from "#src/presentation/review-log-renderer";
10
- import type { ReviewLogger } from "#src/session-logger";
4
+ import type { SessionGrantWidth } from "#src/session/approval-grant";
11
5
  import type { TerminalAuthorizer } from "./authorizer";
6
+ import type { DecisionSource } from "./decision-source";
7
+ import type { PermissionPromptDecision } from "./permission-dialog";
8
+ import type {
9
+ ForwardedAccessFacts,
10
+ ForwardedSessionApproval,
11
+ } from "./permission-forwarding";
12
12
 
13
13
  export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
14
14
 
@@ -1,7 +1,7 @@
1
- import { SUBAGENT_ENV_HINT_KEYS } from "#src/authority/permission-forwarding";
2
- import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
3
1
  import type { PathFlavor } from "#src/path/path-flavor";
4
- import { readSessionId } from "#src/session-identity";
2
+ import { readSessionId } from "#src/session/session-identity";
3
+ import { SUBAGENT_ENV_HINT_KEYS } from "./permission-forwarding";
4
+ import type { SubagentSessionRegistry } from "./subagent-registry";
5
5
 
6
6
  /**
7
7
  * Narrow context for subagent detection — the only session-manager readers
@@ -1,9 +1,9 @@
1
+ import type { PathFlavor } from "#src/path/path-flavor";
1
2
  import {
2
3
  isSubagentExecutionContext,
3
4
  type SubagentDetectionContext,
4
- } from "#src/authority/subagent-context";
5
- import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
6
- import type { PathFlavor } from "#src/path/path-flavor";
5
+ } from "./subagent-context";
6
+ import type { SubagentSessionRegistry } from "./subagent-registry";
7
7
 
8
8
  /**
9
9
  * Narrow seam for the ask-path consumers: "is the current session a subagent?"
@@ -1,6 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { normalize } from "node:path";
3
3
  import type { ZodError } from "zod";
4
+ import { mergeFlatPermissions } from "#src/policy/permission-merge";
5
+ import type { FlatPermissionConfig, PatternValue } from "#src/types";
6
+ import { isDenyWithReason, isPermissionState } from "#src/types";
4
7
  import {
5
8
  getGlobalConfigPath,
6
9
  getLegacyExtensionConfigPath,
@@ -13,9 +16,6 @@ import {
13
16
  type UnifiedPermissionConfig,
14
17
  unifiedConfigSchema,
15
18
  } from "./config-schema";
16
- import { mergeFlatPermissions } from "./permission-merge";
17
- import type { FlatPermissionConfig, PatternValue } from "./types";
18
- import { isDenyWithReason, isPermissionState } from "./types";
19
19
 
20
20
  // The unified config shape is derived from the zod schema (config-schema.ts,
21
21
  // the single source of truth) and re-exported so existing importers keep their
@@ -4,13 +4,12 @@ import {
4
4
  getSettingsListTheme,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { type SettingItem, SettingsList } from "@earendil-works/pi-tui";
7
-
7
+ import type { Ruleset } from "#src/policy/rule";
8
8
  import type { CommandConfigStore } from "./config-store";
9
9
  import {
10
10
  DEFAULT_EXTENSION_CONFIG,
11
11
  type PermissionSystemExtensionConfig,
12
12
  } from "./extension-config";
13
- import type { Ruleset } from "./rule";
14
13
 
15
14
  interface PermissionSystemConfigController {
16
15
  config: CommandConfigStore;
@@ -1,4 +1,4 @@
1
- import type { ResolvedPolicyPaths } from "./permission-manager";
1
+ import type { ResolvedPolicyPaths } from "#src/policy/permission-manager";
2
2
 
3
3
  export interface ResolvedConfigLogEntry {
4
4
  globalConfigPath: string;
@@ -10,7 +10,7 @@ import type {
10
10
  ExtensionCommandContext,
11
11
  ExtensionContext,
12
12
  } from "@earendil-works/pi-coding-agent";
13
-
13
+ import type { DebugReviewLogger } from "#src/logging/session-logger";
14
14
  import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
15
15
  import {
16
16
  getGlobalConfigPath,
@@ -26,7 +26,6 @@ import {
26
26
  type PermissionSystemExtensionConfig,
27
27
  } from "./extension-config";
28
28
  import type { ResolvedPolicyPaths } from "./policy-loader";
29
- import type { DebugReviewLogger } from "./session-logger";
30
29
  import { syncPermissionSystemStatus } from "./status";
31
30
 
32
31
  /** Read-only view of the current config — for consumers that only read. */
@@ -1,14 +1,14 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import {
5
+ OWNER_ONLY_DIRECTORY_MODE,
6
+ restrictExistingPathToOwner,
7
+ } from "#src/logging/log-file-permissions";
4
8
  import type {
5
9
  ShellToolsConfig,
6
10
  UnifiedPermissionConfig,
7
11
  } from "./config-loader";
8
- import {
9
- OWNER_ONLY_DIRECTORY_MODE,
10
- restrictExistingPathToOwner,
11
- } from "./log-file-permissions";
12
12
 
13
13
  export const EXTENSION_ID = "pi-permission-system";
14
14
 
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
+ import { discoverGlobalNodeModulesRoot } from "#src/path/node-modules-discovery";
2
3
  import { getGlobalLogsDir } from "./config-paths";
3
- import { discoverGlobalNodeModulesRoot } from "./node-modules-discovery";
4
4
 
5
5
  /**
6
6
  * Immutable path constants derived from `agentDir` at construction time.
@@ -1,14 +1,14 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import type { ScopeConfig } from "#src/types";
5
+ import { toRecord } from "#src/value-guards";
4
6
  import {
5
7
  loadUnifiedConfig,
6
8
  normalizeFlatPermissionValue,
7
9
  stripJsonComments,
8
10
  } from "./config-loader";
9
11
  import { getGlobalConfigPath } from "./config-paths";
10
- import type { ScopeConfig } from "./types";
11
- import { toRecord } from "./value-guards";
12
12
  import { extractFrontmatter, parseSimpleYamlMap } from "./yaml-frontmatter";
13
13
 
14
14
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  import { dirname } from "node:path";
2
2
 
3
- import type { PathNormalizer } from "./path-normalizer";
4
- import type { PermissionCheckResult, PermissionState } from "./types";
3
+ import type { PathNormalizer } from "#src/path/path-normalizer";
4
+ import type { PermissionCheckResult, PermissionState } from "#src/types";
5
5
 
6
6
  /**
7
7
  * Narrow interface for the raw (no-session-rules) permission checker used by
@@ -1,4 +1,4 @@
1
- import { getNonEmptyString, toRecord } from "./value-guards";
1
+ import { getNonEmptyString, toRecord } from "#src/value-guards";
2
2
 
3
3
  /** Narrow interface for the Pi tool API subset used by handler classes. */
4
4
  export interface ToolRegistry {
@@ -0,0 +1,101 @@
1
+ /** What a turn observed about pi's tool surface. */
2
+ export interface ToolSurfaceObservation {
3
+ /** Names pi reports active right now (`pi.getActiveTools()`). */
4
+ readonly active: readonly string[];
5
+ /** Names pi currently has registered (`pi.getAllTools()`). */
6
+ readonly registered: ReadonlySet<string>;
7
+ }
8
+
9
+ /** The effective tool surface for one turn, and what changed to produce it. */
10
+ export interface ToolSurfaceResolution {
11
+ /** Names to hand to `setActive`, in baseline order. */
12
+ readonly exposed: readonly string[];
13
+ /** Baseline members the current policy withholds. */
14
+ readonly withheld: readonly string[];
15
+ /** Names withheld on an earlier turn that the current policy exposes again. */
16
+ readonly restored: readonly string[];
17
+ /** Whether the withheld set differs from the previous turn's. */
18
+ readonly changed: boolean;
19
+ }
20
+
21
+ /** Decides whether the current policy lets the agent see a tool. */
22
+ type ToolExposurePolicy = (toolName: string) => boolean;
23
+
24
+ /**
25
+ * The runtime tool surface a session has, independent of what policy withholds.
26
+ *
27
+ * Filtering writes its result back through `setActive`, so reading the active
28
+ * set again next turn returns the *filtered* set. Applying policy to that makes
29
+ * the surface monotonically shrink and leaves a tool stranded once its rule is
30
+ * relaxed (#873). This baseline is the stable input policy is applied to
31
+ * instead: `exposed = baseline ∩ policy`, recomputed every turn.
32
+ *
33
+ * The baseline only ever grows from tools observed **active**, never from the
34
+ * whole registry, so a tool pi deliberately left inactive is never activated
35
+ * (#385). A tool that stops being active without this extension withholding it
36
+ * — another extension deactivating it — leaves the baseline with it.
37
+ */
38
+ export class ToolSurfaceBaseline {
39
+ private baseline: readonly string[] = [];
40
+ private withheld: ReadonlySet<string> = new Set();
41
+
42
+ resolveExposed(
43
+ observation: ToolSurfaceObservation,
44
+ isExposed: ToolExposurePolicy,
45
+ ): ToolSurfaceResolution {
46
+ const baseline = this.rebuild(observation);
47
+ const exposed: string[] = [];
48
+ const withheld: string[] = [];
49
+ for (const toolName of baseline) {
50
+ (isExposed(toolName) ? exposed : withheld).push(toolName);
51
+ }
52
+ const restored = exposed.filter((toolName) => this.withheld.has(toolName));
53
+ const changed = !holdsExactly(this.withheld, withheld);
54
+
55
+ this.baseline = baseline;
56
+ this.withheld = new Set(withheld);
57
+ return { exposed, withheld, restored, changed };
58
+ }
59
+
60
+ /** Forget the surface, so the next turn reseeds from what pi reports. */
61
+ reset(): void {
62
+ this.baseline = [];
63
+ this.withheld = new Set();
64
+ }
65
+
66
+ /**
67
+ * Reconstruct the pre-filter surface: the tools still active, plus the ones
68
+ * only this extension's own filtering removed, plus anything newly active.
69
+ *
70
+ * A withheld tool that has left the registry is forgotten rather than kept as
71
+ * a restoration candidate, so re-registering it inactive cannot activate it.
72
+ * The registry is consulted for withheld tools only — an active tool is real
73
+ * by definition — and an active tool is adopted whatever the registry says,
74
+ * so a registry that reports nothing can cost restoration candidates but
75
+ * never removes a tool pi has active.
76
+ */
77
+ private rebuild(observation: ToolSurfaceObservation): readonly string[] {
78
+ const active = new Set(observation.active);
79
+ const baseline = this.baseline.filter(
80
+ (toolName) =>
81
+ active.has(toolName) ||
82
+ (this.withheld.has(toolName) && observation.registered.has(toolName)),
83
+ );
84
+
85
+ const known = new Set(baseline);
86
+ for (const toolName of observation.active) {
87
+ if (!known.has(toolName)) {
88
+ known.add(toolName);
89
+ baseline.push(toolName);
90
+ }
91
+ }
92
+ return baseline;
93
+ }
94
+ }
95
+
96
+ function holdsExactly(
97
+ set: ReadonlySet<string>,
98
+ members: readonly string[],
99
+ ): boolean {
100
+ return set.size === members.length && members.every((m) => set.has(m));
101
+ }
@@ -2,12 +2,17 @@ import type {
2
2
  BeforeAgentStartEventResult,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
- import type { TurnPreparation } from "#src/handlers/session-turn-prep";
6
- import type { PermissionResolver } from "#src/permission-resolver";
7
- import type { PermissionSession } from "#src/permission-session";
8
- import { resolveSkillPromptEntries } from "#src/skill-prompt-sanitizer";
9
- import { sanitizeAvailableToolsSection } from "#src/system-prompt-sanitizer";
10
- import { getToolNameFromValue, type ToolRegistry } from "#src/tool-registry";
5
+ import { resolveSkillPromptEntries } from "#src/exposure/skill-prompt-sanitizer";
6
+ import { sanitizeAvailableToolsSection } from "#src/exposure/system-prompt-sanitizer";
7
+ import {
8
+ getToolNameFromValue,
9
+ type ToolRegistry,
10
+ } from "#src/exposure/tool-registry";
11
+ import type { ToolSurfaceObservation } from "#src/exposure/tool-surface-baseline";
12
+ import type { DebugLogger } from "#src/logging/session-logger";
13
+ import type { PermissionResolver } from "#src/policy/permission-resolver";
14
+ import type { PermissionSession } from "#src/session/permission-session";
15
+ import type { TurnPreparation } from "./session-turn-prep";
11
16
 
12
17
  /** Minimal subset of BeforeAgentStartEvent used by this handler. */
13
18
  interface BeforeAgentStartPayload {
@@ -42,7 +47,11 @@ export function shouldExposeTool(
42
47
  * session state
43
48
  * - `session` — encapsulates all mutable session state and lifecycle operations
44
49
  * - `resolver` — owns permission-query surface: `isToolFullyDenied`, skill check
45
- * - `toolRegistry` — Pi tool API subset (getActive + setActive)
50
+ * - `toolRegistry` — Pi tool API subset (getAll + getActive + setActive)
51
+ * - `logger` — records each change to the effective tool surface
52
+ *
53
+ * The active set is recomputed from the session's pre-filter tool surface
54
+ * every turn, so relaxing a rule restores the tool it had withheld (#873).
46
55
  */
47
56
  export class AgentPrepHandler {
48
57
  constructor(
@@ -50,6 +59,7 @@ export class AgentPrepHandler {
50
59
  private readonly session: PermissionSession,
51
60
  private readonly resolver: PermissionResolver,
52
61
  private readonly toolRegistry: ToolRegistry,
62
+ private readonly logger: DebugLogger,
53
63
  ) {}
54
64
 
55
65
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -60,24 +70,23 @@ export class AgentPrepHandler {
60
70
  this.turnPrep.prepare(ctx);
61
71
 
62
72
  const agentName = this.session.resolveAgentName(ctx, event.systemPrompt);
63
- const activeTools = this.toolRegistry.getActive();
64
- const allowedTools: string[] = [];
65
-
66
- for (const tool of activeTools) {
67
- const toolName = getToolNameFromValue(tool);
68
- if (!toolName) {
69
- continue;
70
- }
71
- if (
73
+ const surface = this.session.resolveExposedTools(
74
+ this.observeToolSurface(),
75
+ (toolName) =>
72
76
  shouldExposeTool(toolName, agentName, (t, a) =>
73
77
  this.resolver.isToolFullyDenied(t, a),
74
- )
75
- ) {
76
- allowedTools.push(toolName);
77
- }
78
- }
78
+ ),
79
+ );
80
+ const allowedTools = [...surface.exposed];
79
81
 
80
82
  this.toolRegistry.setActive(allowedTools);
83
+ if (surface.changed) {
84
+ this.logger.debug("tool_surface.changed", {
85
+ exposed: surface.exposed,
86
+ withheld: surface.withheld,
87
+ restored: surface.restored,
88
+ });
89
+ }
81
90
 
82
91
  const toolPromptResult = sanitizeAvailableToolsSection(
83
92
  event.systemPrompt,
@@ -94,4 +103,22 @@ export class AgentPrepHandler {
94
103
  ? { systemPrompt: skillPromptResult.prompt }
95
104
  : {};
96
105
  }
106
+
107
+ private observeToolSurface(): ToolSurfaceObservation {
108
+ return {
109
+ active: toolNamesOf(this.toolRegistry.getActive()),
110
+ registered: new Set(toolNamesOf(this.toolRegistry.getAll())),
111
+ };
112
+ }
113
+ }
114
+
115
+ function toolNamesOf(tools: readonly unknown[]): string[] {
116
+ const names: string[] = [];
117
+ for (const tool of tools) {
118
+ const toolName = getToolNameFromValue(tool);
119
+ if (toolName) {
120
+ names.push(toolName);
121
+ }
122
+ }
123
+ return names;
97
124
  }
@@ -2,8 +2,8 @@ import type {
2
2
  BashCommand,
3
3
  WrapperKind,
4
4
  } from "#src/access-intent/bash/command-enumeration";
5
- import type { ScopedPermissionResolver } from "#src/permission-resolver";
6
- import { pickMostRestrictive } from "#src/restrictiveness";
5
+ import type { ScopedPermissionResolver } from "#src/policy/permission-resolver";
6
+ import { pickMostRestrictive } from "#src/policy/restrictiveness";
7
7
  import type { PermissionCheckResult } from "#src/types";
8
8
 
9
9
  /**
@@ -1,8 +1,8 @@
1
1
  import type { BashProgram } from "#src/access-intent/bash/program";
2
- import type { PathNormalizer } from "#src/path-normalizer";
3
- import type { ScopedPermissionResolver } from "#src/permission-resolver";
2
+ import type { PathNormalizer } from "#src/path/path-normalizer";
3
+ import type { ScopedPermissionResolver } from "#src/policy/permission-resolver";
4
4
  import { buildBashExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
5
- import { SessionApproval } from "#src/session-approval";
5
+ import { SessionApproval } from "#src/session/session-approval";
6
6
  import type { GateResult } from "./descriptor";
7
7
  import { selectUncoveredExternalPaths } from "./external-directory-policy";
8
8
  import { accessFactsFromPath } from "./helpers";
@@ -1,5 +1,5 @@
1
1
  import { BashProgram } from "#src/access-intent/bash/program";
2
- import type { PathNormalizer } from "#src/path-normalizer";
2
+ import type { PathNormalizer } from "#src/path/path-normalizer";
3
3
 
4
4
  /**
5
5
  * Extract paths from a bash command that resolve outside CWD.
@@ -2,11 +2,11 @@ import type { AccessPath } from "#src/access-intent/access-path";
2
2
  import type { BashProgram } from "#src/access-intent/bash/program";
3
3
  import type { TokenEffect } from "#src/access-intent/effect";
4
4
  import { capabilitySurfaceForEffect } from "#src/access-intent/path-surfaces";
5
- import type { PathNormalizer } from "#src/path-normalizer";
6
- import type { ScopedPermissionResolver } from "#src/permission-resolver";
5
+ import type { PathNormalizer } from "#src/path/path-normalizer";
6
+ import type { ScopedPermissionResolver } from "#src/policy/permission-resolver";
7
+ import { pickMostRestrictive } from "#src/policy/restrictiveness";
7
8
  import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
8
- import { pickMostRestrictive } from "#src/restrictiveness";
9
- import { SessionApproval } from "#src/session-approval";
9
+ import { SessionApproval } from "#src/session/session-approval";
10
10
  import type { PermissionCheckResult } from "#src/types";
11
11
  import type { GateResult } from "./descriptor";
12
12
  import { accessFactsFromPath } from "./helpers";
@@ -1,8 +1,8 @@
1
1
  import type { DecisionSource } from "#src/authority/decision-source";
2
2
  import type { PromptPermissionDetails } from "#src/authority/permission-prompter";
3
- import type { PermissionDecisionEvent } from "#src/permission-events";
4
3
  import type { PromptPayload } from "#src/presentation/prompt-payload";
5
- import type { SessionApproval } from "#src/session-approval";
4
+ import type { PermissionDecisionEvent } from "#src/service/permission-events";
5
+ import type { SessionApproval } from "#src/session/session-approval";
6
6
  import type { PermissionCheckResult, PermissionState } from "#src/types";
7
7
 
8
8
  // ── Descriptor types ───────────────────────────────────────────────────────
@@ -2,8 +2,8 @@ import type { AccessPath } from "#src/access-intent/access-path";
2
2
  import type { BashExternalPath } from "#src/access-intent/bash/bash-path-resolver";
3
3
  import type { TokenEffect } from "#src/access-intent/effect";
4
4
  import { capabilitySurfaceForEffect } from "#src/access-intent/path-surfaces";
5
- import type { ScopedPermissionResolver } from "#src/permission-resolver";
6
- import { pickMostRestrictive } from "#src/restrictiveness";
5
+ import type { ScopedPermissionResolver } from "#src/policy/permission-resolver";
6
+ import { pickMostRestrictive } from "#src/policy/restrictiveness";
7
7
  import type { PermissionCheckResult } from "#src/types";
8
8
 
9
9
  /** An external path whose resolved `external_directory` state is not "allow". */