auto-permission-system 3.9.0 → 3.9.1
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/CHANGELOG.md +0 -9
- package/README.md +19 -20
- package/extensions/guard.ts +7 -7
- package/extensions/permission-mode.ts +4 -4
- package/extensions/sandbox.ts +1 -1
- package/package.json +8 -10
- package/skills/auto-permission-system/SKILL.md +9 -9
- package/src/core/trust.ts +1 -1
- package/src/l1-sandbox/bash-ops.ts +7 -7
- package/src/l1-sandbox/network-ask.ts +6 -6
- package/src/l2-guard/exposure.ts +5 -15
- package/src/l2-guard/program-grants.ts +5 -5
- package/src/l2-guard/program-id.ts +3 -3
- package/src/l2-guard/prompts.ts +4 -4
- package/src/ui/ask-flow.ts +10 -8
- package/src/ui/ask-selector.ts +1 -1
- package/src/ui/ask.ts +2 -2
- /package/{sandbox.cloud.json → sandbox.claude.json} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,15 +3,6 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
Entries below [1.0.0] are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from [Conventional Commits](https://www.conventionalcommits.org/) — do not edit by hand.
|
|
5
5
|
|
|
6
|
-
# [3.9.0](https://github.com/Navin009/auto-permission-system/compare/v3.8.0...v3.9.0) (2026-10-13)
|
|
7
|
-
|
|
8
|
-
### Features
|
|
9
|
-
|
|
10
|
-
* **demo:** crisp widescreen HD terminal recording (`demo.mp4`, `demo.gif`) demonstrating Cloud Auto Mode and Advanced Secure Mode
|
|
11
|
-
* **branding:** official security shield logo and banner in documentation and package
|
|
12
|
-
* **docs:** comprehensive FAQ, Cloud Auto Mode architecture, interactive prompt guides, and zero-trust documentation
|
|
13
|
-
* **metadata:** package keywords, clean npm manifest, and remove legacy PyPI references
|
|
14
|
-
|
|
15
6
|
# [2.0.0](https://github.com/Navin009/auto-permission-system/compare/v1.2.0...v2.0.0) (2026-10-02)
|
|
16
7
|
|
|
17
8
|
|
package/README.md
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
[](./LICENSE)
|
|
12
12
|
[](https://pi.dev/packages)
|
|
13
13
|
[](https://github.com/Navin009/auto-permission-system/actions/workflows/ci.yml)
|
|
14
|
-
[](#secure-auto-mode--claude-auto-mode)
|
|
15
15
|
[](#security-for-pi-how-it-works)
|
|
16
16
|
[](#permission-system--modes)
|
|
17
17
|
|
|
18
|
-
**Auto Permission System** is a zero-trust permission architecture and **Secure Auto Mode /
|
|
18
|
+
**Auto Permission System** is a zero-trust permission architecture and **Secure Auto Mode / Claude Auto Mode for the [Pi coding agent](https://pi.dev)**. It brings the confidence of **Claude's auto mode** to Pi—enabling autonomous agent workflows while actively scanning files, shell commands, and tool content to **prevent data breaches and credential exfiltration**.
|
|
19
19
|
|
|
20
20
|
Whether running interactively with keyboard-driven ask prompts or running headlessly in CI/CD pipelines, this package enforces multi-layer boundaries across filesystem access, network egress, shell commands, and MCP tools.
|
|
21
21
|
|
|
@@ -25,14 +25,12 @@ Whether running interactively with keyboard-driven ask prompts or running headle
|
|
|
25
25
|
|
|
26
26
|

|
|
27
27
|
|
|
28
|
-
> **High-Definition Video:** Watch or download the full-resolution recording at [assets/demo.mp4](https://raw.githubusercontent.com/Navin009/auto-permission-system/main/assets/demo.mp4) (H.264, 60fps). Run `./demo/run-demo.sh` locally to reproduce this live simulation.
|
|
29
|
-
|
|
30
28
|
### Demonstrated Moments in the Recording:
|
|
31
29
|
1. **Interactive `.env` Read Guard:** Intercepts `read(".env")` before the AI can see secrets. Displays a 10s countdown prompt defaulting to `No` (Block).
|
|
32
30
|
2. **Kernel OS Sandbox Confinement:** Prevents bash commands from writing to system paths outside allowed roots (`EROFS: Read-only file system`).
|
|
33
31
|
3. **Governed Network Egress:** Catches unknown outbound connections (`curl`) with an interactive domain ask prompt and a 15s countdown timer.
|
|
34
32
|
4. **Real-Time API Key & Secret Redaction:** Automatically detects exposed API tokens (`sk-proj-...`, `ghp_...`) in command output and replaces them with `***REDACTED***` before reaching the LLM context.
|
|
35
|
-
5. **Autonomous
|
|
33
|
+
5. **Autonomous Claude Auto Mode:** Demonstrates headless, fail-closed CI execution (`pi -p`) with zero deadlocks and zero hangs.
|
|
36
34
|
|
|
37
35
|
---
|
|
38
36
|
|
|
@@ -43,7 +41,7 @@ Whether running interactively with keyboard-driven ask prompts or running headle
|
|
|
43
41
|
- [Key Features at a Glance](#key-features-at-a-glance)
|
|
44
42
|
- [Security for Pi: How It Works](#security-for-pi-how-it-works)
|
|
45
43
|
- [Interactive Ask Prompts](#interactive-ask-prompts)
|
|
46
|
-
- [Secure Auto Mode &
|
|
44
|
+
- [Secure Auto Mode & Claude Auto Mode](#secure-auto-mode--claude-auto-mode)
|
|
47
45
|
- [Real-Time Secret Detection & Content Scanning](#real-time-secret-detection--content-scanning)
|
|
48
46
|
- [Permission System & Modes](#permission-system--modes)
|
|
49
47
|
- [Configuration Reference](#configuration-reference)
|
|
@@ -100,7 +98,7 @@ Once installed, the extension automatically loads whenever you launch `pi`. Chec
|
|
|
100
98
|
| Feature | Description | Protection Level |
|
|
101
99
|
|---|---|---|
|
|
102
100
|
| 🛡️ **3-Layer Defense** | OS Kernel sandbox (`bubblewrap`/`sandbox-exec`) + in-process tool guard + real-time content redactor. | Zero-Trust |
|
|
103
|
-
| ⚡ **Secure Auto Mode** | Autonomous execution inspired by Claude's auto mode; scans files and output to prevent data breaches. | Autonomous /
|
|
101
|
+
| ⚡ **Secure Auto Mode** | Autonomous execution inspired by Claude's auto mode; scans files and output to prevent data breaches. | Autonomous / Claude |
|
|
104
102
|
| 🔑 **Interactive Ask Prompts** | Real-time terminal prompts with 10s countdowns (`Once`, `Session`, `Project`, `Global`). | Interactive Human-in-the-Loop |
|
|
105
103
|
| 🔍 **AST & Regex Secret Scanner** | Scans tool stdout/stderr for API keys, private keys, and tokens, redacting them before model context. | Data Breach Prevention |
|
|
106
104
|
| 🌐 **Network Egress Gate** | Intercepts unapproved network connections and prompts for confirmation with wildcards. | Data Leak Prevention |
|
|
@@ -162,7 +160,7 @@ When an unlisted domain, `.env` file, or directory write is requested during an
|
|
|
162
160
|
|
|
163
161
|
> No
|
|
164
162
|
Yes, just this once
|
|
165
|
-
Yes, for this
|
|
163
|
+
Yes, for this conversation
|
|
166
164
|
|
|
167
165
|
Default: No (10s)
|
|
168
166
|
```
|
|
@@ -179,7 +177,7 @@ When an unlisted domain, `.env` file, or directory write is requested during an
|
|
|
179
177
|
|
|
180
178
|
> No
|
|
181
179
|
Yes, just this once
|
|
182
|
-
Yes, all in group for this
|
|
180
|
+
Yes, all in group for this conversation
|
|
183
181
|
Yes, always…
|
|
184
182
|
|
|
185
183
|
Default: Yes, just this once (15s)
|
|
@@ -191,33 +189,34 @@ When an unlisted domain, `.env` file, or directory write is requested during an
|
|
|
191
189
|
⚠ Sensitive information detected: API Secret Key
|
|
192
190
|
|
|
193
191
|
Tool bash (node verify_service.js)
|
|
194
|
-
Finding sk-(?!ant-)[A-Za-z0-9_-]
|
|
192
|
+
Finding 23: sk-(?!ant-)[A-Za-z0-9_-]
|
|
195
193
|
Question Should the AI be allowed to see it?
|
|
196
194
|
|
|
197
195
|
> No, keep private (recommended)
|
|
198
196
|
Yes, allow
|
|
199
197
|
```
|
|
198
|
+
- `Finding` prints the **complete** detected line and its line number — one `Finding` row per match when several secrets appear, with no truncation.
|
|
200
199
|
- Choosing **No, keep private** replaces the token with `***REDACTED***` in the model's context window, preventing credentials from leaking to LLM providers.
|
|
201
200
|
|
|
202
201
|
---
|
|
203
202
|
|
|
204
|
-
## Secure Auto Mode &
|
|
203
|
+
## Secure Auto Mode & Claude Auto Mode
|
|
205
204
|
|
|
206
205
|
In modern AI coding tools like Claude Code, developers love **auto mode** because the agent can execute tasks autonomously without constantly prompting for confirmation on trivial, harmless steps.
|
|
207
206
|
|
|
208
207
|
However, unconstrained auto mode is a significant data breach risk. If an autonomous agent reads credentials, runs unvetted scripts, or sends code to third-party endpoints, the entire developer environment can be compromised.
|
|
209
208
|
|
|
210
|
-
**
|
|
209
|
+
**Claude Auto Mode for Pi** delivers **Claude-style auto mode with zero-trust security**:
|
|
211
210
|
- **Autonomous Execution with Real-Time Guardrails:** The agent operates autonomously within approved boundaries, allowing rapid development while guarding sensitive resources.
|
|
212
211
|
- **Advanced Secure Scanning:** Uses `advanced-secure` mode to scan files, commands, and content to stop data breaches before they can occur.
|
|
213
212
|
- **Fail-Closed in Headless CI/CD:** When running unattended (`pi -p`, GitHub Actions, Docker, remote devboxes), unapproved operations fail closed immediately with a structured reason instead of hanging or deadlocking the pipeline.
|
|
214
213
|
|
|
215
|
-
### Activating
|
|
214
|
+
### Activating Claude Auto Mode in CI/CD:
|
|
216
215
|
|
|
217
216
|
```bash
|
|
218
|
-
# 1. Copy the shipped
|
|
217
|
+
# 1. Copy the shipped Claude Auto Mode template to your repository:
|
|
219
218
|
mkdir -p .pi
|
|
220
|
-
cp node_modules/auto-permission-system/sandbox.
|
|
219
|
+
cp node_modules/auto-permission-system/sandbox.claude.json .pi/sandbox.json
|
|
221
220
|
|
|
222
221
|
# 2. Run Pi headlessly with complete automated protection:
|
|
223
222
|
pi -p "Review pull request changes, verify dependencies, and execute test suite"
|
|
@@ -240,10 +239,10 @@ jobs:
|
|
|
240
239
|
run: |
|
|
241
240
|
npm install -g @earendil-works/pi-coding-agent
|
|
242
241
|
pi install npm:auto-permission-system
|
|
243
|
-
- name: Activate
|
|
242
|
+
- name: Activate Claude Auto Mode Preset
|
|
244
243
|
run: |
|
|
245
244
|
mkdir -p .pi
|
|
246
|
-
cp node_modules/auto-permission-system/sandbox.
|
|
245
|
+
cp node_modules/auto-permission-system/sandbox.claude.json .pi/sandbox.json
|
|
247
246
|
- name: Run Autonomous Review in Auto Mode
|
|
248
247
|
run: |
|
|
249
248
|
pi -p "Verify changes, run npm test, and flag security issues"
|
|
@@ -394,11 +393,11 @@ grep '"finding"' ~/.pi/agent/audit.log | tail -n 10
|
|
|
394
393
|
|
|
395
394
|
A selection of common questions is below. For the complete, detailed guide, read **[docs/FAQ.md](docs/FAQ.md)**.
|
|
396
395
|
|
|
397
|
-
### How does
|
|
396
|
+
### How does Claude Auto Mode secure unattended runs?
|
|
398
397
|
In headless mode (`pi -p` or subagent executions), interactive ask prompts cannot be presented to a human. Auto Permission System operates in a fail-closed posture: any tool call or bash command targeting an unlisted domain, credential file, or outside-project path is automatically blocked, and the refusal reason is provided to the agent and recorded in the audit log.
|
|
399
398
|
|
|
400
|
-
### What is the relationship between
|
|
401
|
-
Claude Code popularized autonomous "auto mode", allowing developers to let AI code without stopping for confirmation on harmless steps.
|
|
399
|
+
### What is the relationship between Claude Auto Mode and Claude's auto mode?
|
|
400
|
+
Claude Code popularized autonomous "auto mode", allowing developers to let AI code without stopping for confirmation on harmless steps. Claude Auto Mode brings this exact convenience to the Pi coding agent, but adds essential zero-trust boundaries: using Advanced Secure Mode to scan files, shell commands, and content to guarantee that autonomous execution never leads to data breaches or leaked API tokens.
|
|
402
401
|
|
|
403
402
|
### How do I allow the agent to reach a new domain?
|
|
404
403
|
Add it to `network.allowedDomains` in `<cwd>/.pi/sandbox.json` or `~/.pi/agent/extensions/sandbox.json`, or run a curl command once and select **Yes, always… → All in group in this project**.
|
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(`security-guard: allowed for this
|
|
68
|
+
ctx.ui.notify(`security-guard: allowed for this conversation (not saved) → ${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 });
|
|
@@ -288,7 +288,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
288
288
|
if (typeof rawPath === "string" && rawPath) return rawPath;
|
|
289
289
|
if (event.toolName === "bash") {
|
|
290
290
|
const cmd = (event.input as { command?: string } | undefined)?.command;
|
|
291
|
-
return cmd ? `bash
|
|
291
|
+
return cmd ? `bash (${cmd})` : "bash";
|
|
292
292
|
}
|
|
293
293
|
return event.toolName;
|
|
294
294
|
})();
|
|
@@ -301,15 +301,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
301
301
|
if (choice === "program-session" && programId) {
|
|
302
302
|
grantProgramOutputSession(programId);
|
|
303
303
|
audit({ layer: 2, tool: event.toolName, decision: "session", grant: programId, note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
304
|
-
ctx.ui.notify(
|
|
304
|
+
ctx.ui.notify(`🧠 Advanced Secure: you allowed all ${programId} output for the rest of this conversation.`, "info");
|
|
305
305
|
} else {
|
|
306
306
|
audit({ layer: 2, tool: event.toolName, decision: "yes", note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
307
|
-
ctx.ui.notify(
|
|
307
|
+
ctx.ui.notify(`🧠 Advanced Secure: you allowed ${event.toolName} output to be sent.`, "warning");
|
|
308
308
|
}
|
|
309
309
|
return;
|
|
310
310
|
}
|
|
311
311
|
audit({ layer: 2, tool: event.toolName, decision: "no", note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
312
|
-
ctx.ui.notify(
|
|
312
|
+
ctx.ui.notify(`🧠 Advanced Secure: blocked ${event.toolName} output — it may contain sensitive information. Nothing was sent to the model.`, "warning");
|
|
313
313
|
return { content: [{ type: "text", text: withheldNotice(subject) }] };
|
|
314
314
|
});
|
|
315
315
|
|
|
@@ -349,7 +349,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
349
|
}
|
|
350
350
|
recordProjectTrust(file, TRUST_STORE);
|
|
351
351
|
audit({ layer: 2, event: "project-trusted", file, applied: changes, cwd: ctx.cwd });
|
|
352
|
-
ctx.ui.notify("Done. The file applies now. Start a new
|
|
352
|
+
ctx.ui.notify("Done. The file applies now. Start a new conversation so the bash sandbox uses it too.", "info");
|
|
353
353
|
return;
|
|
354
354
|
}
|
|
355
355
|
if (!active) {
|
|
@@ -367,7 +367,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
367
367
|
` cwd: ${ctx.cwd}`,
|
|
368
368
|
` hasUI: ${(ctx as { hasUI?: boolean }).hasUI !== false}`,
|
|
369
369
|
` subagent.network: ${policy.subagent?.network ?? "allow"}`,
|
|
370
|
-
`
|
|
370
|
+
` conversation grants: ${sessionGrantSummary() || "(none)"}`,
|
|
371
371
|
` project file: ${!existsSync(projectPolicyPath(ctx.cwd)) ? "none" : projectTrusted(ctx.cwd) ? "trusted" : untrustedProjectChanges(ctx.cwd).length ? `not trusted, its weaker settings are ignored (/security trust)` : "only block rules, they apply"}`,
|
|
372
372
|
` outside project: read ${policy.filesystem.outsideProject?.read ?? "allow"}${(policy.filesystem.outsideProject?.allowRead ?? []).length ? `, allowRead ${policy.filesystem.outsideProject?.allowRead?.join(", ")}` : ""}`,
|
|
373
373
|
"",
|
|
@@ -46,7 +46,7 @@ const EXPLANATION = [
|
|
|
46
46
|
" No file contents are inspected.",
|
|
47
47
|
"",
|
|
48
48
|
"YOLO",
|
|
49
|
-
" Turns every layer off. Saved like the other modes, so new
|
|
49
|
+
" Turns every layer off. Saved like the other modes, so new conversations",
|
|
50
50
|
" start with it too until you pick Default or Advanced Secure again.",
|
|
51
51
|
].join("\n");
|
|
52
52
|
|
|
@@ -72,8 +72,8 @@ function renderSandboxChip(ctx: ExtensionContext): void {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function modeNotice(mode: PermissionMode): { text: string; level: "info" | "warning" } {
|
|
75
|
-
if (mode === "yolo") return { text: "⚠️ YOLO — all security layers disabled. Saved; new
|
|
76
|
-
if (mode === "advanced-secure") return { text: "
|
|
75
|
+
if (mode === "yolo") return { text: "⚠️ YOLO — all security layers disabled. Saved; new conversations start this way too.", level: "warning" };
|
|
76
|
+
if (mode === "advanced-secure") return { text: "🧠 Advanced Secure on. File reads, MCP calls, and tool output are checked for secrets.", level: "info" };
|
|
77
77
|
return { text: "🛡 Default mode. Rule-based policy only.", level: "info" };
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -92,7 +92,7 @@ async function pickMode(pi: ExtensionAPI, ctx: ExtensionContext, yolo: { active:
|
|
|
92
92
|
emitYolo(pi.events, true);
|
|
93
93
|
audit({ layer: 0, event: "permission-mode", mode: "yolo", cwd: ctx.cwd, note: "runtime-sync" });
|
|
94
94
|
renderSandboxChip(ctx);
|
|
95
|
-
ctx.ui.notify("⚠️ YOLO — all security layers disabled for this
|
|
95
|
+
ctx.ui.notify("⚠️ YOLO — all security layers disabled for this conversation.", "warning");
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
ctx.ui.notify(`Permission mode is already ${modeLabel(next)}.`, "info");
|
package/extensions/sandbox.ts
CHANGED
|
@@ -218,7 +218,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
218
218
|
` Allow Write: ${config.filesystem?.allowWrite?.join(", ") || "(none)"}`,
|
|
219
219
|
` Deny Write: ${config.filesystem?.denyWrite?.join(", ") || "(none)"}`,
|
|
220
220
|
` Outside project reads: ${config.filesystem?.outsideProject?.read ?? "allow"}`,
|
|
221
|
-
`
|
|
221
|
+
` Conversation grants: ${filesystemSessionGrantSummary() || "(none)"}`,
|
|
222
222
|
];
|
|
223
223
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
224
224
|
},
|
package/package.json
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auto-permission-system",
|
|
3
|
-
"version": "3.9.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.9.1",
|
|
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",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"pi-package",
|
|
9
9
|
"pi",
|
|
10
10
|
"pi-coding-agent",
|
|
11
|
+
"claude-auto-mode-for-pi",
|
|
11
12
|
"pi-extension",
|
|
12
13
|
"pi-permission",
|
|
13
14
|
"pi-permissions",
|
|
14
15
|
"pi-permission-system",
|
|
15
16
|
"pi-security",
|
|
16
|
-
"cloud-auto-mode-for-pi",
|
|
17
17
|
"security-for-pi",
|
|
18
18
|
"permission-system-for-pi",
|
|
19
19
|
"secure-pi",
|
|
20
|
-
"
|
|
21
|
-
"pi-
|
|
20
|
+
"claude-auto-mode",
|
|
21
|
+
"pi-claude-auto-mode",
|
|
22
22
|
"auto-permission-system",
|
|
23
23
|
"auto-secure",
|
|
24
24
|
"secure",
|
|
25
25
|
"secure-it",
|
|
26
26
|
"auto-mode",
|
|
27
27
|
"safe-auto-mode",
|
|
28
|
-
"claude-auto-mode",
|
|
29
28
|
"data-breach-prevention",
|
|
30
29
|
"content-scan",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"auto-mode",
|
|
30
|
+
"claude",
|
|
31
|
+
"claude-security",
|
|
34
32
|
"permission",
|
|
35
33
|
"permissions",
|
|
36
34
|
"permission-system",
|
|
@@ -91,7 +89,7 @@
|
|
|
91
89
|
"assets/",
|
|
92
90
|
"sandbox.example.json",
|
|
93
91
|
"sandbox.default.json",
|
|
94
|
-
"sandbox.
|
|
92
|
+
"sandbox.claude.json",
|
|
95
93
|
"banner.png",
|
|
96
94
|
"README.md",
|
|
97
95
|
"CHANGELOG.md",
|
|
@@ -1,11 +1,11 @@
|
|
|
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,
|
|
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Security for Pi (Pi Permission System) — Skill
|
|
7
7
|
|
|
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
|
|
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
10
|
## What this extension does
|
|
11
11
|
|
|
@@ -14,19 +14,19 @@ Wraps the `bash` tool with `sandbox-exec` (macOS) or `bubblewrap` (Linux). Block
|
|
|
14
14
|
- Filesystem writes outside `allowWrite`
|
|
15
15
|
- Filesystem reads of `denyRead` paths (defaults include `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.pi/agent`)
|
|
16
16
|
- Filesystem reads outside the project when `outsideProject.read` is `"ask"` or `"deny"` — when set, the home dir (and the project's parent) is fenced with `denyRead` and the project plus `allowWrite`/`outsideProject.allowRead` are re-exposed (ADR-014). With `"ask"`, a bash command that *plainly* names an outside path is confirmed with you **before it runs** (ADR-015): allow (`yes — this once`, or a persistent `always` grant) → the real, complete output; deny → the command is not run at all (no silently trimmed output). `install`/`npm` under `~/.pi/agent` are pre-allowed via `outsideProject.allowRead`, so they never prompt. Obfuscated reads still hit the fence and stay masked.
|
|
17
|
-
- Network egress to domains not in `allowedDomains`: a host in neither list asks before the proxy decides (ADR-023) — `yes — this once` covers that bash command's connections to the host, `yes — for this
|
|
17
|
+
- Network egress to domains not in `allowedDomains`: a host in neither list asks before the proxy decides (ADR-023) — `yes — this once` covers that bash command's connections to the host, `yes — for this conversation` lasts until the conversation ends, and `always` writes `overrides.allowDomains` and applies live. A host in `deniedDomains`, or `network.strictAllowlist: true`, is a hard deny that is never ask-able; headless blocks.
|
|
18
18
|
|
|
19
19
|
**Layer 2 — In-process tool guard**
|
|
20
20
|
Hooks `tool_call` for `read`, `grep`, `find`, `ls`, `write`, `edit`, `fetch_content`, `web_search`, `get_search_content`. Applies the same policy file as Layer 1. For `grep`, `find` and `ls` the search root is checked like a `read` path. A `tool_result` hook also removes `grep` output lines from denied files beneath an allowed root (for example a `.env` deep in the tree); approving the root does not approve those files. When a call is blocked, shows an interactive prompt with persistence options.
|
|
21
21
|
|
|
22
|
-
**Layer 3 — Subagent posture &
|
|
23
|
-
When `ctx.hasUI === false` (subagents, `-p`, JSON mode, CI/CD runs), applies the `subagent.network` policy: `allow` (default) | `deny` | `research-only`. In
|
|
22
|
+
**Layer 3 — Subagent posture & Claude Auto Mode**
|
|
23
|
+
When `ctx.hasUI === false` (subagents, `-p`, JSON mode, CI/CD runs), applies the `subagent.network` policy: `allow` (default) | `deny` | `research-only`. In Claude Auto Mode, unapproved prompts fail closed.
|
|
24
24
|
|
|
25
25
|
## Policy files
|
|
26
26
|
|
|
27
27
|
Merged in order (later wins):
|
|
28
28
|
1. `sandbox.default.json` — shipped baseline (`outsideProject.read: "ask"`, full `denyRead`/`denyWrite`, `modelDenyRead`)
|
|
29
|
-
2. `sandbox.
|
|
29
|
+
2. `sandbox.claude.json` — shipped Claude Auto Mode preset for headless, Docker, and CI/CD runs (`strictAllowlist: true`, `outsideProject.read: "deny"`)
|
|
30
30
|
3. `~/.pi/agent/extensions/sandbox.json` — global user policy
|
|
31
31
|
4. `<cwd>/.pi/sandbox.json` — project-local overrides (also written by ask-tier prompts)
|
|
32
32
|
|
|
@@ -87,7 +87,7 @@ A project `.pi/sandbox.json` applies in full only after the user trusted its exa
|
|
|
87
87
|
When a normal (non-absolute-deny) call is blocked:
|
|
88
88
|
- **no — block** — hard deny (default, pre-selected)
|
|
89
89
|
- **yes — this once** — allow just this call
|
|
90
|
-
- **yes — for this
|
|
90
|
+
- **yes — for this conversation** — this file, its parent folder, or this domain until the conversation ends; not saved
|
|
91
91
|
- **always for CURRENT project** — whitelist this file in `<cwd>/.pi/sandbox.json`
|
|
92
92
|
- **always for CURRENT project (folder)** — whitelist the parent directory
|
|
93
93
|
- **always for ALL projects** — whitelist in `~/.pi/agent/extensions/sandbox.json`
|
|
@@ -99,7 +99,7 @@ For a Layer 1 bash **write** blocked after the command already ran, `yes — thi
|
|
|
99
99
|
|
|
100
100
|
Every prompt waits **15 seconds** by default and shows it live as `Default: … (Ns)` under the body. For file reads/writes no answer means the safe default: **block / deny**, and Esc blocks. For a network ask, an unanswered countdown means **allow once** (Esc still denies) — ADR-024. In the TUI the `Default:` line disappears as soon as you press ↑/↓ and the timer restarts, so the hint never competes with an active choice (ADR-031). The prompt is keyboard-only (ADR-036): ↑/↓ move the highlight, Enter selects, Esc cancels — a mouse click or wheel over the prompt does nothing.
|
|
101
101
|
|
|
102
|
-
The same options apply to a bash command whose network request reaches a host outside `allowedDomains` (ADR-023), with the countdown default and live footer from ADR-024/ADR-031: `No` / `Yes, just this once` / `Yes, all in group for this
|
|
102
|
+
The same options apply to a bash command whose network request reaches a host outside `allowedDomains` (ADR-023), with the countdown default and live footer from ADR-024/ADR-031: `No` / `Yes, just this once` / `Yes, all in group for this conversation` / `Yes, always…`, and an untouched countdown counts as allow-once. Because the sandbox proxy asks per connection, `Allow (default)` covers that bash command rather than one connection — redirects and parallel connections to the same host do not re-prompt. A host in `deniedDomains`, or `network.strictAllowlist: true`, is blocked outright: the prompt never appears. A remembered host is written to `overrides.allowDomains` and takes effect immediately, so the waiting request completes without a retry. Prompts from both layers are serialized through one queue (`src/ui/ask-flow.ts`), so two unknown hosts cannot replace each other's selector and hang the waiting connection.
|
|
103
103
|
|
|
104
104
|
## Sensitive reads ask
|
|
105
105
|
|
|
@@ -113,7 +113,7 @@ The same options apply to a bash command whose network request reaches a host ou
|
|
|
113
113
|
- **`advanced-secure`** (recommended) — adds secret/credential detection, and **only adds asks or blocks** (never loosens):
|
|
114
114
|
- file reads whose name looks like a credential store ask before the read;
|
|
115
115
|
- 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).
|
|
116
|
-
- before any tool/command/file output reaches the model, a secret-like hit shows the
|
|
116
|
+
- 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 withholds the output and tells the model it was withheld because it may contain sensitive information (not a failure, not an empty result).
|
|
117
117
|
|
|
118
118
|
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).
|
|
119
119
|
|
package/src/core/trust.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function isProjectFileDeclined(projectPath: string, storePath: string): b
|
|
|
67
67
|
* and share only the store file (ADR-018/ADR-020).
|
|
68
68
|
*/
|
|
69
69
|
export interface ProjectTrust {
|
|
70
|
-
/** Record pi's own project-trust answer for this
|
|
70
|
+
/** Record pi's own project-trust answer for this conversation. Declined is never trusted. */
|
|
71
71
|
setDeclined(value: boolean): void;
|
|
72
72
|
/** The project file applies in full only when its current hash is recorded and pi did not decline. */
|
|
73
73
|
isTrusted(cwd: string): boolean;
|
|
@@ -40,7 +40,7 @@ function linkRow(absPath: string, spelled: string | undefined): string[] {
|
|
|
40
40
|
const auditL1 = (entry: Record<string, unknown>) => audit({ layer: 1, tool: "bash", ...entry });
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* In-memory filesystem grants from "Yes, for this
|
|
43
|
+
* In-memory filesystem grants from "Yes, for this conversation" (ADR-010, ADR-012).
|
|
44
44
|
* A read grant is the absolute path; a write grant is the parent directory (a
|
|
45
45
|
* whole path would not let a new file be created — ADR-021), falling back to the
|
|
46
46
|
* exact path when the parent is an unsafe folder to grant (root, home, …).
|
|
@@ -48,7 +48,7 @@ const auditL1 = (entry: Record<string, unknown>) => audit({ layer: 1, tool: "bas
|
|
|
48
48
|
*/
|
|
49
49
|
const sessionReadGrants: string[] = [];
|
|
50
50
|
const sessionWriteGrants: string[] = [];
|
|
51
|
-
/** askRead (.env)
|
|
51
|
+
/** askRead (.env) conversation grants, kept apart so they never leak into allowRead (ADR-019). */
|
|
52
52
|
const sessionAskReadGrants: string[] = [];
|
|
53
53
|
|
|
54
54
|
export function clearFilesystemSessionGrants(): void {
|
|
@@ -158,7 +158,7 @@ async function preflightOutsideReads(
|
|
|
158
158
|
if (choice === MainChoice.Session) {
|
|
159
159
|
sessionReadGrants.push(absPath);
|
|
160
160
|
auditL1({ subject: absPath, decision: "session", scope: "session", reason: "outside-project-read", note: "preflight", cwd });
|
|
161
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} for this
|
|
161
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} for this conversation (not saved)`, "info");
|
|
162
162
|
continue;
|
|
163
163
|
}
|
|
164
164
|
if (choice !== MainChoice.Remember || !grant) {
|
|
@@ -366,8 +366,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
366
366
|
if (choice === MainChoice.Once) readOnce.push(absPath);
|
|
367
367
|
else sessionReadGrants.push(absPath);
|
|
368
368
|
auditL1({ subject: absPath, decision: choice, scope: choice === MainChoice.Once ? "invocation" : "session", cwd: opts.ctx.cwd, note: "outside-project-read" });
|
|
369
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this
|
|
370
|
-
onData(Buffer.from(`\n✅ pi-sandbox: read of ${absPath} allowed ${choice === MainChoice.Once ? "once" : "for this
|
|
369
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed read of ${absPath} ${choice === MainChoice.Once ? "once" : "for this conversation"} — re-running the command`, "warning");
|
|
370
|
+
onData(Buffer.from(`\n✅ pi-sandbox: read of ${absPath} allowed ${choice === MainChoice.Once ? "once" : "for this conversation"}. Command output with the new permission follows.\n`));
|
|
371
371
|
resolve(attempt([]));
|
|
372
372
|
return;
|
|
373
373
|
}
|
|
@@ -441,8 +441,8 @@ export function createSandboxedBashOps(opts?: SandboxedBashOpts): BashOperations
|
|
|
441
441
|
sessionWriteGrants.push(value);
|
|
442
442
|
(value === parentDir ? grantedWriteFolders : grantedWriteFiles).add(value);
|
|
443
443
|
auditL1({ subject: value, granularity: value === parentDir ? "folder" : "file", original: absPath, decision: "session", scope: "session", cwd: opts.ctx.cwd, note: "write-session" });
|
|
444
|
-
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${value} for this
|
|
445
|
-
onData(Buffer.from(`\n✅ pi-sandbox: write to ${value} allowed for this
|
|
444
|
+
opts.ctx.ui?.notify?.(`pi-sandbox: allowed ${value} for this conversation (not saved) — re-running the command`, "info");
|
|
445
|
+
onData(Buffer.from(`\n✅ pi-sandbox: write to ${value} allowed for this conversation. Command output with the new permission follows.\n`));
|
|
446
446
|
resolve(attempt([]));
|
|
447
447
|
return;
|
|
448
448
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses the same two-screen ask-tier as Layer 2 (once / session / remember
|
|
5
5
|
* project|global) and the same grant semantics:
|
|
6
6
|
*
|
|
7
|
-
* -
|
|
7
|
+
* - conversation grants: in memory only, cleared at `session_start`, never saved;
|
|
8
8
|
* - "once" grants: one bash command's connections to that host (the proxy
|
|
9
9
|
* calls back per request, so a redirect or a parallel connection must not
|
|
10
10
|
* re-prompt), scoped by `beginNetworkCommand`/`endNetworkCommand`;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Headless (`hasUI === false`) denies, like every other gate.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Conversation grants use the parent-domain wildcard when useful (covers all sibling
|
|
18
18
|
* subdomains). Screen 2 hides the "in this project" rows in untrusted
|
|
19
19
|
* projects so the user can't quietly trust a widening file.
|
|
20
20
|
*/
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { domainMatches, GrantScope, OverrideKind } from "../core/index";
|
|
23
23
|
import { askMainWithRemember, askRememberHost, ASK_TIMEOUT_MS, MainChoice, parentDomainWildcard, type AskCtx, type HostGrant } from "../ui/ask-flow";
|
|
24
24
|
|
|
25
|
-
/** In-memory
|
|
25
|
+
/** In-memory conversation grants ("Yes, for this conversation"). Cleared at session_start. */
|
|
26
26
|
const sessionGrants: string[] = [];
|
|
27
27
|
/** Hosts allowed for the current bash command ("Yes, just this once" covers its connections). */
|
|
28
28
|
let commandGrants: Set<string> | null = null;
|
|
@@ -87,7 +87,7 @@ function firstWord(command: string | undefined): string | null {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Screen 1 (once / session / remember) plus, for remember, screen 2 (scope and
|
|
90
|
-
* pattern). A
|
|
90
|
+
* pattern). A conversation grant uses the parent-domain wildcard when the host has
|
|
91
91
|
* a useful parent, so any sibling subdomain is covered for the rest of the
|
|
92
92
|
* session; apex hosts (2 parts) fall back to the exact host because
|
|
93
93
|
* `*.example.com` would not match `example.com`.
|
|
@@ -124,7 +124,7 @@ async function decide(deps: NetworkAskDeps, host: string, port: number | undefin
|
|
|
124
124
|
bodyParts.join("\n"),
|
|
125
125
|
{
|
|
126
126
|
session: true,
|
|
127
|
-
sessionLabel: "Yes, all in group for this
|
|
127
|
+
sessionLabel: "Yes, all in group for this conversation",
|
|
128
128
|
allowFirst: true,
|
|
129
129
|
icon: "net",
|
|
130
130
|
timeoutMs: ASK_TIMEOUT_MS,
|
|
@@ -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?.(`pi-sandbox: allowed for this
|
|
164
|
+
ctx.ui.notify?.(`pi-sandbox: allowed for this conversation (not saved) → ${wildcard}`, "info");
|
|
165
165
|
return true;
|
|
166
166
|
}
|
|
167
167
|
|
package/src/l2-guard/exposure.ts
CHANGED
|
@@ -5,15 +5,10 @@ import { scanToolOutput } from "../detect/index";
|
|
|
5
5
|
export interface Exposure {
|
|
6
6
|
/** Finding types, e.g. ["SECRET_ASSIGNMENT"]. Empty means clean. */
|
|
7
7
|
types: string[];
|
|
8
|
-
/** One block: `
|
|
8
|
+
/** One block: `Tool` header then `Finding <lineNo>: <line>` per detected line. */
|
|
9
9
|
hits: string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const MAX_LINES = 8;
|
|
13
|
-
const MAX_LINE_CHARS = 200;
|
|
14
|
-
|
|
15
|
-
const clip = (s: string) => (s.length > MAX_LINE_CHARS ? `${s.slice(0, MAX_LINE_CHARS)}…` : s);
|
|
16
|
-
|
|
17
12
|
export function collectExposure(content: ReadonlyArray<{ type: string; text?: string }>, subject: string): Exposure {
|
|
18
13
|
const types = new Set<string>();
|
|
19
14
|
const lines: string[] = [];
|
|
@@ -29,14 +24,9 @@ export function collectExposure(content: ReadonlyArray<{ type: string; text?: st
|
|
|
29
24
|
if (after[i] !== before[i] && before[i].trim()) matchedIndices.push(i);
|
|
30
25
|
}
|
|
31
26
|
if (matchedIndices.length === 0) continue;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const moreMatched = matchedIndices.length > MAX_LINES;
|
|
36
|
-
const moreBelow = lastIdx < before.length - 1;
|
|
37
|
-
if (firstIdx > 0) lines.push(" \u2026");
|
|
38
|
-
for (const idx of shown) lines.push(`${idx + 1}: ${clip(before[idx].trim())}`);
|
|
39
|
-
if (moreMatched || moreBelow) lines.push(" \u2026");
|
|
27
|
+
lines.push(`Tool ${subject}`);
|
|
28
|
+
// One `Finding` row per detected line, showing the complete line (no clipping).
|
|
29
|
+
for (const idx of matchedIndices) lines.push(`Finding ${idx + 1}: ${before[idx].trim()}`);
|
|
40
30
|
}
|
|
41
|
-
return { types: [...types], hits: lines.length ? [
|
|
31
|
+
return { types: [...types], hits: lines.length ? [lines.join("\n")] : [] };
|
|
42
32
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-memory
|
|
2
|
+
* In-memory conversation grants for the Advanced Secure output gate (ADR-018).
|
|
3
3
|
*
|
|
4
4
|
* When a CLI like `composio` returns output that matches a secret pattern
|
|
5
5
|
* (JWT, OAuth refresh token, etc.), the user can pick "Yes, all <program>
|
|
6
|
-
* output for this
|
|
6
|
+
* output for this conversation" instead of approving every block. The program id
|
|
7
7
|
* is added here, and the output gate short-circuits for the rest of the
|
|
8
|
-
*
|
|
8
|
+
* conversation — same shape as path/host conversation grants, but for tool output.
|
|
9
9
|
*
|
|
10
10
|
* Cleared at session_start (ADR-010), never written to disk. The credential
|
|
11
11
|
* tier (denyRead / askRead) still wins on the file side; this only loosens
|
|
@@ -19,12 +19,12 @@ export function clearProgramOutputSessionGrants(): void {
|
|
|
19
19
|
programOutputSessionGrants.clear();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** Add a program to the
|
|
22
|
+
/** Add a program to the conversation grant list. Idempotent. */
|
|
23
23
|
export function grantProgramOutputSession(programId: string): void {
|
|
24
24
|
programOutputSessionGrants.add(programId.toLowerCase());
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** True when the program is already granted for the rest of the
|
|
27
|
+
/** True when the program is already granted for the rest of the conversation. */
|
|
28
28
|
export function isProgramOutputGranted(programId: string | undefined): boolean {
|
|
29
29
|
if (!programId) return false;
|
|
30
30
|
return programOutputSessionGrants.has(programId.toLowerCase());
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Program-id detection for the Advanced Secure output gate.
|
|
3
3
|
*
|
|
4
|
-
* Used to label the
|
|
4
|
+
* Used to label the Advanced Secure output prompt with the binary
|
|
5
5
|
* that produced it (`composio`, `aws`, `gcloud`, …) and to key the per-program
|
|
6
|
-
*
|
|
6
|
+
* conversation grant (`isProgramOutputGranted`). Pure functions, no pi / fs / audit.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** Strip env assignments (`FOO=bar`), common wrappers (sudo, nice, command, …), and the
|
|
@@ -27,7 +27,7 @@ export function bashBinary(command: string | undefined): string | undefined {
|
|
|
27
27
|
|
|
28
28
|
/** The program id for a tool call. Bash binaries only — MCP and other tools return
|
|
29
29
|
* `undefined` so the Advanced Secure output gate doesn't offer them a per-program
|
|
30
|
-
*
|
|
30
|
+
* conversation grant (MCP tools handle their own secrets and don't share the
|
|
31
31
|
* composio-style "same binary, same surface" pattern). */
|
|
32
32
|
export function programIdForToolCall(toolName: string | undefined, input: unknown): string | undefined {
|
|
33
33
|
if (!toolName) return undefined;
|
package/src/l2-guard/prompts.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Layer 2 ask-tier prompts,
|
|
2
|
+
* Layer 2 ask-tier prompts, conversation grants, and persisted "always" overrides
|
|
3
3
|
* (ADR-009, ADR-010). Owns the interactive UI calls; policy decisions come from
|
|
4
4
|
* matching.ts.
|
|
5
5
|
*/
|
|
@@ -32,7 +32,7 @@ export function persistOverride(scope: GrantScope, cwd: string, kind: OverrideKi
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Grants from "yes — for this
|
|
35
|
+
* Grants from "yes — for this conversation" (ADR-010). In memory only, cleared on
|
|
36
36
|
* session_start, never written to sandbox.json, never consulted for the
|
|
37
37
|
* absolute-deny tier.
|
|
38
38
|
*/
|
|
@@ -67,7 +67,7 @@ export type UICtx = {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Ask the user, then apply the answer:
|
|
70
|
+
* Ask the user, then apply the answer: conversation grants stay in memory, "always"
|
|
71
71
|
* grants are persisted and announced to the other layer via `onPersist`.
|
|
72
72
|
* Headless asks deny. A `null` return means the call may proceed.
|
|
73
73
|
*/
|
|
@@ -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(`security-guard: allowed for this
|
|
105
|
+
ctx.ui.notify(`security-guard: allowed for this conversation (not saved) → ${value}`, "info");
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
108
|
if (decision === Decision.AlwaysCwd || decision === Decision.AlwaysGlobal || decision === Decision.AlwaysCwdFolder || decision === Decision.AlwaysGlobalFolder) {
|
package/src/ui/ask-flow.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* it only calls the caller's `ctx.ui.select`, so the contract tests can script it.
|
|
4
4
|
*
|
|
5
5
|
* Every prompt is a question. Answers are uniformly `No` / `Yes, just this once` /
|
|
6
|
-
* `Yes, for this
|
|
6
|
+
* `Yes, for this conversation` / `Yes, always…` so row 2 is always "once". Screen 2
|
|
7
7
|
* collapses to "All in folder · in this project" / "Only <name> · in this project" /
|
|
8
8
|
* "All in folder · in all projects" / "Only <name> · in all projects" — folder /
|
|
9
9
|
* wildcard is preselected (the wider grant is the right answer almost every time).
|
|
@@ -294,10 +294,10 @@ function withIcon(icon: string | undefined, header: string): string {
|
|
|
294
294
|
export type MainOpts = {
|
|
295
295
|
/** Offer `Yes, just this once` (default on). */
|
|
296
296
|
once?: boolean;
|
|
297
|
-
/** Offer `Yes, for this
|
|
297
|
+
/** Offer `Yes, for this conversation` (opt-in). */
|
|
298
298
|
session?: boolean;
|
|
299
|
-
/** Override the session label so the caller can name what the
|
|
300
|
-
* (`Yes, for this
|
|
299
|
+
/** Override the session label so the caller can name what the conversation grant covers
|
|
300
|
+
* (`Yes, for this conversation` for files, `Yes, all in group for this conversation` for networks). */
|
|
301
301
|
sessionLabel?: string;
|
|
302
302
|
/** Offer `Yes, always…` (default on). */
|
|
303
303
|
remember?: boolean;
|
|
@@ -322,7 +322,7 @@ export type MainOpts = {
|
|
|
322
322
|
* highlighted.
|
|
323
323
|
*/
|
|
324
324
|
export async function askMain(ctx: AskCtx, header: string, body: string, opts: MainOpts = {}): Promise<MainChoice> {
|
|
325
|
-
const sessionLabel = opts.sessionLabel ?? "Yes, for this
|
|
325
|
+
const sessionLabel = opts.sessionLabel ?? "Yes, for this conversation";
|
|
326
326
|
const choices: Array<{ label: string; value: MainChoice }> = [];
|
|
327
327
|
choices.push({ label: "No", value: MainChoice.Block });
|
|
328
328
|
if (opts.once !== false) choices.push({ label: "Yes, just this once", value: MainChoice.Once });
|
|
@@ -444,19 +444,21 @@ export type ExposureChoice = (typeof ExposureChoice)[keyof typeof ExposureChoice
|
|
|
444
444
|
|
|
445
445
|
/** Advanced Secure output gate (ADR-018): show where it was found and ask before the output reaches the model.
|
|
446
446
|
* When `programId` is set, an extra option lets the user grant the rest of the
|
|
447
|
-
*
|
|
447
|
+
* conversation for that program — same pattern as path/host conversation grants, but for
|
|
448
448
|
* tool output (display, not access). */
|
|
449
449
|
export async function askExposure(ctx: AskCtx, hits: string[], programId?: string): Promise<ExposureChoice> {
|
|
450
450
|
const choices: Array<{ label: string; value: ExposureChoice }> = [
|
|
451
451
|
{ label: "No, keep it hidden (recommended)", value: ExposureChoice.Block },
|
|
452
452
|
{ label: "Yes, show it this once", value: ExposureChoice.Allow },
|
|
453
453
|
];
|
|
454
|
-
if (programId) choices.push({ label: `Yes, all ${programId} output for this
|
|
454
|
+
if (programId) choices.push({ label: `Yes, all ${programId} output for this conversation`, value: ExposureChoice.ProgramSession });
|
|
455
455
|
const titleParts = [
|
|
456
|
-
`${ICON.
|
|
456
|
+
`${ICON.warn} Sensitive information detected`,
|
|
457
457
|
"",
|
|
458
458
|
hits.join("\n\n"),
|
|
459
459
|
"",
|
|
460
|
+
"Question Should the AI be allowed to see it?",
|
|
461
|
+
"",
|
|
460
462
|
"Note you'll be asked again each time a secret shows up",
|
|
461
463
|
];
|
|
462
464
|
const { picked } = await askSelect(ctx, titleParts.join("\n"), choices.map((c) => c.label), undefined, { footer: "Default: No" });
|
package/src/ui/ask-selector.ts
CHANGED
package/src/ui/ask.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ADR-030.
|
|
5
5
|
*
|
|
6
6
|
* Every prompt is a question. Answers are uniformly `No` / `Yes, just this once` /
|
|
7
|
-
* `Yes, for this
|
|
7
|
+
* `Yes, for this conversation` / `Yes, always…`. Body uses the canonical field set
|
|
8
8
|
* File / Folder / Site / Group / Command / Why / Risk / Note. Absolute-deny
|
|
9
9
|
* (credentials) collapses to ONE screen with `No` preselected. Untrusted
|
|
10
10
|
* project: screen 2 hides the `in this project` rows and adds a Note.
|
|
@@ -192,7 +192,7 @@ export async function askDecision(ctx: AskCtx, ask: AskKind, absoluteDenyPattern
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
const header = headerFor(action, ask.subject, ask);
|
|
195
|
-
const sessionLabel = action === AskAction.Network ? "Yes, all in group for this
|
|
195
|
+
const sessionLabel = action === AskAction.Network ? "Yes, all in group for this conversation" : "Yes, for this conversation";
|
|
196
196
|
const untrusted = ask.projectTrusted === false;
|
|
197
197
|
const mainOpts = {
|
|
198
198
|
session: true,
|
|
File without changes
|