@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 +8 -1
- package/README.md +17 -2
- package/package.json +1 -1
- package/src/index.ts +37 -12
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.
|
|
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
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
|
|
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
|
|
190
|
+
* @returns The matched pattern and action, or `null` if no rule matches
|
|
186
191
|
*/
|
|
187
|
-
export function
|
|
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
|
|
232
|
-
if (
|
|
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(
|
|
258
|
+
ctx.ui.notify(reason, "warning");
|
|
237
259
|
}
|
|
238
|
-
return { block: true, reason
|
|
260
|
+
return { block: true, reason };
|
|
239
261
|
}
|
|
240
262
|
|
|
241
263
|
// action === "prompt"
|
|
242
264
|
if (!ctx.hasUI) {
|
|
243
|
-
return {
|
|
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\
|
|
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:
|
|
275
|
+
return { block: true, reason: `pi-gate: command denied by user after matching rule ${rule}` };
|
|
251
276
|
}
|
|
252
277
|
return undefined;
|
|
253
278
|
});
|