Skip to content

Commit add625d

Browse files
committed
feat: add getScopeRiskColor function for permission scope risk mapping
1 parent 104acff commit add625d

4 files changed

Lines changed: 135 additions & 5 deletions

File tree

src/common/permissions.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ export function computeToolCallPermissions(options: ComputeToolCallPermissionsOp
164164
const permission = evaluatePermissionScopes(request.scopes, options.settings);
165165
permissions.push({ toolCallId: toolCall.id, permission });
166166
if (permission === "ask") {
167+
const askScopes = getPermissionScopesRequiringAsk(request.scopes, options.settings);
167168
askPermissions.push({
168169
toolCallId: toolCall.id,
169-
scopes: request.scopes,
170+
scopes: askScopes.length > 0 ? askScopes : request.scopes,
170171
name: request.name,
171172
command: request.command,
172173
description: request.description,
@@ -285,6 +286,38 @@ export function evaluatePermissionScopes(
285286
return settings.defaultMode === "askAll" ? "ask" : "allow";
286287
}
287288

289+
export function getPermissionScopesRequiringAsk(
290+
scopes: AskPermissionScope[],
291+
settings: Required<PermissionSettings> = {
292+
allow: [],
293+
deny: [],
294+
ask: [],
295+
defaultMode: "allowAll",
296+
}
297+
): AskPermissionScope[] {
298+
const result: AskPermissionScope[] = [];
299+
for (const scope of scopes) {
300+
if (scope === "unknown") {
301+
result.push(scope);
302+
continue;
303+
}
304+
if (settings.deny.includes(scope)) {
305+
continue;
306+
}
307+
if (settings.ask.includes(scope)) {
308+
result.push(scope);
309+
continue;
310+
}
311+
if (settings.allow.includes(scope)) {
312+
continue;
313+
}
314+
if (settings.defaultMode === "askAll") {
315+
result.push(scope);
316+
}
317+
}
318+
return result;
319+
}
320+
288321
export function parseBashSideEffects(value: unknown): AskPermissionScope[] {
289322
const validScopes = new Set<AskPermissionScope>([
290323
"read-in-cwd",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { test } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { getScopeRiskColor } from "../ui/PermissionPrompt";
4+
5+
test("getScopeRiskColor maps permission scopes by risk", () => {
6+
assert.equal(getScopeRiskColor("read-in-cwd"), "#22c55e");
7+
assert.equal(getScopeRiskColor("query-git-log"), "#22c55e");
8+
9+
assert.equal(getScopeRiskColor("read-out-cwd"), "#f59e0b");
10+
assert.equal(getScopeRiskColor("write-in-cwd"), "#f59e0b");
11+
assert.equal(getScopeRiskColor("network"), "#f59e0b");
12+
assert.equal(getScopeRiskColor("mcp"), "#f59e0b");
13+
14+
assert.equal(getScopeRiskColor("write-out-cwd"), "#ef4444");
15+
assert.equal(getScopeRiskColor("delete-in-cwd"), "#ef4444");
16+
assert.equal(getScopeRiskColor("delete-out-cwd"), "#ef4444");
17+
assert.equal(getScopeRiskColor("mutate-git-log"), "#ef4444");
18+
assert.equal(getScopeRiskColor("unknown"), "#ef4444");
19+
});

src/tests/permissions.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,39 @@ test("computeToolCallPermissions maps tool calls to permission requests", () =>
9393
);
9494
});
9595

96+
test("computeToolCallPermissions only asks for scopes not already allowed", () => {
97+
const projectRoot = createTempDir("deepcode-permissions-filter-workspace-");
98+
const plan = computeToolCallPermissions({
99+
sessionId: "session-1",
100+
projectRoot,
101+
settings: {
102+
allow: ["read-in-cwd"],
103+
deny: [],
104+
ask: [],
105+
defaultMode: "askAll",
106+
},
107+
toolCalls: [
108+
{
109+
id: "call-bash",
110+
type: "function",
111+
function: {
112+
name: "bash",
113+
arguments: JSON.stringify({
114+
command: "curl -s http://localhost:8899/ && ls index.html",
115+
sideEffects: ["network", "read-in-cwd"],
116+
}),
117+
},
118+
},
119+
],
120+
});
121+
122+
assert.deepEqual(plan.permissions, [{ toolCallId: "call-bash", permission: "ask" }]);
123+
assert.deepEqual(
124+
plan.askPermissions.map((item) => ({ id: item.toolCallId, scopes: item.scopes })),
125+
[{ id: "call-bash", scopes: ["network"] }]
126+
);
127+
});
128+
96129
test("appendProjectPermissionAllows writes unique project-level allow scopes", () => {
97130
const projectRoot = createTempDir("deepcode-permission-settings-");
98131
const settingsPath = path.join(projectRoot, ".deepcode", "settings.json");

src/ui/PermissionPrompt.tsx

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ type ScopePrompt = {
2020
scope: AskPermissionScope;
2121
};
2222

23+
type PromptOption = {
24+
kind: "allow" | "always" | "deny";
25+
label: string;
26+
scopeDescription?: string;
27+
scopeColor?: string;
28+
};
29+
2330
const ALWAYS_ALLOWED_SCOPES = new Set<AskPermissionScope>([
2431
"read-in-cwd",
2532
"read-out-cwd",
@@ -138,7 +145,7 @@ export function PermissionPrompt({ requests, onSubmit, onCancel }: Props): React
138145
{options.map((option, optionIndex) => (
139146
<Text key={option.kind} color={optionIndex === cursor ? "cyanBright" : undefined}>
140147
{optionIndex === cursor ? "> " : " "}
141-
{optionIndex + 1}. {option.label}
148+
{optionIndex + 1}. {renderOptionLabel(option)}
142149
</Text>
143150
))}
144151
</Box>
@@ -149,6 +156,18 @@ export function PermissionPrompt({ requests, onSubmit, onCancel }: Props): React
149156
);
150157
}
151158

159+
function renderOptionLabel(option: PromptOption): React.ReactNode {
160+
if (option.scopeDescription && option.scopeColor) {
161+
return (
162+
<>
163+
{option.label}
164+
<Text color={option.scopeColor}>{option.scopeDescription}</Text>
165+
</>
166+
);
167+
}
168+
return option.label;
169+
}
170+
152171
function buildScopePrompts(requests: AskPermissionRequest[]): ScopePrompt[] {
153172
const prompts: ScopePrompt[] = [];
154173
for (const request of requests) {
@@ -159,10 +178,15 @@ function buildScopePrompts(requests: AskPermissionRequest[]): ScopePrompt[] {
159178
return prompts;
160179
}
161180

162-
function buildOptions(scope: AskPermissionScope): Array<{ kind: "allow" | "always" | "deny"; label: string }> {
163-
const options: Array<{ kind: "allow" | "always" | "deny"; label: string }> = [{ kind: "allow", label: "Yes" }];
181+
function buildOptions(scope: AskPermissionScope): PromptOption[] {
182+
const options: PromptOption[] = [{ kind: "allow", label: "Yes" }];
164183
if (isAlwaysAllowedScope(scope)) {
165-
options.push({ kind: "always", label: `Yes, and always allow ${describeScope(scope)}` });
184+
options.push({
185+
kind: "always",
186+
label: "Yes, and always allow ",
187+
scopeDescription: describeScope(scope),
188+
scopeColor: getScopeRiskColor(scope),
189+
});
166190
}
167191
options.push({ kind: "deny", label: "No" });
168192
return options;
@@ -201,6 +225,27 @@ function isAlwaysAllowedScope(scope: AskPermissionScope): scope is PermissionSco
201225
return ALWAYS_ALLOWED_SCOPES.has(scope);
202226
}
203227

228+
export function getScopeRiskColor(scope: AskPermissionScope): string {
229+
switch (scope) {
230+
case "read-in-cwd":
231+
case "query-git-log":
232+
return "#22c55e";
233+
case "read-out-cwd":
234+
case "write-in-cwd":
235+
case "network":
236+
case "mcp":
237+
return "#f59e0b";
238+
case "write-out-cwd":
239+
case "delete-in-cwd":
240+
case "delete-out-cwd":
241+
case "mutate-git-log":
242+
case "unknown":
243+
return "#ef4444";
244+
default:
245+
return "#ef4444";
246+
}
247+
}
248+
204249
function describeScope(scope: PermissionScope): string {
205250
switch (scope) {
206251
case "read-in-cwd":

0 commit comments

Comments
 (0)