@gotgenes/pi-permission-system 34.0.1 → 35.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ 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
+ ## [35.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v34.0.1...pi-permission-system-v35.0.0) (2026-09-26)
9
+
10
+
11
+ ### Features
12
+
13
+ * **pi-permission-system:** **breaking:** stop appending a tool list and rules to an operator's custom system prompt ([672a64c](https://github.com/gotgenes/pi-packages/commit/672a64c0844ec2fff8e113130109919623158107)), closes [#980](https://github.com/gotgenes/pi-packages/issues/980)
14
+
15
+ ### Documentation
16
+
17
+ * **pi-permission-system:** record that a custom system prompt gets no appended tool surface ([7e0816d](https://github.com/gotgenes/pi-packages/commit/7e0816d28ab5377de011b5558b034c5b2bdaca4d))
18
+
8
19
  ## [34.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v34.0.0...pi-permission-system-v34.0.1) (2026-09-26)
9
20
 
10
21
 
@@ -1246,11 +1246,11 @@ permission:
1246
1246
 
1247
1247
  The extension integrates via Pi's lifecycle hooks:
1248
1248
 
1249
- | Hook | Behavior |
1250
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
1251
- | `before_agent_start` | Filters the active tool set (restrict-only), restates the tool list and guidelines at the end of the system prompt to match, and hides denied skills |
1252
- | `tool_call` | Enforces permissions for every tool invocation |
1253
- | `input` | Intercepts `/skill:<name>` requests and enforces skill policy |
1249
+ | Hook | Behavior |
1250
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1251
+ | `before_agent_start` | Filters the active tool set (restrict-only), restates the tool list and guidelines at the end of the system prompt to match (except under a custom system prompt outside a subagent), and hides denied skills |
1252
+ | `tool_call` | Enforces permissions for every tool invocation |
1253
+ | `input` | Intercepts `/skill:<name>` requests and enforces skill policy |
1254
1254
 
1255
1255
  Additional behaviors:
1256
1256
 
@@ -1262,8 +1262,10 @@ Additional behaviors:
1262
1262
  - A tool is removed only when every value under its surface resolves to `deny`; a surface with any reachable `allow` or `ask` pattern stays available (see [Tool Surfaces](#tool-surfaces))
1263
1263
  - The tool list and guidelines are **relocated** rather than edited in place: the copies pi wrote are removed, and this session's own are rendered at the end of the system prompt, after the working directory pi states last.
1264
1264
  They take the shape pi writes them in: `Available tools:` and `Guidelines:` sections after a `Current working directory:` footer through pi 0.85, and `<tools>` and `<rules>` sections after a `<cwd>` section from pi 0.86.
1265
- Each session states its own tool surface, which is what keeps a subagent child's inherited prompt byte-identical to its parent's (see [ADR 0014](decisions/0014-tool-surface-is-node-local-prose.md)); the tool list moves to the end of the prompt for every session, whether or not anything is denied.
1266
- Only the copies pi wrote are removed: a custom system prompt (`.pi/SYSTEM.md`, `~/.pi/agent/SYSTEM.md`, `--system-prompt`) keeps its own text untouched, sections and all, because pi writes no tool surface of its own under one — so a prompt that lists tools itself is shown alongside this session's block rather than replaced by it.
1265
+ Each session states its own tool surface, which is what keeps a subagent child's inherited prompt byte-identical to its parent's (see [ADR 0014](decisions/0014-tool-surface-is-node-local-prose.md)); the tool list moves to the end of the prompt for every session on pi's default prompt, whether or not anything is denied.
1266
+ A custom system prompt (`.pi/SYSTEM.md`, `~/.pi/agent/SYSTEM.md`, `--system-prompt`) is left exactly as pi built it: pi writes no tool list or rules under one, and this extension adds none either, so the prompt describes tools only if you wrote that yourself.
1267
+ Tool filtering and enforcement still apply, and the model still receives only the allowed tools in the request's tool list.
1268
+ A subagent child is the exception: its prompt is always a custom one assembled by the subagent extension, so it still gets its own tool list and rules at the end, while any custom text it inherited stays untouched.
1267
1269
  - The rendered sections follow pi's own rules: a tool is listed only when pi supplied a one-line description for it, and the guideline bullets are the allowed tools' own contributions, then any rules another extension added to `systemPromptOptions.promptGuidelines`, around pi's built-in ones
1268
1270
  - The prompt is recomputed and returned on every turn but is stable across turns for a stable policy/agent, so the provider's prompt cache (tools + system prefix) is preserved rather than rewritten each turn.
1269
1271
  A policy change is an intentional cache transition, as a mid-session agent switch already is.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "34.0.1",
3
+ "version": "35.0.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,6 +2,7 @@ import type {
2
2
  BeforeAgentStartEventResult,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
+ import type { SubagentDetector } from "#src/authority/subagent-detection";
5
6
  import { resolveSkillPromptEntries } from "#src/exposure/skill-prompt-sanitizer";
6
7
  import {
7
8
  type RegisteredTools,
@@ -23,7 +24,8 @@ interface BeforeAgentStartPayload {
23
24
  * handler render the session's own tool list instead of editing the one Pi
24
25
  * wrote — including in a child, whose inherited identity carries none.
25
26
  * `customPrompt` says whether Pi wrote a preamble at all: under one, it
26
- * writes no tool surface, so there is nothing of Pi's to remove.
27
+ * writes no tool surface, so there is nothing of Pi's to remove, and a root
28
+ * node adds none either.
27
29
  * `promptGuidelines` carries rules other extensions added, which removing
28
30
  * Pi's own rules section would otherwise drop.
29
31
  */
@@ -60,6 +62,9 @@ export function shouldExposeTool(
60
62
  *
61
63
  * The tool surface is relocated rather than edited in place, so a subagent
62
64
  * child's inherited identity stays byte-identical to its parent's (#890).
65
+ * A root node whose prompt Pi built from a custom one states no tool surface,
66
+ * matching Pi, which writes none there; a subagent child's prompt is always a
67
+ * custom one, and it still states its own tools.
63
68
  *
64
69
  * Constructor deps:
65
70
  * - `turnPrep` — brings the node up to date for the turn before anything reads
@@ -68,6 +73,8 @@ export function shouldExposeTool(
68
73
  * - `resolver` — owns permission-query surface: `isToolFullyDenied`, skill check
69
74
  * - `toolRegistry` — Pi tool API subset (getAll + getActive + setActive)
70
75
  * - `logger` — records each change to the effective tool surface
76
+ * - `detector` — tells a subagent child from a root, which decides whether a
77
+ * custom prompt gets this node's tool surface
71
78
  *
72
79
  * The active set is recomputed from the session's pre-filter tool surface
73
80
  * every turn, so relaxing a rule restores the tool it had withheld (#873).
@@ -79,6 +86,7 @@ export class AgentPrepHandler {
79
86
  private readonly resolver: PermissionResolver,
80
87
  private readonly toolRegistry: ToolRegistry,
81
88
  private readonly logger: DebugLogger,
89
+ private readonly detector: SubagentDetector,
82
90
  ) {}
83
91
 
84
92
  // eslint-disable-next-line @typescript-eslint/require-await
@@ -108,15 +116,15 @@ export class AgentPrepHandler {
108
116
  });
109
117
  }
110
118
 
111
- const toolSurfacePrompt = renderToolSurface(event.systemPrompt, {
112
- allowedTools,
113
- toolSnippets: event.systemPromptOptions?.toolSnippets ?? {},
114
- guidelinesByTool: registered.guidelinesByTool,
115
- promptGuidelines: event.systemPromptOptions?.promptGuidelines ?? [],
116
- // Pi's own `if (customPrompt)` test, so an empty string reads here the
117
- // way it reads there: as no custom prompt at all.
118
- piAuthoredPreamble: !event.systemPromptOptions?.customPrompt,
119
- });
119
+ const toolSurfacePrompt = this.statesOwnToolSurface(event, ctx)
120
+ ? renderToolSurface(event.systemPrompt, {
121
+ allowedTools,
122
+ toolSnippets: event.systemPromptOptions?.toolSnippets ?? {},
123
+ guidelinesByTool: registered.guidelinesByTool,
124
+ promptGuidelines: event.systemPromptOptions?.promptGuidelines ?? [],
125
+ piAuthoredPreamble: !hasCustomPrompt(event),
126
+ })
127
+ : event.systemPrompt;
120
128
  const skillPromptResult = resolveSkillPromptEntries(
121
129
  toolSurfacePrompt,
122
130
  this.resolver,
@@ -129,6 +137,21 @@ export class AgentPrepHandler {
129
137
  : {};
130
138
  }
131
139
 
140
+ /**
141
+ * Whether this node renders its own tool surface into the prompt.
142
+ *
143
+ * Every node does except a root whose prompt Pi built from a custom one:
144
+ * there Pi writes no tool list or rules, and the operator's prompt is left
145
+ * as Pi built it. A subagent child's prompt is always a custom one, and its
146
+ * inherited identity carries no tool list, so it still states its own.
147
+ */
148
+ private statesOwnToolSurface(
149
+ event: BeforeAgentStartPayload,
150
+ ctx: ExtensionContext,
151
+ ): boolean {
152
+ return !hasCustomPrompt(event) || this.detector.isSubagent(ctx);
153
+ }
154
+
132
155
  private observeToolSurface(
133
156
  registered: RegisteredTools,
134
157
  ): ToolSurfaceObservation {
@@ -138,3 +161,11 @@ export class AgentPrepHandler {
138
161
  };
139
162
  }
140
163
  }
164
+
165
+ /**
166
+ * Whether Pi built the prompt from a custom one, by Pi's own `if (customPrompt)`
167
+ * test, so an empty string reads here the way it reads there: as none.
168
+ */
169
+ function hasCustomPrompt(event: BeforeAgentStartPayload): boolean {
170
+ return Boolean(event.systemPromptOptions?.customPrompt);
171
+ }
package/src/index.ts CHANGED
@@ -344,6 +344,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
344
344
  resolver,
345
345
  toolRegistry,
346
346
  logger,
347
+ subagentDetection,
347
348
  );
348
349
 
349
350
  const gateRunner = new GateRunner(