@manuelstolze/pi-permission-gate 0.2.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/README.md +69 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +106 -0
- package/dist/config.js.map +1 -0
- package/dist/dialog.d.ts +17 -0
- package/dist/dialog.js +118 -0
- package/dist/dialog.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +111 -0
- package/dist/index.js.map +1 -0
- package/dist/rules.d.ts +6 -0
- package/dist/rules.js +16 -0
- package/dist/rules.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @manuelstolze/pi-permission-gate
|
|
2
|
+
|
|
3
|
+
A Pi extension that asks before it runs bash commands that match dangerous patterns. It replaces the permission gate from `pi-guardrails`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @manuelstolze/pi-permission-gate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add the package to your Pi extension configuration:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"pi": {
|
|
16
|
+
"extensions": ["@manuelstolze/pi-permission-gate"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
>> If you are already using a guardrails extentions, make sure you remove the previous one to avoid duplicate checks.
|
|
22
|
+
|
|
23
|
+
## Behavior
|
|
24
|
+
|
|
25
|
+
The extension checks commands in this order:
|
|
26
|
+
|
|
27
|
+
1. Hard-coded auto-deny rules block a command without a prompt. Allow rules cannot override these checks.
|
|
28
|
+
2. File and session allow rules allow a command.
|
|
29
|
+
3. Dangerous patterns show a dialog with four choices: allow once, allow this session, allow forever, or deny.
|
|
30
|
+
4. If the user denies a command, they can send Pi a steer instruction that says what to do instead.
|
|
31
|
+
|
|
32
|
+
“Allow this session” applies only to the exact command and ends when the session ends. “Allow forever” saves an anchored regular expression for the exact command.
|
|
33
|
+
|
|
34
|
+
Without a user interface, the extension blocks dangerous commands. Custom dialog support falls back to Pi's built-in selection dialog when needed.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
The extension reads `~/.pi/agent/extensions/guardrails.json` at session start. It creates the directory when it saves an “allow forever” rule. Changes to the file take effect in the next session.
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"applyBuiltinDefaults": true,
|
|
43
|
+
"permissionGate": {
|
|
44
|
+
"patterns": [
|
|
45
|
+
{ "pattern": "terraform destroy", "description": "Terraform resource deletion" },
|
|
46
|
+
{ "pattern": "^kubectl\\s+delete", "regex": true, "description": "Kubernetes deletion" }
|
|
47
|
+
],
|
|
48
|
+
"allowedPatterns": [
|
|
49
|
+
{ "pattern": "^git status$", "regex": true }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A pattern uses substring matching by default. Set `regex` to `true` to use a JavaScript regular expression. Invalid regular expressions do not match. Built-in dangerous patterns apply unless `applyBuiltinDefaults` is `false`.
|
|
56
|
+
|
|
57
|
+
If the file is missing, the extension uses default settings. If it cannot read or validate the file, it shows a warning, uses built-in dangerous patterns, and leaves the file unchanged.
|
|
58
|
+
|
|
59
|
+
Auto-deny rules use literal substring matching. They do not parse shell syntax. They can miss commands with different spacing and can match text inside quoted strings. Do not treat this check as a complete shell security boundary.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
From the repository root:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run build --workspace @manuelstolze/pi-permission-gate
|
|
68
|
+
npm test
|
|
69
|
+
```
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PatternConfig {
|
|
2
|
+
pattern: string;
|
|
3
|
+
regex?: boolean;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GuardrailsConfig {
|
|
7
|
+
applyBuiltinDefaults?: boolean;
|
|
8
|
+
permissionGate?: {
|
|
9
|
+
patterns?: PatternConfig[];
|
|
10
|
+
allowedPatterns?: PatternConfig[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare const BUILTIN_PATTERNS: PatternConfig[];
|
|
14
|
+
export declare function getConfigPath(): string;
|
|
15
|
+
export declare function readConfig(path?: string): {
|
|
16
|
+
config: GuardrailsConfig;
|
|
17
|
+
error?: Error;
|
|
18
|
+
};
|
|
19
|
+
export declare function writeExactAllowRule(command: string, path?: string): PatternConfig;
|
|
20
|
+
export declare function testPattern(pattern: PatternConfig, command: string): boolean;
|
|
21
|
+
export declare function findPattern(command: string, patterns: PatternConfig[]): PatternConfig | undefined;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export const BUILTIN_PATTERNS = [
|
|
5
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
6
|
+
{ pattern: "sudo", description: "superuser command" },
|
|
7
|
+
{ pattern: "dd of=", description: "disk write operation" },
|
|
8
|
+
{ pattern: "mkfs.", description: "filesystem format" },
|
|
9
|
+
{ pattern: "chmod -R 777", description: "insecure recursive permissions" },
|
|
10
|
+
{ pattern: "chown -R", description: "recursive ownership change" },
|
|
11
|
+
{ pattern: "doas", description: "privileged command execution" },
|
|
12
|
+
{ pattern: "pkexec", description: "privileged command execution" },
|
|
13
|
+
{ pattern: "shred", description: "secure file overwrite" },
|
|
14
|
+
{ pattern: "wipefs", description: "filesystem signature wipe" },
|
|
15
|
+
{ pattern: "blkdiscard", description: "block device discard" },
|
|
16
|
+
{ pattern: "fdisk", description: "disk partitioning" },
|
|
17
|
+
{ pattern: "parted", description: "disk partitioning" },
|
|
18
|
+
{ pattern: "docker run --privileged", description: "container with privileged mode" },
|
|
19
|
+
];
|
|
20
|
+
export function getConfigPath() {
|
|
21
|
+
return join(homedir(), ".pi", "agent", "extensions", "guardrails.json");
|
|
22
|
+
}
|
|
23
|
+
export function readConfig(path = getConfigPath()) {
|
|
24
|
+
let content;
|
|
25
|
+
try {
|
|
26
|
+
content = readFileSync(path, "utf-8");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
30
|
+
return { config: {} };
|
|
31
|
+
return { config: {}, error: asError(error) };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(content);
|
|
35
|
+
if (!isGuardrailsConfig(parsed))
|
|
36
|
+
throw new Error("guardrails.json has an invalid structure");
|
|
37
|
+
return { config: parsed };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return { config: {}, error: asError(error) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function writeExactAllowRule(command, path = getConfigPath()) {
|
|
44
|
+
const { config, error } = readConfig(path);
|
|
45
|
+
if (error)
|
|
46
|
+
throw new Error(`Cannot update invalid guardrails config: ${error.message}`);
|
|
47
|
+
const permissionGate = config.permissionGate ?? {};
|
|
48
|
+
const allowedPatterns = permissionGate.allowedPatterns ?? [];
|
|
49
|
+
const rule = { pattern: `^${escapeRegex(command)}$`, regex: true };
|
|
50
|
+
if (!allowedPatterns.some((item) => item.pattern === rule.pattern && item.regex === true)) {
|
|
51
|
+
allowedPatterns.push(rule);
|
|
52
|
+
}
|
|
53
|
+
config.permissionGate = { ...permissionGate, allowedPatterns };
|
|
54
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
55
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
56
|
+
return rule;
|
|
57
|
+
}
|
|
58
|
+
export function testPattern(pattern, command) {
|
|
59
|
+
if (pattern.regex) {
|
|
60
|
+
try {
|
|
61
|
+
return new RegExp(pattern.pattern).test(command);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return command.includes(pattern.pattern);
|
|
68
|
+
}
|
|
69
|
+
export function findPattern(command, patterns) {
|
|
70
|
+
return patterns.find((pattern) => testPattern(pattern, command));
|
|
71
|
+
}
|
|
72
|
+
function escapeRegex(value) {
|
|
73
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
74
|
+
}
|
|
75
|
+
function isGuardrailsConfig(value) {
|
|
76
|
+
if (!isRecord(value))
|
|
77
|
+
return false;
|
|
78
|
+
if (value.applyBuiltinDefaults !== undefined && typeof value.applyBuiltinDefaults !== "boolean")
|
|
79
|
+
return false;
|
|
80
|
+
if (value.permissionGate === undefined)
|
|
81
|
+
return true;
|
|
82
|
+
if (!isRecord(value.permissionGate))
|
|
83
|
+
return false;
|
|
84
|
+
return isPatternList(value.permissionGate.patterns) && isPatternList(value.permissionGate.allowedPatterns);
|
|
85
|
+
}
|
|
86
|
+
function isPatternList(value) {
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
return true;
|
|
89
|
+
return Array.isArray(value) && value.every(isPatternConfig);
|
|
90
|
+
}
|
|
91
|
+
function isPatternConfig(value) {
|
|
92
|
+
return (isRecord(value) &&
|
|
93
|
+
typeof value.pattern === "string" &&
|
|
94
|
+
(value.regex === undefined || typeof value.regex === "boolean") &&
|
|
95
|
+
(value.description === undefined || typeof value.description === "string"));
|
|
96
|
+
}
|
|
97
|
+
function isRecord(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function isNodeError(error) {
|
|
101
|
+
return error instanceof Error && "code" in error;
|
|
102
|
+
}
|
|
103
|
+
function asError(error) {
|
|
104
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ConfirmResult = "allow-once" | "allow-session" | "allow-forever" | "deny";
|
|
2
|
+
interface MinimalTheme {
|
|
3
|
+
fg(color: string, text: string): string;
|
|
4
|
+
bold(text: string): string;
|
|
5
|
+
}
|
|
6
|
+
export declare function createDialog(command: string, description: string): (tui: {
|
|
7
|
+
terminal: {
|
|
8
|
+
rows: number;
|
|
9
|
+
columns: number;
|
|
10
|
+
};
|
|
11
|
+
requestRender(): void;
|
|
12
|
+
}, theme: MinimalTheme, _kb: unknown, done: (result: ConfirmResult) => void) => {
|
|
13
|
+
render: (width: number) => string[];
|
|
14
|
+
invalidate: () => void;
|
|
15
|
+
handleInput: (data: string) => void;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
package/dist/dialog.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { DynamicBorder, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, Key, Spacer, Text, matchesKey, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
3
|
+
const OPTIONS = [
|
|
4
|
+
{ label: "Allow once", result: "allow-once" },
|
|
5
|
+
{ label: "Allow this session", result: "allow-session" },
|
|
6
|
+
{ label: "Allow forever", result: "allow-forever" },
|
|
7
|
+
{ label: "Deny (tell pi what to do instead)", result: "deny" },
|
|
8
|
+
];
|
|
9
|
+
const COMMAND_VIEWPORT_LINES = 8;
|
|
10
|
+
export function createDialog(command, description) {
|
|
11
|
+
return (tui, theme, _kb, done) => {
|
|
12
|
+
const container = new Container();
|
|
13
|
+
const redBorder = (text) => theme.fg("error", text);
|
|
14
|
+
const dimBorder = (text) => theme.fg("dim", text);
|
|
15
|
+
let scrollOffset = 0;
|
|
16
|
+
let selectedIndex = 0;
|
|
17
|
+
const commandTopBorder = new Text("", 0, 0);
|
|
18
|
+
const commandText = new Text("", 1, 0);
|
|
19
|
+
const commandBottomBorder = new Text("", 0, 0);
|
|
20
|
+
const optionTexts = OPTIONS.map(() => new Text("", 1, 0));
|
|
21
|
+
container.addChild(new DynamicBorder(redBorder));
|
|
22
|
+
container.addChild(new Text(theme.fg("error", theme.bold("⚠ Dangerous Command Detected")), 1, 0));
|
|
23
|
+
container.addChild(new Spacer(1));
|
|
24
|
+
container.addChild(new Text(theme.fg("warning", `This command contains: ${description}`), 1, 0));
|
|
25
|
+
container.addChild(new Spacer(1));
|
|
26
|
+
container.addChild(commandTopBorder);
|
|
27
|
+
container.addChild(commandText);
|
|
28
|
+
container.addChild(commandBottomBorder);
|
|
29
|
+
container.addChild(new Spacer(1));
|
|
30
|
+
container.addChild(new Text(theme.fg("text", "What do you want to do?"), 1, 0));
|
|
31
|
+
container.addChild(new Spacer(1));
|
|
32
|
+
for (const optionText of optionTexts)
|
|
33
|
+
container.addChild(optionText);
|
|
34
|
+
container.addChild(new Spacer(1));
|
|
35
|
+
container.addChild(new Text(theme.fg("dim", "↑/↓: navigate enter: confirm j/k: scroll command esc: deny"), 1, 0));
|
|
36
|
+
container.addChild(new DynamicBorder(redBorder));
|
|
37
|
+
const getViewport = (contentWidth) => {
|
|
38
|
+
const rows = command.split("\n").flatMap((line, index) => {
|
|
39
|
+
const wrapped = wrapTextWithAnsi(theme.fg("text", line), Math.max(1, contentWidth));
|
|
40
|
+
return (wrapped.length > 0 ? wrapped : [""]).map((rendered) => ({
|
|
41
|
+
lineNum: index + 1,
|
|
42
|
+
rendered,
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
const pinned = rows.filter((row) => row.lineNum === 1);
|
|
46
|
+
const scrollable = rows.filter((row) => row.lineNum !== 1);
|
|
47
|
+
const windowSize = Math.max(0, COMMAND_VIEWPORT_LINES - pinned.length);
|
|
48
|
+
return {
|
|
49
|
+
pinned,
|
|
50
|
+
scrollable,
|
|
51
|
+
windowSize,
|
|
52
|
+
maxOffset: Math.max(0, scrollable.length - windowSize),
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
render: (width) => {
|
|
57
|
+
const contentWidth = Math.max(1, width - 4);
|
|
58
|
+
const { pinned, scrollable, windowSize, maxOffset } = getViewport(contentWidth);
|
|
59
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset, maxOffset));
|
|
60
|
+
const visibleScrollable = scrollable.slice(scrollOffset, scrollOffset + windowSize);
|
|
61
|
+
const visible = [...pinned, ...visibleScrollable];
|
|
62
|
+
const linesBelow = Math.max(0, scrollable.length - (scrollOffset + visibleScrollable.length));
|
|
63
|
+
const borderLine = (label) => {
|
|
64
|
+
const safeWidth = Math.max(1, width);
|
|
65
|
+
const truncated = label.length > safeWidth ? label.slice(0, safeWidth) : label;
|
|
66
|
+
return dimBorder("─".repeat(Math.max(0, safeWidth - truncated.length)) + truncated);
|
|
67
|
+
};
|
|
68
|
+
commandTopBorder.setText(borderLine(scrollOffset > 0 ? `↑ ${scrollOffset} more` : ""));
|
|
69
|
+
commandText.setText(visible.map((row) => row.rendered).join("\n"));
|
|
70
|
+
commandBottomBorder.setText(borderLine(linesBelow > 0 ? `↓ ${linesBelow} more` : ""));
|
|
71
|
+
for (const [index, optionText] of optionTexts.entries()) {
|
|
72
|
+
const label = `${index + 1} ${OPTIONS[index].label}`;
|
|
73
|
+
optionText.setText(index === selectedIndex
|
|
74
|
+
? `${theme.fg("accent", "❯")} ${theme.fg("text", theme.bold(label))}`
|
|
75
|
+
: theme.fg("dim", ` ${label}`));
|
|
76
|
+
}
|
|
77
|
+
return container.render(width);
|
|
78
|
+
},
|
|
79
|
+
invalidate: () => container.invalidate(),
|
|
80
|
+
handleInput: (data) => {
|
|
81
|
+
const contentWidth = Math.max(1, tui.terminal.columns - 4);
|
|
82
|
+
const { maxOffset } = getViewport(contentWidth);
|
|
83
|
+
if (matchesKey(data, Key.up)) {
|
|
84
|
+
selectedIndex = (selectedIndex - 1 + OPTIONS.length) % OPTIONS.length;
|
|
85
|
+
tui.requestRender();
|
|
86
|
+
}
|
|
87
|
+
else if (matchesKey(data, Key.down)) {
|
|
88
|
+
selectedIndex = (selectedIndex + 1) % OPTIONS.length;
|
|
89
|
+
tui.requestRender();
|
|
90
|
+
}
|
|
91
|
+
else if (data === "k") {
|
|
92
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
93
|
+
tui.requestRender();
|
|
94
|
+
}
|
|
95
|
+
else if (data === "j") {
|
|
96
|
+
scrollOffset = Math.min(maxOffset, scrollOffset + 1);
|
|
97
|
+
tui.requestRender();
|
|
98
|
+
}
|
|
99
|
+
else if (matchesKey(data, Key.enter)) {
|
|
100
|
+
done(OPTIONS[selectedIndex].result);
|
|
101
|
+
}
|
|
102
|
+
else if (data === "1") {
|
|
103
|
+
done("allow-once");
|
|
104
|
+
}
|
|
105
|
+
else if (data === "2") {
|
|
106
|
+
done("allow-session");
|
|
107
|
+
}
|
|
108
|
+
else if (data === "3") {
|
|
109
|
+
done("allow-forever");
|
|
110
|
+
}
|
|
111
|
+
else if (data === "4" || matchesKey(data, Key.escape) || data === "n" || data === "N") {
|
|
112
|
+
done("deny");
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.js","sourceRoot":"","sources":["../src/dialog.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createDialog } from "./dialog.js";
|
|
3
|
+
import { BUILTIN_PATTERNS, findPattern, readConfig, testPattern, writeExactAllowRule, } from "./config.js";
|
|
4
|
+
import { findAutoDenyMatch } from "./rules.js";
|
|
5
|
+
const permissionGate = (pi) => {
|
|
6
|
+
const sessionAllowedCommands = new Set();
|
|
7
|
+
let allowedPatterns = [];
|
|
8
|
+
let dangerPatterns = [];
|
|
9
|
+
let configError;
|
|
10
|
+
function reportPermissionGate(active, label) {
|
|
11
|
+
if (process.env.HERDR_ENV !== "1")
|
|
12
|
+
return;
|
|
13
|
+
pi.events.emit("herdr:blocked", { active, label });
|
|
14
|
+
}
|
|
15
|
+
function loadConfig() {
|
|
16
|
+
sessionAllowedCommands.clear();
|
|
17
|
+
const result = readConfig();
|
|
18
|
+
configError = result.error;
|
|
19
|
+
const config = result.error ? {} : result.config;
|
|
20
|
+
const applyDefaults = config.applyBuiltinDefaults !== false;
|
|
21
|
+
const customPatterns = config.permissionGate?.patterns ?? [];
|
|
22
|
+
dangerPatterns = applyDefaults ? [...customPatterns, ...BUILTIN_PATTERNS] : customPatterns;
|
|
23
|
+
allowedPatterns = config.permissionGate?.allowedPatterns ?? [];
|
|
24
|
+
}
|
|
25
|
+
pi.on("session_start", (_event, ctx) => {
|
|
26
|
+
loadConfig();
|
|
27
|
+
if (configError) {
|
|
28
|
+
ctx.ui.notify(`Could not read guardrails.json. Using built-in dangerous-command patterns. ${configError.message}`, "warning");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
32
|
+
if (!isToolCallEventType("bash", event))
|
|
33
|
+
return;
|
|
34
|
+
const command = event.input.command;
|
|
35
|
+
// Auto-deny checks must run first. Config allow rules cannot override them.
|
|
36
|
+
const autoDeny = findAutoDenyMatch(command);
|
|
37
|
+
if (autoDeny) {
|
|
38
|
+
ctx.ui.notify(`Auto-blocked: ${autoDeny.pattern} — ${autoDeny.reason}`, "error");
|
|
39
|
+
return { block: true, reason: `Catastrophic command auto-blocked: ${autoDeny.reason}` };
|
|
40
|
+
}
|
|
41
|
+
if (sessionAllowedCommands.has(command))
|
|
42
|
+
return;
|
|
43
|
+
if (allowedPatterns.some((pattern) => testPattern(pattern, command)))
|
|
44
|
+
return;
|
|
45
|
+
const match = findPattern(command, dangerPatterns);
|
|
46
|
+
if (!match)
|
|
47
|
+
return;
|
|
48
|
+
const description = match.description ?? match.pattern;
|
|
49
|
+
if (!ctx.hasUI) {
|
|
50
|
+
return {
|
|
51
|
+
block: true,
|
|
52
|
+
reason: `Dangerous command blocked (no UI to confirm): ${description}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
reportPermissionGate(true, `Permission gate: ${description}`);
|
|
56
|
+
try {
|
|
57
|
+
let result = await ctx.ui.custom(createDialog(command, description));
|
|
58
|
+
// RPC and headless UI contexts can return undefined for custom dialogs.
|
|
59
|
+
if (result === undefined) {
|
|
60
|
+
const selection = await ctx.ui.select(`Dangerous command: ${description}`, [
|
|
61
|
+
"1 - Allow once",
|
|
62
|
+
"2 - Allow this session",
|
|
63
|
+
"3 - Allow forever",
|
|
64
|
+
"4 - Deny (tell pi what to do instead)",
|
|
65
|
+
]);
|
|
66
|
+
if (selection?.startsWith("1"))
|
|
67
|
+
result = "allow-once";
|
|
68
|
+
else if (selection?.startsWith("2"))
|
|
69
|
+
result = "allow-session";
|
|
70
|
+
else if (selection?.startsWith("3"))
|
|
71
|
+
result = "allow-forever";
|
|
72
|
+
else
|
|
73
|
+
result = "deny";
|
|
74
|
+
}
|
|
75
|
+
if (result === "allow-once")
|
|
76
|
+
return;
|
|
77
|
+
if (result === "allow-session") {
|
|
78
|
+
sessionAllowedCommands.add(command);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (result === "allow-forever") {
|
|
82
|
+
try {
|
|
83
|
+
const rule = writeExactAllowRule(command);
|
|
84
|
+
if (!allowedPatterns.some((item) => item.pattern === rule.pattern && item.regex === true)) {
|
|
85
|
+
allowedPatterns.push(rule);
|
|
86
|
+
}
|
|
87
|
+
ctx.ui.notify(`Added an exact command to the global allowlist: ${command.slice(0, 60)}${command.length > 60 ? "…" : ""}`, "info");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
ctx.ui.notify(`Could not save the allow rule. Command remains blocked. ${message}`, "error");
|
|
93
|
+
return { block: true, reason: "Could not save the permanent allow rule" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const instruction = await ctx.ui.input("Tell pi what to do instead:", "");
|
|
97
|
+
if (instruction?.trim()) {
|
|
98
|
+
pi.sendUserMessage(`The command \`${command}\` was blocked. Please do the following instead: ${instruction.trim()}`, { deliverAs: "steer" });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
ctx.ui.notify("Command blocked.", "error");
|
|
102
|
+
}
|
|
103
|
+
return { block: true, reason: "User denied dangerous command" };
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
reportPermissionGate(false);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
export default permissionGate;
|
|
111
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/rules.d.ts
ADDED
package/dist/rules.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const AUTO_DENY_PATTERNS = [
|
|
2
|
+
{ pattern: "az group delete", reason: "deletes an entire Azure resource group and all its resources" },
|
|
3
|
+
{ pattern: "az subscription cancel", reason: "cancels the entire Azure subscription" },
|
|
4
|
+
{ pattern: "az account management-group delete", reason: "deletes Azure management group hierarchy" },
|
|
5
|
+
{ pattern: "az ad user delete", reason: "deletes an Azure AD user" },
|
|
6
|
+
{ pattern: "az ad group delete", reason: "deletes an Azure AD security group" },
|
|
7
|
+
{ pattern: "az lock delete", reason: "removes an Azure resource lock (enables further destruction)" },
|
|
8
|
+
{ pattern: "az keyvault purge", reason: "permanently purges a Key Vault, bypassing soft-delete" },
|
|
9
|
+
{ pattern: "kubectl delete namespace", reason: "deletes a Kubernetes namespace and all resources inside" },
|
|
10
|
+
{ pattern: "kubectl delete --all", reason: "deletes all Kubernetes resources of a type" },
|
|
11
|
+
{ pattern: "kubectl drain", reason: "evicts all pods from a Kubernetes node" },
|
|
12
|
+
];
|
|
13
|
+
export function findAutoDenyMatch(command) {
|
|
14
|
+
return AUTO_DENY_PATTERNS.find((rule) => command.includes(rule.pattern));
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,8DAA8D,EAAE;IACtG,EAAE,OAAO,EAAE,wBAAwB,EAAE,MAAM,EAAE,uCAAuC,EAAE;IACtF,EAAE,OAAO,EAAE,oCAAoC,EAAE,MAAM,EAAE,0CAA0C,EAAE;IACrG,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,0BAA0B,EAAE;IACpE,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,oCAAoC,EAAE;IAC/E,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,8DAA8D,EAAE;IACrG,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,uDAAuD,EAAE;IACjG,EAAE,OAAO,EAAE,0BAA0B,EAAE,MAAM,EAAE,yDAAyD,EAAE;IAC1G,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,4CAA4C,EAAE;IACzF,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,wCAAwC,EAAE;CAC/E,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;AAC3E,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@manuelstolze/pi-permission-gate",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pi extension that asks before running dangerous bash commands.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi-package",
|
|
12
|
+
"permissions",
|
|
13
|
+
"guardrails"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./dist/index.js"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -b",
|
|
28
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1 <1",
|
|
32
|
+
"@earendil-works/pi-tui": ">=0.85.1 <1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
36
|
+
"@earendil-works/pi-tui": "^0.85.1"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/manuelstolze/pi-integrations",
|
|
41
|
+
"directory": "packages/permission-gate"
|
|
42
|
+
}
|
|
43
|
+
}
|