auto-permission-system 3.19.0 → 3.20.4
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 +210 -364
- package/extensions/permission-mode.ts +112 -73
- package/extensions/sandbox.ts +228 -138
- package/package.json +1 -1
- package/src/core/grep-filter.ts +33 -32
- package/src/core/index.ts +16 -20
- package/src/core/policy/classify.ts +27 -20
- package/src/core/policy/commands.ts +47 -30
- package/src/core/policy/fence.ts +101 -80
- package/src/core/policy/overrides.ts +31 -35
- package/src/core/policy/patterns.ts +39 -0
- package/src/core/policy/policy-file.ts +20 -36
- package/src/core/trust.ts +217 -142
- package/src/core/user-named.ts +88 -63
- package/src/detect/filename-gate/classify.ts +2 -2
- package/src/detect/filename-gate/index.ts +4 -4
- package/src/detect/index.ts +4 -4
- package/src/detect/mcp-gate/evaluate.ts +4 -4
- package/src/detect/mcp-gate/index.ts +3 -3
- package/src/detect/mcp-gate/signals.ts +3 -3
- package/src/detect/mcp-gate/verbs.ts +1 -1
- package/src/detect/sensitive-file-scanner/assignment.ts +2 -2
- package/src/detect/sensitive-file-scanner/content-scan.ts +10 -10
- package/src/detect/sensitive-file-scanner/file-scan.ts +8 -8
- package/src/detect/sensitive-file-scanner/index.ts +7 -7
- package/src/detect/sensitive-file-scanner/patterns.ts +1 -1
- package/src/detect/sensitive-file-scanner/redaction.ts +6 -6
- package/src/detect/sensitive-file-scanner/scoring.ts +3 -3
- package/src/detect/sensitive-file-scanner/value-analysis.ts +3 -3
- package/src/detect/tool-output-scanner/evaluate.ts +5 -5
- package/src/detect/tool-output-scanner/index.ts +3 -3
- package/src/detect/tool-output-scanner/scan.ts +6 -5
- package/src/detect/tool-output-scanner/size.ts +2 -2
- package/src/detect/tool-output-scanner/types.ts +1 -1
- package/src/l1-sandbox/bash-artifacts.ts +70 -0
- package/src/l1-sandbox/bash-ops.ts +130 -437
- package/src/l1-sandbox/bash-preflight.ts +292 -0
- package/src/l1-sandbox/bash-recovery.ts +566 -0
- package/src/l1-sandbox/manager.ts +0 -14
- package/src/l2-guard/detection.ts +140 -0
- package/src/l2-guard/output-gates.ts +191 -0
- package/src/l2-guard/report.ts +158 -0
- package/src/l2-guard/tool-gates.ts +189 -0
- package/src/shared/audit.ts +13 -8
- package/src/shared/canonicalize.ts +23 -20
- package/src/shared/network-grants.ts +12 -19
- package/src/shared/yolo.ts +24 -50
- package/src/ui/ask.ts +403 -225
package/extensions/guard.ts
CHANGED
|
@@ -1,426 +1,272 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi Security Guard — Layer 2 entrypoint (thin pi wiring).
|
|
3
|
-
*
|
|
4
|
-
* Hooks `tool_call` for the in-process tools the bash sandbox can't reach, and
|
|
5
|
-
* `tool_result` to filter grep output and (in Advanced Secure) gate secrets.
|
|
6
|
-
* Logic lives in `src/l2-guard/` and `src/core/`. Disabled by `--yolo`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
10
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
11
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
2
|
import { isToolCallEventType, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
-
import { classifyFilename
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
kind: "read" | "tool",
|
|
49
|
-
subject: string,
|
|
50
|
-
reason: string,
|
|
51
|
-
): Promise<{ block: true; reason: string } | null> {
|
|
52
|
-
const key = `${kind}:${subject}`;
|
|
53
|
-
if (detectionGrants.has(key)) return null;
|
|
54
|
-
if (ctx.hasUI === false) {
|
|
55
|
-
audit({ layer: 2, tool: kind, subject, reason, decision: "no", note: "advanced-secure-headless", cwd: ctx.cwd });
|
|
56
|
-
return { block: true, reason: detectionBlock(kind, reason) };
|
|
57
|
-
}
|
|
58
|
-
const header = kind === "read" ? "Read blocked — sensitive file" : "Tool call blocked — risky";
|
|
59
|
-
const body = ` ${kind === "read" ? "file" : "tool"}: ${subject}\n why: ${reason}`;
|
|
60
|
-
const choice = await askMain(ctx, header, body, { session: true, remember: false });
|
|
61
|
-
if (choice === "once") {
|
|
62
|
-
audit({ layer: 2, tool: kind, subject, reason, decision: "yes", note: "advanced-secure", cwd: ctx.cwd });
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
if (choice === "session") {
|
|
66
|
-
detectionGrants.add(key);
|
|
67
|
-
audit({ layer: 2, tool: kind, subject, reason, decision: "session", note: "advanced-secure", cwd: ctx.cwd });
|
|
68
|
-
ctx.ui.notify(`Allowed for this conversation: ${subject}`, "info");
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
audit({ layer: 2, tool: kind, subject, reason, decision: "no", note: "advanced-secure", cwd: ctx.cwd });
|
|
72
|
-
return { block: true, reason: detectionBlock(kind, reason) };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function detectMcpAsk(
|
|
76
|
-
pi: ExtensionAPI,
|
|
77
|
-
toolName: string,
|
|
78
|
-
input: Record<string, unknown>,
|
|
79
|
-
ctx: UICtx,
|
|
80
|
-
mcpPolicy?: McpPolicy,
|
|
81
|
-
): Promise<{ block: true; reason: string } | null> {
|
|
82
|
-
const info = pi.getAllTools().find((t) => t.name === toolName);
|
|
83
|
-
const result = evaluateMcpCall({
|
|
84
|
-
tool: {
|
|
85
|
-
name: toolName,
|
|
86
|
-
description: info?.description,
|
|
87
|
-
inputSchema: /* SAFETY: ToolInfo.parameters is a JSON Schema; JsonSchemaLike is its structural subset. */ info?.parameters as JsonSchemaLike | undefined,
|
|
88
|
-
annotations: /* SAFETY: pi's ToolAnnotations mirrors the MCP hints McpToolAnnotations names. */ info?.annotations as McpToolAnnotations | undefined,
|
|
89
|
-
},
|
|
90
|
-
call: { name: toolName, arguments: input },
|
|
91
|
-
policy: mcpPolicy,
|
|
92
|
-
});
|
|
93
|
-
if (result.decision !== "ask") return null;
|
|
94
|
-
|
|
95
|
-
// Include detailed call parameters or script preview if present
|
|
96
|
-
let detailedSummary = result.summary;
|
|
97
|
-
if (input && Object.keys(input).length > 0) {
|
|
98
|
-
const rawScript = input.code ?? input.script ?? input.command ?? input.query;
|
|
99
|
-
if (typeof rawScript === "string" && rawScript.includes("\n")) {
|
|
100
|
-
detailedSummary += `\n\n${rawScript}`;
|
|
101
|
-
} else {
|
|
102
|
-
const formattedArgs = JSON.stringify(input, null, 2);
|
|
103
|
-
if (formattedArgs.includes("\n")) {
|
|
104
|
-
detailedSummary += `\n\nArguments:\n${formattedArgs}`;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return askDetection(ctx, "tool", toolName, detailedSummary);
|
|
110
|
-
}
|
|
3
|
+
import { policyFileError, isProjectFileDeclined } from "../src/core/index.ts";
|
|
4
|
+
import { classifyFilename } from "../src/detect/index.ts";
|
|
5
|
+
import {
|
|
6
|
+
loadPolicy,
|
|
7
|
+
projectPolicyPath,
|
|
8
|
+
setPiDeclinedTrust,
|
|
9
|
+
untrustedProjectChanges,
|
|
10
|
+
TRUST_STORE,
|
|
11
|
+
} from "../src/l2-guard/policy.ts";
|
|
12
|
+
import { canonicalize } from "../src/l2-guard/matching.ts";
|
|
13
|
+
import { audit } from "../src/shared/audit.ts";
|
|
14
|
+
import { clearSessionGrants, type UICtx } from "../src/l2-guard/prompts.ts";
|
|
15
|
+
import { clearProgramOutputSessionGrants } from "../src/l2-guard/program-grants.ts";
|
|
16
|
+
import { programIdForToolCall } from "../src/l2-guard/program-id.ts";
|
|
17
|
+
import { emitDomainGrant } from "../src/shared/network-grants.ts";
|
|
18
|
+
import { subagentNetworkBlock } from "../src/l2-guard/subagent.ts";
|
|
19
|
+
import { onYolo } from "../src/shared/yolo.ts";
|
|
20
|
+
import {
|
|
21
|
+
askDetectionConfirmation,
|
|
22
|
+
clearDetectionGrants,
|
|
23
|
+
detectMcpAsk,
|
|
24
|
+
} from "../src/l2-guard/detection.ts";
|
|
25
|
+
import {
|
|
26
|
+
checkNetworkAccess,
|
|
27
|
+
checkReadAccess,
|
|
28
|
+
checkWriteAccess,
|
|
29
|
+
extractUrlsFromInput,
|
|
30
|
+
} from "../src/l2-guard/tool-gates.ts";
|
|
31
|
+
import {
|
|
32
|
+
clearProgramIds,
|
|
33
|
+
filterGrepToolResult,
|
|
34
|
+
gateSensitiveToolOutput,
|
|
35
|
+
recordProgramIdForCall,
|
|
36
|
+
} from "../src/l2-guard/output-gates.ts";
|
|
37
|
+
import { formatSecurityGuardReport, handleProjectTrustCommand } from "../src/l2-guard/report.ts";
|
|
111
38
|
|
|
112
39
|
export default function (pi: ExtensionAPI) {
|
|
113
|
-
let
|
|
114
|
-
let
|
|
115
|
-
let
|
|
40
|
+
let isGuardActive = false;
|
|
41
|
+
let isPolicyEnabled = false;
|
|
42
|
+
let isRuntimeYolo = false;
|
|
116
43
|
|
|
117
|
-
const
|
|
118
|
-
|
|
44
|
+
const updateActiveState = () => {
|
|
45
|
+
isGuardActive = isPolicyEnabled && !isRuntimeYolo;
|
|
119
46
|
};
|
|
120
47
|
|
|
121
48
|
onYolo(pi.events, (enabled) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
49
|
+
isRuntimeYolo = enabled;
|
|
50
|
+
updateActiveState();
|
|
51
|
+
|
|
52
|
+
if (enabled) {
|
|
53
|
+
clearDetectionGrants();
|
|
54
|
+
}
|
|
125
55
|
});
|
|
126
56
|
|
|
127
|
-
pi.on("session_start", (_event,
|
|
128
|
-
for (const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
57
|
+
pi.on("session_start", (_event, context) => {
|
|
58
|
+
for (const policyPath of [`${getAgentDir()}/extensions/sandbox.json`, `${context.cwd}/.pi/sandbox.json`]) {
|
|
59
|
+
const parseError = policyFileError(policyPath);
|
|
60
|
+
|
|
61
|
+
if (!parseError) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
audit({
|
|
66
|
+
layer: 2,
|
|
67
|
+
event: "policy-parse-error",
|
|
68
|
+
file: policyPath,
|
|
69
|
+
error: parseError,
|
|
70
|
+
cwd: context.cwd,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.error(`security-guard: ${policyPath} does not parse (${parseError}); its rules are NOT applied`);
|
|
74
|
+
|
|
75
|
+
const isProject = policyPath === `${context.cwd}/.pi/sandbox.json`;
|
|
76
|
+
const errorMessage = isProject
|
|
135
77
|
? "⚠️ Syntax error in settings file — project configuration not loaded."
|
|
136
78
|
: "⚠️ Syntax error in settings file — global configuration not loaded.";
|
|
137
|
-
|
|
79
|
+
|
|
80
|
+
context.ui.notify(errorMessage, "error");
|
|
138
81
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
82
|
+
|
|
83
|
+
setPiDeclinedTrust((context as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() === false);
|
|
84
|
+
|
|
85
|
+
const changes = untrustedProjectChanges(context.cwd);
|
|
86
|
+
|
|
87
|
+
if (changes.length && !isProjectFileDeclined(projectPolicyPath(context.cwd), TRUST_STORE)) {
|
|
88
|
+
audit({
|
|
89
|
+
layer: 2,
|
|
90
|
+
event: "untrusted-project-policy",
|
|
91
|
+
file: projectPolicyPath(context.cwd),
|
|
92
|
+
changes,
|
|
93
|
+
cwd: context.cwd,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
context.ui.notify("🛡️ Project-level configuration detected: run /security trust to approve.", "warning");
|
|
144
97
|
}
|
|
145
|
-
|
|
98
|
+
|
|
99
|
+
const policy = loadPolicy(context.cwd);
|
|
100
|
+
|
|
146
101
|
if (!policy.enabled) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
102
|
+
isPolicyEnabled = false;
|
|
103
|
+
updateActiveState();
|
|
104
|
+
context.ui.notify("Tool guard is disabled in your sandbox.json settings.", "info");
|
|
150
105
|
return;
|
|
151
106
|
}
|
|
152
|
-
|
|
153
|
-
|
|
107
|
+
|
|
108
|
+
isPolicyEnabled = true;
|
|
109
|
+
updateActiveState();
|
|
110
|
+
|
|
154
111
|
clearSessionGrants();
|
|
155
|
-
|
|
112
|
+
clearDetectionGrants();
|
|
156
113
|
clearProgramOutputSessionGrants();
|
|
157
|
-
|
|
114
|
+
clearProgramIds();
|
|
158
115
|
});
|
|
159
116
|
|
|
160
|
-
pi.on("tool_call", async (event,
|
|
161
|
-
if (!
|
|
162
|
-
|
|
117
|
+
pi.on("tool_call", async (event, context) => {
|
|
118
|
+
if (!isGuardActive) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
163
121
|
|
|
122
|
+
const policy = loadPolicy(context.cwd);
|
|
164
123
|
const programId = programIdForToolCall(event.toolName, event.input);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!reason) reason = isAskRead(rawPath, ctx.cwd, policy);
|
|
171
|
-
let outside = false;
|
|
172
|
-
if (!reason) {
|
|
173
|
-
reason = outsideProjectReason(abs, ctx.cwd, policy);
|
|
174
|
-
outside = reason !== null;
|
|
175
|
-
}
|
|
176
|
-
if (!reason) return undefined;
|
|
177
|
-
if (outside && policy.filesystem.outsideProject?.read === "deny") {
|
|
178
|
-
audit({ layer: 2, tool, subject: abs, reason, decision: "no", note: "outside-project-deny", cwd: ctx.cwd });
|
|
179
|
-
return { block: true as const, reason: denyMessage("allowRead", reason) };
|
|
180
|
-
}
|
|
181
|
-
const e = expandHome(rawPath);
|
|
182
|
-
const spelled = isAbsolute(e) ? e : resolve(ctx.cwd, e);
|
|
183
|
-
const result = await askOrBlock(/* SAFETY: pi's ctx carries cwd/UI at runtime; local UICtx names the members used. */ ctx as unknown as UICtx, { layer: 2, tool, subject: abs, reason, overrideKind: "allowRead", overrideValue: abs, outside, spelled }, isAbsoluteDeny(abs, ctx.cwd));
|
|
184
|
-
return result ?? undefined;
|
|
185
|
-
};
|
|
124
|
+
|
|
125
|
+
if (event.toolCallId && programId) {
|
|
126
|
+
recordProgramIdForCall(event.toolCallId, programId);
|
|
127
|
+
}
|
|
128
|
+
|
|
186
129
|
if (isToolCallEventType("read", event)) {
|
|
187
|
-
const result = await
|
|
188
|
-
|
|
130
|
+
const result = await checkReadAccess(event.input.path, "read", context, policy);
|
|
131
|
+
|
|
132
|
+
if (result) {
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
189
136
|
if (policy.mode === "advanced-secure") {
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
137
|
+
const canonicalPath = canonicalize(event.input.path, context.cwd);
|
|
138
|
+
const classification = classifyFilename(canonicalPath);
|
|
139
|
+
|
|
140
|
+
if (classification.risk === "strong") {
|
|
141
|
+
const verdict = await askDetectionConfirmation(
|
|
142
|
+
/* SAFETY: pi's ctx carries cwd/UI at runtime. */
|
|
143
|
+
context as unknown as UICtx,
|
|
144
|
+
"read",
|
|
145
|
+
canonicalPath,
|
|
146
|
+
classification.reasons[0] ?? "sensitive filename",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (verdict) {
|
|
150
|
+
return verdict;
|
|
151
|
+
}
|
|
195
152
|
}
|
|
196
153
|
}
|
|
197
154
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
155
|
+
|
|
156
|
+
const searchRoot =
|
|
157
|
+
isToolCallEventType("grep", event) || isToolCallEventType("find", event) || isToolCallEventType("ls", event)
|
|
158
|
+
? (event.input.path ?? ".")
|
|
159
|
+
: null;
|
|
160
|
+
|
|
201
161
|
if (searchRoot !== null) {
|
|
202
|
-
const result = await
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const reason = isDeniedWrite(event.input.path, ctx.cwd, policy);
|
|
207
|
-
if (reason) {
|
|
208
|
-
const abs = canonicalize(event.input.path, ctx.cwd);
|
|
209
|
-
const result = await askOrBlock(/* SAFETY: pi's ctx carries cwd/UI at runtime. */ ctx as unknown as UICtx, { layer: 2, tool: "write", subject: abs, reason, overrideKind: "allowWrite", overrideValue: abs }, isAbsoluteDeny(abs, ctx.cwd));
|
|
210
|
-
if (result) return result;
|
|
162
|
+
const result = await checkReadAccess(searchRoot, event.toolName, context, policy);
|
|
163
|
+
|
|
164
|
+
if (result) {
|
|
165
|
+
return result;
|
|
211
166
|
}
|
|
212
167
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
168
|
+
|
|
169
|
+
if (isToolCallEventType("write", event) || isToolCallEventType("edit", event)) {
|
|
170
|
+
const result = await checkWriteAccess(event.input.path, event.toolName, context, policy);
|
|
171
|
+
|
|
172
|
+
if (result) {
|
|
173
|
+
return result;
|
|
219
174
|
}
|
|
220
175
|
}
|
|
221
176
|
|
|
222
|
-
const
|
|
223
|
-
const collectUrls = (): string[] => {
|
|
224
|
-
if (!input) return [];
|
|
225
|
-
const urls: string[] = [];
|
|
226
|
-
if (typeof input.url === "string") urls.push(input.url);
|
|
227
|
-
if (Array.isArray(input.urls)) {
|
|
228
|
-
for (const u of input.urls) if (typeof u === "string") urls.push(u);
|
|
229
|
-
}
|
|
230
|
-
return urls;
|
|
231
|
-
};
|
|
177
|
+
const toolInput = event.input as Record<string, unknown> | undefined;
|
|
232
178
|
|
|
233
179
|
if (event.toolName === "fetch_content" || event.toolName === "get_search_content") {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
180
|
+
const subagentReason = subagentNetworkBlock(
|
|
181
|
+
context as { hasUI?: boolean; sessionManager?: unknown },
|
|
182
|
+
policy,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (subagentReason) {
|
|
186
|
+
return { block: true, reason: subagentReason };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const targetUrls = extractUrlsFromInput(toolInput);
|
|
190
|
+
const networkResult = await checkNetworkAccess(
|
|
191
|
+
targetUrls,
|
|
192
|
+
event.toolName,
|
|
193
|
+
context,
|
|
194
|
+
policy,
|
|
195
|
+
(domain) => emitDomainGrant(pi.events, domain),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (networkResult) {
|
|
199
|
+
return networkResult;
|
|
249
200
|
}
|
|
250
201
|
}
|
|
251
202
|
|
|
252
203
|
if (event.toolName === "web_search") {
|
|
253
|
-
const
|
|
254
|
-
|
|
204
|
+
const subagentReason = subagentNetworkBlock(
|
|
205
|
+
context as { hasUI?: boolean; sessionManager?: unknown },
|
|
206
|
+
policy,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (subagentReason) {
|
|
210
|
+
return { block: true, reason: subagentReason };
|
|
211
|
+
}
|
|
255
212
|
}
|
|
256
213
|
|
|
257
214
|
if (policy.mode === "advanced-secure" && event.toolName.startsWith("mcp__")) {
|
|
258
|
-
const
|
|
259
|
-
|
|
215
|
+
const verdict = await detectMcpAsk(
|
|
216
|
+
pi,
|
|
217
|
+
event.toolName,
|
|
218
|
+
(event.input ?? {}) as Record<string, unknown>,
|
|
219
|
+
/* SAFETY: pi's ctx carries cwd/UI at runtime. */
|
|
220
|
+
context as unknown as UICtx,
|
|
221
|
+
policy.mcp,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (verdict) {
|
|
225
|
+
return verdict;
|
|
226
|
+
}
|
|
260
227
|
}
|
|
261
228
|
});
|
|
262
229
|
|
|
263
|
-
pi.on("tool_result", (event,
|
|
264
|
-
if (!
|
|
265
|
-
const input = event.input as { path?: string } | undefined;
|
|
266
|
-
const policy = loadPolicy(ctx.cwd);
|
|
267
|
-
const root = canonicalize(input?.path ?? ".", ctx.cwd);
|
|
268
|
-
let rootIsDir = false;
|
|
269
|
-
try {
|
|
270
|
-
rootIsDir = statSync(root).isDirectory();
|
|
271
|
-
} catch {
|
|
230
|
+
pi.on("tool_result", (event, context) => {
|
|
231
|
+
if (!isGuardActive) {
|
|
272
232
|
return;
|
|
273
233
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const content = event.content.map((c) => {
|
|
278
|
-
if (c.type !== "text") return c;
|
|
279
|
-
const r = filterGrepOutput(c.text, (printed) => isDeniedRead(join(root, printed), ctx.cwd, policy) !== null);
|
|
280
|
-
removedLines += r.removedLines;
|
|
281
|
-
for (const f of r.removedFiles) removedFiles.add(f);
|
|
282
|
-
return { ...c, text: r.text };
|
|
283
|
-
});
|
|
284
|
-
if (removedLines === 0) return;
|
|
285
|
-
audit({ layer: 2, tool: "grep", subject: root, decision: "redacted", removedLines, removedFiles: [...removedFiles], cwd: ctx.cwd });
|
|
286
|
-
content.push({ type: "text", text: `\n[security-guard: removed ${removedLines} line(s) from ${removedFiles.size} file(s) that match the read-deny policy]` });
|
|
287
|
-
return { content };
|
|
234
|
+
|
|
235
|
+
const policy = loadPolicy(context.cwd);
|
|
236
|
+
return filterGrepToolResult(event, context, policy);
|
|
288
237
|
});
|
|
289
238
|
|
|
290
|
-
pi.on("tool_result", async (event,
|
|
291
|
-
if (!
|
|
292
|
-
const policy = loadPolicy(ctx.cwd);
|
|
293
|
-
if (policy.mode !== "advanced-secure") return;
|
|
294
|
-
const programId = event.toolCallId ? programIdByCallId.get(event.toolCallId) : undefined;
|
|
295
|
-
if (programId && isProgramOutputGranted(programId)) {
|
|
296
|
-
audit({ layer: 2, tool: event.toolName, decision: "session-grant", grant: programId, note: "advanced-secure-output", cwd: ctx.cwd });
|
|
239
|
+
pi.on("tool_result", async (event, context) => {
|
|
240
|
+
if (!isGuardActive) {
|
|
297
241
|
return;
|
|
298
242
|
}
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
if (typeof rawPath === "string" && rawPath) return rawPath;
|
|
303
|
-
if (event.toolName === "bash") {
|
|
304
|
-
const cmd = (event.input as { command?: string } | undefined)?.command;
|
|
305
|
-
return cmd ? `bash (${cmd})` : "bash";
|
|
306
|
-
}
|
|
307
|
-
return event.toolName;
|
|
308
|
-
})();
|
|
309
|
-
const { types, hits } = collectExposure(event.content, subject);
|
|
310
|
-
if (!types.length) return;
|
|
311
|
-
const findings = types;
|
|
312
|
-
const ui = /* SAFETY: pi's ctx carries cwd/UI at runtime; the local type only names the members used. */ ctx as unknown as UICtx;
|
|
313
|
-
const choice = ui.hasUI !== false ? await askExposure(ui, hits, programId) : "block";
|
|
314
|
-
if (choice === "allow" || choice === "program-session") {
|
|
315
|
-
if (choice === "program-session" && programId) {
|
|
316
|
-
grantProgramOutputSession(programId);
|
|
317
|
-
audit({ layer: 2, tool: event.toolName, decision: "session", grant: programId, note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
318
|
-
ctx.ui.notify(`Output permitted: allowed all ${programId} output for this conversation.`, "info");
|
|
319
|
-
} else {
|
|
320
|
-
audit({ layer: 2, tool: event.toolName, decision: "yes", note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
321
|
-
ctx.ui.notify(`Output permitted: allowed ${event.toolName} output for this call.`, "info");
|
|
322
|
-
}
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
audit({ layer: 2, tool: event.toolName, decision: "no", note: "advanced-secure-output", findings, cwd: ctx.cwd });
|
|
326
|
-
ctx.ui.notify(`Sensitive data protected: ${event.toolName} output was hidden from the AI (potential secrets detected).`, "warning");
|
|
327
|
-
return { content: [{ type: "text", text: withheldNotice(subject) }] };
|
|
243
|
+
|
|
244
|
+
const policy = loadPolicy(context.cwd);
|
|
245
|
+
return gateSensitiveToolOutput(event, context, policy);
|
|
328
246
|
});
|
|
329
247
|
|
|
330
248
|
pi.registerCommand?.("security", {
|
|
331
|
-
description:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (sub === "untrust") {
|
|
341
|
-
forgetProjectTrust(file, TRUST_STORE);
|
|
342
|
-
audit({ layer: 2, event: "project-untrusted", file, cwd: ctx.cwd });
|
|
343
|
-
ctx.ui.notify("OK. The file is not trusted now. Only its block rules apply.", "info");
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (projectTrusted(ctx.cwd)) {
|
|
347
|
-
ctx.ui.notify("This file is already trusted.", "info");
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const changes = untrustedProjectChanges(ctx.cwd);
|
|
351
|
-
if (!changes.length) {
|
|
352
|
-
ctx.ui.notify("This file only adds block rules. They apply already. You do not have to trust it.", "info");
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const options = ["No — ignore these changes (default)", "Yes — I wrote this file. Apply it."];
|
|
356
|
-
const title = `Do you trust this file?\n${file}\n\nIt will:\n${bullets(changes)}\n\nIf the file changes, auto-permission-system asks again.`;
|
|
357
|
-
const chosen = await ctx.ui.select(title, options, { timeout: 120_000 });
|
|
358
|
-
if (chosen !== options[1]) {
|
|
359
|
-
recordProjectDeclined(file, TRUST_STORE);
|
|
360
|
-
audit({ layer: 2, event: "project-trust-declined", file, changes, cwd: ctx.cwd });
|
|
361
|
-
ctx.ui.notify("OK. auto-permission-system ignores these changes. It does not ask again until the file changes.", "info");
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
recordProjectTrust(file, TRUST_STORE);
|
|
365
|
-
audit({ layer: 2, event: "project-trusted", file, applied: changes, cwd: ctx.cwd });
|
|
366
|
-
ctx.ui.notify("Done. The file applies now. Start a new conversation so the bash sandbox uses it too.", "info");
|
|
249
|
+
description:
|
|
250
|
+
"Show Layer 2 policy and status. /security trust | untrust: trust this project's .pi/sandbox.json as it is now, or stop trusting it",
|
|
251
|
+
handler: async (args, context) => {
|
|
252
|
+
const subcommand = String(args ?? "").trim();
|
|
253
|
+
|
|
254
|
+
if (subcommand === "trust" || subcommand === "untrust") {
|
|
255
|
+
await handleProjectTrustCommand(context, subcommand);
|
|
367
256
|
return;
|
|
368
257
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"
|
|
373
|
-
|
|
258
|
+
|
|
259
|
+
if (!isGuardActive) {
|
|
260
|
+
const inactiveMessage = isRuntimeYolo
|
|
261
|
+
? "security-guard: inactive — YOLO is on (/permission-mode to turn it off)"
|
|
262
|
+
: "security-guard: inactive (yolo or disabled)";
|
|
263
|
+
|
|
264
|
+
context.ui.notify(inactiveMessage, "info");
|
|
374
265
|
return;
|
|
375
266
|
}
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
"Security Guard (Layer 2):",
|
|
380
|
-
"",
|
|
381
|
-
` cwd: ${ctx.cwd}`,
|
|
382
|
-
` hasUI: ${(ctx as { hasUI?: boolean }).hasUI !== false}`,
|
|
383
|
-
` subagent.network: ${policy.subagent?.network ?? "allow"}`,
|
|
384
|
-
` conversation grants: ${sessionGrantSummary() || "(none)"}`,
|
|
385
|
-
` 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"}`,
|
|
386
|
-
` outside project: read ${policy.filesystem.outsideProject?.read ?? "allow"}${(policy.filesystem.outsideProject?.allowRead ?? []).length ? `, allowRead ${policy.filesystem.outsideProject?.allowRead?.join(", ")}` : ""}`,
|
|
387
|
-
"",
|
|
388
|
-
"Filesystem:",
|
|
389
|
-
` denyRead: ${policy.filesystem.denyRead.join(", ") || "(none)"}`,
|
|
390
|
-
` allowWrite: ${policy.filesystem.allowWrite.join(", ") || "(none)"}`,
|
|
391
|
-
` denyWrite: ${policy.filesystem.denyWrite.join(", ") || "(none)"}`,
|
|
392
|
-
"",
|
|
393
|
-
"Network (URL tools):",
|
|
394
|
-
` allowed: ${policy.network.allowedDomains.join(", ") || "(none)"}`,
|
|
395
|
-
` denied: ${policy.network.deniedDomains.join(", ") || "(none)"}`,
|
|
396
|
-
"",
|
|
397
|
-
"Project-local overrides (<cwd>/.pi/sandbox.json `overrides`):",
|
|
398
|
-
` allowRead: ${(overrides.allowRead ?? []).join(", ") || "(none)"}`,
|
|
399
|
-
` allowWrite: ${(overrides.allowWrite ?? []).join(", ") || "(none)"}`,
|
|
400
|
-
` allowDomains: ${(overrides.allowDomains ?? []).join(", ") || "(none)"}`,
|
|
401
|
-
];
|
|
402
|
-
if (policy.mode === "advanced-secure") {
|
|
403
|
-
const mcp = policy.mcp ?? {};
|
|
404
|
-
lines.push(
|
|
405
|
-
"",
|
|
406
|
-
"Advanced Secure (MCP gate, ADR-032):",
|
|
407
|
-
` allowTools: ${(mcp.allowTools ?? []).join(", ") || "(none)"}`,
|
|
408
|
-
` allowPrefixes: ${(mcp.allowPrefixes ?? []).join(", ") || "(none)"}`,
|
|
409
|
-
` askTools: ${(mcp.askTools ?? []).join(", ") || "(none)"}`,
|
|
410
|
-
` trustAnnotations: ${mcp.trustAnnotations === true}`,
|
|
411
|
-
` allowSimpleUpdates: ${mcp.allowSimpleUpdates !== false}`,
|
|
412
|
-
` askThreshold: ${mcp.askThreshold ?? 30}`,
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
try {
|
|
416
|
-
const tail = readFileSync(AUDIT_PATH, "utf-8").trim().split("\n").slice(-10);
|
|
417
|
-
if (tail.length && tail[0]) {
|
|
418
|
-
lines.push("", "Recent audit (last 10):");
|
|
419
|
-
for (const l of tail) lines.push(` ${l}`);
|
|
420
|
-
}
|
|
421
|
-
} catch {
|
|
422
|
-
}
|
|
423
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
267
|
+
|
|
268
|
+
const policy = loadPolicy(context.cwd);
|
|
269
|
+
context.ui.notify(formatSecurityGuardReport(policy, context), "info");
|
|
424
270
|
},
|
|
425
271
|
});
|
|
426
272
|
}
|