@zeldrisho/pi-gate 0.1.0 → 0.1.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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] - 2026-09-01
11
+
12
+ ### Changed
13
+
14
+ - identify the matched rule in confirmation prompts, block notifications, and agent-visible error results
15
+
10
16
  ## [0.1.0] - 2026-08-25
11
17
 
12
18
  ### Added
@@ -17,5 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
23
  - support `prompt`, `block`, and `allow` actions, with longest-pattern-wins resolution
18
24
  - block rather than auto-approve in non-UI modes
19
25
 
20
- [Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.1.0...HEAD
26
+ [Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.1.1...HEAD
27
+ [0.1.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.1.0...pi-gate-v0.1.1
21
28
  [0.1.0]: https://github.com/zeldrisho/pi-packages/releases/tag/pi-gate-v0.1.0
package/README.md CHANGED
@@ -48,11 +48,26 @@ After editing, run `/reload` to apply the new rules in the current session.
48
48
  ## Behavior
49
49
 
50
50
  - 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`).
52
- - `block` rules never ask and always deny.
51
+ - `prompt` rules ask the user with a two-button confirmation (`Allow` / `Deny`). The dialog identifies the matched rule.
52
+ - `block` rules never ask and always deny. The warning identifies the matched rule.
53
53
  - `allow` rules are explicit pass-throughs, useful for carving out exceptions.
54
54
  - In non-interactive modes (`-p`, JSON), `prompt` and `block` rules both block the command instead of auto-approving.
55
55
 
56
+ ### What the agent sees
57
+
58
+ The confirmation dialog and your choice are not sent directly to the agent.
59
+
60
+ - If you allow a `prompt` rule, the command runs and the agent receives its normal bash result.
61
+ - If you deny or dismiss a `prompt`, the agent receives an error result naming the matched rule and saying the user denied it.
62
+ - If a `block` rule matches, the agent receives an error result naming that rule.
63
+ - If a `prompt` rule matches without an available UI, the agent receives an error explaining that the matched rule required a prompt.
64
+
65
+ For example, a blocked `sudo apt update` call reports:
66
+
67
+ ```text
68
+ pi-gate: command blocked by rule "sudo": "block"
69
+ ```
70
+
56
71
  ## Uninstall
57
72
 
58
73
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeldrisho/pi-gate",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
package/src/index.ts CHANGED
@@ -36,6 +36,11 @@ export interface GateConfig {
36
36
  operations: Record<string, Action>;
37
37
  }
38
38
 
39
+ export interface RuleMatch {
40
+ pattern: string;
41
+ action: Action;
42
+ }
43
+
39
44
  const CONFIG_FILE_NAME = "pi-gate.json";
40
45
  const EXAMPLE_FILE_NAME = "pi-gate.json.example";
41
46
  const ACTIONS: readonly Action[] = ["prompt", "block", "allow"] as const;
@@ -174,7 +179,7 @@ export function ensureExampleConfig(): void {
174
179
  }
175
180
 
176
181
  /**
177
- * Resolves the action that applies to a command against the configured rules.
182
+ * Resolves the rule that applies to a command against the configured rules.
178
183
  *
179
184
  * Returns `null` when no rule matches, in which case the caller should let the
180
185
  * command through. When multiple rules match, the longest pattern wins so
@@ -182,9 +187,9 @@ export function ensureExampleConfig(): void {
182
187
  *
183
188
  * @param command - The bash command to evaluate against the rules
184
189
  * @param rules - A map of pattern strings to actions
185
- * @returns The action to take, or `null` if no rule matches
190
+ * @returns The matched pattern and action, or `null` if no rule matches
186
191
  */
187
- export function resolveAction(command: string, rules: Record<string, Action>): Action | null {
192
+ export function resolveRule(command: string, rules: Record<string, Action>): RuleMatch | null {
188
193
  let bestPattern: string | null = null;
189
194
  for (const pattern of Object.keys(rules)) {
190
195
  if (!command.includes(pattern)) continue;
@@ -193,7 +198,22 @@ export function resolveAction(command: string, rules: Record<string, Action>): A
193
198
  }
194
199
  }
195
200
  if (bestPattern === null) return null;
196
- return rules[bestPattern];
201
+ return { pattern: bestPattern, action: rules[bestPattern] };
202
+ }
203
+
204
+ /** Returns only the action selected by {@link resolveRule}. */
205
+ export function resolveAction(command: string, rules: Record<string, Action>): Action | null {
206
+ return resolveRule(command, rules)?.action ?? null;
207
+ }
208
+
209
+ /**
210
+ * Formats a rule match as a human-readable string for display in prompts and notifications.
211
+ *
212
+ * @param match - The rule match to format
213
+ * @returns A string representation showing the pattern and action
214
+ */
215
+ function formatRule(match: RuleMatch): string {
216
+ return `${JSON.stringify(match.pattern)}: ${JSON.stringify(match.action)}`;
197
217
  }
198
218
 
199
219
  /** Gate the built-in `bash` tool against the user-provided rules. */
@@ -228,26 +248,31 @@ export default function piGate(pi: ExtensionAPI): void {
228
248
  if (event.toolName !== "bash") return undefined;
229
249
  if (!isToolCallEventType("bash", event)) return undefined;
230
250
  const command = event.input.command;
231
- const action = resolveAction(command, config.operations);
232
- if (action === null || action === "allow") return undefined;
251
+ const match = resolveRule(command, config.operations);
252
+ if (match === null || match.action === "allow") return undefined;
253
+ const rule = formatRule(match);
233
254
 
234
- if (action === "block") {
255
+ if (match.action === "block") {
256
+ const reason = `pi-gate: command blocked by rule ${rule}`;
235
257
  if (ctx.hasUI) {
236
- ctx.ui.notify(`pi-gate: blocked command matching a rule`, "warning");
258
+ ctx.ui.notify(reason, "warning");
237
259
  }
238
- return { block: true, reason: `pi-gate: command matches a "block" rule` };
260
+ return { block: true, reason };
239
261
  }
240
262
 
241
263
  // action === "prompt"
242
264
  if (!ctx.hasUI) {
243
- return { block: true, reason: "pi-gate: prompt required but no UI is available" };
265
+ return {
266
+ block: true,
267
+ reason: `pi-gate: command blocked because rule ${rule} requires a prompt, but no UI is available`,
268
+ };
244
269
  }
245
270
  const choice = await ctx.ui.select(
246
- `pi-gate: allow this command?\n\n ${command}\n\nA rule in pi-gate.json requests confirmation.`,
271
+ `pi-gate: allow this command?\n\n ${command}\n\nMatched rule: ${rule}`,
247
272
  ["Allow", "Deny"],
248
273
  );
249
274
  if (choice !== "Allow") {
250
- return { block: true, reason: "pi-gate: denied by user" };
275
+ return { block: true, reason: `pi-gate: command denied by user after matching rule ${rule}` };
251
276
  }
252
277
  return undefined;
253
278
  });