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
|
|
@ -94,6 +94,47 @@ function getExtraDestructiveRegex() {
|
|||
return extraDestructiveCacheRegex;
|
||||
}
|
||||
|
||||
// Operator-supplied path exemptions. Comma-separated globs (`GATEGUARD_EXEMPT_GLOBS`)
|
||||
// matched against the normalized (forward-slash, lowercased) file path. First-touch
|
||||
// fact-forcing is skipped for a matching Edit/Write/MultiEdit target — intended for
|
||||
// low-import-value trees (tests, generated artifacts, scratch dirs) where "who imports
|
||||
// this / what schema" carries no signal. Memoized on the env value; fail-open (a
|
||||
// malformed pattern is dropped, never throws). `*` matches within a path segment,
|
||||
// `**` across segments, `?` a single char.
|
||||
let exemptCacheKey = null;
|
||||
let exemptCacheRegexes = null;
|
||||
function getExemptMatchers() {
|
||||
const raw = process.env.GATEGUARD_EXEMPT_GLOBS || '';
|
||||
if (raw === exemptCacheKey) {
|
||||
return exemptCacheRegexes;
|
||||
}
|
||||
exemptCacheKey = raw;
|
||||
exemptCacheRegexes = raw
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
.map(glob => {
|
||||
const source = glob
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex metachars, keep * and ?
|
||||
.replace(/\*\*/g, '\x00') // ** placeholder (cross-segment)
|
||||
.replace(/\*/g, '[^/]*') // * -> within a segment
|
||||
.replace(/\x00/g, '.*') // ** -> across segments
|
||||
.replace(/\?/g, '.');
|
||||
try {
|
||||
return new RegExp(source);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
return exemptCacheRegexes;
|
||||
}
|
||||
|
||||
function isExemptPath(filePath) {
|
||||
const norm = normalizeForMatch(filePath);
|
||||
return getExemptMatchers().some(re => re.test(norm));
|
||||
}
|
||||
|
||||
function isRoutineBashGateDisabled() {
|
||||
return ECC_ENABLE_VALUES.has(normalizeEnvValue(process.env.GATEGUARD_BASH_ROUTINE_DISABLED));
|
||||
}
|
||||
|
|
@ -1151,7 +1192,7 @@ function run(rawInput) {
|
|||
|
||||
if (toolName === 'Edit' || toolName === 'Write') {
|
||||
const filePath = toolInput.file_path || '';
|
||||
if (!filePath || isClaudeSettingsPath(filePath)) {
|
||||
if (!filePath || isClaudeSettingsPath(filePath) || isExemptPath(filePath)) {
|
||||
return rawInput; // allow
|
||||
}
|
||||
|
||||
|
|
@ -1182,7 +1223,7 @@ function run(rawInput) {
|
|||
const edits = toolInput.edits || [];
|
||||
for (const edit of edits) {
|
||||
const filePath = edit.file_path || '';
|
||||
if (filePath && !isClaudeSettingsPath(filePath) && !isChecked(filePath)) {
|
||||
if (filePath && !isClaudeSettingsPath(filePath) && !isExemptPath(filePath) && !isChecked(filePath)) {
|
||||
const { ok, denials } = markCheckedAndCountDenial(filePath);
|
||||
if (!ok) {
|
||||
return allowWithStateWarning();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue