@zeldrisho/pi-gate 0.1.1 → 0.3.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
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-09-07
11
+
12
+ ### Added
13
+
14
+ - ship a JSON Schema for `pi-gate.json` and reference it from newly seeded configurations for editor completion and validation
15
+
16
+ ### Changed
17
+
18
+ - wrap visible matched-rule occurrences in confirmation-dialog command text so guarded operations are easier to identify
19
+ - escape terminal and bidirectional control characters and bound long or multiline command text in confirmation dialogs without changing the command executed
20
+ - ignore empty or oversized rule patterns and load at most 1,000 valid rules
21
+ - document and test RPC-host confirmation behavior
22
+
23
+ ## [0.2.0] - 2026-09-02
24
+
25
+ ### Changed
26
+
27
+ - request termination of the current agent turn when a command is blocked, denied, dismissed, or times out; mixed parallel batches containing allowed calls may continue
28
+ - auto-deny interactive prompts after a configurable `promptTimeoutMs` (30 seconds by default)
29
+ - create `pi-gate.json` directly on first load with the default timeout and starter prompt/block/allow rules instead of writing an example file
30
+
10
31
  ## [0.1.1] - 2026-09-01
11
32
 
12
33
  ### Changed
@@ -23,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
44
  - support `prompt`, `block`, and `allow` actions, with longest-pattern-wins resolution
24
45
  - block rather than auto-approve in non-UI modes
25
46
 
26
- [Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.1.1...HEAD
47
+ [Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.3.0...HEAD
48
+ [0.3.0]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.2.0...pi-gate-v0.3.0
49
+ [0.2.0]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.1.1...pi-gate-v0.2.0
27
50
  [0.1.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.1.0...pi-gate-v0.1.1
28
51
  [0.1.0]: https://github.com/zeldrisho/pi-packages/releases/tag/pi-gate-v0.1.0
package/README.md CHANGED
@@ -16,42 +16,42 @@ pi install -l npm:@zeldrisho/pi-gate
16
16
 
17
17
  ## Configure
18
18
 
19
- The extension ships with **no default rules**. On first load, an example configuration is written to `~/.pi/agent/pi-gate.json.example` and a warning notifies you that no configuration is active. Every bash command is allowed until you create a configuration.
19
+ On first load, the extension creates `~/.pi/agent/pi-gate.json` with a default 30-second prompt timeout, starter rules, and a `$schema` reference for editor validation. Existing configuration files are never overwritten.
20
20
 
21
- Copy the example and edit it:
21
+ The published package includes [`config.schema.json`](config.schema.json). New configurations reference its canonical URL automatically; existing configurations can add the same `$schema` property shown below to enable completion and validation in compatible editors.
22
22
 
23
- ```bash
24
- cp ~/.pi/agent/pi-gate.json.example ~/.pi/agent/pi-gate.json
25
- ```
26
-
27
- Then edit `~/.pi/agent/pi-gate.json` to add your rules. Each rule is a substring pattern and one of three actions:
23
+ Edit `~/.pi/agent/pi-gate.json` to customize the rules. `promptTimeoutMs` controls how long a confirmation remains open (30 seconds by default if omitted, with a maximum of one day). Each rule is a substring pattern and one of three actions:
28
24
 
29
25
  - `prompt` — ask the user to allow or deny the command
30
26
  - `block` — deny the command without asking
31
- - `allow` — explicitly allow (use to carve out an exception to a broader rule)
27
+ - `allow` — explicitly allow an exception to a broader matching rule
32
28
 
33
29
  ```json
34
30
  {
31
+ "$schema": "https://raw.githubusercontent.com/zeldrisho/pi-packages/main/packages/pi-gate/config.schema.json",
32
+ "promptTimeoutMs": 30000,
35
33
  "operations": {
36
34
  "rm -rf": "prompt",
37
35
  "sudo": "prompt",
36
+ "sudo apt update": "allow",
38
37
  "chmod 777": "block",
39
38
  "corepack enable": "block"
40
39
  }
41
40
  }
42
41
  ```
43
42
 
44
- Patterns use simple substring matching. When several patterns match the same command, the longest pattern wins, so a narrow `allow` rule can override a broader `block` or `prompt` rule.
43
+ Patterns use simple substring matching. Empty patterns and patterns longer than 1,024 characters are ignored, and at most 1,000 valid rules are loaded. Commands with no matching rule are allowed automatically. The explicit `allow` action is only needed to carve out an exception: in the starter configuration, `sudo apt update` is allowed even though the broader `sudo` rule prompts. When several patterns match, the longest pattern wins.
45
44
 
46
45
  After editing, run `/reload` to apply the new rules in the current session.
47
46
 
48
47
  ## Behavior
49
48
 
50
49
  - Only the built-in `bash` tool is gated. Other tools pass through unchanged.
51
- - `prompt` rules ask the user with a two-button confirmation (`Allow` / `Deny`). The dialog identifies the matched rule.
50
+ - `prompt` rules ask the user with a two-button confirmation (`Allow` / `Deny`). The dialog identifies the matched rule, wraps its visible occurrences in `»…«`, and auto-denies after `promptTimeoutMs` instead of waiting indefinitely. Commands are normalized and terminal control characters are escaped for display; prompts show at most 2,000 command characters and 20 lines, but an allowed command always executes in full and unchanged.
52
51
  - `block` rules never ask and always deny. The warning identifies the matched rule.
53
52
  - `allow` rules are explicit pass-throughs, useful for carving out exceptions.
54
- - In non-interactive modes (`-p`, JSON), `prompt` and `block` rules both block the command instead of auto-approving.
53
+ - Blocked, denied, dismissed, and timed-out calls request early termination. Pi ends the turn only when every finalized result in the tool-call batch requests termination; a mixed parallel batch containing allowed calls may continue.
54
+ - RPC hosts use their native selection dialog. In non-interactive modes (`-p`, JSON), `prompt` and `block` rules both block and request termination instead of auto-approving.
55
55
 
56
56
  ### What the agent sees
57
57
 
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/zeldrisho/pi-packages/main/packages/pi-gate/config.schema.json",
4
+ "title": "pi-gate configuration",
5
+ "description": "Rules used by @zeldrisho/pi-gate to allow, block, or confirm bash commands.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["operations"],
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "format": "uri-reference",
13
+ "description": "Optional JSON Schema reference for editor validation."
14
+ },
15
+ "promptTimeoutMs": {
16
+ "type": "integer",
17
+ "minimum": 1,
18
+ "maximum": 86400000,
19
+ "default": 30000,
20
+ "description": "Maximum time to wait for an interactive confirmation, in milliseconds."
21
+ },
22
+ "operations": {
23
+ "type": "object",
24
+ "description": "Command substring patterns mapped to actions. When multiple patterns match, the longest wins.",
25
+ "maxProperties": 1000,
26
+ "propertyNames": {
27
+ "minLength": 1,
28
+ "maxLength": 1024
29
+ },
30
+ "additionalProperties": {
31
+ "type": "string",
32
+ "enum": ["prompt", "block", "allow"]
33
+ }
34
+ }
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeldrisho/pi-gate",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension that blocks or confirms bash commands based on a user-provided JSON configuration",
5
5
  "keywords": [
6
6
  "command-blocking",
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "src",
27
+ "config.schema.json",
27
28
  "README.md",
28
29
  "CHANGELOG.md",
29
30
  "LICENSE"
package/src/index.ts CHANGED
@@ -2,14 +2,13 @@
2
2
  * pi-gate Extension
3
3
  *
4
4
  * Intercepts the built-in `bash` tool and gates each command against a
5
- * user-provided JSON configuration. Without a configuration file, every
6
- * command is allowed and the extension posts a warning on `session_start`
7
- * reminding the user to create one.
5
+ * user-provided JSON configuration. Without a configuration file, the
6
+ * extension creates one with starter rules and a default prompt timeout.
8
7
  *
9
8
  * Configuration file: `~/.pi/agent/pi-gate.json`
10
9
  *
11
- * The configuration has a single section, `operations`, which maps a
12
- * substring pattern to one of three actions:
10
+ * The configuration has an `operations` section, which maps a substring
11
+ * pattern to one of three actions, and an optional `promptTimeoutMs` setting:
13
12
  *
14
13
  * - `prompt` - ask the user to allow or deny the command
15
14
  * - `block` - deny the command without asking
@@ -18,11 +17,13 @@
18
17
  * When multiple patterns match a command, the longest pattern wins, so a
19
18
  * narrow `allow` rule can override a broader `prompt` or `block` rule.
20
19
  *
21
- * On first run, an example configuration is written next to the real one
22
- * at `~/.pi/agent/pi-gate.json.example` to document the format.
20
+ * On first run, a configuration with the default prompt timeout and starter
21
+ * operation rules is written to `~/.pi/agent/pi-gate.json`.
23
22
  *
24
23
  * Non-UI modes (print, JSON) never auto-approve. A `prompt` or `block` rule
25
- * in non-UI mode always blocks the command.
24
+ * in non-UI mode always blocks and requests termination of the agent turn.
25
+ * Interactive prompts auto-deny after `promptTimeoutMs` rather than waiting
26
+ * indefinitely.
26
27
  */
27
28
 
28
29
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -34,6 +35,7 @@ export type Action = "prompt" | "block" | "allow";
34
35
 
35
36
  export interface GateConfig {
36
37
  operations: Record<string, Action>;
38
+ promptTimeoutMs: number;
37
39
  }
38
40
 
39
41
  export interface RuleMatch {
@@ -42,14 +44,28 @@ export interface RuleMatch {
42
44
  }
43
45
 
44
46
  const CONFIG_FILE_NAME = "pi-gate.json";
45
- const EXAMPLE_FILE_NAME = "pi-gate.json.example";
47
+ export const CONFIG_SCHEMA_URL =
48
+ "https://raw.githubusercontent.com/zeldrisho/pi-packages/main/packages/pi-gate/config.schema.json";
46
49
  const ACTIONS: readonly Action[] = ["prompt", "block", "allow"] as const;
50
+ export const DEFAULT_PROMPT_TIMEOUT_MS = 30_000;
51
+ export const MAX_RULE_COUNT = 1_000;
52
+ export const MAX_RULE_PATTERN_LENGTH = 1_024;
53
+ export const MAX_DISPLAY_COMMAND_CHARACTERS = 2_000;
54
+ export const MAX_DISPLAY_COMMAND_LINES = 20;
55
+ export const MAX_PROMPT_TIMEOUT_MS = 86_400_000;
56
+ const DISPLAY_TRUNCATION_MARKER = "\n … [command display truncated]";
57
+ const BIDI_CONTROL_CODE_POINTS = new Set([
58
+ 0x061c, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d, 0x202e, 0x2066, 0x2067, 0x2068, 0x2069,
59
+ ]);
47
60
 
48
- const EXAMPLE_CONFIG: GateConfig = {
61
+ const DEFAULT_CONFIG: GateConfig = {
62
+ promptTimeoutMs: DEFAULT_PROMPT_TIMEOUT_MS,
49
63
  operations: {
50
64
  "rm -rf": "prompt",
51
65
  sudo: "prompt",
66
+ "sudo apt update": "allow",
52
67
  "chmod 777": "block",
68
+ "corepack enable": "block",
53
69
  },
54
70
  };
55
71
 
@@ -71,15 +87,6 @@ function configPath(): string {
71
87
  return join(agentDir(), CONFIG_FILE_NAME);
72
88
  }
73
89
 
74
- /**
75
- * Returns the full path to the example pi-gate configuration file.
76
- *
77
- * @returns The absolute path to pi-gate.json.example
78
- */
79
- function examplePath(): string {
80
- return join(agentDir(), EXAMPLE_FILE_NAME);
81
- }
82
-
83
90
  /**
84
91
  * Type guard that checks if an unknown value is a valid Action.
85
92
  *
@@ -118,25 +125,38 @@ function isJsonObject(value: unknown): value is Record<string, unknown> {
118
125
  */
119
126
  export function parseConfig(content: string): GateConfig {
120
127
  const rules: Record<string, Action> = {};
128
+ let acceptedRuleCount = 0;
121
129
  let parsed: unknown;
122
130
  try {
123
131
  parsed = JSON.parse(content);
124
132
  } catch {
125
- return { operations: rules };
133
+ return { operations: rules, promptTimeoutMs: DEFAULT_PROMPT_TIMEOUT_MS };
126
134
  }
127
135
  if (!isJsonObject(parsed)) {
128
- return { operations: rules };
136
+ return { operations: rules, promptTimeoutMs: DEFAULT_PROMPT_TIMEOUT_MS };
129
137
  }
138
+ const configuredTimeout = parsed["promptTimeoutMs"];
139
+ const promptTimeoutMs =
140
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- validate the optional numeric field at the untrusted JSON boundary before applying range checks
141
+ typeof configuredTimeout === "number" &&
142
+ Number.isInteger(configuredTimeout) &&
143
+ configuredTimeout > 0 &&
144
+ configuredTimeout <= MAX_PROMPT_TIMEOUT_MS
145
+ ? configuredTimeout
146
+ : DEFAULT_PROMPT_TIMEOUT_MS;
130
147
  const operations = parsed["operations"];
131
148
  if (!isJsonObject(operations)) {
132
- return { operations: rules };
149
+ return { operations: rules, promptTimeoutMs };
133
150
  }
134
151
  for (const [pattern, action] of Object.entries(operations)) {
152
+ if (acceptedRuleCount >= MAX_RULE_COUNT) break;
153
+ if (pattern.length === 0 || pattern.length > MAX_RULE_PATTERN_LENGTH) continue;
135
154
  if (isAction(action)) {
136
155
  rules[pattern] = action;
156
+ acceptedRuleCount += 1;
137
157
  }
138
158
  }
139
- return { operations: rules };
159
+ return { operations: rules, promptTimeoutMs };
140
160
  }
141
161
 
142
162
  /**
@@ -146,35 +166,35 @@ export function parseConfig(content: string): GateConfig {
146
166
  export function loadConfig(): GateConfig {
147
167
  const path = configPath();
148
168
  if (!existsSync(path)) {
149
- return { operations: {} };
169
+ return { operations: {}, promptTimeoutMs: DEFAULT_PROMPT_TIMEOUT_MS };
150
170
  }
151
171
  let content: string;
152
172
  try {
153
173
  content = readFileSync(path, "utf-8");
154
174
  } catch {
155
- return { operations: {} };
175
+ return { operations: {}, promptTimeoutMs: DEFAULT_PROMPT_TIMEOUT_MS };
156
176
  }
157
177
  return parseConfig(content);
158
178
  }
159
179
 
160
180
  /**
161
- * Writes the example configuration next to the real one. Existing example
162
- * files are left alone so the user can rely on them to track the latest
163
- * supported shape.
181
+ * Writes the default configuration on first run. Existing configuration files
182
+ * are never overwritten.
164
183
  */
165
- export function ensureExampleConfig(): void {
166
- const path = examplePath();
184
+ export function ensureConfig(): void {
185
+ const path = configPath();
167
186
  if (existsSync(path)) return;
168
187
  try {
169
188
  mkdirSync(agentDir(), { recursive: true });
170
189
  } catch {
171
190
  return;
172
191
  }
173
- const example = JSON.stringify(EXAMPLE_CONFIG, null, 2) + "\n";
192
+ const content = JSON.stringify({ $schema: CONFIG_SCHEMA_URL, ...DEFAULT_CONFIG }, null, 2) + "\n";
174
193
  try {
175
- writeFileSync(path, example, "utf-8");
194
+ writeFileSync(path, content, { encoding: "utf-8", flag: "wx", mode: 0o600 });
176
195
  } catch {
177
- // Best-effort: the warning banner still surfaces the missing config.
196
+ // Best-effort: another process may have created it, or the warning banner
197
+ // will surface that no configuration is active.
178
198
  }
179
199
  }
180
200
 
@@ -206,19 +226,129 @@ export function resolveAction(command: string, rules: Record<string, Action>): A
206
226
  return resolveRule(command, rules)?.action ?? null;
207
227
  }
208
228
 
229
+ /** Converts terminal controls and bidirectional formatting characters to visible escapes. */
230
+ function escapeUnsafeDisplayCharacter(character: string): string {
231
+ const codePoint = character.codePointAt(0);
232
+ if (codePoint === undefined) return "";
233
+ if (
234
+ codePoint < 0x20 ||
235
+ (codePoint >= 0x7f && codePoint <= 0x9f) ||
236
+ BIDI_CONTROL_CODE_POINTS.has(codePoint)
237
+ ) {
238
+ return `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
239
+ }
240
+ return character;
241
+ }
242
+
243
+ interface MatchRange {
244
+ start: number;
245
+ end: number;
246
+ }
247
+
248
+ /** Finds non-overlapping raw match ranges before any display escaping occurs. */
249
+ function findMatchRanges(command: string, pattern?: string): MatchRange[] {
250
+ if (!pattern) return [];
251
+ const ranges: MatchRange[] = [];
252
+ let offset = 0;
253
+ while (offset <= command.length - pattern.length) {
254
+ const start = command.indexOf(pattern, offset);
255
+ if (start === -1) break;
256
+ ranges.push({ start, end: start + pattern.length });
257
+ offset = start + pattern.length;
258
+ }
259
+ return ranges;
260
+ }
261
+
262
+ /** Produces bounded terminal-safe text, optionally marking raw matched spans. */
263
+ function renderCommandForDisplay(command: string, pattern?: string): string {
264
+ const ranges = findMatchRanges(command, pattern);
265
+ let rangeIndex = 0;
266
+ let activeRange: MatchRange | undefined;
267
+ let output = "";
268
+ let lineCount = 1;
269
+ let offset = 0;
270
+
271
+ const truncate = (closeActiveRange = activeRange !== undefined): string =>
272
+ output + (closeActiveRange ? "«" : "") + DISPLAY_TRUNCATION_MARKER;
273
+
274
+ while (offset < command.length) {
275
+ const rawStart = offset;
276
+ let rawEnd: number;
277
+ let rendered: string;
278
+ const codePoint = command.codePointAt(offset);
279
+ if (codePoint === 0x0d) {
280
+ rawEnd = offset + (command.codePointAt(offset + 1) === 0x0a ? 2 : 1);
281
+ rendered = "\n";
282
+ } else {
283
+ const character = String.fromCodePoint(codePoint!);
284
+ rawEnd = offset + character.length;
285
+ rendered = character === "\n" ? character : escapeUnsafeDisplayCharacter(character);
286
+ }
287
+
288
+ const nextRange = ranges[rangeIndex];
289
+ const wasInsideRange = activeRange !== undefined;
290
+ const opensRange =
291
+ activeRange === undefined &&
292
+ nextRange !== undefined &&
293
+ nextRange.start < rawEnd &&
294
+ nextRange.end > rawStart;
295
+ if (opensRange) activeRange = nextRange;
296
+ const closesRange = activeRange !== undefined && activeRange.end <= rawEnd;
297
+ const decorated = `${opensRange ? "»" : ""}${rendered}${closesRange ? "«" : ""}`;
298
+
299
+ if (rendered === "\n") {
300
+ if (lineCount >= MAX_DISPLAY_COMMAND_LINES) return truncate(wasInsideRange);
301
+ lineCount += 1;
302
+ }
303
+ if (output.length + decorated.length > MAX_DISPLAY_COMMAND_CHARACTERS) {
304
+ return truncate(wasInsideRange);
305
+ }
306
+
307
+ output += decorated;
308
+ if (closesRange) {
309
+ activeRange = undefined;
310
+ rangeIndex += 1;
311
+ }
312
+ offset = rawEnd;
313
+ }
314
+ return output;
315
+ }
316
+
209
317
  /**
210
- * Formats a rule match as a human-readable string for display in prompts and notifications.
318
+ * Produces bounded terminal-safe text for a command without changing the command that is executed.
319
+ */
320
+ export function formatCommandForDisplay(command: string): string {
321
+ return renderCommandForDisplay(command);
322
+ }
323
+
324
+ /** Marks raw matched-rule occurrences while producing terminal-safe command text. */
325
+ export function highlightRuleForDisplay(command: string, pattern: string): string {
326
+ return renderCommandForDisplay(command, pattern);
327
+ }
328
+
329
+ /** Indents every line of terminal-safe command text for the confirmation dialog. */
330
+ function formatPromptCommand(command: string, pattern: string): string {
331
+ return highlightRuleForDisplay(command, pattern)
332
+ .split("\n")
333
+ .map((line) => ` ${line}`)
334
+ .join("\n");
335
+ }
336
+
337
+ /**
338
+ * Formats a rule match as a terminal-safe, human-readable string.
211
339
  *
212
340
  * @param match - The rule match to format
213
341
  * @returns A string representation showing the pattern and action
214
342
  */
215
343
  function formatRule(match: RuleMatch): string {
216
- return `${JSON.stringify(match.pattern)}: ${JSON.stringify(match.action)}`;
344
+ return formatCommandForDisplay(
345
+ `${JSON.stringify(match.pattern)}: ${JSON.stringify(match.action)}`,
346
+ );
217
347
  }
218
348
 
219
349
  /** Gate the built-in `bash` tool against the user-provided rules. */
220
350
  export default function piGate(pi: ExtensionAPI): void {
221
- ensureExampleConfig();
351
+ ensureConfig();
222
352
  const config = loadConfig();
223
353
 
224
354
  pi.on("session_start", (_event, ctx) => {
@@ -238,7 +368,7 @@ export default function piGate(pi: ExtensionAPI): void {
238
368
  }
239
369
  } else {
240
370
  ctx.ui.notify(
241
- `pi-gate: no configuration found at ${path}; all commands are allowed. Copy ${examplePath()} to ${path} to start gating.`,
371
+ `pi-gate: could not create configuration at ${path}; all commands are allowed until that file is created.`,
242
372
  "warning",
243
373
  );
244
374
  }
@@ -257,7 +387,7 @@ export default function piGate(pi: ExtensionAPI): void {
257
387
  if (ctx.hasUI) {
258
388
  ctx.ui.notify(reason, "warning");
259
389
  }
260
- return { block: true, reason };
390
+ return { block: true, reason, terminate: true };
261
391
  }
262
392
 
263
393
  // action === "prompt"
@@ -265,14 +395,20 @@ export default function piGate(pi: ExtensionAPI): void {
265
395
  return {
266
396
  block: true,
267
397
  reason: `pi-gate: command blocked because rule ${rule} requires a prompt, but no UI is available`,
398
+ terminate: true,
268
399
  };
269
400
  }
270
401
  const choice = await ctx.ui.select(
271
- `pi-gate: allow this command?\n\n ${command}\n\nMatched rule: ${rule}`,
402
+ `pi-gate: allow this command?\n\n${formatPromptCommand(command, match.pattern)}\n\nMatched rule: ${rule}\nMatched command text is wrapped in »…«`,
272
403
  ["Allow", "Deny"],
404
+ { timeout: config.promptTimeoutMs },
273
405
  );
274
406
  if (choice !== "Allow") {
275
- return { block: true, reason: `pi-gate: command denied by user after matching rule ${rule}` };
407
+ return {
408
+ block: true,
409
+ reason: `pi-gate: command denied, dismissed, or timed out after matching rule ${rule}`,
410
+ terminate: true,
411
+ };
276
412
  }
277
413
  return undefined;
278
414
  });