auto-permission-system 3.17.0 → 3.19.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
CHANGED
|
@@ -323,7 +323,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
323
323
|
return;
|
|
324
324
|
}
|
|
325
325
|
audit({ layer: 2, tool: event.toolName, decision: "no", note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
326
|
-
ctx.ui.notify(`Sensitive data protected:
|
|
326
|
+
ctx.ui.notify(`Sensitive data protected: ${event.toolName} output was hidden from the AI (potential secrets detected).`, "warning");
|
|
327
327
|
return { content: [{ type: "text", text: withheldNotice(subject) }] };
|
|
328
328
|
});
|
|
329
329
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auto-permission-system",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.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",
|
|
@@ -123,7 +123,7 @@ The same options apply to a bash command whose network request reaches a host ou
|
|
|
123
123
|
- **`advanced-secure`** (recommended) — adds secret/credential detection, and **only adds asks or blocks** (never loosens):
|
|
124
124
|
- file reads whose name looks like a credential store ask before the read;
|
|
125
125
|
- risky `mcp__<server>__<tool>` calls ask before they run. The gate ignores the `mcp__<server>__` namespace, takes the operation from the head verb of the name (or the first verb of the description), and asks for destructive names, mutating names (`update_*`, `create_*`, `run_*`, `evaluate_*`), and destructive arguments (SQL `DELETE`/`UPDATE` without `WHERE`, `rm -rf`, `*` targets, HTTP `DELETE`, production targets). A documented tool with an unclassified name (`context`, `capture`, `logs`) does not ask; a tool with **no description** counts as opaque and does. Tune it under `mcp` in `sandbox.json` (ADR-032): `allowTools` / `allowPrefixes` skip asks, `askTools` forces them, `trustAnnotations` lets a server's `readOnlyHint` allow the call, `askThreshold` moves the score line (default 30). An untrusted project file may only tighten it (add `askTools`, lower `askThreshold`, turn `allowSimpleUpdates`/`trustAnnotations` off).
|
|
126
|
-
- before any tool/command/file output reaches the model, a secret-like hit shows the tool and each detected line as `Finding <lineNo>: <complete line>` — one `Finding` row per match, no truncation — plus `Question Should the AI be allowed to see it?`, with two choices — **No, keep private** or **Yes, allow**. No, keep private
|
|
126
|
+
- before any tool/command/file output reaches the model, a secret-like hit shows the tool and each detected line as `Finding <lineNo>: <complete line>` — one `Finding` row per match, no truncation — plus `Question Should the AI be allowed to see it?`, with two choices — **No, keep private** or **Yes, allow**. No, keep private hides the output and tells the model it was hidden because it may contain sensitive credentials (not a failure, not an empty result).
|
|
127
127
|
|
|
128
128
|
Switch with `/permission-mode`; all three modes are saved to the global `sandbox.json` and reflected in the footer's sandbox chip (`Sandbox: 🧠 N domains, M paths` for advanced-secure, `Sandbox: 🔒 …` for default). An untrusted project may move the mode **up** (toward stricter), never down: it cannot turn on YOLO or turn off Advanced Secure. The third choice, **YOLO**, disables every layer; the chip reads `⚠️ YOLO — all security layers disabled` until you pick another mode (ADR-020).
|
|
129
129
|
|
|
@@ -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[Security Block]
|
|
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[Security Block]
|
|
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[Security Block]
|
|
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);
|
|
@@ -163,7 +166,7 @@ async function preflightOutsideReads(
|
|
|
163
166
|
if (choice === MainChoice.Session) {
|
|
164
167
|
sessionReadGrants.push(absPath);
|
|
165
168
|
auditL1({ subject: absPath, decision: "session", scope: "session", reason: "outside-project-read", note: "preflight", cwd });
|
|
166
|
-
opts.ctx.ui?.notify?.(`
|
|
169
|
+
opts.ctx.ui?.notify?.(`sandbox: allowed read of ${absPath} for this conversation (not saved)`, "info");
|
|
167
170
|
continue;
|
|
168
171
|
}
|
|
169
172
|
if (choice !== MainChoice.Remember || !grant) {
|
|
@@ -176,10 +179,10 @@ async function preflightOutsideReads(
|
|
|
176
179
|
try {
|
|
177
180
|
const persistedTo = await opts.onAlwaysRead(subject, scope);
|
|
178
181
|
auditL1({ subject, decision: scope === "cwd" ? "always-cwd" : "always-global", scope, cwd, persisted_to: persistedTo, note: "preflight" });
|
|
179
|
-
opts.ctx.ui?.notify?.(`
|
|
182
|
+
opts.ctx.ui?.notify?.(`sandbox: allowed read of ${subject} (${scope})`, "warning");
|
|
180
183
|
} catch (e) {
|
|
181
184
|
auditL1({ subject, scope, cwd, error: String(e), note: "preflight" });
|
|
182
|
-
onData(Buffer.from(`\n❌
|
|
185
|
+
onData(Buffer.from(`\n❌ sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`));
|
|
183
186
|
return { blocked: { exitCode: 1 } };
|
|
184
187
|
}
|
|
185
188
|
}
|
|
@@ -370,7 +373,7 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
370
373
|
blockedHint = "";
|
|
371
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" });
|
|
372
375
|
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]
|
|
376
|
+
decisionHint = `\n[System Error] 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`;
|
|
374
377
|
onData(Buffer.from(decisionHint));
|
|
375
378
|
} else if (offending && outsideDenied && !hardDenied && outsideMode === "ask" && opts?.ctx?.hasUI && opts.ctx.ui?.select && opts.onAlwaysRead) {
|
|
376
379
|
const absPath = offending;
|
|
@@ -390,8 +393,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
390
393
|
if (choice === MainChoice.Once) readOnce.push(absPath);
|
|
391
394
|
else sessionReadGrants.push(absPath);
|
|
392
395
|
auditL1({ subject: absPath, decision: choice, scope: choice === MainChoice.Once ? "invocation" : "session", cwd: opts.ctx.cwd, note: "outside-project-read" });
|
|
393
|
-
opts.ctx.ui?.notify?.(`
|
|
394
|
-
onData(Buffer.from(`\n[Permission Granted]
|
|
396
|
+
opts.ctx.ui?.notify?.(`sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"}`, "info");
|
|
397
|
+
onData(Buffer.from(`\n[Permission Granted] sandbox: read of ${absPath} allowed ${choice === MainChoice.Once ? "once" : "for this conversation"}. Command output with the new permission follows.\n`));
|
|
395
398
|
resolve(attempt([]));
|
|
396
399
|
return;
|
|
397
400
|
}
|
|
@@ -400,13 +403,13 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
400
403
|
try {
|
|
401
404
|
const persistedTo = await opts.onAlwaysRead(subject, grant.scope);
|
|
402
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" });
|
|
403
|
-
opts.ctx.ui?.notify?.(`
|
|
404
|
-
onData(Buffer.from(`\n[Permission Granted]
|
|
406
|
+
opts.ctx.ui?.notify?.(`sandbox: allowed read of ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
|
|
407
|
+
onData(Buffer.from(`\n[Permission Granted] sandbox: read of ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
|
|
405
408
|
resolve(attempt([]));
|
|
406
409
|
return;
|
|
407
410
|
} catch (e) {
|
|
408
411
|
auditL1({ subject, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e), note: "outside-project-read" });
|
|
409
|
-
decisionHint = `\n[Security Notice]
|
|
412
|
+
decisionHint = `\n[Security Notice] sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
|
|
410
413
|
}
|
|
411
414
|
}
|
|
412
415
|
} catch (e) {
|
|
@@ -456,8 +459,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
456
459
|
if (choice === MainChoice.Once) {
|
|
457
460
|
grantedWriteFolders.add(parentDir);
|
|
458
461
|
auditL1({ subject: absPath, granularity: "folder", original: absPath, decision: "once", scope: "invocation", cwd: opts.ctx.cwd, note: "write-once" });
|
|
459
|
-
opts.ctx.ui?.notify?.(`
|
|
460
|
-
onData(Buffer.from(`\n[Permission Granted]
|
|
462
|
+
opts.ctx.ui?.notify?.(`sandbox: allowed write to ${absPath} once`, "info");
|
|
463
|
+
onData(Buffer.from(`\n[Permission Granted] sandbox: write to ${absPath} allowed once. Command output with the new permission follows.\n`));
|
|
461
464
|
resolve(attempt([...writeOnce, parentDir]));
|
|
462
465
|
return;
|
|
463
466
|
}
|
|
@@ -466,8 +469,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
466
469
|
sessionWriteGrants.push(value);
|
|
467
470
|
(value === parentDir ? grantedWriteFolders : grantedWriteFiles).add(value);
|
|
468
471
|
auditL1({ subject: value, granularity: value === parentDir ? "folder" : "file", original: absPath, decision: "session", scope: "session", cwd: opts.ctx.cwd, note: "write-session" });
|
|
469
|
-
opts.ctx.ui?.notify?.(`
|
|
470
|
-
onData(Buffer.from(`\n[Permission Granted]
|
|
472
|
+
opts.ctx.ui?.notify?.(`sandbox: allowed write to ${value} for this conversation`, "info");
|
|
473
|
+
onData(Buffer.from(`\n[Permission Granted] sandbox: write to ${value} allowed for this conversation. Command output with the new permission follows.\n`));
|
|
471
474
|
resolve(attempt([]));
|
|
472
475
|
return;
|
|
473
476
|
}
|
|
@@ -477,14 +480,14 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
477
480
|
const persistedTo = await opts.onAlways(subject, grant.scope);
|
|
478
481
|
(grant.folder ? grantedWriteFolders : grantedWriteFiles).add(subject);
|
|
479
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 });
|
|
480
|
-
opts.ctx.ui?.notify?.(`
|
|
481
|
-
onData(Buffer.from(`\n[Permission Granted]
|
|
483
|
+
opts.ctx.ui?.notify?.(`sandbox: allowed write to ${subject} (${grant.scope}${grant.folder ? ", folder" : ""})`, "info");
|
|
484
|
+
onData(Buffer.from(`\n[Permission Granted] sandbox: write to ${subject} allowed (${grant.scope}${grant.folder ? ", folder" : ""}). Command output with the new permission follows.\n`));
|
|
482
485
|
resolve(attempt([]));
|
|
483
486
|
return;
|
|
484
487
|
} catch (e) {
|
|
485
488
|
auditL1({ subject, granularity: grant.folder ? "folder" : "file", original: absPath, scope: grant.scope, cwd: opts.ctx.cwd, error: String(e) });
|
|
486
|
-
opts.ctx.ui?.notify?.(`
|
|
487
|
-
decisionHint = `\n[Security Notice]
|
|
489
|
+
opts.ctx.ui?.notify?.(`sandbox: could not save the permission (${e})`, "error");
|
|
490
|
+
decisionHint = `\n[Security Notice] sandbox: could not save the permission (${e}). ${subject} remains blocked.\n`;
|
|
488
491
|
}
|
|
489
492
|
}
|
|
490
493
|
} catch (e) {
|
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
|
-
`[Security Policy Notice] Output from \`${subject}\` was
|
|
485
|
+
`[Security Policy Notice] Output from \`${subject}\` was hidden 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.",
|