|
8 | 8 | computeToolCallPermissions, |
9 | 9 | evaluatePermissionScopes, |
10 | 10 | hasUserPermissionReplies, |
| 11 | + isPathInAnyDirectory, |
11 | 12 | parseBashSideEffects, |
12 | 13 | } from "../common/permissions"; |
13 | 14 |
|
@@ -126,6 +127,77 @@ test("computeToolCallPermissions only asks for scopes not already allowed", () = |
126 | 127 | ); |
127 | 128 | }); |
128 | 129 |
|
| 130 | +test("computeToolCallPermissions allows read tool calls under skill scan paths", () => { |
| 131 | + const projectRoot = createTempDir("deepcode-permissions-skill-read-workspace-"); |
| 132 | + const home = createTempDir("deepcode-permissions-skill-read-home-"); |
| 133 | + const skillRoot = path.join(home, ".agents", "skills"); |
| 134 | + const skillResourcePath = path.join(skillRoot, "pdf", "scripts", "extract.py"); |
| 135 | + const outsidePath = path.join(home, "notes.txt"); |
| 136 | + const plan = computeToolCallPermissions({ |
| 137 | + sessionId: "session-1", |
| 138 | + projectRoot, |
| 139 | + readPermissionExemptPaths: [skillRoot], |
| 140 | + settings: { |
| 141 | + allow: [], |
| 142 | + deny: [], |
| 143 | + ask: [], |
| 144 | + defaultMode: "askAll", |
| 145 | + }, |
| 146 | + toolCalls: [ |
| 147 | + { |
| 148 | + id: "call-skill-read", |
| 149 | + type: "function", |
| 150 | + function: { name: "read", arguments: JSON.stringify({ file_path: skillResourcePath }) }, |
| 151 | + }, |
| 152 | + { |
| 153 | + id: "call-outside-read", |
| 154 | + type: "function", |
| 155 | + function: { name: "read", arguments: JSON.stringify({ file_path: outsidePath }) }, |
| 156 | + }, |
| 157 | + ], |
| 158 | + }); |
| 159 | + |
| 160 | + assert.deepEqual(plan.permissions, [ |
| 161 | + { toolCallId: "call-skill-read", permission: "allow" }, |
| 162 | + { toolCallId: "call-outside-read", permission: "ask" }, |
| 163 | + ]); |
| 164 | + assert.deepEqual( |
| 165 | + plan.askPermissions.map((item) => ({ id: item.toolCallId, scopes: item.scopes })), |
| 166 | + [{ id: "call-outside-read", scopes: ["read-out-cwd"] }] |
| 167 | + ); |
| 168 | +}); |
| 169 | + |
| 170 | +test("isPathInAnyDirectory matches absolute and project-relative directories without sibling leaks", () => { |
| 171 | + const projectRoot = createTempDir("deepcode-permissions-directory-match-workspace-"); |
| 172 | + const home = createTempDir("deepcode-permissions-directory-match-home-"); |
| 173 | + const absoluteSkillRoot = path.join(home, ".agents", "skills"); |
| 174 | + const relativeSkillRoot = path.join(".deepcode", "skills"); |
| 175 | + |
| 176 | + assert.equal( |
| 177 | + isPathInAnyDirectory(projectRoot, path.join(absoluteSkillRoot, "pdf", "scripts", "extract.py"), [ |
| 178 | + absoluteSkillRoot, |
| 179 | + ]), |
| 180 | + true |
| 181 | + ); |
| 182 | + assert.equal( |
| 183 | + isPathInAnyDirectory(projectRoot, path.join(projectRoot, relativeSkillRoot, "local", "SKILL.md"), [ |
| 184 | + relativeSkillRoot, |
| 185 | + ]), |
| 186 | + true |
| 187 | + ); |
| 188 | + assert.equal( |
| 189 | + isPathInAnyDirectory(projectRoot, path.join(`${absoluteSkillRoot}-backup`, "extract.py"), [absoluteSkillRoot]), |
| 190 | + false |
| 191 | + ); |
| 192 | + assert.equal( |
| 193 | + isPathInAnyDirectory(projectRoot, path.join(projectRoot, ".deepcode", "skills-extra", "file.md"), [ |
| 194 | + relativeSkillRoot, |
| 195 | + ]), |
| 196 | + false |
| 197 | + ); |
| 198 | + assert.equal(isPathInAnyDirectory(projectRoot, path.join(home, "notes.txt"), undefined), false); |
| 199 | +}); |
| 200 | + |
129 | 201 | test("appendProjectPermissionAllows writes unique project-level allow scopes", () => { |
130 | 202 | const projectRoot = createTempDir("deepcode-permission-settings-"); |
131 | 203 | const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); |
|
0 commit comments