@zeldrisho/pi-gate 0.7.0 → 0.7.2
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 +20 -1
- package/README.md +3 -3
- package/package.json +5 -2
- package/src/approval-prompt.ts +140 -0
- package/src/index.ts +108 -23
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.2] - 2026-10-25
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Show approval prompts in Pi’s normal dialog area, using the terminal background and Pi theme accents.
|
|
15
|
+
- Highlight matched command text and clarify agent guidance for timeouts, denials, and policy blocks.
|
|
16
|
+
|
|
17
|
+
## [0.7.1] - 2026-10-25
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Create starter configuration when a session starts rather than when the extension is loaded.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Cancel active approval dialogs and reject queued calls during shutdown, reload, and session replacement.
|
|
26
|
+
|
|
10
27
|
## [0.7.0] - 2026-10-24
|
|
11
28
|
|
|
12
29
|
### Changed
|
|
@@ -95,7 +112,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
95
112
|
- support `prompt`, `block`, and `allow` actions, with longest-pattern-wins resolution
|
|
96
113
|
- block rather than auto-approve in non-UI modes
|
|
97
114
|
|
|
98
|
-
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.
|
|
115
|
+
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.2...HEAD
|
|
116
|
+
[0.7.2]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.1...pi-gate-v0.7.2
|
|
117
|
+
[0.7.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.0...pi-gate-v0.7.1
|
|
99
118
|
[0.7.0]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.6.1...pi-gate-v0.7.0
|
|
100
119
|
[0.6.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.6.0...pi-gate-v0.6.1
|
|
101
120
|
[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
|
|
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,9 +46,9 @@ 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
|
-
Long commands are truncated in approval dialogs, but an approved command executes in full.
|
|
51
|
+
The matched command text is highlighted in the TUI approval dialog. Long commands are truncated in approval dialogs, but an approved command executes in full. Denials, policy blocks, dismissals, and timeouts stop the turn; the agent is told to ask before retrying a denial or proceeding past a block, while a timeout asks whether to retry approval. Other allowed calls in a parallel batch may still run. In non-interactive modes (`-p`, JSON), commands requiring approval are denied.
|
|
52
52
|
|
|
53
53
|
In Herdr-managed panes, install Herdr's Pi integration with `herdr integration install pi` to report pending approvals as `blocked`. Herdr is optional.
|
|
54
54
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeldrisho/pi-gate",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Pi extension that gates bash commands and protects its configuration from tool edits",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"command-blocking",
|
|
@@ -33,12 +33,15 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
36
|
+
"@earendil-works/pi-tui": "^1.4.0",
|
|
37
|
+
"typebox": "^1.4.2",
|
|
36
38
|
"typescript": "^7.0.2",
|
|
37
39
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
|
|
38
40
|
"vite-plus": "1.3.0"
|
|
39
41
|
},
|
|
40
42
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
43
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
44
|
+
"@earendil-works/pi-tui": "*"
|
|
42
45
|
},
|
|
43
46
|
"engines": {
|
|
44
47
|
"node": ">=24"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
const ACTIONS = ["Allow", "Deny"] as const;
|
|
5
|
+
|
|
6
|
+
type Action = (typeof ACTIONS)[number];
|
|
7
|
+
|
|
8
|
+
type Foreground = "text" | "muted" | "accent" | "toolOutput" | "border";
|
|
9
|
+
|
|
10
|
+
export interface ApprovalPromptTheme {
|
|
11
|
+
style(text: string, options: { fg?: Foreground; bold?: boolean }): string;
|
|
12
|
+
bold(text: string): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ApprovalPromptKeybindings {
|
|
16
|
+
matches(
|
|
17
|
+
data: string,
|
|
18
|
+
action: "tui.select.up" | "tui.select.down" | "tui.select.confirm" | "tui.select.cancel",
|
|
19
|
+
): boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ApprovalPromptTui {
|
|
23
|
+
requestRender(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Theme-aware approval dialog used by the interactive TUI. */
|
|
27
|
+
export function createApprovalPrompt(
|
|
28
|
+
tui: ApprovalPromptTui,
|
|
29
|
+
theme: ApprovalPromptTheme,
|
|
30
|
+
keybindings: ApprovalPromptKeybindings,
|
|
31
|
+
done: (result: Action | undefined) => void,
|
|
32
|
+
command: string,
|
|
33
|
+
rule: string,
|
|
34
|
+
): Component {
|
|
35
|
+
let selectedIndex = 0;
|
|
36
|
+
|
|
37
|
+
const invalidate = () => {};
|
|
38
|
+
|
|
39
|
+
const innerWidth = (width: number) => Math.max(1, width - 4);
|
|
40
|
+
|
|
41
|
+
const panelLine = (
|
|
42
|
+
text: string,
|
|
43
|
+
width: number,
|
|
44
|
+
color: "text" | "muted" | "accent" | "toolOutput" = "text",
|
|
45
|
+
) => {
|
|
46
|
+
const contentWidth = innerWidth(width);
|
|
47
|
+
const content = truncateToWidth(text, contentWidth, "…");
|
|
48
|
+
const padding = " ".repeat(Math.max(0, contentWidth - visibleWidth(content)));
|
|
49
|
+
|
|
50
|
+
const styled = theme.style(`${content}${padding}`, { fg: color });
|
|
51
|
+
|
|
52
|
+
return ` ${styled} `;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const component: Component = {
|
|
56
|
+
render(width: number): string[] {
|
|
57
|
+
const available = innerWidth(width);
|
|
58
|
+
const border = theme.style("─".repeat(available + 4), { fg: "border" });
|
|
59
|
+
|
|
60
|
+
const lines = [
|
|
61
|
+
border,
|
|
62
|
+
panelLine(theme.bold("pi-gate: allow this command?"), width, "accent"),
|
|
63
|
+
panelLine("Review the requested command or file change:", width, "muted"),
|
|
64
|
+
panelLine("", width),
|
|
65
|
+
...command.split("\n").map((line) => {
|
|
66
|
+
const contentWidth = innerWidth(width);
|
|
67
|
+
const parts = line.split(/([»«])/u);
|
|
68
|
+
let matched = false;
|
|
69
|
+
|
|
70
|
+
const rendered = parts
|
|
71
|
+
.map((part) => {
|
|
72
|
+
if (part === "»") {
|
|
73
|
+
matched = true;
|
|
74
|
+
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (part === "«") {
|
|
79
|
+
matched = false;
|
|
80
|
+
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return theme.style(part, { fg: matched ? "accent" : "toolOutput", bold: matched });
|
|
85
|
+
})
|
|
86
|
+
.join("");
|
|
87
|
+
|
|
88
|
+
const visible = line.replace(/[»«]/gu, "");
|
|
89
|
+
const content = truncateToWidth(visible, contentWidth, "…");
|
|
90
|
+
const padding = " ".repeat(Math.max(0, contentWidth - visibleWidth(content)));
|
|
91
|
+
|
|
92
|
+
return ` ${rendered}${theme.style(padding, { fg: "toolOutput" })} `;
|
|
93
|
+
}),
|
|
94
|
+
panelLine("", width),
|
|
95
|
+
panelLine(`Matched rule: ${rule}`, width, "muted"),
|
|
96
|
+
panelLine("", width),
|
|
97
|
+
...ACTIONS.map((action, index) => {
|
|
98
|
+
const selected = index === selectedIndex;
|
|
99
|
+
const label = `${selected ? "→ " : " "}${action}`;
|
|
100
|
+
const row = truncateToWidth(label, available);
|
|
101
|
+
const padded = `${row}${" ".repeat(Math.max(0, available - visibleWidth(row)))}`;
|
|
102
|
+
|
|
103
|
+
return theme.style(` ${padded} `, {
|
|
104
|
+
fg: selected ? "accent" : "text",
|
|
105
|
+
});
|
|
106
|
+
}),
|
|
107
|
+
panelLine("", width),
|
|
108
|
+
panelLine("↑↓ move · enter confirm · esc cancel", width, "muted"),
|
|
109
|
+
border,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
113
|
+
},
|
|
114
|
+
handleInput(data: string): void {
|
|
115
|
+
if (keybindings.matches(data, "tui.select.up") || data === "k") {
|
|
116
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
117
|
+
} else if (keybindings.matches(data, "tui.select.down") || data === "j") {
|
|
118
|
+
selectedIndex = Math.min(ACTIONS.length - 1, selectedIndex + 1);
|
|
119
|
+
} else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
120
|
+
done(ACTIONS[selectedIndex]);
|
|
121
|
+
|
|
122
|
+
return;
|
|
123
|
+
} else if (keybindings.matches(data, "tui.select.cancel")) {
|
|
124
|
+
done(undefined);
|
|
125
|
+
|
|
126
|
+
return;
|
|
127
|
+
} else {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
component.invalidate();
|
|
132
|
+
tui.requestRender();
|
|
133
|
+
},
|
|
134
|
+
invalidate,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return component;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type ApprovalAction = Action;
|
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
|
|
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
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
import {
|
|
32
32
|
isToolCallEventType,
|
|
33
33
|
type ExtensionAPI,
|
|
34
|
+
type ExtensionContext,
|
|
34
35
|
type ToolCallEventResult,
|
|
35
36
|
} from "@earendil-works/pi-coding-agent";
|
|
37
|
+
import { createApprovalPrompt, type ApprovalAction } from "./approval-prompt";
|
|
36
38
|
import { configPath, ensureConfig, loadConfigResult } from "./config";
|
|
37
39
|
import {
|
|
38
40
|
bashTouchesConfig,
|
|
@@ -44,6 +46,76 @@ import { formatCommandForDisplay, formatPromptCommand, formatRule } from "./disp
|
|
|
44
46
|
|
|
45
47
|
const HERDR_BLOCKED_LABEL = "pi-gate: approval required";
|
|
46
48
|
|
|
49
|
+
/** Uses a themed terminal component in TUI mode and Pi's standard selector elsewhere. */
|
|
50
|
+
async function selectApproval(
|
|
51
|
+
ctx: ExtensionContext,
|
|
52
|
+
timeoutMs: number,
|
|
53
|
+
signal: AbortSignal,
|
|
54
|
+
command: string,
|
|
55
|
+
rule: string,
|
|
56
|
+
showMatchHint: boolean,
|
|
57
|
+
): Promise<string | undefined> {
|
|
58
|
+
if (ctx.mode !== "tui" || !ctx.ui.custom) {
|
|
59
|
+
return ctx.ui.select(
|
|
60
|
+
`pi-gate: allow this command?\n\n${command}\n\nMatched rule: ${rule}${showMatchHint ? "\nMatched command text is wrapped in »…«" : ""}`,
|
|
61
|
+
["Allow", "Deny"],
|
|
62
|
+
{ timeout: timeoutMs, signal },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let uiDone: ((value: ApprovalAction | undefined) => void) | undefined;
|
|
67
|
+
let settled = false;
|
|
68
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
69
|
+
let removeAbortListener: (() => void) | undefined;
|
|
70
|
+
|
|
71
|
+
return new Promise<ApprovalAction | undefined>((resolve, reject) => {
|
|
72
|
+
const finish = (value: ApprovalAction | undefined) => {
|
|
73
|
+
if (settled) return;
|
|
74
|
+
settled = true;
|
|
75
|
+
|
|
76
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
77
|
+
removeAbortListener?.();
|
|
78
|
+
resolve(value);
|
|
79
|
+
uiDone?.(value);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const onAbort = () => finish(undefined);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
timer = setTimeout(() => finish(undefined), timeoutMs);
|
|
86
|
+
|
|
87
|
+
const uiPromise = ctx.ui.custom<ApprovalAction | undefined>(
|
|
88
|
+
(tui, theme, keybindings, done) => {
|
|
89
|
+
uiDone = done;
|
|
90
|
+
|
|
91
|
+
return createApprovalPrompt(tui, theme, keybindings, done, command, rule);
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
uiPromise
|
|
96
|
+
.then(finish)
|
|
97
|
+
.catch(reject)
|
|
98
|
+
.finally(() => {
|
|
99
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
100
|
+
removeAbortListener?.();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (signal) {
|
|
104
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
105
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
106
|
+
|
|
107
|
+
if (signal.aborted) onAbort();
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
settled = true;
|
|
111
|
+
|
|
112
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
113
|
+
removeAbortListener?.();
|
|
114
|
+
reject(error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
47
119
|
const MAX_REASON_RULE_NAME_LENGTH = 27;
|
|
48
120
|
|
|
49
121
|
/** Escapes a rule pattern for terminal display and bounds its length in denial messages. */
|
|
@@ -57,7 +129,7 @@ function reasonRuleName(pattern: string): string {
|
|
|
57
129
|
|
|
58
130
|
/** Formats an approval timeout with the matched rule, elapsed limit in seconds, and retry guidance. */
|
|
59
131
|
function timeoutReason(pattern: string, timeoutMs: number): string {
|
|
60
|
-
return `pi-gate: no response to rule "${reasonRuleName(pattern)}" within ${timeoutMs / 1_000}s; command not run. Ask the user whether to retry.`;
|
|
132
|
+
return `pi-gate: no response to rule "${reasonRuleName(pattern)}" within ${timeoutMs / 1_000}s; command not run. Ask the user whether to retry approval.`;
|
|
61
133
|
}
|
|
62
134
|
|
|
63
135
|
function reportHerdrBlocked(pi: ExtensionAPI, ctx: { mode?: string }, active: boolean): void {
|
|
@@ -112,14 +184,16 @@ export {
|
|
|
112
184
|
|
|
113
185
|
/**
|
|
114
186
|
* Gates `bash` calls against user rules and requires approval for protected
|
|
115
|
-
* configuration writes and edits. Attempts to create a starter configuration
|
|
187
|
+
* configuration writes and edits. Attempts to create a starter configuration
|
|
188
|
+
* at session start if absent.
|
|
116
189
|
* Reloads rules before gating and after each prompt; approvals apply per call.
|
|
117
|
-
* Dialogs are serialized, and
|
|
118
|
-
*
|
|
190
|
+
* Dialogs are serialized. Cancellation, shutdown, and session replacement deny
|
|
191
|
+
* active and queued gated calls; policy changes invalidate an active approval.
|
|
192
|
+
* UI failures reject the affected tool call.
|
|
119
193
|
*/
|
|
120
194
|
export default function piGate(pi: ExtensionAPI): void {
|
|
121
|
-
ensureConfig();
|
|
122
195
|
let loadResult = loadConfigResult();
|
|
196
|
+
let session = new AbortController();
|
|
123
197
|
let policy = JSON.stringify(loadResult);
|
|
124
198
|
let revision = 0;
|
|
125
199
|
let pending = Promise.resolve();
|
|
@@ -135,8 +209,16 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
135
209
|
}
|
|
136
210
|
};
|
|
137
211
|
|
|
212
|
+
pi.on("session_shutdown", () => {
|
|
213
|
+
session.abort();
|
|
214
|
+
revision += 1;
|
|
215
|
+
});
|
|
216
|
+
|
|
138
217
|
pi.on("session_start", (_event, ctx) => {
|
|
218
|
+
session.abort();
|
|
219
|
+
session = new AbortController();
|
|
139
220
|
revision += 1;
|
|
221
|
+
ensureConfig();
|
|
140
222
|
refreshConfig();
|
|
141
223
|
const path = configPath();
|
|
142
224
|
const ruleCount = Object.keys(loadResult.config.operations).length;
|
|
@@ -160,7 +242,14 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
160
242
|
});
|
|
161
243
|
|
|
162
244
|
pi.on("tool_call", (event, ctx) => {
|
|
245
|
+
// Capture ownership before queueing so old calls cannot enter a replacement session.
|
|
246
|
+
const signal = ctx.signal ? AbortSignal.any([ctx.signal, session.signal]) : session.signal;
|
|
247
|
+
|
|
163
248
|
// Pi can dispatch sibling tools concurrently; never overlap approval dialogs.
|
|
249
|
+
/**
|
|
250
|
+
* Returns a terminating block result for denied calls, or undefined to allow;
|
|
251
|
+
* UI errors propagate.
|
|
252
|
+
*/
|
|
164
253
|
const run = async (): Promise<ToolCallEventResult | undefined> => {
|
|
165
254
|
let command: string;
|
|
166
255
|
let configChange = false;
|
|
@@ -181,6 +270,14 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
181
270
|
return undefined;
|
|
182
271
|
}
|
|
183
272
|
|
|
273
|
+
if (signal.aborted) {
|
|
274
|
+
return {
|
|
275
|
+
block: true,
|
|
276
|
+
reason: "pi-gate: approval cancelled; command not run.",
|
|
277
|
+
terminate: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
184
281
|
refreshConfig();
|
|
185
282
|
|
|
186
283
|
const match = configChange
|
|
@@ -193,7 +290,7 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
193
290
|
const rule = configChange ? CONFIG_CHANGE_REASON : formatRule(match);
|
|
194
291
|
|
|
195
292
|
if (match.action === "block") {
|
|
196
|
-
const reason = `pi-gate: blocked by rule "${reasonRuleName(match.pattern)}".
|
|
293
|
+
const reason = `pi-gate: command blocked by rule "${reasonRuleName(match.pattern)}". Ask the user before proceeding.`;
|
|
197
294
|
|
|
198
295
|
if (ctx.hasUI) ctx.ui.notify(reason, "warning");
|
|
199
296
|
|
|
@@ -205,15 +302,7 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
205
302
|
block: true,
|
|
206
303
|
reason: configChange
|
|
207
304
|
? CONFIG_CHANGE_REASON
|
|
208
|
-
: `pi-gate: rule "${reasonRuleName(match.pattern)}" needs approval but no UI is available.
|
|
209
|
-
terminate: true,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (ctx.signal?.aborted) {
|
|
214
|
-
return {
|
|
215
|
-
block: true,
|
|
216
|
-
reason: "pi-gate: approval cancelled; command not run.",
|
|
305
|
+
: `pi-gate: rule "${reasonRuleName(match.pattern)}" needs approval but no UI is available. Ask the user before proceeding.`,
|
|
217
306
|
terminate: true,
|
|
218
307
|
};
|
|
219
308
|
}
|
|
@@ -231,11 +320,7 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
231
320
|
|
|
232
321
|
try {
|
|
233
322
|
const promptStartedAt = Date.now();
|
|
234
|
-
choice = await ctx
|
|
235
|
-
`pi-gate: allow this command?\n\n${promptCommand}\n\nMatched rule: ${rule}${configChange ? "" : "\nMatched command text is wrapped in »…«"}`,
|
|
236
|
-
["Allow", "Deny"],
|
|
237
|
-
{ timeout: timeoutMs, signal: ctx.signal },
|
|
238
|
-
);
|
|
323
|
+
choice = await selectApproval(ctx, timeoutMs, signal, promptCommand, rule, !configChange);
|
|
239
324
|
// Pi returns undefined for both dismissal and timeout; elapsed time separates the timeout case.
|
|
240
325
|
timedOut = choice === undefined && Date.now() - promptStartedAt >= timeoutMs;
|
|
241
326
|
} finally {
|
|
@@ -244,7 +329,7 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
244
329
|
|
|
245
330
|
refreshConfig();
|
|
246
331
|
|
|
247
|
-
if (
|
|
332
|
+
if (signal.aborted || revision !== promptRevision) {
|
|
248
333
|
return {
|
|
249
334
|
block: true,
|
|
250
335
|
reason:
|
|
@@ -260,7 +345,7 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
260
345
|
? timeoutReason(match.pattern, timeoutMs)
|
|
261
346
|
: configChange
|
|
262
347
|
? CONFIG_CHANGE_REASON
|
|
263
|
-
: `pi-gate: denied
|
|
348
|
+
: `pi-gate: command not run because approval was denied. Ask the user before retrying (rule "${reasonRuleName(match.pattern)}").`,
|
|
264
349
|
terminate: true,
|
|
265
350
|
};
|
|
266
351
|
}
|