auto-permission-system 3.12.0 → 3.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.
@@ -34,8 +34,8 @@ import { onYolo } from "../src/shared/yolo";
34
34
  const detectionGrants = new Set<string>();
35
35
 
36
36
  function detectionBlock(action: "read" | "tool", why: string): string {
37
- if (action === "read") return `Read blocked by policy: ${why}. Nothing was read — ask the user.`;
38
- return `Tool call blocked by policy: ${why}. Nothing was run — ask the user.`;
37
+ if (action === "read") return `Read blocked by policy: ${why}. Nothing was read — ask the user to adjust the permission mode (/permission-mode) if needed. Do not attempt workarounds or inspect plugin files.`;
38
+ return `Tool call blocked by policy: ${why}. Nothing was run — ask the user to adjust the permission mode (/permission-mode) if needed. Do not attempt workarounds or inspect plugin files.`;
39
39
  }
40
40
 
41
41
  /** Maps each in-flight tool call to the program id we'll show in the
@@ -19,37 +19,32 @@ import { emitYolo, onYolo, YOLO_STATUS } from "../src/shared/yolo";
19
19
 
20
20
  const GLOBAL_POLICY = `${getAgentDir()}/extensions/sandbox.json`;
21
21
 
22
- const MODES: Array<{ label: string; value: PermissionMode }> = [
22
+ const MODE_DEFS: Array<{
23
+ value: PermissionMode;
24
+ icon: string;
25
+ name: string;
26
+ desc: string;
27
+ }> = [
23
28
  {
24
- label: "Advanced Secure (recommended) — detect secrets in reads, MCP calls, and output",
25
29
  value: "advanced-secure",
30
+ icon: "🧠",
31
+ name: "Advanced Secure",
32
+ desc: "Blocks sensitive data leaks from file reads, MCP tools & command outputs",
26
33
  },
27
34
  {
28
- label: "Default — rules only",
29
35
  value: "default",
36
+ icon: "🔒",
37
+ name: "Default",
38
+ desc: "Restricts file paths & network access at the sandbox level",
30
39
  },
31
40
  {
32
- label: "YOLO — disable ALL security layers",
33
41
  value: "yolo",
42
+ icon: "⚠️",
43
+ name: "YOLO",
44
+ desc: "Turns off all file sandboxing and leak protections",
34
45
  },
35
46
  ];
36
47
 
37
- const EXPLANATION = [
38
- "Advanced Secure (recommended)",
39
- " Adds secret/credential detection on top of the rules:",
40
- " • File reads — scans contents for keys, tokens, and passwords",
41
- " • MCP calls — flags risky tool calls before they run",
42
- " • Tool/command/file output — redacts secrets before they reach the model",
43
- "",
44
- "Default",
45
- " Rule-based only: sandbox.json paths, domains, and commands.",
46
- " No file contents are inspected.",
47
- "",
48
- "YOLO",
49
- " Turns every layer off. Saved like the other modes, so new conversations",
50
- " start with it too until you pick Default or Advanced Secure again.",
51
- ].join("\n");
52
-
53
48
  /**
54
49
  * The single sandbox footer chip: `Sandbox: <icon> N domains, M paths`.
55
50
  * `🧠` marks advanced-secure, `🔒` default; YOLO replaces it with the warning.
@@ -79,13 +74,23 @@ function modeNotice(mode: PermissionMode): { text: string; level: "info" | "warn
79
74
 
80
75
  async function pickMode(pi: ExtensionAPI, ctx: ExtensionContext, yolo: { active: boolean }): Promise<void> {
81
76
  const current: PermissionMode = yolo.active ? "yolo" : normalizeMode(loadPolicy(ctx.cwd).mode);
77
+
78
+ const options = MODE_DEFS.map((m) => {
79
+ const isActive = m.value === current;
80
+ const nameWithStatus = isActive ? `${m.name} (active)` : m.name;
81
+ const labelCol = nameWithStatus.padEnd(26);
82
+ return `${m.icon} ${labelCol} — ${m.desc}`;
83
+ });
84
+
82
85
  const picked = await ctx.ui.select(
83
- `Permission mode — current: ${modeLabel(current)}\n\n${EXPLANATION}\n\nEsc keeps the current mode.`,
84
- MODES.map((m) => m.label),
86
+ "Select permission mode:",
87
+ options,
85
88
  { timeout: 120_000 },
86
89
  );
87
90
  if (picked === undefined) return;
88
- const next = MODES.find((m) => m.label === picked)?.value ?? current;
91
+
92
+ const pickedIndex = options.indexOf(picked);
93
+ const next = pickedIndex >= 0 ? MODE_DEFS[pickedIndex].value : current;
89
94
  if (next === current) {
90
95
  if (next === "yolo" && !yolo.active) {
91
96
  yolo.active = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-permission-system",
3
- "version": "3.12.0",
3
+ "version": "3.15.0",
4
4
  "description": "Claude auto mode for Pi & security for Pi. A zero-trust permission system for Pi coding agent powered by Advanced Secure mode to scan files, content, and tools to prevent data breaches.",
5
5
  "type": "module",
6
6
  "author": "Navin Kumar",
@@ -3,6 +3,7 @@
3
3
  "mode": "advanced-secure",
4
4
  "network": {
5
5
  "allowLocalhost": true,
6
+ "allowAllUnixSockets": true,
6
7
  "allowedDomains": [
7
8
  "registry.npmjs.org",
8
9
  "registry.yarnpkg.com",
@@ -1,12 +1,21 @@
1
1
  ---
2
2
  name: auto-permission-system
3
- description: Use this skill when the user asks about auto-permission-system, security for Pi, permission system for Pi, how to secure Pi, Claude auto mode for Pi, security policy configuration, why a tool call was blocked, how to add an allow-list entry, or how to configure sandbox behavior. Covers the two-layer sandbox (OS-level bash sandbox and in-process tool guard), policy file locations and fields, Claude auto mode preset (sandbox.claude.json), ask-tier prompt options, absolute-deny paths, escape hatches (--yolo, --no-sandbox, /permission-mode → YOLO), the audit log, and common tasks like allowing a new domain or write path.
3
+ description: Use this skill ONLY when the user explicitly asks about auto-permission-system, security for Pi, configuring security policies, or permission modes. Do NOT invoke this skill automatically when an action or tool is blocked during regular coding tasks.
4
4
  ---
5
5
 
6
6
  # Security for Pi (Pi Permission System) — Skill
7
7
 
8
8
  Use this skill when the user asks about the auto-permission-system extension, security for Pi, permission system for Pi, how to secure Pi, or configuring Claude auto mode for Pi.
9
9
 
10
+ ## Handling Blocked Operations (Agent Behavior Guideline)
11
+
12
+ When a command, file operation, or tool call is blocked by the security policy:
13
+ - **Do not inspect plugin internals**: Do NOT read files inside `auto-permission-system`, do NOT inspect sandbox runtime code, and do NOT attempt to find bypasses or workarounds.
14
+ - **Explain clearly**: Calmly and concisely inform the user what specific action was restricted and why.
15
+ - **Ask to change permission mode**: Ask the user if they wish to adjust the permission mode using `/permission-mode` to allow the action.
16
+ - **Wait for user input**: Do not proceed with speculative workarounds or retry loops until the user responds or updates the mode.
17
+
18
+
10
19
  ## What this extension does
11
20
 
12
21
  **Layer 1 — Bash sandbox (OS-level)**
@@ -57,13 +57,11 @@ export interface LoopbackEndpointInfo {
57
57
  export function extractLoopbackEndpoint(output: string, command?: string): LoopbackEndpointInfo | undefined {
58
58
  if (!isLoopbackConnectionRefused(output)) return undefined;
59
59
 
60
- // Check for Diffusion Studio specific error or command
61
60
  const isDiffusion = /diffusion studio is not running/i.test(output) || (command && /\b(?:dapi|diffusion)\b/i.test(command));
62
61
  if (isDiffusion) {
63
62
  return { host: "127.0.0.1", port: "3274", service: "Diffusion Studio" };
64
63
  }
65
64
 
66
- // Match "Failed to connect to 127.0.0.1 port 3274" or "localhost port 8080"
67
65
  const curlMatch = output.match(/Failed to connect to (127\.0\.0\.1|localhost|::1) port (\d+)/i);
68
66
  if (curlMatch) {
69
67
  return {
@@ -73,7 +71,6 @@ export function extractLoopbackEndpoint(output: string, command?: string): Loopb
73
71
  };
74
72
  }
75
73
 
76
- // Match "connect ECONNREFUSED 127.0.0.1:3274"
77
74
  const nodeMatch = output.match(/(?:ECONNREFUSED|connect to)\s+(127\.0\.0\.1|localhost|::1)(?::| port )(\d+)/i);
78
75
  if (nodeMatch) {
79
76
  return {
@@ -83,7 +80,6 @@ export function extractLoopbackEndpoint(output: string, command?: string): Loopb
83
80
  };
84
81
  }
85
82
 
86
- // Generic host match
87
83
  const hostMatch = output.match(/(127\.0\.0\.1|localhost|::1)/i);
88
84
  return { host: hostMatch ? hostMatch[1] : "127.0.0.1" };
89
85
  }
@@ -30,7 +30,7 @@ export interface SandboxedBashOpts {
30
30
 
31
31
  /** One denial shape for Layer 1, mirroring denyMessage() in Layer 2. */
32
32
  function blockedLine(action: "Read" | "Write", why: string, outcome: "read" | "written" | "run"): string {
33
- return `❌ pi-sandbox: ${action} blocked by policy: ${why}. Nothing was ${outcome} — ask the user.`;
33
+ return `🛡️ pi-sandbox: ${action} blocked by policy: ${why}. Nothing was ${outcome}. Assistant guideline: Inform the user and ask if they wish to adjust the permission mode (/permission-mode); do not attempt workarounds or inspect plugin internals.`;
34
34
  }
35
35
 
36
36
  /** `Link` body row for a symlinked block path; empty when the spellings agree (ADR-035). */
@@ -327,32 +327,21 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
327
327
  }
328
328
  readDenied = hardDenied || outsideDenied;
329
329
  }
330
- const configDirHint = offending && /\.config\/|\.kube\/|\.docker\/|\.netrc|\.aws\/|\.npmrc|\.gitconfig/.test(offending);
331
-
332
- blockedHint = `\n💡 pi-sandbox: filesystem access blocked.\n`;
330
+ blockedHint = `\n🛡️ Security Policy Notice: filesystem access blocked by sandbox policy.\n`;
333
331
  if (offending) {
334
332
  let why = "";
335
- if (writeDenied && !hardDenied) why = " (not under an allowWrite root: writes outside it are read-only)";
333
+ if (writeDenied && !hardDenied) why = " (writes outside allowWrite are read-only)";
336
334
  else if (hardDenied) why = ` (${hardWhy})`;
337
- else if (outsideDenied) why = " (outside the project: reading it is gated by policy)";
338
- blockedHint += ` Path: ${offending}${why}\n`;
339
- if (spelledOffending && spelledOffending !== offending) blockedHint += ` Link: ${spelledOffending} (symlink target shown as Path)\n`;
340
- }
341
- blockedHint += ` This is the pi sandbox (Layer 1), NOT macOS Full Disk Access / TCC.\n`;
342
- if (configDirHint) {
343
- blockedHint +=
344
- ` Looks like a tool's own config dir. To allow this tool in the\n` +
345
- ` current project, add a project-local policy:\n` +
346
- ` mkdir -p ${cwd}/.pi && cat > ${cwd}/.pi/sandbox.json <<'JSON'\n` +
347
- ` { "filesystem": { "allowWrite": [".", "${offending?.replace(/^~/, "$HOME") ?? "~/.config/<tool>"}"] } }\n` +
348
- ` JSON\n` +
349
- ` Or run pi with --yolo for one-off elevated access (disables ALL layers).\n`;
350
- } else {
351
- blockedHint +=
352
- ` Use $TMPDIR (= ${piTmp}/) for scratch files,\n` +
353
- ` or write inside the project directory (${cwd}).\n`;
335
+ else if (outsideDenied) why = " (outside the project directory)";
336
+ blockedHint += ` Target: ${offending}${why}\n`;
337
+ if (spelledOffending && spelledOffending !== offending) blockedHint += ` Link: ${spelledOffending}\n`;
354
338
  }
355
- blockedHint += ` Policy: ~/.pi/agent/extensions/sandbox.json (+ <cwd>/.pi/sandbox.json overrides).\n`;
339
+ blockedHint +=
340
+ ` Explanation: The current sandbox policy restricts this operation to protect your system.\n` +
341
+ ` Guideline for Assistant:\n` +
342
+ ` • Stop and inform the user clearly about what was blocked.\n` +
343
+ ` • Ask the user if they wish to adjust the permission mode (/permission-mode) to allow this action.\n` +
344
+ ` • Do not inspect security plugin internals or attempt unauthorized bypasses.\n`;
356
345
  } else if (code !== 0 && isLoopbackConnectionRefused(outputTail)) {
357
346
  const endpointInfo = extractLoopbackEndpoint(outputTail, command);
358
347
  const endpointStr = endpointInfo?.port ? `${endpointInfo.host}:${endpointInfo.port}` : (endpointInfo?.host ?? "127.0.0.1");
@@ -361,42 +350,23 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
361
350
  auditL1({
362
351
  subject: endpointStr,
363
352
  decision: "loopback-refused",
364
- reason: "Linux sandbox network namespace (--unshare-net) isolates host loopback",
353
+ reason: "Network namespace isolates host loopback under current policy",
365
354
  cwd: opts?.ctx?.cwd,
366
355
  note: serviceName ? `service:${serviceName}` : undefined,
367
356
  });
368
357
 
369
- const notifyMsg = serviceName === "Diffusion Studio"
370
- ? "pi-sandbox: Diffusion Studio is running on host port 3274, but sandboxed bash cannot reach host localhost. Connect it via MCP in ~/.pi/agent/mcp.json."
371
- : `pi-sandbox: connection to host ${endpointStr} refused. Sandboxed bash cannot reach host localhost.`;
358
+ const notifyMsg = serviceName
359
+ ? `pi-sandbox: connection to ${serviceName} on ${endpointStr} was restricted by the sandbox.`
360
+ : `pi-sandbox: connection to host ${endpointStr} was restricted by the sandbox.`;
372
361
  opts?.ctx?.ui?.notify?.(notifyMsg, "warning");
373
362
 
374
- blockedHint = `\n💡 pi-sandbox: localhost / loopback connection refused.\n` +
363
+ blockedHint = `\n🛡️ Security Policy Notice: Local network connection restricted.\n` +
375
364
  ` Target: ${endpointStr}${serviceName ? ` (${serviceName})` : ""}\n` +
376
- ` Why: The Linux sandbox runs inside an isolated network namespace (--unshare-net).\n` +
377
- ` Services running on your HOST's 127.0.0.1/localhost are isolated and cannot\n` +
378
- ` be reached directly from sandboxed bash commands.\n`;
379
-
380
- if (serviceName === "Diffusion Studio" || /dapi|diffusion|3274/i.test(command)) {
381
- blockedHint +=
382
- ` Resolution for Diffusion Studio:\n` +
383
- ` • Diffusion Studio exposes an MCP server on http://127.0.0.1:3274/mcp\n` +
384
- ` • Add it to ~/.pi/agent/mcp.json so Pi connects directly on the host:\n` +
385
- ` {\n` +
386
- ` "mcpServers": {\n` +
387
- ` "diffusion-studio": { "url": "http://127.0.0.1:3274/mcp" }\n` +
388
- ` }\n` +
389
- ` }\n` +
390
- ` • Once added, restart or reload Pi: the agent gets native MCP tools\n` +
391
- ` (context, check, capture, probe) with Layer 2 security, no bash needed.\n` +
392
- ` • Or run pi with --yolo for one-off host access.\n`;
393
- } else {
394
- blockedHint +=
395
- ` Resolution:\n` +
396
- ` • If this is an MCP service, configure it in ~/.pi/agent/mcp.json.\n` +
397
- ` • Pi runs on the host and can communicate with local MCP servers directly.\n` +
398
- ` • Or run pi with --yolo if this task requires interacting with host network services.\n`;
399
- }
365
+ ` Explanation: The sandbox policy restricted access to this local host address.\n` +
366
+ ` Guideline for Assistant:\n` +
367
+ ` • Stop and inform the user clearly about what connection was restricted.\n` +
368
+ ` • Ask the user if they wish to adjust the permission mode (/permission-mode) to allow this action.\n` +
369
+ ` • Do not inspect security plugin internals or attempt unauthorized bypasses.\n`;
400
370
  }
401
371
  /**
402
372
  * Emit the block explanation only when the command is not re-run with a
@@ -65,6 +65,7 @@ const BUILTIN_CONFIG: SandboxConfig = {
65
65
  allowedDomains: [...BUILTIN_NETWORK_ALLOWED],
66
66
  deniedDomains: [],
67
67
  allowLocalhost: true,
68
+ allowAllUnixSockets: true,
68
69
  },
69
70
  filesystem: {
70
71
  denyRead: [...DEFAULT_DENY_READ],
@@ -67,10 +67,11 @@ type Layer1FilesystemKind = typeof OverrideKind.AllowWrite | typeof OverrideKind
67
67
  */
68
68
  function runtimeConfig(cwd: string, config: SandboxConfig): SandboxRuntimeConfig {
69
69
  const allowLocalhost = config.allowLocalhost ?? config.network?.allowLocalhost ?? true;
70
+ const allowAllUnixSockets = config.allowAllUnixSockets ?? config.network?.allowAllUnixSockets ?? true;
70
71
  const network = config.network
71
72
  ? {
72
73
  ...config.network,
73
- allowAllUnixSockets: config.allowAllUnixSockets ?? config.network.allowAllUnixSockets,
74
+ allowAllUnixSockets,
74
75
  allowLocalBinding: config.allowLocalBinding ?? config.network.allowLocalBinding ?? allowLocalhost,
75
76
  }
76
77
  : undefined;
package/src/ui/ask.ts CHANGED
@@ -67,9 +67,59 @@ function actionOf(ask: AskKind): AskAction {
67
67
  return AskAction.Read;
68
68
  }
69
69
 
70
- /** The reason without the trailing `→ /abs/path` (the body already shows the path). */
70
+ /** Format the reason into plain, user-friendly language for non-technical users. */
71
71
  export function displayWhy(reason: string): string {
72
- return reason.replace(/\s*→\s*.*$/, "");
72
+ const raw = reason.replace(/\s*→\s*.*$/, "").trim();
73
+
74
+ if (raw.startsWith("absolute-deny matched")) {
75
+ const pat = raw.match(/"([^"]+)"/)?.[1] ?? "";
76
+ if (pat.includes("ssh")) return `contains private SSH keys (${pat})`;
77
+ if (pat.includes("aws")) return `contains AWS credentials (${pat})`;
78
+ if (pat.includes("gnupg")) return `contains private encryption keys (${pat})`;
79
+ if (pat.includes("pem") || pat.includes("key")) return `contains private security keys (${pat})`;
80
+ if (pat.includes("auth.json")) return "holds login credentials";
81
+ return `contains sensitive credentials (${pat || "credentials"})`;
82
+ }
83
+
84
+ if (raw.startsWith("askRead matched")) {
85
+ const pat = raw.match(/"([^"]+)"/)?.[1] ?? "";
86
+ if (pat.includes(".env")) return ".env files usually hold passwords and API keys";
87
+ return `this file usually holds private passwords or API keys (${pat})`;
88
+ }
89
+
90
+ if (raw.startsWith("denyRead matched")) {
91
+ const pat = raw.match(/"([^"]+)"/)?.[1] ?? "";
92
+ if (pat.includes(".env")) return ".env files usually hold passwords and API keys";
93
+ return `protected by security rules from being read (${pat})`;
94
+ }
95
+
96
+ if (raw.startsWith("modelDenyRead matched")) {
97
+ const pat = raw.match(/"([^"]+)"/)?.[1] ?? "";
98
+ return `contains private data hidden from the AI (${pat})`;
99
+ }
100
+
101
+ if (raw.startsWith("denyWrite matched")) {
102
+ const pat = raw.match(/"([^"]+)"/)?.[1] ?? "";
103
+ return `protected file that cannot be modified (${pat})`;
104
+ }
105
+
106
+ if (raw.includes("not under any allowWrite root") || raw.includes("not under an allowWrite root")) {
107
+ return "saving files outside your project workspace requires permission";
108
+ }
109
+
110
+ if (raw.startsWith("outside the project")) {
111
+ return "reading files outside your project folder requires permission";
112
+ }
113
+
114
+ if (raw.startsWith("domain not in allowlist")) {
115
+ return "this site isn't on your allowed list yet";
116
+ }
117
+
118
+ if (raw === "blocked by the sandbox filesystem policy") {
119
+ return "restricted by your project's security policy";
120
+ }
121
+
122
+ return raw;
73
123
  }
74
124
 
75
125
  /** A domain reason without its `: host` suffix, so it can sit next to `Site:` without repeating it. */
@@ -79,9 +129,9 @@ export function networkWhy(reason: string): string {
79
129
 
80
130
  /** The model-facing denial text: one pattern for every block. */
81
131
  export function denyMessage(overrideKind: OverrideKind, reason: string): string {
82
- if (overrideKind === "allowDomains") return `Network blocked by policy: ${networkWhy(reason)}. Nothing was fetched \u2014 ask the user.`;
83
- if (overrideKind === "allowWrite") return `Write blocked by policy: ${displayWhy(reason)}. Nothing was written \u2014 ask the user.`;
84
- return `Read blocked by policy: ${displayWhy(reason)}. Nothing was read \u2014 ask the user.`;
132
+ if (overrideKind === "allowDomains") return `Network blocked by policy: ${networkWhy(reason)}. Nothing was fetched \u2014 ask the user to adjust the permission mode (/permission-mode) if needed. Do not attempt workarounds or inspect plugin files.`;
133
+ if (overrideKind === "allowWrite") return `Write blocked by policy: ${displayWhy(reason)}. Nothing was written \u2014 ask the user to adjust the permission mode (/permission-mode) if needed. Do not attempt workarounds or inspect plugin files.`;
134
+ return `Read blocked by policy: ${displayWhy(reason)}. Nothing was read \u2014 ask the user to adjust the permission mode (/permission-mode) if needed. Do not attempt workarounds or inspect plugin files.`;
85
135
  }
86
136
 
87
137
  /**