@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 +11 -0
- package/docs/configuration.md +9 -7
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +41 -10
- package/src/index.ts +1 -0
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
|
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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 =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|