@gotgenes/pi-permission-system 25.4.0 → 26.0.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +14 -12
  3. package/config/config.example.json +1 -2
  4. package/dist/public.d.ts +31 -10
  5. package/docs/configuration.md +23 -19
  6. package/docs/cross-extension-api.md +30 -4
  7. package/docs/migration/0745-prompt-payload-contracts.md +68 -0
  8. package/docs/migration/0746-review-log-fields.md +69 -0
  9. package/docs/troubleshooting.md +2 -1
  10. package/package.json +1 -1
  11. package/schemas/permissions.schema.json +14 -4
  12. package/src/access-intent/tool-kind.ts +1 -1
  13. package/src/authority/approval-escalator.ts +6 -4
  14. package/src/authority/authorizer.ts +3 -3
  15. package/src/authority/forwarded-request-server.ts +0 -2
  16. package/src/authority/forwarding-io.ts +7 -5
  17. package/src/authority/permission-forwarding.ts +12 -3
  18. package/src/authority/permission-prompter.ts +5 -4
  19. package/src/config-loader.ts +31 -0
  20. package/src/config-schema.ts +13 -4
  21. package/src/extension-config.ts +8 -9
  22. package/src/handlers/gates/bash-external-directory.ts +1 -12
  23. package/src/handlers/gates/bash-path.ts +1 -10
  24. package/src/handlers/gates/descriptor.ts +16 -5
  25. package/src/handlers/gates/external-directory.ts +1 -13
  26. package/src/handlers/gates/path.ts +1 -9
  27. package/src/handlers/gates/runner.ts +24 -14
  28. package/src/handlers/gates/skill-input-gate-pipeline.ts +2 -2
  29. package/src/handlers/gates/skill-input.ts +1 -10
  30. package/src/handlers/gates/skill-read.ts +1 -11
  31. package/src/handlers/gates/tool.ts +1 -11
  32. package/src/log-field-cap.ts +82 -0
  33. package/src/logging.ts +24 -3
  34. package/src/permission-events.ts +15 -2
  35. package/src/permission-prompts.ts +4 -3
  36. package/src/permission-session.ts +1 -1
  37. package/src/permission-ui-prompt.ts +4 -2
  38. package/src/presentation/agent-renderer.ts +215 -0
  39. package/src/presentation/dialog-renderer.ts +8 -64
  40. package/src/presentation/fact-vocabulary.ts +103 -0
  41. package/src/presentation/forwarded-ask-payload.ts +42 -17
  42. package/src/presentation/path-ask-payload.ts +8 -1
  43. package/src/presentation/prompt-payload.ts +165 -4
  44. package/src/presentation/review-log-renderer.ts +51 -0
  45. package/src/service.ts +11 -0
  46. package/src/tool-input-preview.ts +0 -1
  47. package/src/tool-preview-formatter.ts +18 -33
  48. package/src/denial-messages.ts +0 -269
  49. package/src/presentation/legacy-message.ts +0 -117
package/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [26.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v25.4.0...pi-permission-system-v26.0.0) (2026-08-16)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * **pi-permission-system:** the `message` field is removed from every `permission_request.*` review-log entry. A consumer parsing it should read the structured fields instead: `surface`, `matchedPattern`, `executedUnit`, `commandContext`, `invokedToolName`, `forwarded`, and `requesterSessionId`, alongside the existing `toolName`, `command`, `path`, `target`, and `toolInputPreview`.
14
+ * **pi-permission-system:** values in the permission review log are now truncated at `reviewLogFieldMaxWidth` (default 1000) with a trailing ellipsis, where a bash `command` was previously written whole. Raise `reviewLogFieldMaxWidth` to keep longer values. `ToolPreviewFormatterOptions.toolInputLogPreviewMaxLength` and `TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH` are removed, superseded by that setting.
15
+ * **pi-permission-system:** `toolInputPreviewMaxLength` and `toolTextSummaryMaxLength` are still accepted but ignored, and a config setting either now logs a deprecation warning at session start. Remove them; use `promptMaxRows` and `promptFieldMaxWidth` to bound what a permission prompt renders.
16
+ * **pi-permission-system:** `PermissionUiPromptEvent.message` is removed. Read `request.value` for the decision-relevant value and `request.matchedPattern` for the rule that fired; `surface`, `value`, `agentName`, and `forwarding` are unchanged. `DirectPromptInput.message` likewise becomes `payload: PromptPayload`.
17
+ * **pi-permission-system:** `ForwardedPermissionRequest.message` is removed and replaced by `payload: PromptPayload`, which carries the ask's complete structured facts. A serving node on this version accepts an older child's `message`-only request and renders it from `surface`, `value`, and the requester provenance; an older *parent* rejects a newer child's request, so upgrade the parent session first.
18
+
19
+ ### Features
20
+
21
+ * **pi-permission-system:** bound review-log field width with reviewLogFieldMaxWidth ([#746](https://github.com/gotgenes/pi-packages/issues/746)) ([dceb931](https://github.com/gotgenes/pi-packages/commit/dceb93197e5f0e16287d63a68fcb2cad0bec2e97))
22
+ * **pi-permission-system:** carry the prompt payload on the forwarded-request wire ([bf67cd6](https://github.com/gotgenes/pi-packages/commit/bf67cd6673d1e4bf57c339873d8642295bd643c0)), closes [#745](https://github.com/gotgenes/pi-packages/issues/745)
23
+ * **pi-permission-system:** ignore the deprecated tool-preview caps and notice their use ([47d7610](https://github.com/gotgenes/pi-packages/commit/47d7610d1217ebd6c0a968999dbfff4ad426af88)), closes [#745](https://github.com/gotgenes/pi-packages/issues/745)
24
+ * **pi-permission-system:** narrow the ui_prompt broadcast to the request facts ([fcdb174](https://github.com/gotgenes/pi-packages/commit/fcdb17494f73d7a506fe1ab967cf2db8f815b50a)), closes [#745](https://github.com/gotgenes/pi-packages/issues/745)
25
+ * **pi-permission-system:** render a forwarded ask from the child's own payload ([8587269](https://github.com/gotgenes/pi-packages/commit/8587269a066e87007b8e8dea4b85649d0240b697)), closes [#745](https://github.com/gotgenes/pi-packages/issues/745)
26
+ * **pi-permission-system:** render the review log from the prompt payload ([#746](https://github.com/gotgenes/pi-packages/issues/746)) ([b373876](https://github.com/gotgenes/pi-packages/commit/b3738761e572b91a3388b0681d2a34862b11859d))
27
+ * **pi-permission-system:** replace the forwarded-request message with the structured payload ([1af41a9](https://github.com/gotgenes/pi-packages/commit/1af41a9a18cf5850da3bc787c9e0ed0db493921a)), closes [#745](https://github.com/gotgenes/pi-packages/issues/745)
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * **pi-permission-system:** stop echoing tool input in agent-facing denial text ([#746](https://github.com/gotgenes/pi-packages/issues/746)) ([525b7e4](https://github.com/gotgenes/pi-packages/commit/525b7e4b19cfa997029a178b413a2838b3834021))
33
+
34
+
35
+ ### Documentation
36
+
37
+ * **pi-permission-system:** document the payload contracts and mark Phase 13 Step 3 complete ([a2381ac](https://github.com/gotgenes/pi-packages/commit/a2381ac700e08c80c415b31f732a481367226264)), closes [#745](https://github.com/gotgenes/pi-packages/issues/745)
38
+ * **pi-permission-system:** record the agent and review-log renderers ([#746](https://github.com/gotgenes/pi-packages/issues/746)) ([af88cc1](https://github.com/gotgenes/pi-packages/commit/af88cc11e31e98c0fa43d3629c64f31d6b73de05))
39
+ * **pi-permission-system:** retire references to the dissolved denial module ([#746](https://github.com/gotgenes/pi-packages/issues/746)) ([53647b2](https://github.com/gotgenes/pi-packages/commit/53647b2b0930737940714378d8421cd2ce76e66f))
40
+
8
41
  ## [25.4.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v25.3.0...pi-permission-system-v25.4.0) (2026-08-15)
9
42
 
10
43
 
package/README.md CHANGED
@@ -139,18 +139,20 @@ If you relied on the old permissive behavior for bash, set an explicit permissiv
139
139
 
140
140
  ## Documentation
141
141
 
142
- | Document | Contents |
143
- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
144
- | [docs/configuration.md](docs/configuration.md) | Full policy reference, runtime knobs, per-agent overrides, recipes |
145
- | [docs/session-approvals.md](docs/session-approvals.md) | Session-scoped rules, pattern suggestions, bash arity table |
146
- | [docs/cross-extension-api.md](docs/cross-extension-api.md) | Cross-extension service accessor, event bus integration, prompt and decision broadcasts |
147
- | [docs/subagent-integration.md](docs/subagent-integration.md) | Permission forwarding, coexistence with subagent extensions |
148
- | [docs/guides/permission-frontmatter-for-subagent-extensions.md](docs/guides/permission-frontmatter-for-subagent-extensions.md) | Convention guide for subagent extension authors |
149
- | [docs/opencode-compatibility.md](docs/opencode-compatibility.md) | OpenCode compatibility — shared concepts, divergences, porting guide |
150
- | [docs/troubleshooting.md](docs/troubleshooting.md) | Common issues, diagnostic logging, threat model |
151
- | [docs/migration/legacy-to-flat.md](docs/migration/legacy-to-flat.md) | Migration from pre-v2 config layout |
152
- | [docs/migration/strict-config-validation.md](docs/migration/strict-config-validation.md) | Strict config validation (breaking) — rejected configs, and the cross-scope fail-closed clamp |
153
- | [docs/migration/0644-project-trust-gating.md](docs/migration/0644-project-trust-gating.md) | Project-trust gating (breaking) — project config loads only after project trust |
142
+ | Document | Contents |
143
+ | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
144
+ | [docs/configuration.md](docs/configuration.md) | Full policy reference, runtime knobs, per-agent overrides, recipes |
145
+ | [docs/session-approvals.md](docs/session-approvals.md) | Session-scoped rules, pattern suggestions, bash arity table |
146
+ | [docs/cross-extension-api.md](docs/cross-extension-api.md) | Cross-extension service accessor, event bus integration, prompt and decision broadcasts |
147
+ | [docs/subagent-integration.md](docs/subagent-integration.md) | Permission forwarding, coexistence with subagent extensions |
148
+ | [docs/guides/permission-frontmatter-for-subagent-extensions.md](docs/guides/permission-frontmatter-for-subagent-extensions.md) | Convention guide for subagent extension authors |
149
+ | [docs/opencode-compatibility.md](docs/opencode-compatibility.md) | OpenCode compatibility — shared concepts, divergences, porting guide |
150
+ | [docs/troubleshooting.md](docs/troubleshooting.md) | Common issues, diagnostic logging, threat model |
151
+ | [docs/migration/legacy-to-flat.md](docs/migration/legacy-to-flat.md) | Migration from pre-v2 config layout |
152
+ | [docs/migration/strict-config-validation.md](docs/migration/strict-config-validation.md) | Strict config validation (breaking) — rejected configs, and the cross-scope fail-closed clamp |
153
+ | [docs/migration/0644-project-trust-gating.md](docs/migration/0644-project-trust-gating.md) | Project-trust gating (breaking) — project config loads only after project trust |
154
+ | [docs/migration/0745-prompt-payload-contracts.md](docs/migration/0745-prompt-payload-contracts.md) | Prompt payload contracts (breaking) — the forwarded wire, the `ui_prompt` broadcast, and the deprecated preview caps |
155
+ | [docs/migration/0746-review-log-fields.md](docs/migration/0746-review-log-fields.md) | Review-log fields (breaking) — `message` replaced by request facts, and the `reviewLogFieldMaxWidth` bound |
154
156
 
155
157
  ## Development
156
158
 
@@ -11,8 +11,7 @@
11
11
  "promptMaxRows": 24,
12
12
  "promptFieldMaxWidth": 400,
13
13
 
14
- "toolInputPreviewMaxLength": 400,
15
- "toolTextSummaryMaxLength": 120,
14
+ "reviewLogFieldMaxWidth": 1000,
16
15
 
17
16
  "piInfrastructureReadPaths": [],
18
17
 
package/dist/public.d.ts CHANGED
@@ -68,10 +68,10 @@ interface PromptPayload {
68
68
  *
69
69
  * Present because the ask shapes are not separable by surface alone: a tool
70
70
  * external-directory ask and a bash one share the `external_directory` surface,
71
- * and the `path` gate and the per-tool gate differ only in wording. It mirrors
72
- * `DenialContext`'s discriminated union, the shape ADR 0011 §7 names as already
73
- * correct, and gives every renderer an exhaustive switch rather than a set of
74
- * string comparisons a new variant sails past.
71
+ * and the `path` gate and the per-tool gate differ only in wording. It gives
72
+ * every renderer an exhaustive switch rather than a set of string comparisons a
73
+ * new variant sails past which is what let the parallel denial-context union
74
+ * ADR 0011 §7 described dissolve into this one (#746).
75
75
  */
76
76
  type PromptPayloadKind = "bash" | "mcp" | "tool" | "path" | "external_directory" | "bash_external_directory" | "skill" | "skill_read" | "forwarded";
77
77
  /**
@@ -148,6 +148,16 @@ interface PromptAnnotation {
148
148
  readonly text: string;
149
149
  }
150
150
 
151
+ /**
152
+ * Permission event channel — public contract.
153
+ *
154
+ * Exports channel name constants, TypeScript types for all emitted events,
155
+ * and thin emit helpers.
156
+ *
157
+ * Stability guarantee: fields may be added, but existing fields will not be
158
+ * removed or renamed without a semver-major version bump.
159
+ */
160
+
151
161
  /** Emitted at `session_start`, after the service is published. */
152
162
  declare const PERMISSIONS_READY_CHANNEL = "permissions:ready";
153
163
  /** Emitted when a permission request is committed to the active UI prompt path. */
@@ -198,8 +208,19 @@ interface PermissionUiPromptEvent {
198
208
  value: string | null;
199
209
  /** Agent name (when known). */
200
210
  agentName: string | null;
201
- /** Message displayed to the user. */
202
- message: string;
211
+ /**
212
+ * The ask's invariant core (ADR 0011 §3), verbatim from the prompt payload.
213
+ *
214
+ * Nested rather than flattened so the event and the payload share one shape:
215
+ * a fact added to `PromptRequestFacts` reaches the bus without a second
216
+ * hand-maintained declaration. Carries no evidence and no annotations — the
217
+ * bus is the narrowest renderer (ADR 0011 §6), observable by any loaded
218
+ * extension without the operator having named it.
219
+ *
220
+ * `request.surface` is the *gate* surface the rule fired on; the top-level
221
+ * `surface` is the display projection. Both are here on purpose.
222
+ */
223
+ request: PromptRequestFacts;
203
224
  /** Forwarding context, or null for a direct prompt. */
204
225
  forwarding: ForwardedPromptContext | null;
205
226
  }
@@ -281,13 +302,13 @@ interface PromptPermissionDetails {
281
302
  requestId: string;
282
303
  source: PermissionReviewSource;
283
304
  agentName: string | null;
284
- message: string;
285
305
  /**
286
306
  * The complete structured description of this ask (ADR 0011 §2).
287
307
  *
288
308
  * Required: every ask carries one, and the type is what guarantees it rather
289
- * than a convention each gate has to remember. `message` is a render over it
290
- * for the duration of the transition, so the two cannot disagree.
309
+ * than a convention each gate has to remember. Every consumer the dialog,
310
+ * the wire, the broadcast, the review log, the agent-facing denial text — is
311
+ * a render over it, so no two of them can disagree.
291
312
  */
292
313
  payload: PromptPayload;
293
314
  toolCallId?: string;
@@ -529,4 +550,4 @@ declare function getPermissionsService(): PermissionsService | undefined;
529
550
  declare function unpublishPermissionsService(service: PermissionsService): void;
530
551
 
531
552
  export { PERMISSIONS_DECISION_CHANNEL, PERMISSIONS_READY_CHANNEL, PERMISSIONS_UI_PROMPT_CHANNEL, getPermissionsService, publishPermissionsService, unpublishPermissionsService };
532
- export type { Authorizer, AuthorizerLog, AuthorizerVerdict, ForwardedPromptContext, PermissionCheckResult, PermissionDecisionEvent, PermissionQuery, PermissionState, PermissionUiPromptEvent, PermissionUiPromptSource, PermissionsReadyEvent, PermissionsService, PromptPermissionDetails, ToolInputFormatter };
553
+ export type { Authorizer, AuthorizerLog, AuthorizerVerdict, ForwardedPromptContext, PermissionCheckResult, PermissionDecisionEvent, PermissionQuery, PermissionState, PermissionUiPromptEvent, PermissionUiPromptSource, PermissionsReadyEvent, PermissionsService, PromptAnnotation, PromptEvidence, PromptPayload, PromptPayloadKind, PromptPermissionDetails, PromptRequestFacts, PromptRequester, ToolInputFormatter };
@@ -58,8 +58,6 @@ This clamp is deny-preserving and, like `yoloMode`, applied at composition; when
58
58
  "yoloMode": false,
59
59
  "doublePressToConfirm": true,
60
60
  "forwardingTimeoutMs": 600000,
61
- "toolInputPreviewMaxLength": 400,
62
- "toolTextSummaryMaxLength": 120,
63
61
  "piInfrastructureReadPaths": [],
64
62
 
65
63
  // Non-bash tools that carry shell semantics
@@ -99,19 +97,20 @@ This clamp is deny-preserving and, like `yoloMode`, applied at composition; when
99
97
 
100
98
  ## Runtime Knobs
101
99
 
102
- | Key | Default | Description |
103
- | --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
- | `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
105
- | `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl`. Records bash command strings verbatim — see [Log file sensitivity](#log-file-sensitivity) |
106
- | `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled |
107
- | `doublePressToConfirm` | `true` | Requires a confirming second press of a decision hotkey in the inline TUI dialog (see below). TUI sessions only; set to `false` for single-press. |
108
- | `forwardingTimeoutMs` | `600000` | How long a subagent waits for the parent session to answer a forwarded permission request, in milliseconds. A child whose in-process parent is not draining its inbox gives up in ~2 s regardless. |
109
- | `promptMaxRows` | `24` | Max rows a permission prompt renders before eliding its evidence. The request's own facts are never elided by this budget; `Ctrl+O` expands the prompt to the complete request. |
110
- | `promptFieldMaxWidth` | `400` | Max characters of any one field shown in a permission prompt. This is what bounds a single long field (a here-string command, say) that would otherwise fill the prompt through wrapping. |
111
- | `toolInputPreviewMaxLength` | `200` | Max characters of inline JSON shown in permission prompts for tool inputs. Omit to use the default. Set to a large value to disable truncation. |
112
- | `toolTextSummaryMaxLength` | `80` | Max characters of inline pattern/path summaries (grep patterns, find globs, ls paths) in permission prompts. Omit to use the default. |
113
- | `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate. Supports `~`/`$HOME`/`${HOME}` expansion and wildcard patterns (`*`, `?`). |
114
- | `authorizerChain` | `[]` | Ordered names of registered live-authority chain links to consult before the terminal authorizer (see [Authorizer chain](#authorizer-chain--case-by-case-decision-links)). |
100
+ | Key | Default | Description |
101
+ | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
103
+ | `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl`. Records bash command strings unredacted — see [Log file sensitivity](#log-file-sensitivity) |
104
+ | `yoloMode` | `false` | Auto-approves `ask` results instead of prompting when yolo mode is enabled |
105
+ | `doublePressToConfirm` | `true` | Requires a confirming second press of a decision hotkey in the inline TUI dialog (see below). TUI sessions only; set to `false` for single-press. |
106
+ | `forwardingTimeoutMs` | `600000` | How long a subagent waits for the parent session to answer a forwarded permission request, in milliseconds. A child whose in-process parent is not draining its inbox gives up in ~2 s regardless. |
107
+ | `promptMaxRows` | `24` | Max rows a permission prompt renders before eliding its evidence. The request's own facts are never elided by this budget; `Ctrl+O` expands the prompt to the complete request. |
108
+ | `promptFieldMaxWidth` | `400` | Max characters of any one field shown in a permission prompt. This is what bounds a single long field (a here-string command, say) that would otherwise fill the prompt through wrapping. |
109
+ | `reviewLogFieldMaxWidth` | `1000` | Max characters of any one value written to the review log. A longer value is stored shortened, marked with an ellipsis. Raise it to keep longer values; this is a length bound, not redaction. |
110
+ | `toolInputPreviewMaxLength` | | **Deprecated and ignored.** Superseded by `promptMaxRows` / `promptFieldMaxWidth`. Still accepted so an existing config is not rejected, but the value no longer applies; setting it logs a warning. |
111
+ | `toolTextSummaryMaxLength` | | **Deprecated and ignored.** Superseded by `promptMaxRows` / `promptFieldMaxWidth`. Still accepted so an existing config is not rejected, but the value no longer applies; setting it logs a warning. |
112
+ | `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate. Supports `~`/`$HOME`/`${HOME}` expansion and wildcard patterns (`*`, `?`). |
113
+ | `authorizerChain` | `[]` | Ordered names of registered live-authority chain links to consult before the terminal authorizer (see [Authorizer chain](#authorizer-chain--case-by-case-decision-links)). |
115
114
 
116
115
  Both logs write to `~/.pi/agent/extensions/pi-permission-system/logs/`.
117
116
  No debug output is printed to the terminal.
@@ -962,16 +961,16 @@ Additional behaviors:
962
961
  - The narrowed prompt is recomputed and returned on every turn but is byte-stable for a stable policy/agent, so the provider's prompt cache (tools + system prefix) is preserved rather than rewritten each turn
963
962
  - Extension-provided tools like `task`, `mcp`, and third-party tools are handled by exact registered name
964
963
  - Generic extension-tool approval prompts include a bounded input preview; built-in file tools use concise human-readable summaries
965
- - Permission review logs include bounded `toolInputPreview` values for non-bash/non-MCP tool calls, with sensitive-keyed values masked (see [Log file sensitivity](#log-file-sensitivity))
964
+ - Permission review logs include `toolInputPreview` values for non-bash/non-MCP tool calls, with sensitive-keyed values masked and every value bounded by `reviewLogFieldMaxWidth` (see [Log file sensitivity](#log-file-sensitivity))
966
965
 
967
966
  ---
968
967
 
969
968
  ## Log file sensitivity
970
969
 
971
- The review log is enabled by default and records what the agent actually did, which means it records payload as well as decisions: the complete bash command string for every bash decision, and a bounded JSON preview of the tool input for other tools.
970
+ The review log is enabled by default and records what the agent actually did, which means it records payload as well as decisions: the bash command string for every bash decision, and a JSON preview of the tool input for other tools.
972
971
  The debug log carries the same payload when `debugLog` is on.
973
972
 
974
- Two protections apply.
973
+ Three protections apply.
975
974
 
976
975
  Both logs are created **owner-only** (`0600`, in a `0700` directory), and a log created by an earlier version is tightened on the next write.
977
976
  The permission-forwarding request and response files are written the same way.
@@ -984,9 +983,14 @@ The boundary is worth stating exactly, because it is easy to over-read:
984
983
 
985
984
  > A value bound to a sensitive key name is masked; a secret embedded in a bash command string is not.
986
985
 
987
- A command string has no keys, so `deploy --token abc123` is logged verbatim.
986
+ A command string has no keys, so `deploy --token abc123` is logged unredacted.
988
987
  The extension deliberately does not try to guess which parts of a command look secret-shaped — see [ADR 0010] for the measured reasoning.
989
988
 
989
+ Every value the **review** log writes is narrowed to `reviewLogFieldMaxWidth` (1000 characters by default) and marked with an ellipsis, so a single pathological command cannot put tens of kilobytes in one entry.
990
+ This is a length bound, not redaction: it never inspects a value to decide what to hide, and it applies to every field alike.
991
+ The two compose — a sensitive-keyed value is masked whole however long it was.
992
+ The debug log is left unbounded, since it is opt-in and exists to be read in full.
993
+
990
994
  Practical guidance:
991
995
 
992
996
  - Treat both log files as sensitive when sharing them: scrub before pasting into an issue or a chat.
@@ -320,17 +320,21 @@ Forwarded prompts that do reach the human are not degraded: the parent emits the
320
320
  The payload is lean by design — `surface`/`value` are the normalized display projection a notification consumer reads, not a mirror of the internal review log.
321
321
  Read defensively rather than version-gating: broadcast payloads carry no `protocolVersion`.
322
322
 
323
+ The event carries no assembled sentence.
324
+ It carries `request`, the permission ask's invariant core, verbatim from the prompt payload — no evidence and no annotations.
325
+ The bus is the narrowest renderer: any loaded extension can observe it without the operator having named that extension, whereas every other route to an ask's evidence requires that consent (a registered tool-input formatter, or an `Authorizer` link the operator lists in `authorizerChain`).
326
+
323
327
  ```typescript
324
328
  import type { PermissionUiPromptEvent } from "@gotgenes/pi-permission-system";
325
329
 
326
330
  pi.events.on("permissions:ui_prompt", (raw) => {
327
331
  const event = raw as PermissionUiPromptEvent;
328
332
  // Defensive read: tolerate any shape skew between sibling extensions.
329
- if (typeof event.value !== "string" && typeof event.message !== "string") {
333
+ if (typeof event.value !== "string") {
330
334
  return;
331
335
  }
332
- notify(event.surface, event.value, event.message);
333
- // e.g. "bash" "git push" "Allow git push?"
336
+ notify(event.surface, event.value, event.request.matchedPattern);
337
+ // e.g. "bash" "git push" "git *"
334
338
  });
335
339
  ```
336
340
 
@@ -343,11 +347,33 @@ pi.events.on("permissions:ui_prompt", (raw) => {
343
347
  | `surface` | `string \| null` | Normalized display surface (e.g. `"bash"`, `"skill"`), when known |
344
348
  | `value` | `string \| null` | Normalized display value (command, path, skill name, etc.), when known |
345
349
  | `agentName` | `string \| null` | Active/requesting agent name, when known |
346
- | `message` | `string` | Message displayed in the permission prompt |
350
+ | `request` | `PromptRequestFacts` | The ask's invariant core no evidence, no annotations |
347
351
  | `forwarding` | `ForwardedPromptContext \| null` | Forwarding context, or `null` for a direct prompt |
348
352
 
349
353
  Forwarding is orthogonal to origin: a forwarded subagent prompt keeps its original `source` and is identified by a non-null `forwarding` field, not by a dedicated source value.
350
354
 
355
+ #### `PromptRequestFacts`
356
+
357
+ The facts every render of the ask shows, that no renderer's budget may elide.
358
+ Nested rather than flattened so the event and the prompt payload share one shape: a fact added here reaches the bus without a second hand-maintained declaration.
359
+
360
+ | Field | Type | Description |
361
+ | ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------ |
362
+ | `requester` | `PromptRequester` | Who is asking, and whether the ask arrived from a subagent |
363
+ | `surface` | `string` | The **gate** surface the rule fired on — `"external_directory"`, `"path"`, `"bash"`, a tool name |
364
+ | `toolName` | `string \| null` | The gated tool name; `null` when the ask is not tool-shaped |
365
+ | `invokedToolName` | `string \| null` | The invoked name when a shell alias re-exposes bash under another name |
366
+ | `value` | `string` | The decision-relevant value: the command, path, MCP target, or skill name |
367
+ | `matchedPattern` | `string \| null` | The matched rule, including a sentinel such as `<indirection-bash-wrapper>` |
368
+ | `commandContext` | `BashCommandContext \| null` | Where the offending bash unit runs, when it came from a substitution or subshell |
369
+ | `executedUnit` | `string \| null` | For bash, the unit that will actually run, including inside an unstrippable wrapper |
370
+
371
+ `PromptRequester` carries `agentName` (`string | null`), `forwarded` (`boolean`), and `sessionId` (`string | null`, the requesting session for a forwarded ask).
372
+
373
+ The top-level `surface` and `request.surface` are two different facts and both belong on the event.
374
+ The top-level one is the **display** projection — the child's tool name, what a notification shows.
375
+ `request.surface` is the **gate** surface the rule fired on: a `read` of a path outside the working directory displays as `"read"` and gates on `"external_directory"`.
376
+
351
377
  #### `ForwardedPromptContext`
352
378
 
353
379
  Present only when the prompt was forwarded from a non-UI subagent.
@@ -0,0 +1,68 @@
1
+ # Migration guide: the prompt payload replaces the assembled message
2
+
3
+ Starting with the release that closes #745, a permission ask crosses two boundaries as **structured facts** rather than as a pre-rendered sentence: the forwarded-permission-request file a subagent writes for its parent, and the `permissions:ui_prompt` broadcast.
4
+ A third change deprecates the two tool-preview cap config fields.
5
+
6
+ This is a **breaking change** on three surfaces.
7
+ Each is independent — you may be affected by one and not the others.
8
+
9
+ | Surface | Break | Who is affected |
10
+ | -------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------- |
11
+ | `ForwardedPermissionRequest` (on-disk wire) | `message` removed; `payload` added | Anyone running **out-of-process** subagents across mixed versions |
12
+ | `PermissionUiPromptEvent` (`permissions:ui_prompt`) | `message` removed; `request` added | Any extension reading `event.message` |
13
+ | `toolInputPreviewMaxLength` / `toolTextSummaryMaxLength` | Accepted but ignored | Anyone who set either in `config.json` |
14
+
15
+ ## Upgrade the parent session first
16
+
17
+ The forwarded-request wire is the only surface with an ordering constraint, and it only exists for an **out-of-process** subagent (one whose parent session is resolved through `PI_SUBAGENT_PARENT_SESSION` or a sibling env var).
18
+ An in-process child shares its parent's loaded extension, so the two can never skew.
19
+
20
+ A serving node on this version accepts an older child's `message`-only request: the field is no longer required, and the ask is rendered from the `surface`, `value`, and requester provenance the request does carry.
21
+ The reverse does not hold.
22
+ An **older** parent still demands `message` and rejects a newer child's request outright, deleting the file; the child then waits out its forwarding timeout (ten minutes by default) and reports the block as `confirmationUnavailable` rather than as a user denial.
23
+
24
+ So: upgrade the session that serves prompts before the sessions that forward to it.
25
+ Carrying both fields indefinitely was declined deliberately — it would keep the child's un-budgeted prose alive on the wire, which is the defect this change exists to remove.
26
+
27
+ ## What changed on the wire
28
+
29
+ The child used to assemble a sentence under **its** configuration and write it into the request file; the serving node carried that string forward as a single evidence entry, so the parent's own render budget never applied to it.
30
+ A forwarded ask therefore could not be made consistent with a local one.
31
+
32
+ Now the child writes its complete `PromptPayload`, and the serving node renders the child's own facts under the **parent's** budget.
33
+ A forwarded bash ask reads `command : …` exactly as a local one does, because the serving node holds the child's real payload kind.
34
+
35
+ The request files are still mode-restricted and still not redacted — the parent reads them to render the ask.
36
+ The payload's evidence is the same disclosure class the `message` string already was, so this is not a widening.
37
+
38
+ ## What changed on the broadcast
39
+
40
+ `permissions:ui_prompt` drops `message` and gains `request`, the ask's invariant core, verbatim from the prompt payload.
41
+
42
+ ```typescript
43
+ // Before
44
+ notify(event.surface, event.value, event.message);
45
+
46
+ // After
47
+ notify(event.surface, event.value, event.request.matchedPattern);
48
+ ```
49
+
50
+ `request.value` is the decision-relevant value (the command, path, MCP target, or skill name) and `request.matchedPattern` is the rule that fired, including a sentinel such as `<indirection-bash-wrapper>`.
51
+ See the [`PromptRequestFacts` table](../cross-extension-api.md#promptrequestfacts) for every field.
52
+
53
+ `surface`, `value`, `agentName`, `source`, `requestId`, and `forwarding` are unchanged — what narrows here is evidence, never correlation.
54
+ A forwarded ask's broadcast still carries its full `forwarding.requesterAgentName` / `forwarding.requesterSessionId` provenance.
55
+
56
+ For a `write`, an `edit`, or an MCP call this is a **net narrowing** of what the bus discloses: an incidental tool-input preview of up to 200 characters used to ride `message`, and now nothing from the payload's evidence reaches the bus at all.
57
+ That is deliberate — the bus is the narrowest renderer, observable by any loaded extension without the operator having named it, whereas every other route to an ask's evidence requires that consent.
58
+
59
+ ## The deprecated tool-preview caps
60
+
61
+ `toolInputPreviewMaxLength` and `toolTextSummaryMaxLength` are still accepted by the config schema but **no longer take effect**.
62
+ Setting either logs a deprecation notice through the ordinary config-issue channel at session start.
63
+
64
+ They bounded one preview *inside* a prompt, never the prompt itself — which is why they never bounded it.
65
+ Use `promptMaxRows` (default `24`) and `promptFieldMaxWidth` (default `400`) instead: those bound what a permission prompt renders, and `Ctrl+O` still expands the prompt to the complete request.
66
+
67
+ Remove both fields from your `config.json`.
68
+ They remain valid so that an existing config is not rejected fail-closed — which would empty that scope's whole policy — but they will be removed in a later major.
@@ -0,0 +1,69 @@
1
+ # Review-log fields and width bound (breaking)
2
+
3
+ Two changes to the permission review log (`logs/pi-permission-system-permission-review.jsonl`) take effect on upgrade without a config edit.
4
+ Both come from [ADR 0011] §6, which makes the log a renderer over the prompt payload with its own configured limits, rather than a place the assembled prompt sentence happened to land.
5
+
6
+ Nothing about **redaction** changes.
7
+ Key-name masking applies exactly as before, and the boundary is unchanged: a value bound to a sensitive key name is masked; a secret embedded in a bash command string is not.
8
+ See [ADR 0010].
9
+
10
+ ## The `message` field is removed
11
+
12
+ Every `permission_request.*` entry previously carried `message`, the same assembled sentence the prompt showed.
13
+ It is gone.
14
+ What the log accumulated was a side effect of how a prompt happened to be worded, and the sentence duplicated facts the entry already carried in structured form.
15
+
16
+ In its place each entry carries the ask's own request facts:
17
+
18
+ | Field | Present when | Meaning |
19
+ | -------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------- |
20
+ | `surface` | always | The gate surface the rule fired on (`bash`, `path`, `external_directory`, `skill`, a tool name) |
21
+ | `matchedPattern` | a rule matched | The rule that fired, including a sentinel such as `<indirection-bash-wrapper>` |
22
+ | `executedUnit` | a wrapper hides an inner command | The unit that will actually run, e.g. `grep foo` inside `xargs grep foo` |
23
+ | `commandContext` | the unit came from a substitution or subshell | `command_substitution`, `process_substitution`, or `subshell` |
24
+ | `invokedToolName` | a shell alias re-exposed bash | The tool name the agent actually called, e.g. `exec_command` |
25
+ | `forwarded` | the ask arrived from a subagent | `true` |
26
+ | `requesterSessionId` | the ask arrived from a subagent | The requesting session's id |
27
+
28
+ A field the ask does not carry is omitted rather than written as `null`.
29
+
30
+ The existing structured columns are unchanged: `requestId`, `source`, `agentName`, `toolCallId`, `toolName`, `skillName`, `path`, `command`, `target`, `toolInputPreview`, `resolution`, and `denialReason`.
31
+
32
+ If you parse the log, read those fields instead of `message`.
33
+ Two of the new ones close gaps a `message` reader never had: a `permission_request.blocked` entry recorded that policy denied the call but never which rule, and `executedUnit` had not reached the log at all.
34
+
35
+ ## Every review-log value is bounded
36
+
37
+ Each string the review log writes is now narrowed to `reviewLogFieldMaxWidth` characters and marked with a trailing ellipsis.
38
+ The default is `1000` — the width that already bounded `toolInputPreview`, applied to every field so the log has one limit rather than one limit and an unbounded remainder.
39
+
40
+ The practical effect is on `command`: a bash command longer than the width is stored shortened, where it was previously written whole.
41
+ On a real 9,484-entry log, 188 of 4,325 command entries (4.3%) exceed the default.
42
+
43
+ To keep longer values, raise the setting:
44
+
45
+ ```jsonc
46
+ {
47
+ "reviewLogFieldMaxWidth": 8000
48
+ }
49
+ ```
50
+
51
+ There is no "unbounded" value by design — the point of the bound is that log growth is a decision rather than an accident.
52
+ The **debug** log is unaffected: it is opt-in and exists to be read in full.
53
+
54
+ This bound is a length cap, not redaction.
55
+ It narrows by length alone and never inspects a value to decide what to hide, and the two compose independently: a sensitive-keyed value is masked whole however long it was.
56
+
57
+ ## Removed exports
58
+
59
+ `ToolPreviewFormatterOptions.toolInputLogPreviewMaxLength` and `TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH` are removed, superseded by `reviewLogFieldMaxWidth`.
60
+ `ToolPreviewFormatter` no longer truncates what it produces for the log; the writer bounds it instead.
61
+
62
+ ## Agent-facing denial text
63
+
64
+ Not a contract, but visible: the text returned to the agent when a call is refused no longer echoes the bash command or the tool input.
65
+ It names the surface, the tool, the rule that fired, the flagged path or MCP target or skill, and the operator's or human's reason.
66
+ [ADR 0011] §7 states the rule — the agent renderer identifies the call; it does not reproduce it — and the agent already holds its own arguments, which the harness keeps beside the refusal.
67
+
68
+ [ADR 0010]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0010-permission-log-secret-exposure.md
69
+ [ADR 0011]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0011-prompt-presentation-contract.md
@@ -51,8 +51,9 @@ This makes it easy to verify which files the extension actually loaded:
51
51
 
52
52
  - If a dangerous action is possible via an allowed tool, policy must explicitly restrict it
53
53
  - This is a permission decision layer, not a sandbox — for true isolation see [Agent Sandboxes](https://engine.build/lab/agent-sandboxes)
54
- - The review log records bash command strings verbatim.
54
+ - The review log records bash command strings unredacted.
55
55
  Log files are created owner-only (`0600`), and values bound to a sensitive key name (`authorization`, `token`, `password`, …) are masked — but a secret embedded in a command string is not.
56
+ Review-log values are shortened at `reviewLogFieldMaxWidth` (1000 characters by default), which bounds the file's growth but is a length cap, not redaction.
56
57
  See [Log file sensitivity](configuration.md#log-file-sensitivity) and [ADR 0010].
57
58
 
58
59
  [ADR 0010]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0010-permission-log-secret-exposure.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "25.4.0",
3
+ "version": "26.0.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -55,16 +55,26 @@
55
55
  "minimum": 1,
56
56
  "maximum": 9007199254740991
57
57
  },
58
+ "reviewLogFieldMaxWidth": {
59
+ "description": "Maximum characters of any one value written to the permission review log. Omit to use the default (1000).",
60
+ "markdownDescription": "Maximum characters of any one value written to the permission review log.\n\nOmit to use the default (1000). Every string the review log writes is narrowed to this width and marked with an ellipsis, so the log's growth is a decision you make rather than a side effect of how long a command happened to be. Raise it to keep longer values — a bash command exceeding the width is stored shortened.\n\nThis is a length bound, not redaction: it never inspects a value to decide what to hide. Key-name masking is unchanged and applies independently.",
61
+ "default": 1000,
62
+ "type": "integer",
63
+ "minimum": 1,
64
+ "maximum": 9007199254740991
65
+ },
58
66
  "toolInputPreviewMaxLength": {
59
- "description": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts. Omit to use the default (200). Set to a large value to disable truncation.",
60
- "markdownDescription": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts.\n\nOmit to use the default (200). Set to a large value (e.g. `10000`) to effectively disable truncation and see the full input.",
67
+ "deprecated": true,
68
+ "description": "Deprecated and ignored. Superseded by promptMaxRows and promptFieldMaxWidth, which bound the whole prompt rather than one preview. Still accepted so an existing config is not rejected; remove it.",
69
+ "markdownDescription": "**Deprecated and ignored.** Superseded by `promptMaxRows` and `promptFieldMaxWidth`, which bound the whole permission prompt rather than one preview inside it.\n\nStill accepted so an existing config is not rejected fail-closed, but the value no longer takes effect. Remove it.",
61
70
  "type": "integer",
62
71
  "minimum": 1,
63
72
  "maximum": 9007199254740991
64
73
  },
65
74
  "toolTextSummaryMaxLength": {
66
- "description": "Maximum character length of inline pattern/path summaries (e.g. grep patterns, find globs, ls paths) in permission prompts. Omit to use the default (80).",
67
- "markdownDescription": "Maximum character length of inline pattern/path summaries (e.g. grep patterns, find globs, ls paths) shown in permission prompts.\n\nOmit to use the default (80). Increase this when working with long regexes or deep paths that are being cut off.",
75
+ "deprecated": true,
76
+ "description": "Deprecated and ignored. Superseded by promptMaxRows and promptFieldMaxWidth, which bound the whole prompt rather than one summary. Still accepted so an existing config is not rejected; remove it.",
77
+ "markdownDescription": "**Deprecated and ignored.** Superseded by `promptMaxRows` and `promptFieldMaxWidth`, which bound the whole permission prompt rather than one summary inside it.\n\nStill accepted so an existing config is not rejected fail-closed, but the value no longer takes effect. Remove it.",
68
78
  "type": "integer",
69
79
  "minimum": 1,
70
80
  "maximum": 9007199254740991
@@ -10,7 +10,7 @@ import { PATH_BEARING_TOOLS } from "./path-surfaces";
10
10
  * `toolName === "bash"`/`"mcp"` re-derivation across the extraction consumers
11
11
  * (`input-normalizer`, `tool-input-path`, the tool-call gate pipeline, and
12
12
  * `permission-manager`'s source derivation) and the presentation consumers
13
- * (`tool-preview-formatter`, `permission-prompts`, `denial-messages`, and
13
+ * (`tool-preview-formatter`, `permission-prompts`, the payload builders, and
14
14
  * `deriveDecisionValue`), which dispatch on {@link classifyToolKind} or
15
15
  * {@link isMcpCheck}. Adding a tool kind means editing {@link classifyToolKind}
16
16
  * plus the exhaustive switches the compiler flags — an OCP win over silent
@@ -36,6 +36,7 @@ import type { ServingLookup } from "#src/authority/serving-registry";
36
36
  import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
37
37
  import { createPermissionRequestId } from "#src/permission-request-id";
38
38
  import { buildUiPrompt } from "#src/permission-ui-prompt";
39
+ import type { PromptPayload } from "#src/presentation/prompt-payload";
39
40
  import type { DebugReviewLogger } from "#src/session-logger";
40
41
  import { toRecord } from "#src/value-guards";
41
42
  import type { TerminalAuthorizer } from "./authorizer";
@@ -68,7 +69,7 @@ function getContextSystemPrompt(ctx: ForwarderContext): string | undefined {
68
69
 
69
70
  /**
70
71
  * The facts a forwarded request relays unchanged from the child's ask: the
71
- * prompt message, the optional display projection, and the optional
72
+ * prompt payload, the optional display projection, and the optional
72
73
  * session-approval suggestion.
73
74
  *
74
75
  * Bundled into one object so the two-hop private chain
@@ -82,7 +83,8 @@ interface ForwardedRequestFacts {
82
83
  * decision instead of a third being minted here.
83
84
  */
84
85
  requestId: string;
85
- message: string;
86
+ /** The child's complete prompt payload, relayed for the serving node to render. */
87
+ payload: PromptPayload;
86
88
  display?: ForwardedPromptDisplay;
87
89
  sessionApproval?: ForwardedSessionApproval;
88
90
  /** The child-fixed access facts; the edge completes them into a `ForwardedAccessIntent`. */
@@ -171,7 +173,7 @@ export class ParentAuthorizer implements TerminalAuthorizer {
171
173
  const uiPrompt = buildUiPrompt(details);
172
174
  return this.waitForForwardedApproval(this.ctx, {
173
175
  requestId: details.requestId,
174
- message: details.message,
176
+ payload: details.payload,
175
177
  display: {
176
178
  source: uiPrompt.source,
177
179
  surface: uiPrompt.surface,
@@ -300,7 +302,7 @@ export class ParentAuthorizer implements TerminalAuthorizer {
300
302
  requesterSessionId,
301
303
  targetSessionId,
302
304
  requesterAgentName,
303
- message: facts.message,
305
+ payload: facts.payload,
304
306
  ...(facts.display
305
307
  ? {
306
308
  source: facts.display.source,
@@ -49,9 +49,9 @@ export interface Authorizer {
49
49
  * ADR 0007's terminal-cannot-defer invariant.
50
50
  *
51
51
  * One method, one responsibility. `DenyingAuthorizer` ignores `details`;
52
- * `LocalUserAuthorizer` reads `message`/`sessionLabel` and derives the UI
53
- * event from it; `ParentAuthorizer` reads `message` and derives the
54
- * forwarded display from it.
52
+ * `LocalUserAuthorizer` renders `payload` for the human and derives the UI
53
+ * event from the request facts; `ParentAuthorizer` ships `payload` over the
54
+ * wire so the serving node renders it under its own budget.
55
55
  */
56
56
  export interface TerminalAuthorizer {
57
57
  authorize(
@@ -14,7 +14,6 @@ import {
14
14
  } from "#src/authority/permission-forwarding";
15
15
  import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
16
16
  import { buildForwardedAskPayload } from "#src/presentation/forwarded-ask-payload";
17
- import { renderLegacyMessage } from "#src/presentation/legacy-message";
18
17
  import { SessionApproval } from "#src/session-approval";
19
18
  import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
20
19
  import type { DebugReviewLogger } from "#src/session-logger";
@@ -100,7 +99,6 @@ function buildForwardedAskDetails(
100
99
  requestId: request.id,
101
100
  source: request.source ?? "tool_call",
102
101
  agentName: request.requesterAgentName || null,
103
- message: renderLegacyMessage(payload),
104
102
  payload,
105
103
  surface: request.surface ?? null,
106
104
  value: request.value ?? null,