Skip to content

Commit 6c57cef

Browse files
committed
fix(web/shell): #107 review — an agent's command cannot make or remove a folder in a protected place either
Found while fixing the letter-case finding: the protected-path check ran over file writes and deletes only. Folders are left to the shell to make and remove (`rest`), so a command that made an empty .vscode, or removed an empty folder under .git, had that applied: a change in a place the review says commands never change, in either direction. A mkdir or rmdir whose path is protected (folded as for files) is now a refusal like any other: the whole set is refused, and the shell is handed no folder to make or remove. Folders elsewhere are unchanged. Test: shellCommit.test.js "protected folders are not made or removed either…" (an rmdir under .git, mkdir of .vscode, .GIT/hooks, .github/workflows, an rmdir of sub/.Husky — each beside an ordinary file write that must not be applied either; and two look-alike folders that still go through). It fails on the previous shellCommit.js.
1 parent c1e7d46 commit 6c57cef

2 files changed

Lines changed: 23 additions & 0 deletions

File tree

‎extensions/levelcode-ai/shellCommit.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,13 @@ async function commitShellChanges(changes, ctx, io) {
227227
plan.push({ c, review: p.review, rel: p.rel });
228228
}
229229
}
230+
// Folders are the shell's to make and remove (`rest`), but not in the protected places: an empty `.vscode` made, or
231+
// an empty folder under `.git` removed, is a change there as much as a file is.
232+
for (const c of changes) {
233+
if (c.kind !== 'mkdir' && c.kind !== 'rmdir') { continue; }
234+
const p = reviewPathFor(c.path, folders);
235+
if (p && isProtected(p.rel)) { refusals.push({ c, why: 'repository metadata, tool settings and instruction files are not changed by commands — the user does that' }); }
236+
}
230237
// A case-only rename: a delete and a write whose paths are equal ignoring case but not equal.
231238
for (const c of files) {
232239
if (c.kind !== 'delete') { continue; }

‎extensions/levelcode-ai/test/shellCommit.test.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,22 @@ const ids = (out) => out.failed.map((f) => f.c.path).sort();
220220
for (const p of ['GIT/config', 'src/Git.js', '.GITIGNORE', '.Gitattributes', 'VSCODE/x', '.github/WORKFLOW.md', 'my.GIT/x', 'Agents.txt']) { assert.ok(!isProtected(p), p + ' is not protected'); }
221221
});
222222

223+
await test('protected folders are not made or removed either: a command that would is refused whole, and its folders are not left to the shell', async () => {
224+
for (const c of [{ kind: 'rmdir', path: '/ws/.git/refs/tags' }, { kind: 'mkdir', path: '/ws/.vscode' }, { kind: 'mkdir', path: '/ws/.GIT/hooks' },
225+
{ kind: 'mkdir', path: '/ws/.github/workflows' }, { kind: 'rmdir', path: '/ws/sub/.Husky' }]) {
226+
const r = rig();
227+
const out = await commitShellChanges([W('/ws/ok.txt', 'fine'), c], r.ctx, r.io);
228+
assert.strictEqual(out.refused, true, c.kind + ' ' + c.path);
229+
assert.deepStrictEqual(out.rest, [], 'the shell is handed no folder to make or remove: ' + c.path);
230+
assert.deepStrictEqual(r.edits, [], 'and the file beside it is not applied either');
231+
assert.match(out.notes[0], /refused: .* — repository metadata/);
232+
}
233+
const ok = rig();
234+
const fine = await commitShellChanges([{ kind: 'mkdir', path: '/ws/src/git' }, { kind: 'rmdir', path: '/ws/vscode-notes' }], ok.ctx, ok.io);
235+
assert.strictEqual(fine.refused, false);
236+
assert.deepStrictEqual(fine.rest.map((c) => c.path), ['/ws/src/git', '/ws/vscode-notes']);
237+
});
238+
223239
await test('outside the workspace folders: refused', async () => {
224240
const r = rig();
225241
const out = await commitShellChanges([W('/etc/passwd', 'x')], r.ctx, r.io);

0 commit comments

Comments
 (0)