auto-permission-system 3.12.0 → 3.17.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/extensions/guard.ts +13 -14
- package/extensions/permission-mode.ts +30 -25
- package/extensions/sandbox.ts +1 -1
- package/package.json +1 -1
- package/sandbox.default.json +1 -0
- package/skills/auto-permission-system/SKILL.md +10 -1
- package/src/core/policy/patterns.ts +4 -6
- package/src/l1-sandbox/bash-ops.ts +31 -77
- package/src/l1-sandbox/config.ts +1 -0
- package/src/l1-sandbox/manager.ts +2 -1
- package/src/l1-sandbox/network-ask.ts +4 -3
- package/src/l2-guard/prompts.ts +5 -4
- package/src/l2-guard/subagent.ts +2 -2
- package/src/shared/yolo.ts +1 -1
- package/src/ui/ask-flow.ts +1 -1
- package/src/ui/ask.ts +55 -5
package/extensions/guard.ts
CHANGED
|
@@ -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
|
|
@@ -65,7 +65,7 @@ async function askDetection(
|
|
|
65
65
|
if (choice === "session") {
|
|
66
66
|
detectionGrants.add(key);
|
|
67
67
|
audit({ layer: 2, tool: kind, subject, reason, decision: "session", note: "advanced-secure", cwd: ctx.cwd });
|
|
68
|
-
ctx.ui.notify(`
|
|
68
|
+
ctx.ui.notify(`Allowed for this conversation: ${subject}`, "info");
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
audit({ layer: 2, tool: kind, subject, reason, decision: "no", note: "advanced-secure", cwd: ctx.cwd });
|
|
@@ -130,24 +130,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
130
|
if (!err) continue;
|
|
131
131
|
audit({ layer: 2, event: "policy-parse-error", file: p, error: err, cwd: ctx.cwd });
|
|
132
132
|
console.error(`security-guard: ${p} does not parse (${err}); its rules are NOT applied`);
|
|
133
|
-
|
|
133
|
+
const isProject = p === `${ctx.cwd}/.pi/sandbox.json`;
|
|
134
|
+
const msg = isProject
|
|
135
|
+
? "⚠️ Syntax error in settings file — project configuration not loaded."
|
|
136
|
+
: "⚠️ Syntax error in settings file — global configuration not loaded.";
|
|
137
|
+
ctx.ui.notify(msg, "error");
|
|
134
138
|
}
|
|
135
139
|
setPiDeclinedTrust((ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() === false);
|
|
136
140
|
const changes = untrustedProjectChanges(ctx.cwd);
|
|
137
141
|
if (changes.length && !isProjectFileDeclined(projectPolicyPath(ctx.cwd), TRUST_STORE)) {
|
|
138
142
|
audit({ layer: 2, event: "untrusted-project-policy", file: projectPolicyPath(ctx.cwd), changes, cwd: ctx.cwd });
|
|
139
|
-
ctx.ui.notify(
|
|
140
|
-
`⚠️ This folder has a .pi/sandbox.json that tries to make your security weaker:\n${bullets(changes)}\n` +
|
|
141
|
-
"auto-permission-system ignores these changes. Its block rules still apply.\n" +
|
|
142
|
-
"Did you write this file? Then type /security trust.",
|
|
143
|
-
"warning",
|
|
144
|
-
);
|
|
143
|
+
ctx.ui.notify("🛡️ Project-level configuration detected: run /security trust to approve.", "warning");
|
|
145
144
|
}
|
|
146
145
|
const policy = loadPolicy(ctx.cwd);
|
|
147
146
|
if (!policy.enabled) {
|
|
148
147
|
policyEnabled = false;
|
|
149
148
|
applyActive();
|
|
150
|
-
ctx.ui.notify("
|
|
149
|
+
ctx.ui.notify("Tool guard is disabled in your sandbox.json settings.", "info");
|
|
151
150
|
return;
|
|
152
151
|
}
|
|
153
152
|
policyEnabled = true;
|
|
@@ -316,15 +315,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
316
315
|
if (choice === "program-session" && programId) {
|
|
317
316
|
grantProgramOutputSession(programId);
|
|
318
317
|
audit({ layer: 2, tool: event.toolName, decision: "session", grant: programId, note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
319
|
-
ctx.ui.notify(
|
|
318
|
+
ctx.ui.notify(`Output permitted: allowed all ${programId} output for this conversation.`, "info");
|
|
320
319
|
} else {
|
|
321
320
|
audit({ layer: 2, tool: event.toolName, decision: "yes", note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
322
|
-
ctx.ui.notify(
|
|
321
|
+
ctx.ui.notify(`Output permitted: allowed ${event.toolName} output for this call.`, "info");
|
|
323
322
|
}
|
|
324
323
|
return;
|
|
325
324
|
}
|
|
326
325
|
audit({ layer: 2, tool: event.toolName, decision: "no", note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
327
|
-
ctx.ui.notify(
|
|
326
|
+
ctx.ui.notify(`Sensitive data protected: withheld ${event.toolName} output from the AI (potential secrets detected).`, "warning");
|
|
328
327
|
return { content: [{ type: "text", text: withheldNotice(subject) }] };
|
|
329
328
|
});
|
|
330
329
|
|
|
@@ -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
|
|
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.
|
|
@@ -72,27 +67,37 @@ function renderSandboxChip(ctx: ExtensionContext): void {
|
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
function modeNotice(mode: PermissionMode): { text: string; level: "info" | "warning" } {
|
|
75
|
-
if (mode === "yolo") return { text: "
|
|
70
|
+
if (mode === "yolo") return { text: "🚨 YOLO — all security layers disabled. Saved; new conversations start this way too.", level: "warning" };
|
|
76
71
|
if (mode === "advanced-secure") return { text: "🧠 Advanced Secure on. File reads, MCP calls, and tool output are checked for secrets.", level: "info" };
|
|
77
72
|
return { text: "🛡 Default mode. Rule-based policy only.", level: "info" };
|
|
78
73
|
}
|
|
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
|
-
|
|
84
|
-
|
|
86
|
+
"Select permission mode:",
|
|
87
|
+
options,
|
|
85
88
|
{ timeout: 120_000 },
|
|
86
89
|
);
|
|
87
90
|
if (picked === undefined) return;
|
|
88
|
-
|
|
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;
|
|
92
97
|
emitYolo(pi.events, true);
|
|
93
98
|
audit({ layer: 0, event: "permission-mode", mode: "yolo", cwd: ctx.cwd, note: "runtime-sync" });
|
|
94
99
|
renderSandboxChip(ctx);
|
|
95
|
-
ctx.ui.notify("
|
|
100
|
+
ctx.ui.notify("🚨 YOLO — all security layers disabled for this conversation.", "warning");
|
|
96
101
|
return;
|
|
97
102
|
}
|
|
98
103
|
ctx.ui.notify(`Permission mode is already ${modeLabel(next)}.`, "info");
|
package/extensions/sandbox.ts
CHANGED
|
@@ -169,7 +169,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
169
|
if (runtimeYolo) {
|
|
170
170
|
await stopSandbox();
|
|
171
171
|
ctx.ui.setStatus("sandbox", ctx.ui.theme.fg("error", YOLO_STATUS));
|
|
172
|
-
ctx.ui.notify(ctx.ui.theme.fg("error", "
|
|
172
|
+
ctx.ui.notify(ctx.ui.theme.fg("error", "🚨 YOLO mode — all pi security layers disabled."), "info");
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
175
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auto-permission-system",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.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",
|
package/sandbox.default.json
CHANGED
|
@@ -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,
|
|
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,15 +80,16 @@ 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
|
}
|
|
90
86
|
|
|
91
87
|
/** A shell-name/empty token is not a path; everything else resolves against cwd unless absolute. */
|
|
92
88
|
function toAbsolutePath(rawToken: string, cwd: string, home: string): string | undefined {
|
|
93
|
-
let token = rawToken.trim().replace(/[
|
|
94
|
-
if (!token || /^(bash|sh|zsh)
|
|
89
|
+
let token = rawToken.trim().replace(/[.,;:]+$/, "");
|
|
90
|
+
if (!token || /^(bash|sh|zsh|EROFS)$/i.test(token)) return undefined;
|
|
91
|
+
token = token.replace(/\/EROFS$/i, "");
|
|
92
|
+
if (!token) return undefined;
|
|
95
93
|
if (token === "~" || token.startsWith("~/")) token = home + token.slice(1);
|
|
96
94
|
const absolute = token.startsWith("/") ? token : `${cwd.replace(/\/+$/, "")}/${token}`;
|
|
97
95
|
return normalizeAbs(absolute);
|
|
@@ -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
|
|
33
|
+
return `[Security Policy Notice] ${action} blocked by policy: ${why}. Nothing was ${outcome}. Inform the user; do not attempt workarounds.`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** `Link` body row for a symlinked block path; empty when the spellings agree (ADR-035). */
|
|
@@ -93,7 +93,7 @@ async function preflightSensitiveCommands(
|
|
|
93
93
|
return undefined;
|
|
94
94
|
}
|
|
95
95
|
auditL1({ subject, decision: "no", note: "sensitive-command", cwd });
|
|
96
|
-
onData(Buffer.from(`\n
|
|
96
|
+
onData(Buffer.from(`\n[Security Block] pi-sandbox: command blocked — it can print secrets: ${subject}. Nothing was run — ask the user.\n`));
|
|
97
97
|
return { exitCode: 1 };
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -116,7 +116,7 @@ async function preflightAskReads(
|
|
|
116
116
|
if (sessionAskReadGrants.includes(absPath)) continue;
|
|
117
117
|
if (!ui?.hasUI || !ui.ui?.select) {
|
|
118
118
|
auditL1({ subject: absPath, decision: "no", note: "ask-read-headless", cwd });
|
|
119
|
-
onData(Buffer.from(`\n
|
|
119
|
+
onData(Buffer.from(`\n[Security Block] pi-sandbox: sensitive file read blocked: ${absPath}. Nothing was run — ask the user.\n`));
|
|
120
120
|
return { exitCode: 1 };
|
|
121
121
|
}
|
|
122
122
|
const choice = await askMain(ui, "Let this command read your .env file?", [`File ${absPath}`, `Why .env files usually hold passwords and API keys`, `Risk the AI will see any keys in it`, `Note can't be saved as a permanent rule (it holds secrets)`].join("\n"), { session: true, remember: false, icon: "cred" });
|
|
@@ -126,7 +126,7 @@ async function preflightAskReads(
|
|
|
126
126
|
continue;
|
|
127
127
|
}
|
|
128
128
|
auditL1({ subject: absPath, decision: "no", note: "ask-read", cwd });
|
|
129
|
-
onData(Buffer.from(`\n
|
|
129
|
+
onData(Buffer.from(`\n[Security Block] pi-sandbox: sensitive file read blocked: ${absPath}. Nothing was run — ask the user.\n`));
|
|
130
130
|
return { exitCode: 1 };
|
|
131
131
|
}
|
|
132
132
|
return undefined;
|
|
@@ -327,32 +327,10 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
327
327
|
}
|
|
328
328
|
readDenied = hardDenied || outsideDenied;
|
|
329
329
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
let why = "";
|
|
335
|
-
if (writeDenied && !hardDenied) why = " (not under an allowWrite root: writes outside it are read-only)";
|
|
336
|
-
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`;
|
|
354
|
-
}
|
|
355
|
-
blockedHint += ` Policy: ~/.pi/agent/extensions/sandbox.json (+ <cwd>/.pi/sandbox.json overrides).\n`;
|
|
330
|
+
let reason = "writes outside allowWrite are read-only";
|
|
331
|
+
if (hardDenied) reason = hardWhy;
|
|
332
|
+
else if (outsideDenied) reason = "target is outside the project directory";
|
|
333
|
+
blockedHint = `\n[Security Policy Notice] filesystem access blocked by sandbox policy: ${writeDenied ? "Write" : "Read"} access to ${offending ?? "target"} is restricted.\nReason: ${reason}. Inform the user; do not attempt workarounds.\n`;
|
|
356
334
|
} else if (code !== 0 && isLoopbackConnectionRefused(outputTail)) {
|
|
357
335
|
const endpointInfo = extractLoopbackEndpoint(outputTail, command);
|
|
358
336
|
const endpointStr = endpointInfo?.port ? `${endpointInfo.host}:${endpointInfo.port}` : (endpointInfo?.host ?? "127.0.0.1");
|
|
@@ -361,42 +339,17 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
361
339
|
auditL1({
|
|
362
340
|
subject: endpointStr,
|
|
363
341
|
decision: "loopback-refused",
|
|
364
|
-
reason: "
|
|
342
|
+
reason: "Network namespace isolates host loopback under current policy",
|
|
365
343
|
cwd: opts?.ctx?.cwd,
|
|
366
344
|
note: serviceName ? `service:${serviceName}` : undefined,
|
|
367
345
|
});
|
|
368
346
|
|
|
369
|
-
const notifyMsg = serviceName
|
|
370
|
-
?
|
|
371
|
-
: `
|
|
347
|
+
const notifyMsg = serviceName
|
|
348
|
+
? `Local network notice: connection to ${serviceName} (${endpointStr}) was blocked by security policy.`
|
|
349
|
+
: `Local network notice: connection to host ${endpointStr} was blocked by security policy.`;
|
|
372
350
|
opts?.ctx?.ui?.notify?.(notifyMsg, "warning");
|
|
373
351
|
|
|
374
|
-
blockedHint = `\n
|
|
375
|
-
` 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
|
-
}
|
|
352
|
+
blockedHint = `\n[Security Policy Notice] Connection to ${endpointStr}${serviceName ? ` (${serviceName})` : ""} blocked by sandbox policy.\nReason: Local network connections are restricted. Inform the user; do not attempt workarounds.\n`;
|
|
400
353
|
}
|
|
401
354
|
/**
|
|
402
355
|
* Emit the block explanation only when the command is not re-run with a
|
|
@@ -416,8 +369,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
416
369
|
if (sandboxSetupError && !hardDenied && !outsideDenied && (!offending || offending === cwd || offending.startsWith(`${cwd}/`))) {
|
|
417
370
|
blockedHint = "";
|
|
418
371
|
auditL1({ subject: offending ?? spelledOffending, decision: "write-denied", reason: "the OS sandbox could not start (bwrap mount setup)", cwd: opts?.ctx?.cwd, note: "sandbox-setup-error" });
|
|
419
|
-
opts?.ctx?.ui?.notify?.(`
|
|
420
|
-
decisionHint = `\n
|
|
372
|
+
opts?.ctx?.ui?.notify?.(`Sandbox setup notice: could not prepare folder isolation for ${offending ?? spelledOffending ?? "the sandbox"}. System defaults will be used.`, "warning");
|
|
373
|
+
decisionHint = `\n[System Error] pi-sandbox: the OS sandbox could not start (bwrap could not create a protected mount point), so nothing was permission-prompted. This is a sandbox setup problem, not a policy denial.\n`;
|
|
421
374
|
onData(Buffer.from(decisionHint));
|
|
422
375
|
} else if (offending && outsideDenied && !hardDenied && outsideMode === "ask" && opts?.ctx?.hasUI && opts.ctx.ui?.select && opts.onAlwaysRead) {
|
|
423
376
|
const absPath = offending;
|
|
@@ -437,8 +390,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
437
390
|
if (choice === MainChoice.Once) readOnce.push(absPath);
|
|
438
391
|
else sessionReadGrants.push(absPath);
|
|
439
392
|
auditL1({ subject: absPath, decision: choice, scope: choice === MainChoice.Once ? "invocation" : "session", cwd: opts.ctx.cwd, note: "outside-project-read" });
|
|
440
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"}
|
|
441
|
-
onData(Buffer.from(`\n
|
|
393
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"}`, "info");
|
|
394
|
+
onData(Buffer.from(`\n[Permission Granted] pi-sandbox: read of ${absPath} allowed ${choice === MainChoice.Once ? "once" : "for this conversation"}. Command output with the new permission follows.\n`));
|
|
442
395
|
resolve(attempt([]));
|
|
443
396
|
return;
|
|
444
397
|
}
|
|
@@ -447,13 +400,13 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
447
400
|
try {
|
|
448
401
|
const persistedTo = await opts.onAlwaysRead(subject, grant.scope);
|
|
449
402
|
auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, decision: grant.scope === "cwd" ? "always-cwd" : "always-global", scope: grant.scope, cwd: opts.ctx.cwd, persisted_to: persistedTo, note: "outside-project-read" });
|
|
450
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})
|
|
451
|
-
onData(Buffer.from(`\n
|
|
403
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
|
|
404
|
+
onData(Buffer.from(`\n[Permission Granted] pi-sandbox: read of ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
|
|
452
405
|
resolve(attempt([]));
|
|
453
406
|
return;
|
|
454
407
|
} catch (e) {
|
|
455
408
|
auditL1({ subject, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e), note: "outside-project-read" });
|
|
456
|
-
decisionHint = `\n
|
|
409
|
+
decisionHint = `\n[Security Notice] pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
|
|
457
410
|
}
|
|
458
411
|
}
|
|
459
412
|
} catch (e) {
|
|
@@ -475,12 +428,13 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
475
428
|
const action: "Read" | "Write" = writeDenied ? "Write" : "Read";
|
|
476
429
|
const outcome = action === "Write" ? "written" : "read";
|
|
477
430
|
auditL1({ subject: offending, decision: readDenied && !writeDenied ? "read-denied" : "write-denied", reason: why, cwd });
|
|
478
|
-
opts?.ctx?.ui?.notify?.(`
|
|
431
|
+
opts?.ctx?.ui?.notify?.(`Folder protected: unable to ${action.toLowerCase()} ${offending}. Location is restricted under current policy.`, "warning");
|
|
479
432
|
decisionHint = `\n${blockedLine(action, why, outcome)}\n`;
|
|
480
433
|
onData(Buffer.from(decisionHint));
|
|
481
434
|
} else if (offending && writeGrantRepeated && writeGrantParent) {
|
|
482
435
|
auditL1({ subject: offending, decision: "write-denied", reason: `grant for ${writeGrantParent} did not take effect`, cwd: opts?.ctx?.cwd, note: "grant-ineffective" });
|
|
483
|
-
|
|
436
|
+
const targetFolder = (writeGrantParent || offending || "").replace(/\/EROFS$/i, "");
|
|
437
|
+
opts?.ctx?.ui?.notify?.(`🚧 Folder protected: unable to write to ${targetFolder} under sandbox policy. Review sandbox.json or /permission-mode.`, "warning");
|
|
484
438
|
decisionHint = `\n${blockedLine("Write", `the grant for ${writeGrantParent} did not take effect`, "written")}\n`;
|
|
485
439
|
onData(Buffer.from(decisionHint));
|
|
486
440
|
} else if (offending && opts?.ctx?.hasUI && opts.ctx.ui?.select && opts.onAlways) {
|
|
@@ -502,8 +456,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
502
456
|
if (choice === MainChoice.Once) {
|
|
503
457
|
grantedWriteFolders.add(parentDir);
|
|
504
458
|
auditL1({ subject: absPath, granularity: "folder", original: absPath, decision: "once", scope: "invocation", cwd: opts.ctx.cwd, note: "write-once" });
|
|
505
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${absPath} once
|
|
506
|
-
onData(Buffer.from(`\n
|
|
459
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${absPath} once`, "info");
|
|
460
|
+
onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${absPath} allowed once. Command output with the new permission follows.\n`));
|
|
507
461
|
resolve(attempt([...writeOnce, parentDir]));
|
|
508
462
|
return;
|
|
509
463
|
}
|
|
@@ -512,8 +466,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
512
466
|
sessionWriteGrants.push(value);
|
|
513
467
|
(value === parentDir ? grantedWriteFolders : grantedWriteFiles).add(value);
|
|
514
468
|
auditL1({ subject: value, granularity: value === parentDir ? "folder" : "file", original: absPath, decision: "session", scope: "session", cwd: opts.ctx.cwd, note: "write-session" });
|
|
515
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${value} for this conversation
|
|
516
|
-
onData(Buffer.from(`\n
|
|
469
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${value} for this conversation`, "info");
|
|
470
|
+
onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${value} allowed for this conversation. Command output with the new permission follows.\n`));
|
|
517
471
|
resolve(attempt([]));
|
|
518
472
|
return;
|
|
519
473
|
}
|
|
@@ -523,14 +477,14 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
523
477
|
const persistedTo = await opts.onAlways(subject, grant.scope);
|
|
524
478
|
(grant.folder ? grantedWriteFolders : grantedWriteFiles).add(subject);
|
|
525
479
|
auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, decision: grant.scope === "cwd" ? "always-cwd" : "always-global", scope: grant.scope, cwd: opts.ctx.cwd, persisted_to: persistedTo });
|
|
526
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})
|
|
527
|
-
onData(Buffer.from(`\n
|
|
480
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
|
|
481
|
+
onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
|
|
528
482
|
resolve(attempt([]));
|
|
529
483
|
return;
|
|
530
484
|
} catch (e) {
|
|
531
485
|
auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e) });
|
|
532
486
|
opts.ctx.ui?.notify?.(`pi-sandbox: could not save the permission (${e})`, "error");
|
|
533
|
-
decisionHint = `\n
|
|
487
|
+
decisionHint = `\n[Security Notice] pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
|
|
534
488
|
}
|
|
535
489
|
}
|
|
536
490
|
} catch (e) {
|
|
@@ -549,7 +503,7 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
549
503
|
onData(Buffer.from(decisionHint));
|
|
550
504
|
} else if (offending && writeDenied) {
|
|
551
505
|
auditL1({ subject: offending, decision: "write-denied", reason: "not under any allowWrite root", cwd });
|
|
552
|
-
opts?.ctx?.ui?.notify?.(`
|
|
506
|
+
opts?.ctx?.ui?.notify?.(`Folder protected: unable to write to ${offending}. Location is outside allowed project folders.`, "warning");
|
|
553
507
|
onData(Buffer.from(`\n${blockedLine("Write", "not under any allowWrite root", "written")}\n`));
|
|
554
508
|
}
|
|
555
509
|
|
package/src/l1-sandbox/config.ts
CHANGED
|
@@ -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
|
|
74
|
+
allowAllUnixSockets,
|
|
74
75
|
allowLocalBinding: config.allowLocalBinding ?? config.network.allowLocalBinding ?? allowLocalhost,
|
|
75
76
|
}
|
|
76
77
|
: undefined;
|
|
@@ -161,7 +161,7 @@ async function decide(deps: NetworkAskDeps, host: string, port: number | undefin
|
|
|
161
161
|
if (mainChoice === MainChoice.Session) {
|
|
162
162
|
sessionGrants.push(wildcard);
|
|
163
163
|
deps.audit({ ...base, decision: "session", grant: wildcard, requested: host });
|
|
164
|
-
ctx.ui.notify?.(`
|
|
164
|
+
ctx.ui.notify?.(`Allowed for this conversation: ${wildcard}`, "info");
|
|
165
165
|
return true;
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -176,11 +176,12 @@ async function decide(deps: NetworkAskDeps, host: string, port: number | undefin
|
|
|
176
176
|
const path = await deps.persist(pattern, scope);
|
|
177
177
|
deps.applyLive(pattern);
|
|
178
178
|
deps.audit({ ...base, decision: scope === GrantScope.Cwd ? "always-cwd" : "always-global", scope, persisted_to: path, pattern, requested: host });
|
|
179
|
-
|
|
179
|
+
const scopeLabel = scope === GrantScope.Cwd ? "this project (stored in project settings)" : "all projects (stored in global settings)";
|
|
180
|
+
ctx.ui.notify?.(`Rule saved for ${scopeLabel}.`, "info");
|
|
180
181
|
} catch (error) {
|
|
181
182
|
commandGrants?.add(host);
|
|
182
183
|
deps.audit({ ...base, decision: "yes", note: `always-persist-failed: ${error}`, scope });
|
|
183
|
-
ctx.ui.notify?.(`
|
|
184
|
+
ctx.ui.notify?.(`Could not save rule for ${scope === GrantScope.Cwd ? "this project" : "all projects"} (${error}); allowing this command only.`, "warning");
|
|
184
185
|
}
|
|
185
186
|
return true;
|
|
186
187
|
}
|
package/src/l2-guard/prompts.ts
CHANGED
|
@@ -87,7 +87,7 @@ export async function askOrBlock(
|
|
|
87
87
|
const named = userNamedOutside(ctx as SessionCtx, ask);
|
|
88
88
|
if (named) {
|
|
89
89
|
audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision: "user-named", named, cwd: ctx.cwd });
|
|
90
|
-
ctx.ui.notify(`
|
|
90
|
+
ctx.ui.notify(`Allowed reading ${ask.subject} once (mentioned in your prompt).`, "info");
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -102,7 +102,7 @@ export async function askOrBlock(
|
|
|
102
102
|
const value = ask.overrideKind === OverrideKind.AllowDomains || isSafeFolderGrant(parent, homedir()) ? parent : ask.overrideValue;
|
|
103
103
|
sessionGrants.push({ kind: ask.overrideKind, value });
|
|
104
104
|
audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision, grant: value, cwd: ctx.cwd });
|
|
105
|
-
ctx.ui.notify(`
|
|
105
|
+
ctx.ui.notify(`Allowed for this conversation: ${value}`, "info");
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
108
|
if (decision === Decision.AlwaysCwd || decision === Decision.AlwaysGlobal || decision === Decision.AlwaysCwdFolder || decision === Decision.AlwaysGlobalFolder) {
|
|
@@ -117,10 +117,11 @@ export async function askOrBlock(
|
|
|
117
117
|
const path = persistOverride(scope, ctx.cwd, ask.overrideKind, value);
|
|
118
118
|
onPersist?.(ask.overrideKind, value, scope);
|
|
119
119
|
audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision, scope, granularity: useFolder ? "folder" : "file", cwd: ctx.cwd, persisted_to: path, override: { [ask.overrideKind]: value } });
|
|
120
|
-
|
|
120
|
+
const scopeLabel = scope === GrantScope.Cwd ? "this project (stored in project settings)" : "all projects (stored in global settings)";
|
|
121
|
+
ctx.ui.notify(`Rule saved for ${scopeLabel}.`, "info");
|
|
121
122
|
} catch (error) {
|
|
122
123
|
audit({ layer: 2, tool: ask.tool, subject: ask.subject, reason: ask.reason, decision: "yes", note: `always-persist-failed: ${error}`, scope, cwd: ctx.cwd });
|
|
123
|
-
ctx.ui.notify(`
|
|
124
|
+
ctx.ui.notify(`Could not save rule for ${scope === GrantScope.Cwd ? "this project" : "all projects"} (${error}); allowing this call only.`, "warning");
|
|
124
125
|
}
|
|
125
126
|
return null;
|
|
126
127
|
}
|
package/src/l2-guard/subagent.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function subagentNetworkBlock(ctx: { hasUI?: boolean; sessionManager?: un
|
|
|
21
21
|
if (ctx.hasUI !== false) return null;
|
|
22
22
|
const mode = policy.subagent?.network ?? SubagentNetwork.Allow;
|
|
23
23
|
if (mode === SubagentNetwork.Allow) return null;
|
|
24
|
-
if (mode === SubagentNetwork.Deny) return "Network blocked
|
|
24
|
+
if (mode === SubagentNetwork.Deny) return "[Security Policy Notice] Network access blocked: automated subagent network connections are restricted. Nothing was fetched. Inform the user; do not attempt workarounds.";
|
|
25
25
|
if (mode === SubagentNetwork.ResearchOnly) {
|
|
26
26
|
const sessionManager = ctx.sessionManager as { getBranch?: () => Array<{ type: string; text?: string }> } | undefined;
|
|
27
27
|
const branch = sessionManager?.getBranch?.() ?? [];
|
|
@@ -31,7 +31,7 @@ export function subagentNetworkBlock(ctx: { hasUI?: boolean; sessionManager?: un
|
|
|
31
31
|
.join(" ")
|
|
32
32
|
.toLowerCase();
|
|
33
33
|
for (const agent of RESEARCH_AGENTS) if (recentText.includes(agent)) return null;
|
|
34
|
-
return "Network
|
|
34
|
+
return "[Security Policy Notice] Network access restricted: subagents may only connect to approved research domains. Nothing was fetched. Inform the user; do not attempt workarounds.";
|
|
35
35
|
}
|
|
36
36
|
return null;
|
|
37
37
|
}
|
package/src/shared/yolo.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export const YOLO_CHANNEL = "auto-permission-system:yolo";
|
|
27
27
|
|
|
28
28
|
/** Footer text shown while every layer is disabled. */
|
|
29
|
-
export const YOLO_STATUS = "
|
|
29
|
+
export const YOLO_STATUS = "🚨 YOLO — all security layers disabled";
|
|
30
30
|
|
|
31
31
|
/** The pi APIs this module needs; kept structural so tests can fake them. */
|
|
32
32
|
export interface YoloFlagPI {
|
package/src/ui/ask-flow.ts
CHANGED
|
@@ -482,7 +482,7 @@ export async function askExposure(ctx: AskCtx, hits: string[], programId?: strin
|
|
|
482
482
|
*/
|
|
483
483
|
export function withheldNotice(subject: string): string {
|
|
484
484
|
return [
|
|
485
|
-
|
|
485
|
+
`[Security Policy Notice] Output from \`${subject}\` was withheld because it contains sensitive credentials.`,
|
|
486
486
|
"",
|
|
487
487
|
"The tool completed successfully, but the output is unavailable.",
|
|
488
488
|
"Continue with the available information, or ask the user to allow access if the output is needed.",
|
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
|
-
/**
|
|
70
|
+
/** Format the reason into plain, user-friendly language for non-technical users. */
|
|
71
71
|
export function displayWhy(reason: string): string {
|
|
72
|
-
|
|
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
|
/**
|