@xynogen/pix-gate 0.2.8 → 0.2.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-gate",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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,8 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@xynogen/pix-pretty": "^1.11.2",
46
- "@xynogen/pix-data": "^0.4.3",
47
- "@xynogen/pix-runtime": "^0.6.0"
46
+ "@xynogen/pix-runtime": "^0.8.0"
48
47
  },
49
48
  "peerDependencies": {
50
49
  "@earendil-works/pi-coding-agent": "*",
package/src/gate.test.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { createEventBus } from "@earendil-works/pi-coding-agent";
3
+ import { setUnattendedMode } from "@xynogen/pix-runtime";
2
4
  import {
3
5
  buildRules,
4
6
  classify,
7
+ classifyPath,
8
+ DEFAULT_PATH_RULES,
5
9
  DEFAULT_RULES,
6
10
  extractPathsFromBash,
7
11
  isCircuitBreaker,
@@ -9,6 +13,23 @@ import {
9
13
  unattendedGateDecision,
10
14
  } from "./lib.ts";
11
15
 
16
+ // ── .env path gating ─────────────────────────────────────────────
17
+ describe("classifyPath .env", () => {
18
+ const blocked = [".env", ".env.local", ".env.production", "path/to/.env", "src/.env.staging"];
19
+ const allowed = [".env.example", ".env.sample", ".env.template", ".env.dist"];
20
+
21
+ for (const p of blocked) {
22
+ test(`reading ${p} is blocked`, () => {
23
+ expect(classifyPath(p, "read", DEFAULT_PATH_RULES)?.severity).toBe("block");
24
+ });
25
+ }
26
+ for (const p of allowed) {
27
+ test(`reading ${p} is not blocked`, () => {
28
+ expect(classifyPath(p, "read", DEFAULT_PATH_RULES)).toBeUndefined();
29
+ });
30
+ }
31
+ });
32
+
12
33
  // ── isSudoCommand ─────────────────────────────────────────────────────────────
13
34
 
14
35
  describe("isSudoCommand", () => {
@@ -47,43 +68,26 @@ describe("isSudoCommand", () => {
47
68
  // ── AFK behavior ──────────────────────────────────────────────────────────────
48
69
 
49
70
  describe("unattendedGateDecision", () => {
50
- type G = { __pixAfk?: boolean; __pixYolo?: boolean };
51
- const reset = () => {
52
- delete (globalThis as G).__pixAfk;
53
- delete (globalThis as G).__pixYolo;
54
- };
55
-
56
71
  test("does nothing while every mode is off", () => {
57
- reset();
58
- expect(unattendedGateDecision(5)).toBeUndefined();
72
+ expect(unattendedGateDecision(createEventBus(), 5)).toBeUndefined();
59
73
  });
60
74
 
61
75
  test("AFK allows yellow concerns and denies red concerns", () => {
62
- reset();
63
- (globalThis as G).__pixAfk = true;
64
- expect(unattendedGateDecision(1)).toBe("allow");
65
- expect(unattendedGateDecision(2)).toBe("allow");
66
- expect(unattendedGateDecision(3)).toBe("allow");
67
- expect(unattendedGateDecision(4)).toBe("deny");
68
- expect(unattendedGateDecision(5)).toBe("deny");
69
- reset();
76
+ const events = createEventBus();
77
+ setUnattendedMode(events, "afk");
78
+ expect(unattendedGateDecision(events, 1)).toBe("allow");
79
+ expect(unattendedGateDecision(events, 2)).toBe("allow");
80
+ expect(unattendedGateDecision(events, 3)).toBe("allow");
81
+ expect(unattendedGateDecision(events, 4)).toBe("deny");
82
+ expect(unattendedGateDecision(events, 5)).toBe("deny");
70
83
  });
71
84
 
72
85
  test("YOLO allows every tier including red/critical", () => {
73
- reset();
74
- (globalThis as G).__pixYolo = true;
75
- expect(unattendedGateDecision(1)).toBe("allow");
76
- expect(unattendedGateDecision(4)).toBe("allow");
77
- expect(unattendedGateDecision(5)).toBe("allow");
78
- reset();
79
- });
80
-
81
- test("YOLO wins when both flags are somehow set", () => {
82
- reset();
83
- (globalThis as G).__pixAfk = true;
84
- (globalThis as G).__pixYolo = true;
85
- expect(unattendedGateDecision(5)).toBe("allow");
86
- reset();
86
+ const events = createEventBus();
87
+ setUnattendedMode(events, "yolo");
88
+ expect(unattendedGateDecision(events, 1)).toBe("allow");
89
+ expect(unattendedGateDecision(events, 4)).toBe("allow");
90
+ expect(unattendedGateDecision(events, 5)).toBe("allow");
87
91
  });
88
92
  });
89
93
 
package/src/index.ts CHANGED
@@ -72,7 +72,7 @@ export default function (pi: ExtensionAPI): void {
72
72
  return undefined;
73
73
  }
74
74
 
75
- const unattended = unattendedGateDecision(hit.severity === "block" ? 4 : 2);
75
+ const unattended = unattendedGateDecision(pi.events, hit.severity === "block" ? 4 : 2);
76
76
  if (unattended === "allow") return undefined;
77
77
  if (unattended === "deny") {
78
78
  return {
@@ -160,7 +160,7 @@ export default function (pi: ExtensionAPI): void {
160
160
  // sudo: hard redirect — no prompt, no bypass. Even YOLO cannot run bare
161
161
  // `sudo` in bash (the password can't be auto-typed); it must use sudo_run.
162
162
  if (isSudoCommand(command)) {
163
- if (unattendedGateDecision(highest.tier) === "deny") {
163
+ if (unattendedGateDecision(pi.events, highest.tier) === "deny") {
164
164
  return { block: true, reason: "[AFK] sudo is denied while user is away." };
165
165
  }
166
166
  ctx.ui.notify(
@@ -176,13 +176,13 @@ export default function (pi: ExtensionAPI): void {
176
176
  // Circuit breaker: catastrophic commands never auto-approve, even under YOLO.
177
177
  // They fall through to the interactive dialog (or the no-UI block below).
178
178
  const breaker = isCircuitBreaker(command);
179
- if (breaker && unattendedGateDecision(highest.tier) === "allow") {
179
+ if (breaker && unattendedGateDecision(pi.events, highest.tier) === "allow") {
180
180
  ctx.ui.notify(
181
181
  `⛔ ${ctx.ui.theme.fg("error", "CIRCUIT BREAKER")} — refused even under YOLO; confirm manually`,
182
182
  "error",
183
183
  );
184
184
  }
185
- const unattended = breaker ? undefined : unattendedGateDecision(highest.tier);
185
+ const unattended = breaker ? undefined : unattendedGateDecision(pi.events, highest.tier);
186
186
  if (unattended === "allow") return undefined;
187
187
  if (unattended === "deny") {
188
188
  return {
package/src/lib.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  * Pure helpers for pix-gate — no Pi API deps, fully unit-testable.
3
3
  */
4
4
 
5
+ import type { EventBus } from "@earendil-works/pi-coding-agent";
6
+ import { getUnattendedMode } from "@xynogen/pix-runtime";
5
7
  import { config } from "@xynogen/pix-runtime/config";
6
8
  import { type GateRuleConfig, gateSection } from "@xynogen/pix-runtime/sections";
7
9
 
@@ -222,9 +224,15 @@ export const DEFAULT_PATH_RULES: PathRule[] = [
222
224
  severity: "block",
223
225
  reason: "credentials file",
224
226
  },
227
+ // Real .env files hold live secrets — block. Placeholder variants
228
+ // (.env.example/.sample/.template/.dist) carry no secrets, so exclude them.
229
+ {
230
+ pattern: /(^|\/)\.env(?:$|\.(?!example|sample|template|dist)[A-Za-z0-9_-]+)/i,
231
+ severity: "block",
232
+ reason: ".env file (live secrets)",
233
+ },
225
234
 
226
235
  // warn — yellow allow-first
227
- { pattern: /(^|\/)\.env(\.|$)/i, severity: "warn", reason: ".env file" },
228
236
  { pattern: /(^|\/)\.envrc$/i, severity: "warn", reason: "direnv file" },
229
237
  {
230
238
  pattern: /(^|\/)\.npmrc$/i,
@@ -312,11 +320,13 @@ export function isCircuitBreaker(command: string): boolean {
312
320
  * yolo — auto-allow every tier, including red/critical.
313
321
  * afk — auto-allow yellow (tier < 4), auto-deny red (tier >= 4).
314
322
  * off — undefined (fall through to the interactive dialog).
315
- * Mutually exclusive globals set by pix-commands' /afk and /yolo.
316
323
  */
317
- export function unattendedGateDecision(tier: number): "allow" | "deny" | undefined {
318
- const g = globalThis as { __pixAfk?: boolean; __pixYolo?: boolean };
319
- if (g.__pixYolo === true) return "allow";
320
- if (g.__pixAfk === true) return tier >= 4 ? "deny" : "allow";
324
+ export function unattendedGateDecision(
325
+ events: EventBus,
326
+ tier: number,
327
+ ): "allow" | "deny" | undefined {
328
+ const mode = getUnattendedMode(events);
329
+ if (mode === "yolo") return "allow";
330
+ if (mode === "afk") return tier >= 4 ? "deny" : "allow";
321
331
  return undefined;
322
332
  }