@aliou/pi-guardrails 0.14.0 → 0.15.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 CHANGED
@@ -64,7 +64,7 @@ Granted paths are stored in `pathAccess.allowedPaths` as explicit `{ kind, path
64
64
 
65
65
  The `permission-gate` extension detects dangerous bash commands before they run.
66
66
 
67
- It catches built-in risky patterns like recursive deletes, privileged commands, disk formatting, broad permission changes, and configured custom patterns. You can allow once, allow for the session, deny, or configure auto-deny rules.
67
+ It catches built-in risky patterns like recursive deletes, privileged commands, disk formatting, broad permission changes, and configured custom patterns. You can allow once, allow for the session, deny, decline and stop (which also aborts the current turn), or configure auto-deny rules.
68
68
 
69
69
  [![Guardrails permission gate walkthrough](https://assets.aliou.me/github/aliou/pi-guardrails/v0.12.0/permission-gate.gif)](https://assets.aliou.me/github/aliou/pi-guardrails/v0.12.0/permission-gate.mp4)
70
70
 
@@ -89,7 +89,7 @@ export default async function permissionGate(pi: ExtensionAPI) {
89
89
  return { block: true, reason };
90
90
  }
91
91
 
92
- type ConfirmResult = "allow" | "allow-session" | "deny";
92
+ type ConfirmResult = "allow" | "allow-session" | "deny" | "stop";
93
93
  emitActionPrompted(pi, {
94
94
  feature: "permissionGate",
95
95
  action: safety.action,
@@ -108,10 +108,11 @@ export default async function permissionGate(pi: ExtensionAPI) {
108
108
  if (result === undefined) {
109
109
  const selection = await ctx.ui.select(
110
110
  `Dangerous command: ${safety.reason}`,
111
- ["Allow once", "Allow for session", "Deny"],
111
+ ["Allow once", "Allow for session", "Deny", "Decline and stop"],
112
112
  );
113
113
  if (selection === "Allow once") result = "allow";
114
114
  else if (selection === "Allow for session") result = "allow-session";
115
+ else if (selection === "Decline and stop") result = "stop";
115
116
  else result = "deny";
116
117
  }
117
118
 
@@ -121,6 +122,19 @@ export default async function permissionGate(pi: ExtensionAPI) {
121
122
  return;
122
123
  }
123
124
 
125
+ if (result === "stop") {
126
+ const reason = "User declined and stopped dangerous command";
127
+ emitActionBlocked(pi, {
128
+ feature: "permissionGate",
129
+ action: safety.action,
130
+ reason,
131
+ block: { source: "user-stop", metadata: safety.metadata },
132
+ context: { toolName: "bash", input: event.input },
133
+ });
134
+ ctx.abort();
135
+ return { block: true, reason };
136
+ }
137
+
124
138
  const reason = "User denied dangerous command";
125
139
  emitActionBlocked(pi, {
126
140
  feature: "permissionGate",
@@ -103,7 +103,7 @@ export function createPermissionGateConfirmComponent(
103
103
  tui: { terminal: { rows: number; columns: number }; requestRender(): void },
104
104
  theme: MinimalTheme,
105
105
  _kb: unknown,
106
- done: (result: "allow" | "allow-session" | "deny") => void,
106
+ done: (result: "allow" | "allow-session" | "deny" | "stop") => void,
107
107
  ) => {
108
108
  const container = new Container();
109
109
  const redBorder = (s: string) => theme.fg("error", s);
@@ -140,7 +140,7 @@ export function createPermissionGateConfirmComponent(
140
140
  new Text(
141
141
  theme.fg(
142
142
  "dim",
143
- "↑/↓ or j/k: scroll • y/enter: allow • a: session • n/esc: deny",
143
+ "↑/↓ or j/k: scroll • y/enter: allow • a: session • n/esc: deny • s: decline & stop",
144
144
  ),
145
145
  1,
146
146
  0,
@@ -215,6 +215,8 @@ export function createPermissionGateConfirmComponent(
215
215
  data === "N"
216
216
  ) {
217
217
  done("deny");
218
+ } else if (data === "s" || data === "S") {
219
+ done("stop");
218
220
  }
219
221
  },
220
222
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-guardrails",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -5,7 +5,12 @@ export function shouldRun(config: GuardrailsConfig): boolean {
5
5
  const raw = config as Record<string, unknown>;
6
6
  const pathAccess = raw.pathAccess as Record<string, unknown> | undefined;
7
7
  if (!Array.isArray(pathAccess?.allowedPaths)) return false;
8
- return pathAccess.allowedPaths.some((item) => typeof item !== "string");
8
+ return pathAccess.allowedPaths.some(
9
+ (item) =>
10
+ typeof item === "object" &&
11
+ item !== null &&
12
+ typeof (item as Record<string, unknown>).pattern === "string",
13
+ );
9
14
  }
10
15
 
11
16
  export function run(config: GuardrailsConfig): GuardrailsConfig {
@@ -27,8 +32,11 @@ function normalizeAllowedPaths(items: unknown): string[] {
27
32
  if (typeof item === "string") {
28
33
  path = item;
29
34
  } else if (typeof item === "object" && item !== null) {
30
- const pattern = (item as Record<string, unknown>).pattern;
35
+ const obj = item as Record<string, unknown>;
36
+ const pattern = obj.pattern;
37
+ const objectPath = obj.path;
31
38
  if (typeof pattern === "string") path = pattern;
39
+ else if (typeof objectPath === "string") path = objectPath;
32
40
  }
33
41
 
34
42
  const normalized = path?.trim();
@@ -32,6 +32,7 @@ export type GuardrailsBlockSource =
32
32
  | "policy"
33
33
  | "permission"
34
34
  | "user"
35
+ | "user-stop"
35
36
  | "nonInteractive";
36
37
 
37
38
  export type GuardrailsActionBlockedPayload<TMeta = unknown> =