@zeldrisho/pi-gate 0.7.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.1] - 2026-10-25
11
+
12
+ ### Changed
13
+
14
+ - Create starter configuration when a session starts rather than when the extension is loaded.
15
+
16
+ ### Fixed
17
+
18
+ - Cancel active approval dialogs and reject queued calls during shutdown, reload, and session replacement.
19
+
10
20
  ## [0.7.0] - 2026-10-24
11
21
 
12
22
  ### Changed
@@ -95,7 +105,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
95
105
  - support `prompt`, `block`, and `allow` actions, with longest-pattern-wins resolution
96
106
  - block rather than auto-approve in non-UI modes
97
107
 
98
- [Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.0...HEAD
108
+ [Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.1...HEAD
109
+ [0.7.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.0...pi-gate-v0.7.1
99
110
  [0.7.0]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.6.1...pi-gate-v0.7.0
100
111
  [0.6.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.6.0...pi-gate-v0.6.1
101
112
  [0.6.0]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.5.0...pi-gate-v0.6.0
package/README.md CHANGED
@@ -15,7 +15,7 @@ Use `pi config` to enable or disable this extension interactively. Run `pi list`
15
15
 
16
16
  ## Configure
17
17
 
18
- Edit `~/.pi/agent/gate.json` to choose which commands need approval. The extension creates this file with starter rules on first load. Changes apply before the next gated call, without restarting Pi. Add the optional `$schema` below for editor completion and validation:
18
+ Edit `~/.pi/agent/gate.json` to choose which commands need approval. The extension creates this file with starter rules when the first session starts. Changes apply before the next gated call, without restarting Pi. Add the optional `$schema` below for editor completion and validation:
19
19
 
20
20
  ```json
21
21
  {
@@ -46,7 +46,7 @@ pi-gate is a guard rail, not a sandbox. Complex shell syntax, pipelines, redirec
46
46
 
47
47
  The built-in `bash` tool is gated, and `write`/`edit` calls targeting the gate config require approval. Prompt dialogs offer `Allow` and `Deny`; approvals are never remembered. To stop repeated prompts, add a specific `allow` rule to `gate.json`.
48
48
 
49
- Approval dialogs are serialized for concurrent tool calls; cancellation, session replacement, or a policy change while a dialog is open denies the pending call.
49
+ Approval dialogs are serialized for concurrent tool calls; cancellation, shutdown, reload, session replacement, or a policy change while a dialog is open denies the pending call. Calls queued before shutdown or session replacement are also denied.
50
50
 
51
51
  Long commands are truncated in approval dialogs, but an approved command executes in full. Denied, dismissed, blocked, and timed-out calls stop the turn; other allowed calls in a parallel batch may still run. In non-interactive modes (`-p`, JSON), commands requiring approval are denied.
52
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeldrisho/pi-gate",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Pi extension that gates bash commands and protects its configuration from tool edits",
5
5
  "keywords": [
6
6
  "command-blocking",
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^1.4.0",
36
+ "typebox": "^1.4.2",
36
37
  "typescript": "^7.0.2",
37
38
  "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
38
39
  "vite-plus": "1.3.0"
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * Matching is case-insensitive and whitespace-normalized. Allow occurrences
20
20
  * suppress contained restrictions in the same command; surviving blocks win.
21
21
  *
22
- * On first run, a configuration with the default prompt timeout and starter
22
+ * On first session start, a configuration with the default prompt timeout and starter
23
23
  * operation rules is written to `~/.pi/agent/gate.json` when neither file exists.
24
24
  *
25
25
  * Non-UI modes (print, JSON) never auto-approve. A `prompt` or `block` rule
@@ -112,14 +112,16 @@ export {
112
112
 
113
113
  /**
114
114
  * Gates `bash` calls against user rules and requires approval for protected
115
- * configuration writes and edits. Attempts to create a starter configuration if absent.
115
+ * configuration writes and edits. Attempts to create a starter configuration
116
+ * at session start if absent.
116
117
  * Reloads rules before gating and after each prompt; approvals apply per call.
117
- * Dialogs are serialized, and cancellation, session starts, or policy changes
118
- * invalidate an active approval. UI failures reject the affected tool call.
118
+ * Dialogs are serialized. Cancellation, shutdown, and session replacement deny
119
+ * active and queued gated calls; policy changes invalidate an active approval.
120
+ * UI failures reject the affected tool call.
119
121
  */
120
122
  export default function piGate(pi: ExtensionAPI): void {
121
- ensureConfig();
122
123
  let loadResult = loadConfigResult();
124
+ let session = new AbortController();
123
125
  let policy = JSON.stringify(loadResult);
124
126
  let revision = 0;
125
127
  let pending = Promise.resolve();
@@ -135,8 +137,16 @@ export default function piGate(pi: ExtensionAPI): void {
135
137
  }
136
138
  };
137
139
 
140
+ pi.on("session_shutdown", () => {
141
+ session.abort();
142
+ revision += 1;
143
+ });
144
+
138
145
  pi.on("session_start", (_event, ctx) => {
146
+ session.abort();
147
+ session = new AbortController();
139
148
  revision += 1;
149
+ ensureConfig();
140
150
  refreshConfig();
141
151
  const path = configPath();
142
152
  const ruleCount = Object.keys(loadResult.config.operations).length;
@@ -160,7 +170,14 @@ export default function piGate(pi: ExtensionAPI): void {
160
170
  });
161
171
 
162
172
  pi.on("tool_call", (event, ctx) => {
173
+ // Capture ownership before queueing so old calls cannot enter a replacement session.
174
+ const signal = ctx.signal ? AbortSignal.any([ctx.signal, session.signal]) : session.signal;
175
+
163
176
  // Pi can dispatch sibling tools concurrently; never overlap approval dialogs.
177
+ /**
178
+ * Returns a terminating block result for denied calls, or undefined to allow;
179
+ * UI errors propagate.
180
+ */
164
181
  const run = async (): Promise<ToolCallEventResult | undefined> => {
165
182
  let command: string;
166
183
  let configChange = false;
@@ -181,6 +198,14 @@ export default function piGate(pi: ExtensionAPI): void {
181
198
  return undefined;
182
199
  }
183
200
 
201
+ if (signal.aborted) {
202
+ return {
203
+ block: true,
204
+ reason: "pi-gate: approval cancelled; command not run.",
205
+ terminate: true,
206
+ };
207
+ }
208
+
184
209
  refreshConfig();
185
210
 
186
211
  const match = configChange
@@ -210,14 +235,6 @@ export default function piGate(pi: ExtensionAPI): void {
210
235
  };
211
236
  }
212
237
 
213
- if (ctx.signal?.aborted) {
214
- return {
215
- block: true,
216
- reason: "pi-gate: approval cancelled; command not run.",
217
- terminate: true,
218
- };
219
- }
220
-
221
238
  const promptRevision = revision;
222
239
 
223
240
  reportHerdrBlocked(pi, ctx, true);
@@ -234,7 +251,7 @@ export default function piGate(pi: ExtensionAPI): void {
234
251
  choice = await ctx.ui.select(
235
252
  `pi-gate: allow this command?\n\n${promptCommand}\n\nMatched rule: ${rule}${configChange ? "" : "\nMatched command text is wrapped in »…«"}`,
236
253
  ["Allow", "Deny"],
237
- { timeout: timeoutMs, signal: ctx.signal },
254
+ { timeout: timeoutMs, signal },
238
255
  );
239
256
  // Pi returns undefined for both dismissal and timeout; elapsed time separates the timeout case.
240
257
  timedOut = choice === undefined && Date.now() - promptStartedAt >= timeoutMs;
@@ -244,7 +261,7 @@ export default function piGate(pi: ExtensionAPI): void {
244
261
 
245
262
  refreshConfig();
246
263
 
247
- if (ctx.signal?.aborted || revision !== promptRevision) {
264
+ if (signal.aborted || revision !== promptRevision) {
248
265
  return {
249
266
  block: true,
250
267
  reason: