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:
Souptik Chakraborty
2026-08-29 14:55:13 -04:00
committed by haelyra
parent 2f8a5a271d
commit 4377ea1753
2 changed files with 120 additions and 0 deletions
+88
View File
@@ -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);
}