mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-18 05:57:40 +02:00
fix(gateguard): cap session fact-force denials
This commit is contained in:
@@ -953,6 +953,17 @@ function getFullDenialBudget() {
|
||||
return DEFAULT_FULL_DENIALS;
|
||||
}
|
||||
|
||||
// Edit/Write fact forcing can be capped per session instead of denying every
|
||||
// new path forever. Keep the existing behavior when unset; hosts can opt in
|
||||
// to a session-wide ceiling without weakening the destructive-Bash gate.
|
||||
function getMaxDenialBudget() {
|
||||
const raw = Number.parseInt(process.env.GATEGUARD_FACT_FORCE_MAX_DENIALS || '', 10);
|
||||
if (Number.isInteger(raw) && raw >= 0) {
|
||||
return raw;
|
||||
}
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
function getDenialCount(state) {
|
||||
const n = Number(state && state.fact_force_denials);
|
||||
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
|
||||
@@ -1262,6 +1273,9 @@ function run(rawInput) {
|
||||
if (!ok) {
|
||||
return allowWithStateWarning();
|
||||
}
|
||||
if (denials > getMaxDenialBudget()) {
|
||||
return rawInput;
|
||||
}
|
||||
if (denials > getFullDenialBudget()) {
|
||||
const action = toolName === 'Edit' ? 'edit' : 'creation';
|
||||
return denyResult(condensedGateMsg(action, filePath, denials), { includeRecoveryHint: false });
|
||||
@@ -1287,6 +1301,9 @@ function run(rawInput) {
|
||||
if (!ok) {
|
||||
return allowWithStateWarning();
|
||||
}
|
||||
if (denials > getMaxDenialBudget()) {
|
||||
return rawInput;
|
||||
}
|
||||
if (denials > getFullDenialBudget()) {
|
||||
return denyResult(condensedGateMsg('edit', filePath, denials), { includeRecoveryHint: false });
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ load-bearing destructive-Bash checks keep running:
|
||||
| `GATEGUARD_BASH_ROUTINE_DISABLED` | unset (gate on) | Disables the **routine-Bash** gate only. The destructive-Bash gate (`rm -rf`, `git reset --hard`, `drop table`, `dd if=`, …) is unaffected. |
|
||||
| `GATEGUARD_EXEMPT_GLOBS` | unset (no exemptions) | Comma-separated globs; a matching Edit/Write/MultiEdit target skips first-touch fact-forcing. Intended for low-import-value trees (tests, generated artifacts, scratch dirs) where "who imports this / what schema" carries no signal. |
|
||||
| `GATEGUARD_FACT_FORCE_FULL_DENIALS` | `3` | How many denials emit the full four-fact block before later ones condense to a single line. `0` condenses from the very first denial. |
|
||||
| `GATEGUARD_FACT_FORCE_MAX_DENIALS` | unset (no cap) | Caps the total number of new-path Edit/Write denials in a session. Once the cap is reached, new paths are allowed instead of denied; destructive Bash stays gated independently. Opt-in — unset preserves the existing deny-every-new-path behaviour. Unlike `GATEGUARD_FACT_FORCE_FULL_DENIALS`, which only controls message detail, this changes whether the operation is blocked. |
|
||||
| `GATEGUARD_BASH_EXTRA_DESTRUCTIVE` | unset | Extra destructive-command patterns, as regex source, added to the built-in set. A malformed regex is treated as unset (built-ins still apply) and logged once to stderr. |
|
||||
| `GATEGUARD_STATE_DIR` | `~/.gateguard` | Where per-session gate state is kept. If state cannot be persisted the gate allows the operation rather than looping, and names this variable in the warning. |
|
||||
|
||||
|
||||
@@ -2655,6 +2655,38 @@ function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
clearState();
|
||||
if (
|
||||
test('allows new paths after the session-wide denial budget', () => {
|
||||
writeState({ checked: [], last_active: Date.now(), fact_force_denials: 3 });
|
||||
const result = runHook(
|
||||
{ tool_name: 'Edit', tool_input: { file_path: '/src/after-budget.js' } },
|
||||
{ GATEGUARD_FACT_FORCE_MAX_DENIALS: '3' }
|
||||
);
|
||||
assert.strictEqual(result.code, 0);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(!output || !output.hookSpecificOutput, 'paths after the budget should pass through');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
clearState();
|
||||
if (
|
||||
test('MultiEdit allows new paths after the session-wide denial budget', () => {
|
||||
writeState({ checked: [], last_active: Date.now(), fact_force_denials: 3 });
|
||||
const result = runHook(
|
||||
{ tool_name: 'MultiEdit', tool_input: { edits: [{ file_path: '/src/after-multi-budget.js', old_string: 'a', new_string: 'b' }] } },
|
||||
{ GATEGUARD_FACT_FORCE_MAX_DENIALS: '3' }
|
||||
);
|
||||
assert.strictEqual(result.code, 0);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(!output || !output.hookSpecificOutput, 'MultiEdit paths after the budget should pass through');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
clearState();
|
||||
if (
|
||||
test('malformed denial counter in state is treated as zero (full block, no crash)', () => {
|
||||
|
||||
Reference in New Issue
Block a user