mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-08 00:55:55 +00:00
fix(config-protection): check MultiEdit edits[].file_path targets
Fixes #3761. config-protection only read tool_input.file_path, so a MultiEdit with the config path inside edits[] edited an existing ruff.toml/.eslintrc without the block. Check every effective target and block if any is an existing protected config.
This commit is contained in:
1 parent
25278e7d7b
commit
ed2f02dd06
2 files changed
+78
-4
No files matched your search
@@ -157,9 +157,32 @@ function run(inputOrRaw, options = {}) {
|
||||
}
|
||||
|
||||
const input = parseInput(inputOrRaw);
|
||||
const filePath = input?.tool_input?.file_path || input?.tool_input?.file || '';
|
||||
if (!filePath) return { exitCode: 0 };
|
||||
for (const filePath of targetPaths(input?.tool_input)) {
|
||||
const blocked = checkTarget(filePath);
|
||||
if (blocked) return blocked;
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Every path the tool call would write. A MultiEdit entry may carry its own
|
||||
* `file_path`; one without falls back to the top-level path, the same rule
|
||||
* GateGuard applies (#3761). Checking only the top-level path let a batch
|
||||
* edit an existing config through `edits[].file_path`.
|
||||
*/
|
||||
function targetPaths(toolInput) {
|
||||
const shared = toolInput?.file_path || toolInput?.file || '';
|
||||
const paths = new Set();
|
||||
if (typeof shared === 'string' && shared) paths.add(shared);
|
||||
const edits = Array.isArray(toolInput?.edits) ? toolInput.edits : [];
|
||||
for (const edit of edits) {
|
||||
const entryPath = edit && typeof edit === 'object' ? edit.file_path : undefined;
|
||||
if (typeof entryPath === 'string' && entryPath) paths.add(entryPath);
|
||||
}
|
||||
return [...paths];
|
||||
}
|
||||
|
||||
function checkTarget(filePath) {
|
||||
const basename = path.basename(filePath);
|
||||
// Match case-insensitively. Every PROTECTED_FILES entry is lowercase, and on
|
||||
// case-insensitive filesystems (macOS APFS/HFS+, Windows NTFS) a write to
|
||||
@@ -194,7 +217,7 @@ function run(inputOrRaw, options = {}) {
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
return { exitCode: 0 };
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -207,7 +230,7 @@ function run(inputOrRaw, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
return { exitCode: 0 };
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
|
||||
@@ -466,6 +466,57 @@ function runTests() {
|
||||
})
|
||||
);
|
||||
|
||||
results.push(
|
||||
test('blocks a MultiEdit whose existing config target is only in edits[].file_path (#3761)', () => {
|
||||
return withOwnedDirectory(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-multi-')), tmpDir => {
|
||||
const ruff = path.join(tmpDir, 'ruff.toml');
|
||||
const eslintrc = path.join(tmpDir, '.eslintrc.json');
|
||||
fs.writeFileSync(ruff, '{}');
|
||||
fs.writeFileSync(eslintrc, '{}');
|
||||
const ordinary = path.join(tmpDir, 'app.py');
|
||||
|
||||
const perEntryOnly = runHook({
|
||||
tool_name: 'MultiEdit',
|
||||
tool_input: { edits: [{ file_path: ruff, old_string: '{}', new_string: '{ }' }] }
|
||||
});
|
||||
assert.strictEqual(perEntryOnly.code, 2, 'per-entry-only batch should be blocked: ' + perEntryOnly.stderr);
|
||||
assert.ok(perEntryOnly.stderr.includes('BLOCKED: Modifying ruff.toml is not allowed.'), perEntryOnly.stderr);
|
||||
|
||||
const mixed = runHook({
|
||||
tool_name: 'MultiEdit',
|
||||
tool_input: {
|
||||
file_path: ordinary,
|
||||
edits: [
|
||||
{ old_string: 'a', new_string: 'b' },
|
||||
{ file_path: eslintrc, old_string: '{}', new_string: '{ }' }
|
||||
]
|
||||
}
|
||||
});
|
||||
assert.strictEqual(mixed.code, 2, 'mixed ordinary/config batch should be blocked: ' + mixed.stderr);
|
||||
assert.ok(mixed.stderr.includes('BLOCKED: Modifying .eslintrc.json is not allowed.'), mixed.stderr);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
results.push(
|
||||
test('allows a MultiEdit that only creates or touches non-protected files', () => {
|
||||
return withOwnedDirectory(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-multi-ok-')), tmpDir => {
|
||||
const result = runHook({
|
||||
tool_name: 'MultiEdit',
|
||||
tool_input: {
|
||||
file_path: path.join(tmpDir, 'app.py'),
|
||||
edits: [
|
||||
{ old_string: 'a', new_string: 'b' },
|
||||
{ file_path: path.join(tmpDir, 'lib.py'), old_string: 'a', new_string: 'b' },
|
||||
{ file_path: path.join(tmpDir, 'ruff.toml'), old_string: '', new_string: '[lint]' }
|
||||
]
|
||||
}
|
||||
});
|
||||
assert.strictEqual(result.code, 0, 'batch with no existing protected target should pass: ' + result.stderr);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
results.push(
|
||||
test('does not block build or test tooling configs', () => {
|
||||
// Pins the boundary: this hook guards LINTER configs. A future widening
|
||||
|
||||
Reference in new issue
Block a user