@bongos/core 1.19.703 → 1.19.705

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,15 @@ const OK_DEF = Object.freeze({
41
41
  model_tier: 'default',
42
42
  scope_modules: ['agents'],
43
43
  });
44
- const OPTS = Object.freeze({ authorRank: 'archon', allowedModules: ['agents'] });
44
+ // task 1002492 added the scope_paths half of the rank wall, and with it the rule
45
+ // that DECLARING paths requires the caller to supply the means to check them —
46
+ // there is no `allowedPaths` counterpart to fail closed on, so an absent matcher
47
+ // would be a silent hole. The baseline options therefore carry a matcher; the
48
+ // uncheckable and protected cases each get their own test below.
49
+ const NOTHING_PROTECTED = () => false;
50
+ const OPTS = Object.freeze({
51
+ authorRank: 'archon', allowedModules: ['agents'], isProtectedPath: NOTHING_PROTECTED,
52
+ });
45
53
 
46
54
  const codes = (r) => r.errors.map((e) => e.code);
47
55
  const hasCode = (r, code) => codes(r).includes(code);
@@ -241,6 +249,115 @@ await test('a scope_path that escapes the tree is refused', () => {
241
249
  assert.equal(validateAgentDefinition({ ...OK_DEF, scope_paths: ['modules/agents/'] }, OPTS).ok, true);
242
250
  });
243
251
 
252
+ // --- THE scope_paths HALF OF THE WALL (task 1002492) ------------------------
253
+ //
254
+ // The defect this closes: the rank wall ran over scope_modules only, so
255
+ // `scope_modules: ['government']` was refused below Metic while
256
+ // `scope_paths: ['modules/government/']` reached the same code and passed at any
257
+ // rank. One wall, two halves, disagreeing — and the path half was the one an
258
+ // author would reach for to name a specific surface.
259
+
260
+ const REACHES_GOVERNMENT = (p) => String(p).startsWith('modules/government');
261
+
262
+ await test('a scope_path onto a protected surface is refused below the floor', () => {
263
+ const r = validateAgentDefinition(
264
+ { ...OK_DEF, scope_paths: ['modules/government/catalog.js'] },
265
+ { authorRank: 'thetes', allowedModules: ['agents'], isProtectedPath: REACHES_GOVERNMENT }
266
+ );
267
+ assert.equal(r.ok, false, 'a thetes author may not scope onto the authority surface by path');
268
+ assert.ok(hasCode(r, 'scope_protected_rank'), JSON.stringify(codes(r)));
269
+ assert.match(r.errors.find((e) => e.code === 'scope_protected_rank').message, /scope_paths/);
270
+ });
271
+
272
+ await test('the two halves of the wall now agree', () => {
273
+ // The asymmetry itself, asserted: the same surface named as a module and as a
274
+ // path must reach the same verdict for the same author.
275
+ const asModule = validateAgentDefinition(
276
+ { ...OK_DEF, scope_modules: ['government'] },
277
+ { authorRank: 'thetes', allowedModules: ['agents', 'government'], protectedModules: ['government'], isProtectedPath: NOTHING_PROTECTED }
278
+ );
279
+ const asPath = validateAgentDefinition(
280
+ { ...OK_DEF, scope_paths: ['modules/government/'] },
281
+ { authorRank: 'thetes', allowedModules: ['agents'], isProtectedPath: REACHES_GOVERNMENT }
282
+ );
283
+ assert.equal(asModule.ok, false, 'the module spelling was always refused');
284
+ assert.equal(asPath.ok, false, 'and the path spelling must be too — this is the whole task');
285
+ assert.ok(hasCode(asModule, 'scope_protected_rank'));
286
+ assert.ok(hasCode(asPath, 'scope_protected_rank'));
287
+ });
288
+
289
+ await test('a Metic+ author may scope onto a protected path', () => {
290
+ // The wall is a FLOOR, not a ban — a tightening that refused everyone would
291
+ // pass the test above while breaking the feature.
292
+ const r = validateAgentDefinition(
293
+ { ...OK_DEF, scope_paths: ['modules/government/catalog.js'] },
294
+ { authorRank: 'metic', allowedModules: ['agents'], isProtectedPath: REACHES_GOVERNMENT }
295
+ );
296
+ assert.equal(r.ok, true, JSON.stringify(codes(r)));
297
+ assert.deepEqual(r.value.scope_paths, ['modules/government/catalog.js']);
298
+ });
299
+
300
+ await test('an UNPROTECTED path is unaffected at any rank', () => {
301
+ const r = validateAgentDefinition(
302
+ { ...OK_DEF, scope_paths: ['modules/agents/lib/'] },
303
+ { authorRank: 'xenos', allowedModules: ['agents'], isProtectedPath: REACHES_GOVERNMENT }
304
+ );
305
+ assert.equal(r.ok, true, 'the wall must not become a blanket refusal of path scopes');
306
+ });
307
+
308
+ await test('declaring scope_paths with NO matcher is refused, not admitted', () => {
309
+ // scope_modules fails closed through `allowedModules` defaulting to empty.
310
+ // scope_paths has no such counterpart, so an absent matcher would be a silent
311
+ // hole — "nobody checked" must not read the same as "nothing was protected".
312
+ const r = validateAgentDefinition(
313
+ { ...OK_DEF, scope_paths: ['anything/at/all'] },
314
+ { authorRank: 'archon', allowedModules: ['agents'] }
315
+ );
316
+ assert.equal(r.ok, false);
317
+ assert.ok(hasCode(r, 'scope_paths_uncheckable'), JSON.stringify(codes(r)));
318
+ // …and an ARCHON is refused too: this is not a rank question, it is a
319
+ // "we could not answer the question" question.
320
+ assert.equal(r.value, null);
321
+ });
322
+
323
+ await test('a THROWING matcher counts as a hit, never as a clean path', () => {
324
+ const boom = () => { throw new Error('registry unreadable'); };
325
+ const low = validateAgentDefinition(
326
+ { ...OK_DEF, scope_paths: ['whatever'] },
327
+ { authorRank: 'xenos', allowedModules: ['agents'], isProtectedPath: boom }
328
+ );
329
+ assert.equal(low.ok, false, 'an unanswered question must not resolve to "not protected"');
330
+ assert.ok(hasCode(low, 'scope_protected_rank'));
331
+ // The floor still applies: a Metic+ author is admitted, because the
332
+ // fail-closed reading is "treat as protected", not "refuse everyone".
333
+ const high = validateAgentDefinition(
334
+ { ...OK_DEF, scope_paths: ['whatever'] },
335
+ { authorRank: 'archon', allowedModules: ['agents'], isProtectedPath: boom }
336
+ );
337
+ assert.equal(high.ok, true);
338
+ });
339
+
340
+ await test('ONLY an explicit false clears a path — ambiguity reads as protected', () => {
341
+ // The fail-open trap: `answer === true` would read undefined (a matcher that
342
+ // forgot a return), a truthy string, or null as "not protected" — silently
343
+ // opening the wall on the exact question it exists to ask. Reading ambiguity
344
+ // as protected is loud and gets fixed; reading it as clean is not.
345
+ for (const answer of [undefined, null, 'yes', 0, '']) {
346
+ const r = validateAgentDefinition(
347
+ { ...OK_DEF, scope_paths: ['x'] },
348
+ { authorRank: 'xenos', allowedModules: ['agents'], isProtectedPath: () => answer }
349
+ );
350
+ assert.equal(r.ok, false, `a matcher answering ${JSON.stringify(answer)} must not clear the path`);
351
+ assert.ok(hasCode(r, 'scope_protected_rank'));
352
+ }
353
+ // …and a real `false` does clear it, or the wall would refuse everything.
354
+ const clean = validateAgentDefinition(
355
+ { ...OK_DEF, scope_paths: ['x'] },
356
+ { authorRank: 'xenos', allowedModules: ['agents'], isProtectedPath: () => false }
357
+ );
358
+ assert.equal(clean.ok, true);
359
+ });
360
+
244
361
  // --- THE RANK WALL (ADR 0016 — authority is the author's LIVE rank) ---------
245
362
 
246
363
  await test('RANK_ORDER is the ladder low→high, and the scope floor is Metic', () => {
@@ -0,0 +1,248 @@
1
+ // tests/exec_path_guard.mjs — task 1003371, audit ref B50 (2026-08-29 security audit).
2
+ //
3
+ // WHAT THE CHECK IS FOR. Nothing in the repo asked what the SERVER EXECUTES, or out of
4
+ // whose tree. route-rank-check asks who may call a route; permission-path-check asks who
5
+ // may edit a file; neither looks at execution. All three CRITICALs in the audit lived in
6
+ // that gap.
7
+ //
8
+ // WHAT THESE TESTS PIN. Two things, and the second is the one that matters:
9
+ // 1. the check CATCHES the defect shape — a script path assembled from a runtime value;
10
+ // 2. it does not catch everything that merely LOOKS like one. A scan that fires on
11
+ // `/re/.exec(s)` or on the word "spawn (" inside a sentence gets exempted into
12
+ // uselessness within a month, so the negative cases are pinned as hard as the
13
+ // positive ones. Both of those false positives were real during development.
14
+ import assert from 'node:assert/strict';
15
+ import { test } from 'node:test';
16
+ import { createRequire } from 'node:module';
17
+ import path from 'node:path';
18
+ import { execFileSync } from 'node:child_process';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ // fileURLToPath, not URL.pathname. Both a test (tests/test_path_guard.mjs) and a
22
+ // fitness check (scripts/gds/test-path-guard.js, Check 30) reject the pathname form.
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
25
+ const g = require(path.join(ROOT, 'scripts', 'gds', 'exec-path-guard.js'));
26
+
27
+ const scan = (src, rel = 'modules/probe/x.js') => g.scanFile(rel, src);
28
+ const CP = "const { execFile, execFileSync, spawn, execSync } = require('node:child_process');\n";
29
+
30
+ // ---- the defect shape it must catch ---------------------------------------
31
+
32
+ test('a script path built from a runtime value is flagged', () => {
33
+ // conflict-resolve.js:382 in miniature: arg 0 is perfectly static (process.execPath)
34
+ // and the DANGER is in the args array, which is why the check reads inside it.
35
+ const f = scan(CP + "execFile(process.execPath, [path.join(work, rel)], { cwd: work });");
36
+ assert.equal(f.length, 1, 'the clone-rooted script path must be reported');
37
+ assert.equal(f[0].role, 'script', 'the finding must name the args-array script, not arg 0');
38
+ assert.match(f[0].expr, /work/);
39
+ });
40
+
41
+ test('a non-static EXECUTABLE is flagged too, not just the script', () => {
42
+ const f = scan(CP + 'spawn(bin, args);');
43
+ assert.equal(f.length, 1);
44
+ assert.equal(f[0].role, 'executable');
45
+ });
46
+
47
+ test('a path laundered through a helper still reads as non-static (fails toward noise)', () => {
48
+ // The scan does not follow values across functions. The honest consequence is that an
49
+ // indirect path is FLAGGED rather than missed — pinned so the direction of failure is
50
+ // a decision on record, not an accident.
51
+ const f = scan(CP + 'execFile(process.execPath, [resolveScript()], {});');
52
+ assert.equal(f.length, 1);
53
+ });
54
+
55
+ // ---- what it must NOT catch ------------------------------------------------
56
+
57
+ test("a regex's .exec() is never mistaken for child_process.exec", () => {
58
+ // The dominant false positive: `.exec(` outnumbers real spawner calls ~5:1 in this repo.
59
+ const src = "const m = /^Bearer\\s+(\\w+)$/.exec(header);\nconst k = RE.exec(line);";
60
+ assert.deepEqual(scan(src), [], 'a file that never requires child_process has no call sites');
61
+ });
62
+
63
+ test('the word "spawn (" inside a string is not a call site', () => {
64
+ // Real regression: modules/grading/grader.js carries "before worker spawn
65
+ // (GRADER_BYPASS_ENABLED / runOpts.bypass)" in a note string, and the first draft
66
+ // reported that sentence as an executed path.
67
+ const src = CP + "const note = 'Grader bypassed before worker spawn (GRADER_BYPASS_ENABLED / x) — none ran.';";
68
+ assert.deepEqual(scan(src), []);
69
+ });
70
+
71
+ test('a call site inside a comment is not a call site', () => {
72
+ assert.deepEqual(scan(CP + '// execFile(process.execPath, [path.join(work, rel)]);'), []);
73
+ assert.deepEqual(scan(CP + '/* spawn(bin, args); */'), []);
74
+ });
75
+
76
+ test('a literal binary and a __dirname-rooted script are static', () => {
77
+ assert.deepEqual(scan(CP + "execSync('git rev-parse HEAD');"), []);
78
+ assert.deepEqual(scan(CP + "execFile(process.execPath, [path.join(__dirname, 'serve.js')]);"), []);
79
+ });
80
+
81
+ test('a module-level const rooted at __dirname is static, transitively', () => {
82
+ const src = CP
83
+ + "const KIT_DIR = path.join(__dirname, 'kit');\n"
84
+ + "const ENTRY = path.join(KIT_DIR, 'serve.js');\n"
85
+ + 'spawn(process.execPath, [ENTRY]);';
86
+ assert.deepEqual(scan(src), [], 'const → const → __dirname must resolve to static');
87
+ });
88
+
89
+ test('a const declared INSIDE a function is not a fixed root', () => {
90
+ // Module level is column 0. A const rebound per call is a runtime value however it reads.
91
+ const src = CP + 'function run(work) {\n const entry = path.join(work, "gen.js");\n spawn(process.execPath, [entry]);\n}';
92
+ assert.equal(scan(src).length, 1);
93
+ });
94
+
95
+ // ---- template literals: the one-character bypass (grade round 2) ----------
96
+
97
+ test('a path built from a TEMPLATE LITERAL is flagged, not silently static', () => {
98
+ // The bypass: stripStrings() used to blank a whole template, leaving zero free
99
+ // identifiers, and isStatic()'s every() over an empty set is vacuously true. Writing a
100
+ // backtick instead of calling path.join defeated the entire check for exactly the
101
+ // runtime-derived path B1/B50 are about.
102
+ const f = scan(CP + 'execFile(process.execPath, [`${work}/gen.js`]);');
103
+ assert.equal(f.length, 1, 'a template-literal script path must be reported');
104
+ assert.match(f[0].expr, /work/);
105
+ });
106
+
107
+ test('a template-literal require() specifier is flagged too', () => {
108
+ assert.equal(scan('const m = require(`${dir}/routes.js`);').length, 1);
109
+ });
110
+
111
+ test('stripStrings keeps interpolated expressions and drops literal text', () => {
112
+ // Pinned at unit altitude: a regression here is invisible through scanFile alone,
113
+ // because the symptom is a finding that quietly stops appearing.
114
+ assert.match(g.stripStrings('`${a}/x/${b}`'), /a/);
115
+ assert.match(g.stripStrings('`${a}/x/${b}`'), /b/);
116
+ assert.doesNotMatch(g.stripStrings('`${a}/secretword/${b}`'), /secretword/);
117
+ assert.equal(g.isStatic('`${work}/gen.js`', new Set(['path', '__dirname'])), false);
118
+ assert.equal(g.isStatic('`no-holes-here`', new Set(['path'])), true, 'a hole-less template is a constant');
119
+ });
120
+
121
+ test('a template rooted at __dirname is still static', () => {
122
+ assert.deepEqual(scan(CP + 'execFile(process.execPath, [`${__dirname}/serve.js`]);'), []);
123
+ });
124
+
125
+ // ---- the unbound + ESM forms (grade round 1: the check's own contract) -----
126
+
127
+ test('an UNBOUND require(...).execFileSync(runtimePath) is caught', () => {
128
+ // The gap that failed the first grade. This form binds no name, and the first draft
129
+ // short-circuited the whole file on "no bindings" — so a file spawning this way was
130
+ // skipped entirely. Not hypothetical: scripts/gds/autonomy-gate.js and
131
+ // scripts/gds/ship-land.js already use exactly this shape.
132
+ const f = scan("require('node:child_process').execFileSync(process.execPath, [path.join(work, rel)]);");
133
+ assert.equal(f.length, 1, 'the unbound spawner form must not escape the scan');
134
+ assert.equal(f[0].role, 'script');
135
+ });
136
+
137
+ test('a file with NO child_process binding is still scanned, not skipped wholesale', () => {
138
+ // The severity of the round-1 finding was not the missed call — it was that ONE
139
+ // unbound call made the scan abandon the ENTIRE file.
140
+ const src = "require('child_process').exec(userCmd);\nconst x = 1;";
141
+ assert.equal(scan(src).length, 1);
142
+ });
143
+
144
+ test('an ESM named import of a spawner is tracked, renames included', () => {
145
+ const src = "import { execFile as run } from 'node:child_process';\nrun(process.execPath, [path.join(work, 'x.js')]);";
146
+ assert.equal(scan(src).length, 1, 'the repo is CJS today; a check that only reads CJS dies silently on conversion');
147
+ });
148
+
149
+ // ---- dynamic require(): same question, different mechanism -----------------
150
+
151
+ test('a require() of a runtime path is reported as an executed module', () => {
152
+ // src/module-loader/loader.js:190 is the second site the audit named. The server
153
+ // LOADS AND RUNS that module in-process, which is execution by another name.
154
+ const f = scan("const m = require(path.join(dir, 'routes', rk + '.js'));");
155
+ assert.equal(f.length, 1);
156
+ assert.equal(f[0].role, 'module');
157
+ });
158
+
159
+ test('an ordinary literal require is not a finding', () => {
160
+ // The whole repo is built on these; firing on them would make the check unusable.
161
+ assert.deepEqual(scan("const path = require('node:path');\nconst db = require('../db');"), []);
162
+ });
163
+
164
+ // ---- enumeration coverage --------------------------------------------------
165
+
166
+ test('enumeration is complete for src/ and modules/, and not glob-dependent', () => {
167
+ // Round-1 finding: the scan enumerated via 'src/**/*.js'-style pathspecs, whose
168
+ // completeness rests on git matching '*' ACROSS '/' — true, but a property a reader
169
+ // must already know to trust the line. This pins the result rather than the spelling:
170
+ // whatever the implementation, it must see every tracked .js under src/ and modules/
171
+ // that is not a browser or desktop-app asset.
172
+ const listed = new Set(g.trackedServerFiles());
173
+ const expected = execFileSync('git', ['ls-files', '--', 'src', 'modules'], { cwd: ROOT, encoding: 'utf8' })
174
+ .split('\n').map((s) => s.trim()).filter(Boolean)
175
+ .filter((f) => f.endsWith('.js'))
176
+ .filter((f) => !f.includes('/public/') && !f.includes('/app/'));
177
+ const missing = expected.filter((f) => !listed.has(f));
178
+ assert.deepEqual(missing, [], 'every tracked server .js must be scanned — silent narrowing is the one failure this check cannot survive');
179
+ assert.ok(expected.length > 300, `enumeration collapsed to ${expected.length} files`);
180
+ });
181
+
182
+ // ---- binding resolution ----------------------------------------------------
183
+
184
+ test('only names actually bound to child_process count as spawners', () => {
185
+ // `spawn` here is a local helper with nothing to do with processes.
186
+ const src = "const spawn = (n) => new Array(n);\nspawn(itemCount);";
187
+ assert.deepEqual(scan(src), []);
188
+ });
189
+
190
+ test('a renamed destructured binding is still tracked', () => {
191
+ const src = "const { execFile: ef } = require('node:child_process');\nef(process.execPath, [path.join(work, 'x.js')]);";
192
+ assert.equal(scan(src).length, 1, 'an alias must not escape the scan');
193
+ });
194
+
195
+ test('a namespace binding (cp.execFile) is tracked', () => {
196
+ const src = "const cp = require('child_process');\ncp.execFile(process.execPath, [path.join(work, 'x.js')]);";
197
+ assert.equal(scan(src).length, 1);
198
+ });
199
+
200
+ test('bindings survive masking — the require specifier is a string body', () => {
201
+ // Regression: resolving bindings from the MASKED source blanked
202
+ // 'node:child_process' itself, so no file had bindings and the whole scan passed
203
+ // vacuously. A check that silently passes everything is worse than no check.
204
+ const { direct } = g.resolveBindings(CP);
205
+ assert.ok(direct.has('execFile') && direct.has('spawn'), 'bindings must resolve from real source');
206
+ });
207
+
208
+ // ---- the exemption list is a review, not a mute ----------------------------
209
+
210
+ test('every exemption names a file:line and carries a substantive reason', () => {
211
+ for (const e of g.EXEMPTIONS) {
212
+ assert.match(e.at, /^[\w./-]+:\d+$/, `${e.at} must be a repo-relative file:line`);
213
+ assert.ok(e.why && e.why.length > 80,
214
+ `${e.at} must record WHY a runtime path is correct there — a bare entry is a mute, not a review.`);
215
+ }
216
+ });
217
+
218
+ test('the motivating CRITICAL is on the list with its compensating control named', () => {
219
+ // If conflict-resolve.js:382 ever drops off this list silently, the audit finding has
220
+ // been forgotten rather than fixed.
221
+ const e = g.EXEMPTIONS.find((x) => x.at.startsWith('modules/lifecycle/conflict-resolve.js'));
222
+ assert.ok(e, 'the B1 site must be accounted for explicitly');
223
+ assert.match(e.why, /computeExecutedClosure|1003367/,
224
+ 'the exemption must name the control that makes it safe, not merely assert it is');
225
+ });
226
+
227
+ // ---- the check runs clean on the real tree --------------------------------
228
+
229
+ test('the repo passes, and the scan is not vacuous', () => {
230
+ const r = g.checkExecPathsStatic();
231
+ assert.equal(r.hardFail, false, `unexempted executed paths:\n${r.violations.join('\n')}`);
232
+ assert.ok(/\b(\d{2,})\s+server file\(s\) scanned/.test(r.note),
233
+ 'a scan reporting a handful of files has broken its enumeration');
234
+ assert.equal(r.exempt.length, g.EXEMPTIONS.length,
235
+ 'every exemption must still match a real finding — a stale one means the code moved and the '
236
+ + 'review was silently carried forward');
237
+ });
238
+
239
+ test('exemptions never surface as warnings — a permanently-yellow check stops being read', () => {
240
+ const r = g.checkExecPathsStatic();
241
+ assert.deepEqual(r.warnings, [], 'reviewed exemptions belong in `exempt`, not `warnings`');
242
+ assert.ok(r.exempt.length > 0, 'and they must still be reported somewhere');
243
+ });
244
+
245
+ test('an empty file list is a scan defect, not a pass', () => {
246
+ const r = g.checkExecPathsStatic({ files: [] });
247
+ assert.equal(r.hardFail, true, 'zero files enumerated must fail closed');
248
+ });
@@ -92,7 +92,89 @@ const EXPECTED_GLOBS_BEFORE_R101 = [
92
92
  // Each entry is { after, glob, why }: `after` is the glob it is inserted
93
93
  // directly below, so the expected list is reconstructed in order rather than
94
94
  // re-pasted (a re-paste is how the two lists drift apart).
95
- const ADDED_SINCE_R101 = [];
95
+ // CONSIDERED AND DELIBERATELY NOT ADDED: `.claude/agents/` (task 1002492).
96
+ //
97
+ // It looks like it belongs — an agent definition is local automation that fires
98
+ // on its own and declares its own scope, which is the stated reason
99
+ // `.claude/hooks/` and `.claude/settings.json` sit in the automation group. It
100
+ // was added, and backed out, because protectedness is DERIVED PER MODULE:
101
+ // module-scope-map marks a module protected when ANY of its globs is, and
102
+ // `.claude/agents/` is one of the `agents` module's globs. So the entry silently
103
+ // rank-walls the WHOLE agents module — `modules/agents/` source included — and
104
+ // with it goals 1000038/39/40, changing who may work them. Two guards caught it:
105
+ // module-scope-map's "derived, not hand-set" assertion and publish_manifest's
106
+ // rule that every protected glob be explicitly excluded or publishable.
107
+ //
108
+ // That is a policy decision about a rank floor on three goals. It is NOT the
109
+ // decision task 1002492 was for — "an agent may not scope itself onto the
110
+ // authority surface" — which the scope_paths wall in
111
+ // modules/agents/lib/validate.js now enforces on its own. Protecting the
112
+ // directory would gate who may EDIT a definition; it does nothing about where a
113
+ // definition POINTS.
114
+ //
115
+ // If the owner does want the agents module rank-walled, it needs the entry here,
116
+ // a publish-manifest carve-out, and the module-scope-map roster test updated —
117
+ // three deliberate edits, not a side effect of a scope fix.
118
+ const ADDED_SINCE_R101 = [
119
+ // task 1003366, audit ref MIT1 (2026-08-29 security audit, goal 1000084). The EIGHT
120
+ // execution roots the merge resolver runs out of a cloned branch. The audit named four
121
+ // of them ("the three gen-* scripts and .gitattributes"); modules/lifecycle/executed-
122
+ // closure.js names eight, so the audit's prose was already stale against shipped code.
123
+ // The drift tripwire below is what keeps this list honest from here.
124
+ {
125
+ after: 'scripts/gds/cli-lib.js',
126
+ glob: 'scripts/gds/git-merge-regen.js',
127
+ why: 'the otb-regen MERGE DRIVER. Registered as a relative command with cwd at the '
128
+ + 'clone root, so git merge runs the BRANCH copy with the live server environment '
129
+ + 'BEFORE any post-merge step — the earliest execution vector, and the one the '
130
+ + "audit's \"after the merge\" framing missed.",
131
+ },
132
+ {
133
+ after: 'scripts/gds/git-merge-regen.js',
134
+ glob: 'scripts/gds/gen-repo-map.js',
135
+ why: 'a post-merge generator the resolver runs as execFile(node, [<clone>/...]) — the '
136
+ + "branch's own copy, inheriting DATABASE_URL and BUILDER_SECRET_KEY.",
137
+ },
138
+ {
139
+ after: 'scripts/gds/gen-repo-map.js',
140
+ glob: 'scripts/gds/gen-file-map.js',
141
+ why: 'a post-merge generator the resolver executes out of the clone, on the same path '
142
+ + 'as gen-repo-map.js.',
143
+ },
144
+ {
145
+ after: 'scripts/gds/gen-file-map.js',
146
+ glob: 'scripts/gds/gen-session-index.js',
147
+ why: 'a post-merge generator the resolver executes out of the clone, on the same path '
148
+ + 'as gen-repo-map.js.',
149
+ },
150
+ {
151
+ after: 'scripts/gds/gen-session-index.js',
152
+ glob: 'scripts/gds/copy-inventory.js',
153
+ why: 'a post-merge generator the resolver executes out of the clone. NOT one of the '
154
+ + "audit's \"gen-* trio\" — it is the first of the four roots that prose omitted, "
155
+ + 'which is why this registry is pinned to the closure rather than to the audit.',
156
+ },
157
+ {
158
+ after: 'scripts/gds/copy-inventory.js',
159
+ glob: 'scripts/gds/gen-api-docs.js',
160
+ why: 'a post-merge generator the resolver executes out of the clone, and the widest '
161
+ + 'one: it require()s src/bongos/route-rank-check, api-prefix and routes/_helpers, '
162
+ + 'which is why a restore scoped to scripts/gds/ was never sufficient.',
163
+ },
164
+ {
165
+ after: 'scripts/gds/gen-api-docs.js',
166
+ glob: 'scripts/gds/gen-api-client.js',
167
+ why: 'a post-merge generator the resolver executes out of the clone, inheriting the '
168
+ + 'live server environment like its siblings.',
169
+ },
170
+ {
171
+ after: 'scripts/gds/gen-api-client.js',
172
+ glob: '.gitattributes',
173
+ why: 'never require()d, so it is not code — but it NAMES which paths git hands the '
174
+ + 'otb-regen driver, so editing it changes how much of the tree that driver '
175
+ + 'rewrites during a merge. executed-closure.js carries it as TRUSTED_NON_CODE.',
176
+ },
177
+ ];
96
178
 
97
179
  // Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
98
180
  // a removal on purpose: a rename keeps the protection and moves it, a removal
@@ -172,6 +254,55 @@ test('every glob added since R101 carries a documented reason', () => {
172
254
  }
173
255
  });
174
256
 
257
+ // ---------- the drift tripwire: every EXECUTION ROOT is a protected surface ----------
258
+ //
259
+ // task 1003366, audit ref MIT1. The audit prescribed "the three gen-* scripts and
260
+ // .gitattributes" — four surfaces. modules/lifecycle/executed-closure.js (task 1003367,
261
+ // ref B1) names SEVEN execution roots plus .gitattributes, because GENERATOR_SCRIPTS grew
262
+ // after the audit was written. Pasting the audit's four names in would therefore have
263
+ // shipped a hand-curated list that was ALREADY stale on the day it landed, leaving
264
+ // git-merge-regen.js — the earliest vector of the three — writable by any rank.
265
+ //
266
+ // So the registry is pinned to the closure's own ENTRYPOINTS, and this test is what makes
267
+ // the pin real: add a generator to GENERATOR_SCRIPTS (executed_closure.mjs already asserts
268
+ // those two lists are identical) and the build reds HERE until the registry grows to match.
269
+ //
270
+ // WHY THE ENTRYPOINTS AND NOT THE WHOLE 81-FILE CLOSURE — two controls, different jobs:
271
+ // - the closure's TRANSITIVE TAIL is enforced by conflict-resolve.js steps 5b/4b, which
272
+ // REFUSE to auto-resolve a branch touching any of the 81. That refusal is substrate:
273
+ // it does not depend on the builder cooperating, and it cannot drift, because it is
274
+ // computed rather than listed.
275
+ // - this registry is COOPERATIVE (pre-push hook, grader pre-pass, main-audit). Its job
276
+ // is the rank floor and the audit trail. The task says so: MIT1 is the interim
277
+ // mitigation, B1 is the close.
278
+ // Listing all 81 here would add no enforcement the refusal does not already provide, while
279
+ // floor-gating ordinary source at Metic — src/bongos/logger.js, src/branding.js and
280
+ // src/build-info.js are all in the closure — and locking newcomers out of routine work.
281
+ // The entrypoints are different in kind: they exist ONLY to be executed by the pipeline,
282
+ // which is exactly what group 'pipeline' already means for ship.js and gate-review.js.
283
+ test('every execution root in the closure is a protected surface (MIT1 drift tripwire)', () => {
284
+ const ec = require(path.join(ROOT, 'modules', 'lifecycle', 'executed-closure.js'));
285
+ // ENTRYPOINTS lists both script-tree spellings across the C-series rename window; only
286
+ // the ones that exist can be executed, so only those must be protected.
287
+ const roots = [
288
+ ...ec.ENTRYPOINTS.filter((rel) => fs.existsSync(path.join(ROOT, rel))),
289
+ ...ec.TRUSTED_NON_CODE,
290
+ ];
291
+ assert.ok(roots.length >= 8,
292
+ 'fixture sanity: the closure must declare the execution roots this test checks');
293
+ for (const rel of roots) {
294
+ const surface = ppc.surfaceFor(rel);
295
+ assert.ok(surface,
296
+ `${rel} is an EXECUTION ROOT — the merge resolver runs it out of a cloned branch `
297
+ + 'with the live server environment — but no protected surface covers it. Add it to '
298
+ + 'modules/government/protected-surfaces.json (group "pipeline") and declare it in '
299
+ + 'ADDED_SINCE_R101 above.');
300
+ assert.ok(ppc.RANK_TIER[surface.floor] >= ppc.RANK_TIER.metic,
301
+ `${rel} is an execution root floored at '${surface.floor}' — below Metic it stays `
302
+ + 'writable by the ranks ADR 0043 keeps out of the pipeline.');
303
+ }
304
+ });
305
+
175
306
  // Metic is a FLOOR, not a fixed value (task 1003191). Pinning every surface to
176
307
  // exactly 'metic' made the registry's own contract untestable and, worse,
177
308
  // illegal: governance owns this data and raising one surface to 'archon' is a