auto-permission-system 3.15.0 → 3.18.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 +11 -12
- package/extensions/permission-mode.ts +3 -3
- package/extensions/sandbox.ts +1 -1
- package/package.json +1 -1
- package/src/core/policy/patterns.ts +4 -2
- package/src/l1-sandbox/bash-ops.ts +35 -48
- 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/extensions/guard.ts
CHANGED
|
@@ -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
|
|
|
@@ -39,7 +39,7 @@ const MODE_DEFS: Array<{
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
value: "yolo",
|
|
42
|
-
icon: "
|
|
42
|
+
icon: "🚨",
|
|
43
43
|
name: "YOLO",
|
|
44
44
|
desc: "Turns off all file sandboxing and leak protections",
|
|
45
45
|
},
|
|
@@ -67,7 +67,7 @@ function renderSandboxChip(ctx: ExtensionContext): void {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function modeNotice(mode: PermissionMode): { text: string; level: "info" | "warning" } {
|
|
70
|
-
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" };
|
|
71
71
|
if (mode === "advanced-secure") return { text: "🧠 Advanced Secure on. File reads, MCP calls, and tool output are checked for secrets.", level: "info" };
|
|
72
72
|
return { text: "🛡 Default mode. Rule-based policy only.", level: "info" };
|
|
73
73
|
}
|
|
@@ -97,7 +97,7 @@ async function pickMode(pi: ExtensionAPI, ctx: ExtensionContext, yolo: { active:
|
|
|
97
97
|
emitYolo(pi.events, true);
|
|
98
98
|
audit({ layer: 0, event: "permission-mode", mode: "yolo", cwd: ctx.cwd, note: "runtime-sync" });
|
|
99
99
|
renderSandboxChip(ctx);
|
|
100
|
-
ctx.ui.notify("
|
|
100
|
+
ctx.ui.notify("🚨 YOLO — all security layers disabled for this conversation.", "warning");
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
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.18.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",
|
|
@@ -86,8 +86,10 @@ export function extractLoopbackEndpoint(output: string, command?: string): Loopb
|
|
|
86
86
|
|
|
87
87
|
/** A shell-name/empty token is not a path; everything else resolves against cwd unless absolute. */
|
|
88
88
|
function toAbsolutePath(rawToken: string, cwd: string, home: string): string | undefined {
|
|
89
|
-
let token = rawToken.trim().replace(/[
|
|
90
|
-
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;
|
|
91
93
|
if (token === "~" || token.startsWith("~/")) token = home + token.slice(1);
|
|
92
94
|
const absolute = token.startsWith("/") ? token : `${cwd.replace(/\/+$/, "")}/${token}`;
|
|
93
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] sandbox: command execution was denied by user: ${subject}. Command was not executed.\n`));
|
|
97
97
|
return { exitCode: 1 };
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -109,14 +109,15 @@ async function preflightAskReads(
|
|
|
109
109
|
): Promise<{ exitCode: number } | undefined> {
|
|
110
110
|
const cfg = loadConfig(cwd).filesystem;
|
|
111
111
|
if (!cfg?.askRead?.length) return undefined;
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
112
|
+
const rawPaths = askReadCandidates(command, cwd, homedir(), cfg, undefined, (absPath) => canonicalize(absPath, cwd));
|
|
113
|
+
if (!rawPaths.length) return undefined;
|
|
114
|
+
const paths = Array.from(new Set(rawPaths.map((p) => canonicalize(p, cwd))));
|
|
114
115
|
const ui = opts?.ctx as AskCtx | undefined;
|
|
115
116
|
for (const absPath of paths) {
|
|
116
117
|
if (sessionAskReadGrants.includes(absPath)) continue;
|
|
117
118
|
if (!ui?.hasUI || !ui.ui?.select) {
|
|
118
119
|
auditL1({ subject: absPath, decision: "no", note: "ask-read-headless", cwd });
|
|
119
|
-
onData(Buffer.from(`\n
|
|
120
|
+
onData(Buffer.from(`\n[Security Block] sandbox: read access to ${absPath} was blocked (automated environment). Command was not executed.\n`));
|
|
120
121
|
return { exitCode: 1 };
|
|
121
122
|
}
|
|
122
123
|
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 +127,7 @@ async function preflightAskReads(
|
|
|
126
127
|
continue;
|
|
127
128
|
}
|
|
128
129
|
auditL1({ subject: absPath, decision: "no", note: "ask-read", cwd });
|
|
129
|
-
onData(Buffer.from(`\n
|
|
130
|
+
onData(Buffer.from(`\n[Security Block] sandbox: read access to ${absPath} was denied by user. Command was not executed.\n`));
|
|
130
131
|
return { exitCode: 1 };
|
|
131
132
|
}
|
|
132
133
|
return undefined;
|
|
@@ -143,7 +144,9 @@ async function preflightOutsideReads(
|
|
|
143
144
|
return undefined;
|
|
144
145
|
}
|
|
145
146
|
const once: string[] = [];
|
|
146
|
-
|
|
147
|
+
const rawCandidates = outsideProjectReadCandidates(command, cwd, homedir(), cfg, undefined, (absPath) => canonicalize(absPath, cwd));
|
|
148
|
+
const candidates = Array.from(new Set(rawCandidates.map((p) => canonicalize(p, cwd))));
|
|
149
|
+
for (const absPath of candidates) {
|
|
147
150
|
if (sessionReadGrants.includes(absPath)) continue;
|
|
148
151
|
const ui = opts.ctx as AskCtx;
|
|
149
152
|
const parent = dirname(absPath);
|
|
@@ -327,21 +330,10 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
327
330
|
}
|
|
328
331
|
readDenied = hardDenied || outsideDenied;
|
|
329
332
|
}
|
|
330
|
-
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
else if (hardDenied) why = ` (${hardWhy})`;
|
|
335
|
-
else if (outsideDenied) why = " (outside the project directory)";
|
|
336
|
-
blockedHint += ` Target: ${offending}${why}\n`;
|
|
337
|
-
if (spelledOffending && spelledOffending !== offending) blockedHint += ` Link: ${spelledOffending}\n`;
|
|
338
|
-
}
|
|
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`;
|
|
333
|
+
let reason = "writes outside allowWrite are read-only";
|
|
334
|
+
if (hardDenied) reason = hardWhy;
|
|
335
|
+
else if (outsideDenied) reason = "target is outside the project directory";
|
|
336
|
+
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`;
|
|
345
337
|
} else if (code !== 0 && isLoopbackConnectionRefused(outputTail)) {
|
|
346
338
|
const endpointInfo = extractLoopbackEndpoint(outputTail, command);
|
|
347
339
|
const endpointStr = endpointInfo?.port ? `${endpointInfo.host}:${endpointInfo.port}` : (endpointInfo?.host ?? "127.0.0.1");
|
|
@@ -356,17 +348,11 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
356
348
|
});
|
|
357
349
|
|
|
358
350
|
const notifyMsg = serviceName
|
|
359
|
-
? `
|
|
360
|
-
: `
|
|
351
|
+
? `Local network notice: connection to ${serviceName} (${endpointStr}) was blocked by security policy.`
|
|
352
|
+
: `Local network notice: connection to host ${endpointStr} was blocked by security policy.`;
|
|
361
353
|
opts?.ctx?.ui?.notify?.(notifyMsg, "warning");
|
|
362
354
|
|
|
363
|
-
blockedHint = `\n
|
|
364
|
-
` Target: ${endpointStr}${serviceName ? ` (${serviceName})` : ""}\n` +
|
|
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`;
|
|
355
|
+
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`;
|
|
370
356
|
}
|
|
371
357
|
/**
|
|
372
358
|
* Emit the block explanation only when the command is not re-run with a
|
|
@@ -386,8 +372,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
386
372
|
if (sandboxSetupError && !hardDenied && !outsideDenied && (!offending || offending === cwd || offending.startsWith(`${cwd}/`))) {
|
|
387
373
|
blockedHint = "";
|
|
388
374
|
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" });
|
|
389
|
-
opts?.ctx?.ui?.notify?.(`
|
|
390
|
-
decisionHint = `\n
|
|
375
|
+
opts?.ctx?.ui?.notify?.(`Sandbox setup notice: could not prepare folder isolation for ${offending ?? spelledOffending ?? "the sandbox"}. System defaults will be used.`, "warning");
|
|
376
|
+
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`;
|
|
391
377
|
onData(Buffer.from(decisionHint));
|
|
392
378
|
} else if (offending && outsideDenied && !hardDenied && outsideMode === "ask" && opts?.ctx?.hasUI && opts.ctx.ui?.select && opts.onAlwaysRead) {
|
|
393
379
|
const absPath = offending;
|
|
@@ -407,8 +393,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
407
393
|
if (choice === MainChoice.Once) readOnce.push(absPath);
|
|
408
394
|
else sessionReadGrants.push(absPath);
|
|
409
395
|
auditL1({ subject: absPath, decision: choice, scope: choice === MainChoice.Once ? "invocation" : "session", cwd: opts.ctx.cwd, note: "outside-project-read" });
|
|
410
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"}
|
|
411
|
-
onData(Buffer.from(`\n
|
|
396
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"}`, "info");
|
|
397
|
+
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`));
|
|
412
398
|
resolve(attempt([]));
|
|
413
399
|
return;
|
|
414
400
|
}
|
|
@@ -417,13 +403,13 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
417
403
|
try {
|
|
418
404
|
const persistedTo = await opts.onAlwaysRead(subject, grant.scope);
|
|
419
405
|
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" });
|
|
420
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})
|
|
421
|
-
onData(Buffer.from(`\n
|
|
406
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
|
|
407
|
+
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`));
|
|
422
408
|
resolve(attempt([]));
|
|
423
409
|
return;
|
|
424
410
|
} catch (e) {
|
|
425
411
|
auditL1({ subject, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e), note: "outside-project-read" });
|
|
426
|
-
decisionHint = `\n
|
|
412
|
+
decisionHint = `\n[Security Notice] pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
|
|
427
413
|
}
|
|
428
414
|
}
|
|
429
415
|
} catch (e) {
|
|
@@ -445,12 +431,13 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
445
431
|
const action: "Read" | "Write" = writeDenied ? "Write" : "Read";
|
|
446
432
|
const outcome = action === "Write" ? "written" : "read";
|
|
447
433
|
auditL1({ subject: offending, decision: readDenied && !writeDenied ? "read-denied" : "write-denied", reason: why, cwd });
|
|
448
|
-
opts?.ctx?.ui?.notify?.(`
|
|
434
|
+
opts?.ctx?.ui?.notify?.(`Folder protected: unable to ${action.toLowerCase()} ${offending}. Location is restricted under current policy.`, "warning");
|
|
449
435
|
decisionHint = `\n${blockedLine(action, why, outcome)}\n`;
|
|
450
436
|
onData(Buffer.from(decisionHint));
|
|
451
437
|
} else if (offending && writeGrantRepeated && writeGrantParent) {
|
|
452
438
|
auditL1({ subject: offending, decision: "write-denied", reason: `grant for ${writeGrantParent} did not take effect`, cwd: opts?.ctx?.cwd, note: "grant-ineffective" });
|
|
453
|
-
|
|
439
|
+
const targetFolder = (writeGrantParent || offending || "").replace(/\/EROFS$/i, "");
|
|
440
|
+
opts?.ctx?.ui?.notify?.(`🚧 Folder protected: unable to write to ${targetFolder} under sandbox policy. Review sandbox.json or /permission-mode.`, "warning");
|
|
454
441
|
decisionHint = `\n${blockedLine("Write", `the grant for ${writeGrantParent} did not take effect`, "written")}\n`;
|
|
455
442
|
onData(Buffer.from(decisionHint));
|
|
456
443
|
} else if (offending && opts?.ctx?.hasUI && opts.ctx.ui?.select && opts.onAlways) {
|
|
@@ -472,8 +459,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
472
459
|
if (choice === MainChoice.Once) {
|
|
473
460
|
grantedWriteFolders.add(parentDir);
|
|
474
461
|
auditL1({ subject: absPath, granularity: "folder", original: absPath, decision: "once", scope: "invocation", cwd: opts.ctx.cwd, note: "write-once" });
|
|
475
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${absPath} once
|
|
476
|
-
onData(Buffer.from(`\n
|
|
462
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${absPath} once`, "info");
|
|
463
|
+
onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${absPath} allowed once. Command output with the new permission follows.\n`));
|
|
477
464
|
resolve(attempt([...writeOnce, parentDir]));
|
|
478
465
|
return;
|
|
479
466
|
}
|
|
@@ -482,8 +469,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
482
469
|
sessionWriteGrants.push(value);
|
|
483
470
|
(value === parentDir ? grantedWriteFolders : grantedWriteFiles).add(value);
|
|
484
471
|
auditL1({ subject: value, granularity: value === parentDir ? "folder" : "file", original: absPath, decision: "session", scope: "session", cwd: opts.ctx.cwd, note: "write-session" });
|
|
485
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${value} for this conversation
|
|
486
|
-
onData(Buffer.from(`\n
|
|
472
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${value} for this conversation`, "info");
|
|
473
|
+
onData(Buffer.from(`\n[Permission Granted] pi-sandbox: write to ${value} allowed for this conversation. Command output with the new permission follows.\n`));
|
|
487
474
|
resolve(attempt([]));
|
|
488
475
|
return;
|
|
489
476
|
}
|
|
@@ -493,14 +480,14 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
493
480
|
const persistedTo = await opts.onAlways(subject, grant.scope);
|
|
494
481
|
(grant.folder ? grantedWriteFolders : grantedWriteFiles).add(subject);
|
|
495
482
|
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 });
|
|
496
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})
|
|
497
|
-
onData(Buffer.from(`\n
|
|
483
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed write to ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
|
|
484
|
+
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`));
|
|
498
485
|
resolve(attempt([]));
|
|
499
486
|
return;
|
|
500
487
|
} catch (e) {
|
|
501
488
|
auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e) });
|
|
502
489
|
opts.ctx.ui?.notify?.(`pi-sandbox: could not save the permission (${e})`, "error");
|
|
503
|
-
decisionHint = `\n
|
|
490
|
+
decisionHint = `\n[Security Notice] pi-sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
|
|
504
491
|
}
|
|
505
492
|
}
|
|
506
493
|
} catch (e) {
|
|
@@ -519,7 +506,7 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
519
506
|
onData(Buffer.from(decisionHint));
|
|
520
507
|
} else if (offending && writeDenied) {
|
|
521
508
|
auditL1({ subject: offending, decision: "write-denied", reason: "not under any allowWrite root", cwd });
|
|
522
|
-
opts?.ctx?.ui?.notify?.(`
|
|
509
|
+
opts?.ctx?.ui?.notify?.(`Folder protected: unable to write to ${offending}. Location is outside allowed project folders.`, "warning");
|
|
523
510
|
onData(Buffer.from(`\n${blockedLine("Write", "not under any allowWrite root", "written")}\n`));
|
|
524
511
|
}
|
|
525
512
|
|
|
@@ -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.",
|