mirror of
https://github.com/Jeuners/ECC.git
synced 2026-09-09 15:02:30 +02:00
feat(gateguard): add GATEGUARD_EXEMPT_GLOBS path exemptions (#2432)
The fact-forcing gate fires once per first-touched file per session. In
build-heavy sessions this costs a deny->retry round-trip on every new file,
including trees where the gate's questions ("who imports this? what schema?")
carry no signal: test files, generated artifacts, scratch dirs.
Add an opt-in, comma-separated glob allowlist read from GATEGUARD_EXEMPT_GLOBS.
A matching Edit/Write/MultiEdit target skips the first-touch gate; destructive-
Bash and routine-Bash gates are untouched. Default-off (unset => identical prior
behavior), fail-open (a malformed glob is dropped, never throws), and memoized on
the env value, matching the existing getExtraDestructiveRegex idiom.
"env": { "GATEGUARD_EXEMPT_GLOBS": "**/tests/**,**/scratchpad/**" }
Adds 4 tests; all 144 gateguard tests pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8b6543929e
commit
0a35a0216b
2 changed files with 123 additions and 2 deletions
|
|
@ -2376,6 +2376,86 @@ function runTests() {
|
|||
passed++;
|
||||
else failed++;
|
||||
|
||||
// --- Exempt globs: GATEGUARD_EXEMPT_GLOBS skips first-touch fact-forcing ---
|
||||
clearState();
|
||||
if (
|
||||
test('exempts an Edit whose path matches GATEGUARD_EXEMPT_GLOBS', () => {
|
||||
const input = {
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/proj/tests/test_x.js', old_string: 'a', new_string: 'b' }
|
||||
};
|
||||
const result = runHook(input, { GATEGUARD_EXEMPT_GLOBS: '**/tests/**' });
|
||||
assert.strictEqual(result.code, 0, 'exit code should be 0');
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny', 'exempt path must not be denied');
|
||||
} else {
|
||||
assert.strictEqual(output.tool_name, 'Edit', 'pass-through should preserve input');
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
clearState();
|
||||
if (
|
||||
test('does NOT exempt a non-matching path under the same globs', () => {
|
||||
const input = {
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/proj/src/core/x.js', old_string: 'a', new_string: 'b' }
|
||||
};
|
||||
const result = runHook(input, { GATEGUARD_EXEMPT_GLOBS: '**/tests/**' });
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce JSON output');
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny', 'non-matching path still gated');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('supports multiple comma-separated exempt globs (Write + non-match)', () => {
|
||||
const globs = '**/tests/**,**/scratchpad/**';
|
||||
clearState();
|
||||
const exempt = runHook(
|
||||
{ tool_name: 'Write', tool_input: { file_path: '/tmp/x/scratchpad/s.js', content: 'x' } },
|
||||
{ GATEGUARD_EXEMPT_GLOBS: globs }
|
||||
);
|
||||
const exemptOut = parseOutput(exempt.stdout);
|
||||
assert.ok(exemptOut, 'should produce JSON output');
|
||||
if (exemptOut.hookSpecificOutput) {
|
||||
assert.notStrictEqual(exemptOut.hookSpecificOutput.permissionDecision, 'deny', 'scratchpad path exempt');
|
||||
}
|
||||
clearState();
|
||||
const gated = runHook(
|
||||
{ tool_name: 'Write', tool_input: { file_path: '/proj/src/s.js', content: 'x' } },
|
||||
{ GATEGUARD_EXEMPT_GLOBS: globs }
|
||||
);
|
||||
const gatedOut = parseOutput(gated.stdout);
|
||||
assert.ok(gatedOut, 'should produce JSON output');
|
||||
assert.strictEqual(gatedOut.hookSpecificOutput.permissionDecision, 'deny', 'src path still gated');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
clearState();
|
||||
if (
|
||||
test('is default-off: unset GATEGUARD_EXEMPT_GLOBS gates tests/ as before', () => {
|
||||
const input = {
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/proj/tests/test_x.js', old_string: 'a', new_string: 'b' }
|
||||
};
|
||||
const result = runHook(input, { GATEGUARD_EXEMPT_GLOBS: '' });
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce JSON output');
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny', 'no exemptions when unset');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
// Cleanup only the temp directory created by this test file.
|
||||
try {
|
||||
if (fs.existsSync(stateDir)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue