Skip to content

Commit 104acff

Browse files
committed
feat: enhance appendProjectPermissionAllows to support inherited permissions
1 parent 90c6b2e commit 104acff

4 files changed

Lines changed: 155 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ dist/
55
.vscode/
66
*.tgz
77
*.log
8+
.deepcode/settings.json

src/common/permissions.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,11 @@ export function hasUserPermissionReplies(value: { permissions?: unknown; alwaysA
360360
);
361361
}
362362

363-
export function appendProjectPermissionAllows(projectRoot: string, scopes: PermissionScope[] | undefined): void {
363+
export function appendProjectPermissionAllows(
364+
projectRoot: string,
365+
scopes: PermissionScope[] | undefined,
366+
options: { inheritedPermissions?: Required<PermissionSettings> } = {}
367+
): void {
364368
if (!Array.isArray(scopes) || scopes.length === 0) {
365369
return;
366370
}
@@ -392,14 +396,35 @@ export function appendProjectPermissionAllows(projectRoot: string, scopes: Permi
392396
} catch {
393397
settings = {};
394398
}
395-
const currentAllow = Array.isArray(settings.permissions?.allow) ? settings.permissions.allow : [];
399+
400+
const existingPermissions = settings.permissions;
401+
const permissions: PermissionSettings = existingPermissions
402+
? { ...existingPermissions }
403+
: options.inheritedPermissions
404+
? {
405+
allow: [...options.inheritedPermissions.allow],
406+
deny: [...options.inheritedPermissions.deny],
407+
ask: [...options.inheritedPermissions.ask],
408+
defaultMode: options.inheritedPermissions.defaultMode,
409+
}
410+
: {};
411+
412+
const currentAllow = Array.isArray(permissions.allow) ? permissions.allow : [];
396413
const allow = [...currentAllow];
397414
for (const scope of nextScopes) {
398415
if (!allow.includes(scope)) {
399416
allow.push(scope);
400417
}
401418
}
402-
if (allow.length === currentAllow.length) {
419+
const currentDeny = Array.isArray(permissions.deny) ? permissions.deny : undefined;
420+
const currentAsk = Array.isArray(permissions.ask) ? permissions.ask : undefined;
421+
const deny = currentDeny ? currentDeny.filter((scope) => !nextScopes.includes(scope)) : permissions.deny;
422+
const ask = currentAsk ? currentAsk.filter((scope) => !nextScopes.includes(scope)) : permissions.ask;
423+
const changed =
424+
allow.length !== currentAllow.length ||
425+
(currentDeny ? (deny as PermissionScope[]).length !== currentDeny.length : false) ||
426+
(currentAsk ? (ask as PermissionScope[]).length !== currentAsk.length : false);
427+
if (existingPermissions && !changed) {
403428
return;
404429
}
405430
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
@@ -409,7 +434,9 @@ export function appendProjectPermissionAllows(projectRoot: string, scopes: Permi
409434
{
410435
...settings,
411436
permissions: {
412-
...(settings.permissions ?? {}),
437+
...permissions,
438+
deny,
439+
ask,
413440
allow,
414441
},
415442
},

src/session.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,9 @@ ${skillMd}
10451045
async replySession(sessionId: string, userPrompt: UserPromptContent, controller?: AbortController): Promise<void> {
10461046
const signal = controller?.signal;
10471047
this.throwIfAborted(signal);
1048-
appendProjectPermissionAllows(this.projectRoot, userPrompt.alwaysAllows);
1048+
appendProjectPermissionAllows(this.projectRoot, userPrompt.alwaysAllows, {
1049+
inheritedPermissions: this.getResolvedSettings().permissions,
1050+
});
10491051
const now = new Date().toISOString();
10501052
const updated = this.updateSessionEntry(sessionId, (entry) => ({
10511053
...entry,

src/tests/permissions.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,126 @@ test("appendProjectPermissionAllows writes unique project-level allow scopes", (
106106
assert.deepEqual(settings.permissions.allow, ["read-in-cwd", "write-in-cwd"]);
107107
});
108108

109+
test("appendProjectPermissionAllows seeds inherited permissions before adding allow scopes", () => {
110+
const projectRoot = createTempDir("deepcode-permission-settings-default-");
111+
112+
appendProjectPermissionAllows(projectRoot, ["query-git-log"], {
113+
inheritedPermissions: {
114+
allow: ["read-in-cwd"],
115+
deny: ["write-out-cwd"],
116+
ask: ["network"],
117+
defaultMode: "askAll",
118+
},
119+
});
120+
121+
const settingsPath = path.join(projectRoot, ".deepcode", "settings.json");
122+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
123+
assert.deepEqual(settings.permissions, {
124+
allow: ["read-in-cwd", "query-git-log"],
125+
deny: ["write-out-cwd"],
126+
ask: ["network"],
127+
defaultMode: "askAll",
128+
});
129+
});
130+
131+
test("appendProjectPermissionAllows moves inherited ask and deny scopes into allow", () => {
132+
const projectRoot = createTempDir("deepcode-permission-settings-move-inherited-");
133+
134+
appendProjectPermissionAllows(projectRoot, ["network", "write-out-cwd"], {
135+
inheritedPermissions: {
136+
allow: ["read-in-cwd"],
137+
deny: ["write-out-cwd"],
138+
ask: ["network", "mcp"],
139+
defaultMode: "askAll",
140+
},
141+
});
142+
143+
const settingsPath = path.join(projectRoot, ".deepcode", "settings.json");
144+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
145+
assert.deepEqual(settings.permissions, {
146+
allow: ["read-in-cwd", "network", "write-out-cwd"],
147+
deny: [],
148+
ask: ["mcp"],
149+
defaultMode: "askAll",
150+
});
151+
});
152+
153+
test("appendProjectPermissionAllows writes inherited permissions even when scope is already allowed", () => {
154+
const projectRoot = createTempDir("deepcode-permission-settings-inherited-existing-");
155+
156+
appendProjectPermissionAllows(projectRoot, ["read-in-cwd"], {
157+
inheritedPermissions: {
158+
allow: ["read-in-cwd"],
159+
deny: [],
160+
ask: ["network"],
161+
defaultMode: "askAll",
162+
},
163+
});
164+
165+
const settingsPath = path.join(projectRoot, ".deepcode", "settings.json");
166+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
167+
assert.deepEqual(settings.permissions, {
168+
allow: ["read-in-cwd"],
169+
deny: [],
170+
ask: ["network"],
171+
defaultMode: "askAll",
172+
});
173+
});
174+
175+
test("appendProjectPermissionAllows preserves existing project permissions", () => {
176+
const projectRoot = createTempDir("deepcode-permission-settings-explicit-default-");
177+
const settingsPath = path.join(projectRoot, ".deepcode", "settings.json");
178+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
179+
fs.writeFileSync(
180+
settingsPath,
181+
JSON.stringify({ permissions: { allow: ["read-in-cwd"], defaultMode: "allowAll" } }),
182+
"utf8"
183+
);
184+
185+
appendProjectPermissionAllows(projectRoot, ["query-git-log"], {
186+
inheritedPermissions: {
187+
allow: ["write-in-cwd"],
188+
deny: ["write-out-cwd"],
189+
ask: ["network"],
190+
defaultMode: "askAll",
191+
},
192+
});
193+
194+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
195+
assert.deepEqual(settings.permissions, {
196+
allow: ["read-in-cwd", "query-git-log"],
197+
defaultMode: "allowAll",
198+
});
199+
});
200+
201+
test("appendProjectPermissionAllows removes existing ask and deny conflicts", () => {
202+
const projectRoot = createTempDir("deepcode-permission-settings-existing-conflict-");
203+
const settingsPath = path.join(projectRoot, ".deepcode", "settings.json");
204+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
205+
fs.writeFileSync(
206+
settingsPath,
207+
JSON.stringify({
208+
permissions: {
209+
allow: ["read-in-cwd"],
210+
deny: ["network", "write-out-cwd"],
211+
ask: ["network", "mcp"],
212+
defaultMode: "askAll",
213+
},
214+
}),
215+
"utf8"
216+
);
217+
218+
appendProjectPermissionAllows(projectRoot, ["network"]);
219+
220+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
221+
assert.deepEqual(settings.permissions, {
222+
allow: ["read-in-cwd", "network"],
223+
deny: ["write-out-cwd"],
224+
ask: ["mcp"],
225+
defaultMode: "askAll",
226+
});
227+
});
228+
109229
test("hasUserPermissionReplies detects permission reply payloads", () => {
110230
assert.equal(hasUserPermissionReplies({}), false);
111231
assert.equal(hasUserPermissionReplies({ permissions: [] }), false);

0 commit comments

Comments
 (0)