mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-31 04:09:51 +02:00
docs(gateguard): document the graduated gate controls
GateGuard reads five GATEGUARD_* environment variables that were absent from skills/gateguard/SKILL.md, so the only discoverable escape hatch was ECC_GATEGUARD=off - disabling the load-bearing destructive-Bash gate along with the noisy ones (#2573). Documented, with defaults and exact accepted values read from the hook: - GATEGUARD_BASH_ROUTINE_DISABLED (was undocumented everywhere) - GATEGUARD_EXEMPT_GLOBS (previously only in a 2.1.0 release note) - GATEGUARD_BASH_EXTRA_DESTRUCTIVE (was undocumented) - GATEGUARD_DISABLED (was undocumented) - GATEGUARD_STATE_DIR (was undocumented; named in a runtime warning) - GATEGUARD_FACT_FORCE_FULL_DENIALS (already documented; folded into the same table for one lookup point) Adds tests/ci/gateguard-env-documented.test.js, which asserts every GATEGUARD_* variable the hook reads appears in the skill doc, and that the doc names no variable the hook has stopped reading. That surface test is what found the three knobs beyond the two the issue reported. Docs and test only; no hook behaviour changes. Refs #2573
This commit is contained in:
committed by
haelyra
parent
2f8a5a271d
commit
4377ea1753
@@ -106,6 +106,38 @@ near-identical blocks cannot accumulate in the context window and
|
||||
amplify model repetition loops (#2142). Retrying the same file or
|
||||
command after presenting facts never re-triggers the gate.
|
||||
|
||||
#### Graduated controls
|
||||
|
||||
`ECC_GATEGUARD=off` disables the whole gate. The variables below narrow it
|
||||
instead, so the load-bearing destructive-Bash checks keep running:
|
||||
|
||||
| Variable | Default | Effect |
|
||||
|---|---|---|
|
||||
| `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_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_DISABLED` | unset | `1` disables the gate entirely — equivalent to `ECC_GATEGUARD=off`. |
|
||||
| `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. |
|
||||
|
||||
`GATEGUARD_BASH_ROUTINE_DISABLED` accepts `1`, `true`, `on`, `enabled`,
|
||||
`enable`, or `yes` (case- and whitespace-insensitive); any other value
|
||||
leaves the gate on. `GATEGUARD_DISABLED` recognises `1` only.
|
||||
|
||||
`GATEGUARD_EXEMPT_GLOBS` patterns are matched against the normalized
|
||||
(forward-slash, lowercased) file path: `*` matches within a path segment,
|
||||
`**` across segments, `?` a single character. Matching is fail-open — a
|
||||
malformed pattern is dropped rather than raising.
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"GATEGUARD_BASH_ROUTINE_DISABLED": "1",
|
||||
"GATEGUARD_EXEMPT_GLOBS": "**/tests/**,**/*.test.*,**/docs/**,**/dist/**"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Full package with config
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Surface test for #2573: every GATEGUARD_* environment variable the hook
|
||||
* reads must be documented in the GateGuard skill doc.
|
||||
*
|
||||
* `GATEGUARD_BASH_ROUTINE_DISABLED` shipped with no documentation at all and
|
||||
* `GATEGUARD_EXEMPT_GLOBS` was mentioned only in a release note, so operators
|
||||
* had no discoverable way to narrow the gate short of disabling it outright.
|
||||
* This pins the surface: adding a knob to the hook without documenting it
|
||||
* fails here.
|
||||
*
|
||||
* Run with: node tests/ci/gateguard-env-documented.test.js
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const hookPath = path.join(repoRoot, 'scripts', 'hooks', 'gateguard-fact-force.js');
|
||||
const skillPath = path.join(repoRoot, 'skills', 'gateguard', 'SKILL.md');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function readGateguardEnvNames(source) {
|
||||
// process.env.GATEGUARD_X and process.env['GATEGUARD_X']
|
||||
const names = new Set();
|
||||
const dotted = /process\.env\.(GATEGUARD_[A-Z0-9_]+)/g;
|
||||
const bracketed = /process\.env\[\s*['"](GATEGUARD_[A-Z0-9_]+)['"]\s*\]/g;
|
||||
let m;
|
||||
while ((m = dotted.exec(source)) !== null) names.add(m[1]);
|
||||
while ((m = bracketed.exec(source)) !== null) names.add(m[1]);
|
||||
return names;
|
||||
}
|
||||
|
||||
console.log('\nGateGuard env-var documentation surface\n');
|
||||
|
||||
if (test('hook and skill doc both exist', () => {
|
||||
assert.ok(fs.existsSync(hookPath), `missing ${hookPath}`);
|
||||
assert.ok(fs.existsSync(skillPath), `missing ${skillPath}`);
|
||||
})) passed++; else failed++;
|
||||
|
||||
const hookSource = fs.existsSync(hookPath) ? fs.readFileSync(hookPath, 'utf8') : '';
|
||||
const skillDoc = fs.existsSync(skillPath) ? fs.readFileSync(skillPath, 'utf8') : '';
|
||||
const envNames = readGateguardEnvNames(hookSource);
|
||||
|
||||
if (test('hook reads at least one GATEGUARD_* variable', () => {
|
||||
assert.ok(envNames.size > 0, 'no GATEGUARD_* env reads found - has the hook moved?');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('every GATEGUARD_* variable the hook reads is documented', () => {
|
||||
const undocumented = [...envNames].filter(name => !skillDoc.includes(name)).sort();
|
||||
assert.deepStrictEqual(
|
||||
undocumented,
|
||||
[],
|
||||
`undocumented in skills/gateguard/SKILL.md: ${undocumented.join(', ')}`
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('the documented knobs are the ones the hook actually reads', () => {
|
||||
// Guards the reverse drift: a doc naming a knob the hook no longer reads.
|
||||
const documented = [...new Set(
|
||||
(skillDoc.match(/GATEGUARD_[A-Z0-9_]+/g) || [])
|
||||
)];
|
||||
const stale = documented.filter(name => !hookSource.includes(name)).sort();
|
||||
assert.deepStrictEqual(stale, [], `documented but unread by the hook: ${stale.join(', ')}`);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}\n`);
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user