@xynogen/pix-gate 0.3.0 → 0.3.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +20 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-gate",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",
package/src/index.ts CHANGED
@@ -137,10 +137,15 @@ export default function (pi: ExtensionAPI): void {
137
137
  risky: 1,
138
138
  };
139
139
 
140
+ // The unified command gate also covers pix-proc's `proc` tool: its `start`
141
+ // action carries a shell command in `event.input.command`, identical to bash.
142
+ // proc's other actions (list/logs/stop/rm) carry no command and pass straight
143
+ // through. Install pix-proc without pix-gate = ungated, same as bash.
144
+ const GATED_COMMAND_TOOLS = new Set(["bash", "proc"]);
140
145
  pi.on("tool_call", async (event, ctx) => {
141
- if (event.toolName !== "bash") return undefined;
146
+ if (!GATED_COMMAND_TOOLS.has(String(event.toolName))) return undefined;
142
147
 
143
- const command = String(event.input.command ?? "");
148
+ const command = String((event.input as Record<string, unknown>).command ?? "");
144
149
  if (!command.trim()) return undefined;
145
150
 
146
151
  // Collect all concerns: path hits + command hit
@@ -148,6 +153,16 @@ export default function (pi: ExtensionAPI): void {
148
153
 
149
154
  // Path concerns
150
155
  const candidates = extractPathsFromBash(command);
156
+
157
+ // Steer a blocked ssh-config read toward the dedicated `ssh info` tool
158
+ // (ssh_run action:"info"), which reads the effective config without touching
159
+ // ~/.ssh directly. Only when ssh_run is installed. Appended to the block
160
+ // reason so the model reads it and stops retrying with awk/grep/raw ssh.
161
+ const readsSshConfig = candidates.some((p) => /(^|\/)\.ssh\/config\b/i.test(p));
162
+ const sshInfoSteer =
163
+ readsSshConfig && isRegistered("ssh_run")
164
+ ? ' Use the ssh_run tool with action:"info" to read the effective SSH config instead of reading ~/.ssh/config in bash.'
165
+ : "";
151
166
  for (const p of candidates) {
152
167
  const hit = classifyPath(p, "read", pathRules);
153
168
  if (!hit) continue;
@@ -208,7 +223,7 @@ export default function (pi: ExtensionAPI): void {
208
223
  if (unattended === "deny") {
209
224
  return {
210
225
  block: true,
211
- reason: `[AFK][${highest.label}] ${highest.detail}`,
226
+ reason: `[AFK][${highest.label}] ${highest.detail}${sshInfoSteer}`,
212
227
  };
213
228
  }
214
229
 
@@ -219,7 +234,7 @@ export default function (pi: ExtensionAPI): void {
219
234
  if (highest.tier >= SEVERITY_TIER.block) {
220
235
  return {
221
236
  block: true,
222
- reason: `[${highest.label}] ${highest.detail} (no UI, auto-blocked)`,
237
+ reason: `[${highest.label}] ${highest.detail} (no UI, auto-blocked)${sshInfoSteer}`,
223
238
  };
224
239
  }
225
240
  return undefined;
@@ -260,7 +275,7 @@ export default function (pi: ExtensionAPI): void {
260
275
 
261
276
  if (!decision.approved) {
262
277
  ctx.ui.notify(`${highest.icon} ${decision.reason}: ${highest.detail}`, "warning");
263
- return { block: true, reason: `[${highest.label}] ${decision.reason}` };
278
+ return { block: true, reason: `[${highest.label}] ${decision.reason}${sshInfoSteer}` };
264
279
  }
265
280
 
266
281
  const severityColor =