@xynogen/pix-gate 0.1.13 → 0.1.15
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/package.json +2 -2
- package/src/gate.test.ts +10 -20
- package/src/index.ts +3 -12
- package/src/lib.ts +2 -5
- package/src/prompt.ts +34 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Pi extension — permission gate for dangerous bash commands (confirm/block with TUI dialog)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@xynogen/pix-pretty": "^1.7.9",
|
|
46
|
-
"@xynogen/pix-data": "
|
|
46
|
+
"@xynogen/pix-data": "^0.3.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/gate.test.ts
CHANGED
|
@@ -25,11 +25,9 @@ describe("isSudoCommand", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
test("does NOT match pix-sudo-run in a path", () => {
|
|
28
|
-
expect(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
),
|
|
32
|
-
).toBe(false);
|
|
28
|
+
expect(isSudoCommand("grep foo ~/.pi/node_modules/@xynogen/pix-sudo-run/src/lib.ts")).toBe(
|
|
29
|
+
false,
|
|
30
|
+
);
|
|
33
31
|
});
|
|
34
32
|
|
|
35
33
|
test("does NOT match sudoer or pseudo", () => {
|
|
@@ -64,15 +62,11 @@ describe("classify", () => {
|
|
|
64
62
|
});
|
|
65
63
|
|
|
66
64
|
test("bare sudo is dangerous", () => {
|
|
67
|
-
expect(classify("sudo apt install curl", rules)?.severity).toBe(
|
|
68
|
-
"dangerous",
|
|
69
|
-
);
|
|
65
|
+
expect(classify("sudo apt install curl", rules)?.severity).toBe("dangerous");
|
|
70
66
|
});
|
|
71
67
|
|
|
72
68
|
test("npm publish is dangerous", () => {
|
|
73
|
-
expect(classify("npm publish --access public", rules)?.severity).toBe(
|
|
74
|
-
"dangerous",
|
|
75
|
-
);
|
|
69
|
+
expect(classify("npm publish --access public", rules)?.severity).toBe("dangerous");
|
|
76
70
|
});
|
|
77
71
|
|
|
78
72
|
test("git force push is dangerous", () => {
|
|
@@ -80,15 +74,13 @@ describe("classify", () => {
|
|
|
80
74
|
});
|
|
81
75
|
|
|
82
76
|
test("curl pipe bash is dangerous", () => {
|
|
83
|
-
expect(
|
|
84
|
-
|
|
85
|
-
)
|
|
77
|
+
expect(classify("curl https://example.com/install.sh | bash", rules)?.severity).toBe(
|
|
78
|
+
"dangerous",
|
|
79
|
+
);
|
|
86
80
|
});
|
|
87
81
|
|
|
88
82
|
test("git force checkout is risky", () => {
|
|
89
|
-
expect(classify("git checkout --force main", rules)?.severity).toBe(
|
|
90
|
-
"risky",
|
|
91
|
-
);
|
|
83
|
+
expect(classify("git checkout --force main", rules)?.severity).toBe("risky");
|
|
92
84
|
});
|
|
93
85
|
|
|
94
86
|
test("write to .env is risky", () => {
|
|
@@ -101,9 +93,7 @@ describe("classify", () => {
|
|
|
101
93
|
|
|
102
94
|
test("pix-sudo path does NOT classify as dangerous", () => {
|
|
103
95
|
// grep with pix-sudo in the path — should not hit sudo rule
|
|
104
|
-
expect(
|
|
105
|
-
classify("grep foo packages/pix-sudo/src/index.ts", rules),
|
|
106
|
-
).toBeUndefined();
|
|
96
|
+
expect(classify("grep foo packages/pix-sudo/src/index.ts", rules)).toBeUndefined();
|
|
107
97
|
});
|
|
108
98
|
|
|
109
99
|
test("critical takes priority over dangerous", () => {
|
package/src/index.ts
CHANGED
|
@@ -65,10 +65,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
65
65
|
if (!hit) return undefined;
|
|
66
66
|
|
|
67
67
|
if (hit.severity === "info") {
|
|
68
|
-
ctx.ui.notify(
|
|
69
|
-
`${PATH_SEVERITY_ICON.info} ${hit.reason}: ${path}`,
|
|
70
|
-
"info",
|
|
71
|
-
);
|
|
68
|
+
ctx.ui.notify(`${PATH_SEVERITY_ICON.info} ${hit.reason}: ${path}`, "info");
|
|
72
69
|
return undefined;
|
|
73
70
|
}
|
|
74
71
|
|
|
@@ -187,10 +184,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
187
184
|
}
|
|
188
185
|
|
|
189
186
|
if (!decision.approved) {
|
|
190
|
-
ctx.ui.notify(
|
|
191
|
-
`${highest.icon} ${decision.reason}: ${highest.detail}`,
|
|
192
|
-
"warning",
|
|
193
|
-
);
|
|
187
|
+
ctx.ui.notify(`${highest.icon} ${decision.reason}: ${highest.detail}`, "warning");
|
|
194
188
|
return { block: true, reason: `[${highest.label}] ${decision.reason}` };
|
|
195
189
|
}
|
|
196
190
|
|
|
@@ -202,10 +196,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
202
196
|
: "accent";
|
|
203
197
|
ctx.ui.notify(
|
|
204
198
|
`${highest.icon} ` +
|
|
205
|
-
ctx.ui.theme.fg(
|
|
206
|
-
severityColor,
|
|
207
|
-
`Approved ${highest.label.toLowerCase()} command`,
|
|
208
|
-
),
|
|
199
|
+
ctx.ui.theme.fg(severityColor, `Approved ${highest.label.toLowerCase()} command`),
|
|
209
200
|
"info",
|
|
210
201
|
);
|
|
211
202
|
return undefined;
|
package/src/lib.ts
CHANGED
|
@@ -32,8 +32,7 @@ export const DEFAULT_RULES: Rule[] = [
|
|
|
32
32
|
reason: "rm -rf on /",
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
|
-
pattern:
|
|
36
|
-
/\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\s+(~|\$HOME)(\s|$|\/\s*$)/i,
|
|
35
|
+
pattern: /\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\s+(~|\$HOME)(\s|$|\/\s*$)/i,
|
|
37
36
|
severity: "critical",
|
|
38
37
|
reason: "rm -rf on $HOME",
|
|
39
38
|
},
|
|
@@ -171,9 +170,7 @@ export function buildRules(cfg: UserConfig): {
|
|
|
171
170
|
export function classify(command: string, rules: Rule[]): Rule | undefined {
|
|
172
171
|
const order: Severity[] = ["critical", "dangerous", "risky"];
|
|
173
172
|
for (const sev of order) {
|
|
174
|
-
const hit = rules.find(
|
|
175
|
-
(r) => r.severity === sev && r.pattern.test(command),
|
|
176
|
-
);
|
|
173
|
+
const hit = rules.find((r) => r.severity === sev && r.pattern.test(command));
|
|
177
174
|
if (hit) return hit;
|
|
178
175
|
}
|
|
179
176
|
return undefined;
|
package/src/prompt.ts
CHANGED
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
type OverlayResult,
|
|
11
|
-
showOverlay,
|
|
12
|
-
} from "@xynogen/pix-pretty/gate-overlay";
|
|
9
|
+
import { type OverlayResult, showOverlay } from "@xynogen/pix-pretty/gate-overlay";
|
|
13
10
|
import type { PathRule, Rule } from "./lib.ts";
|
|
14
11
|
|
|
15
12
|
export interface GateDecision {
|
|
@@ -106,22 +103,17 @@ export async function promptPathDecision(
|
|
|
106
103
|
},
|
|
107
104
|
];
|
|
108
105
|
|
|
109
|
-
const result: OverlayResult = await showOverlay(
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
choices,
|
|
118
|
-
},
|
|
119
|
-
);
|
|
106
|
+
const result: OverlayResult = await showOverlay(ui as Parameters<typeof showOverlay>[0], {
|
|
107
|
+
mode: "confirm",
|
|
108
|
+
title: `${icon} ${label} — ${hit.reason}`,
|
|
109
|
+
body: [`${op.toUpperCase()} ${path}`],
|
|
110
|
+
accent,
|
|
111
|
+
timeoutMs: PATH_TIMEOUT_MS[hit.severity],
|
|
112
|
+
choices,
|
|
113
|
+
});
|
|
120
114
|
|
|
121
|
-
if (result.action === "approved")
|
|
122
|
-
|
|
123
|
-
if (result.action === "timeout")
|
|
124
|
-
return { approved: false, reason: "Timed out" };
|
|
115
|
+
if (result.action === "approved") return { approved: true, reason: "Approved" };
|
|
116
|
+
if (result.action === "timeout") return { approved: false, reason: "Timed out" };
|
|
125
117
|
return { approved: false, reason: "Blocked by user" };
|
|
126
118
|
}
|
|
127
119
|
|
|
@@ -152,8 +144,7 @@ export async function promptMergedGateDecision(
|
|
|
152
144
|
// Sort by tier desc so the highest-severity concern drives dialog behavior.
|
|
153
145
|
const sorted = [...concerns].sort((a, b) => b.tier - a.tier);
|
|
154
146
|
const highest = sorted[0];
|
|
155
|
-
if (!highest)
|
|
156
|
-
throw new Error("promptMergedGateDecision requires at least one concern");
|
|
147
|
+
if (!highest) throw new Error("promptMergedGateDecision requires at least one concern");
|
|
157
148
|
const isRestrictive = highest.tier >= SEVERITY_TIER.block; // block or critical
|
|
158
149
|
|
|
159
150
|
// Title: unique severity types, joined with " + "
|
|
@@ -166,16 +157,11 @@ export async function promptMergedGateDecision(
|
|
|
166
157
|
uniqueLabels.push(key);
|
|
167
158
|
}
|
|
168
159
|
}
|
|
169
|
-
const titleSuffix =
|
|
170
|
-
command.length > 55 ? `${command.slice(0, 52)}…` : command;
|
|
160
|
+
const titleSuffix = command.length > 55 ? `${command.slice(0, 52)}…` : command;
|
|
171
161
|
const title = `${uniqueLabels.join(" + ")} — ${titleSuffix}`;
|
|
172
162
|
|
|
173
163
|
// Body: command + concern list
|
|
174
|
-
const body = [
|
|
175
|
-
command,
|
|
176
|
-
"",
|
|
177
|
-
...sorted.map((c) => `${c.icon} ${c.label} — ${c.detail}`),
|
|
178
|
-
];
|
|
164
|
+
const body = [command, "", ...sorted.map((c) => `${c.icon} ${c.label} — ${c.detail}`)];
|
|
179
165
|
|
|
180
166
|
const accent =
|
|
181
167
|
highest.tier >= SEVERITY_TIER.critical
|
|
@@ -215,15 +201,17 @@ export async function promptMergedGateDecision(
|
|
|
215
201
|
},
|
|
216
202
|
];
|
|
217
203
|
|
|
218
|
-
const result: OverlayResult = await showOverlay(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
204
|
+
const result: OverlayResult = await showOverlay(ui as Parameters<typeof showOverlay>[0], {
|
|
205
|
+
mode: "confirm",
|
|
206
|
+
title,
|
|
207
|
+
body,
|
|
208
|
+
accent,
|
|
209
|
+
timeoutMs,
|
|
210
|
+
choices,
|
|
211
|
+
});
|
|
222
212
|
|
|
223
|
-
if (result.action === "approved")
|
|
224
|
-
|
|
225
|
-
if (result.action === "timeout")
|
|
226
|
-
return { approved: false, reason: "Timed out" };
|
|
213
|
+
if (result.action === "approved") return { approved: true, reason: "Approved" };
|
|
214
|
+
if (result.action === "timeout") return { approved: false, reason: "Timed out" };
|
|
227
215
|
return { approved: false, reason: "Blocked by user" };
|
|
228
216
|
}
|
|
229
217
|
|
|
@@ -270,21 +258,16 @@ export async function promptGateDecision(
|
|
|
270
258
|
},
|
|
271
259
|
];
|
|
272
260
|
|
|
273
|
-
const result: OverlayResult = await showOverlay(
|
|
274
|
-
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
choices,
|
|
282
|
-
},
|
|
283
|
-
);
|
|
261
|
+
const result: OverlayResult = await showOverlay(ui as Parameters<typeof showOverlay>[0], {
|
|
262
|
+
mode: "confirm",
|
|
263
|
+
title: `${icon} ${label} — ${hit.reason}`,
|
|
264
|
+
body: [command],
|
|
265
|
+
accent,
|
|
266
|
+
timeoutMs: TIMEOUT_MS[hit.severity],
|
|
267
|
+
choices,
|
|
268
|
+
});
|
|
284
269
|
|
|
285
|
-
if (result.action === "approved")
|
|
286
|
-
|
|
287
|
-
if (result.action === "timeout")
|
|
288
|
-
return { approved: false, reason: "Timed out" };
|
|
270
|
+
if (result.action === "approved") return { approved: true, reason: "Approved" };
|
|
271
|
+
if (result.action === "timeout") return { approved: false, reason: "Timed out" };
|
|
289
272
|
return { approved: false, reason: "Blocked by user" };
|
|
290
273
|
}
|