@bongos/core 1.19.729 → 1.19.731

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.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.729",
6
- "core_contract": "1.19.729",
7
- "source_commit": "399870dc41f0d71a77a79d1d48131070fbfe9828",
5
+ "core_version": "1.19.731",
6
+ "core_contract": "1.19.731",
7
+ "source_commit": "242d39240bacfa3d1d4c88f6a78ae22248c4cbdd",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-13T15:18:33.790Z",
9
+ "built_at": "2026-09-13T15:35:28.869Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 477,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2602,
20
- "tree_sha256": "787a5eba74a590d18c09c0e87a40655d7eb5dcdcd9865660bc209392db78f8ca",
20
+ "tree_sha256": "1117799d57ae5cfa87178217b7fec728cc0de76ee02b0caba9abc559f2ecd9fa",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1237,7 +1237,7 @@
1237
1237
  {
1238
1238
  "path": "docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md",
1239
1239
  "mode": "0000644",
1240
- "sha256": "fe05aa8c4719600b299a638a85ce39b4b6d75b7fc78d58151e9c92cb91825a2e"
1240
+ "sha256": "0ad167bf535aab268bfedece8339cfcf315d85d264028d403ca2c889b6044fe2"
1241
1241
  },
1242
1242
  {
1243
1243
  "path": "docs/adr/0152-landing-gate-served-by-the-core-app.md",
@@ -2482,7 +2482,7 @@
2482
2482
  {
2483
2483
  "path": "docs/module-api-changelog.md",
2484
2484
  "mode": "0000644",
2485
- "sha256": "635d466437068d5f1e6875cb6307c836dc57eb10c2ac7a50c39aba18c423f386"
2485
+ "sha256": "a4d5bc973b439bfbf41bd0b7d59b0cdffde6b0b4c18c6b4d1d73731a92736c84"
2486
2486
  },
2487
2487
  {
2488
2488
  "path": "docs/modules-contract.md",
@@ -4362,7 +4362,7 @@
4362
4362
  {
4363
4363
  "path": "modules/government/protected-surfaces.json",
4364
4364
  "mode": "0000644",
4365
- "sha256": "2a6c392d4499498f0e0fb80c80002ba9d82a3f5e66e48cce9802c21c276af2a0"
4365
+ "sha256": "ec125c2bfba680c7645dda0b65dd6ece183dd57ceecdb5616ea2b8b34b776e56"
4366
4366
  },
4367
4367
  {
4368
4368
  "path": "modules/government/ranks.js",
@@ -7512,12 +7512,12 @@
7512
7512
  {
7513
7513
  "path": "package-lock.json",
7514
7514
  "mode": "0000644",
7515
- "sha256": "51b06b26595e4850db318f091ea0cf01399add4ddff70489a7bd42c62e6dc85c"
7515
+ "sha256": "7027beb6f7d395889a8876a1f54a1c2c69b992f4a78b0d1a53441b748d018669"
7516
7516
  },
7517
7517
  {
7518
7518
  "path": "package.json",
7519
7519
  "mode": "0000644",
7520
- "sha256": "46d1603b0da9d38a1afe85820cfce1f1f485e26f692a7b077bf8951793dc1ac6"
7520
+ "sha256": "5e1af6b1b0101a246e04b20ce6b36a756a3839dc9f2e436d2e524b6a331165ff"
7521
7521
  },
7522
7522
  {
7523
7523
  "path": "public-docs/index.html",
@@ -9317,7 +9317,7 @@
9317
9317
  {
9318
9318
  "path": "src/module-api.js",
9319
9319
  "mode": "0000644",
9320
- "sha256": "c96971200e4ddee6375444c84cd0433158c026423638822b76ae941fa50f3956"
9320
+ "sha256": "39823a07155b3f5a8b13d2a6362985aab8ac0831d46ac25778e7893708d4fd5b"
9321
9321
  },
9322
9322
  {
9323
9323
  "path": "src/module-loader/catalog.js",
@@ -10807,7 +10807,7 @@
10807
10807
  {
10808
10808
  "path": "tests/government_protected_surfaces.mjs",
10809
10809
  "mode": "0000644",
10810
- "sha256": "dfde7ca6e223324317a0d56960db7adc9b30bdfc35f718269e9d6978344545ac"
10810
+ "sha256": "4478853e79266130c2ad3ab7a523f0f20fb98a97e88f1dd2476ef09db6b96dcc"
10811
10811
  },
10812
10812
  {
10813
10813
  "path": "tests/government_rank_sync.mjs",
@@ -79,6 +79,8 @@ This is the pillar that keeps configurability from becoming a foot-gun, and it h
79
79
 
80
80
  **The protected-surface registry.** Today's rank→surface map — `PROTECTED_GLOBS` + the per-tier `EXPECTED_RANKS` — is baked into code (`src/bongos/permission-path-check.js`). Governance takes ownership of it **as data**: which paths are protected, and at what floor, becomes a governance-owned registry. The **out-of-band substrate stays the enforcer** — the pre-push hook, the grader pre-pass, `main-audit.js`, `gate-review.js`, and the goal-scope check *read the registry* instead of a hardcoded list, but enforcement never moves into the app. The Metic floor and the `system`-permission mapping *flow from* the registry. The registry (R101) is also the moment to **close the [ADR 0087](0087-compete-on-governance-not-tooling-cursor.md) §3 path-matching gap** the sweep confirmed: `permission-path-check.js`'s `normalize()` does backslash→slash + strip-`./` only — **no case-folding and no Unicode normalization** — so a case-varied or homoglyph path evades the protected-glob match (Linux-deployed, so latent today, but it is the Cursor-class `.cursorignore`/case-bypass CVE shape). The registry matcher should normalize (case-fold + Unicode NFC) and gate file **creation** as strictly as edits.
81
81
 
82
+ **Amendment, 2026-09-13 (task 1002891).** The registry now protects the files that *hold* the DigitalOcean and Cloudflare credentials, not only the files that decide who may spend them — and it does so from a **computed** set: `tests/government_protected_surfaces.mjs` recomputes every `process.env` credential read from the tracked tree and reds the build if one is unregistered. The task that asked for this named a single file as "the sole holder" of both tokens; by the time it was claimed that file read neither, and the readers had spread to five. A registry entry transcribed from prose would have recorded the gap as closed while leaving every live reader unprotected — the same drift the MIT1 execution-root tripwire exists to catch, which is why this one is built the same way.
83
+
82
84
  The invariant this buys, stated as a testable promise (R112, R114): **the tab can re-map which surfaces are protected and at what floor (policy), but it cannot move enforcement into the request path (substrate).** An Archon can create a custom role and grant it every `system: false` permission in the catalog, assign it to a `xenos`, and that builder gains exactly those operational powers — **while a `system` permission the role "has" stays enforced by the substrate**: the deploy / protected-path / SSH attempt is still blocked, because [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md)'s layer-3 audit reads the real git history and the real registry, independent of anything the app grants. Config owns policy; the substrate owns enforcement; a forced grant cannot cross that line. (Note: `main` on this repo still has **no** GitHub branch protection — re-verified 2026-07-24, [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) §"Branch-protection status" — so `main-audit.js` remains the *authoritative* substrate layer, not a redundant net. The registry does not change that; it only makes the protected list data instead of a constant.)
83
85
 
84
86
  ### 5. Capability in core, settings per project (C4)
@@ -1932,5 +1932,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1932
1932
  landed since 1.19.727 with no explicit bump. run 34764630913. (task 1002620)
1933
1933
  1.19.729 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1934
1934
  landed since 1.19.728 with no explicit bump. run 34765179905. (task 1002620)
1935
+ 1.19.730 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1936
+ landed since 1.19.729 with no explicit bump. run 34765502139. (task 1002620)
1937
+ 1.19.731 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1938
+ landed since 1.19.730 with no explicit bump. run 34766029928. (task 1002620)
1935
1939
  ---------------------------------------------------------------------------
1936
1940
  ```
@@ -38,6 +38,12 @@
38
38
  { "glob": "modules/dev-box/boxes.js", "group": "box-access", "why": "boxScopeForRank / provision floor — decides who gets a full source clone" },
39
39
  { "glob": "modules/dev-box/box-access.js", "group": "box-access", "why": "decideSourceAccess (rank → starter|full source)" },
40
40
  { "glob": "modules/dev-box/box-credential.js", "group": "box-access", "why": "mints the box source credential" },
41
+ { "glob": "scripts/gds/provision-config.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.DO_API_TOKEN. The group already protects who DECIDES box access (boxes.js, box-access.js, box-credential.js) but protected nothing that HOLDS the infrastructure credentials those decisions spend — an asymmetry idea 1000692 raised and this entry closes. The set is computed, not listed: the tripwire in tests/government_protected_surfaces.mjs recomputes every process.env credential read from the tracked tree and reds if one is unregistered." },
42
+ { "glob": "scripts/gds/provision-net.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.CLOUDFLARE_API_TOKEN in checkDnsTokenScope. The filing task asserted this code lived in scripts/gds/provision.js; it had moved, and provision.js reads no credential at all. Registering the named file rather than the computed one would have recorded the gap as closed while closing nothing." },
43
+ { "glob": "scripts/gds/box-infra.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.CLOUDFLARE_API_TOKEN and process.env.CLOUDFLARE_ACCOUNT_ID into the CF config the box plane provisions against." },
44
+ { "glob": "scripts/gds/cf-tunnel.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: defaults its token parameter to process.env.CLOUDFLARE_API_TOKEN, so it reads the credential whenever a caller omits one. Not named by the recorded decision — the recomputation found it, which is the whole argument for computing the set instead of transcribing it." },
45
+ { "glob": "scripts/gds/box.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.DO_API_TOKEN — the DigitalOcean control plane for the box fleet. Also not named by the recorded decision. Registering it does NOT widen any module wall: the dev-box module key already lists this file and already derives protected:true from the modules/dev-box/ entries above, so the derived protected-module set is unchanged (verified before and after against src/bongos/module-scope-map.js)." },
46
+ { "glob": "scripts/gds/do-api.js", "group": "box-access", "why": "task 1002891. The DigitalOcean API client the token is handed TO. Unlike the five above it reads no process.env value — it takes the token as a parameter and throws when a live call has none — so the drift tripwire does not demand it and its absence from that computed set is expected, not a bug. It is registered because the recorded decision named it and because it is the code that puts the credential on the wire." },
41
47
  { "glob": "modules/grading/grader.js", "group": "pipeline", "why": "BV1.R73: grading carved to modules/grading/ — the glob follows the file" },
42
48
  { "glob": "modules/grading/grader-workers/", "group": "pipeline", "why": "the adversarial panel (Narc/Hacker/Quality/Efficiency) — blunting these blunts the gate" },
43
49
  { "glob": "modules/grading/grader-rubric.json", "group": "pipeline", "why": "the grader's scoring rubric (ADR 0058)" },
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.729",
3
+ "version": "1.19.731",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.729",
9
+ "version": "1.19.731",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.729",
3
+ "version": "1.19.731",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.729'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.731'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -174,6 +174,58 @@ const ADDED_SINCE_R101 = [
174
174
  + 'otb-regen driver, so editing it changes how much of the tree that driver '
175
175
  + 'rewrites during a merge. executed-closure.js carries it as TRUSTED_NON_CODE.',
176
176
  },
177
+ // task 1002891 (from idea 1000692, raised by the grader's hacker worker). Group
178
+ // 'box-access' protected who DECIDES box access but nothing that HOLDS the
179
+ // infrastructure credentials those decisions spend. The five readers below are the
180
+ // COMPUTED set — the credential-reader tripwire further down recomputes it from the
181
+ // tracked tree on every run — plus do-api.js, which the credential is handed to.
182
+ {
183
+ after: 'modules/dev-box/box-credential.js',
184
+ glob: 'scripts/gds/provision-config.js',
185
+ why: 'reads process.env.DO_API_TOKEN. The DigitalOcean credential for instance '
186
+ + 'provisioning, previously unprotected at any rank while the files deciding WHO '
187
+ + 'may provision were protected at Metic.',
188
+ },
189
+ {
190
+ after: 'scripts/gds/provision-config.js',
191
+ glob: 'scripts/gds/provision-net.js',
192
+ why: 'reads process.env.CLOUDFLARE_API_TOKEN in checkDnsTokenScope. The filing task '
193
+ + 'placed this code in scripts/gds/provision.js, which by the time of the claim '
194
+ + 'read no credential at all — registering the file the prose named would have '
195
+ + 'recorded the gap as closed while closing nothing.',
196
+ },
197
+ {
198
+ after: 'scripts/gds/provision-net.js',
199
+ glob: 'scripts/gds/box-infra.js',
200
+ why: 'reads process.env.CLOUDFLARE_API_TOKEN and process.env.CLOUDFLARE_ACCOUNT_ID '
201
+ + 'into the Cloudflare config the box plane provisions tunnels and DNS against.',
202
+ },
203
+ {
204
+ after: 'scripts/gds/box-infra.js',
205
+ glob: 'scripts/gds/cf-tunnel.js',
206
+ why: 'defaults its token parameter to process.env.CLOUDFLARE_API_TOKEN, so it reads '
207
+ + 'the credential whenever a caller omits one. The recorded decision did not name '
208
+ + 'this file; the recomputation found it, which is the case for computing the set '
209
+ + 'rather than transcribing a list.',
210
+ },
211
+ {
212
+ after: 'scripts/gds/cf-tunnel.js',
213
+ glob: 'scripts/gds/box.js',
214
+ why: 'reads process.env.DO_API_TOKEN — the DigitalOcean control plane for the box '
215
+ + 'fleet, and also not named by the recorded decision. Adding it widens no module '
216
+ + 'wall: the dev-box key already lists this path and already derives protected '
217
+ + 'from the modules/dev-box/ entries, so the derived protected-module set is '
218
+ + 'unchanged (asserted below).',
219
+ },
220
+ {
221
+ after: 'scripts/gds/box.js',
222
+ glob: 'scripts/gds/do-api.js',
223
+ why: 'the DigitalOcean API client the token is handed TO. It reads no process.env '
224
+ + 'value — the token arrives as a parameter and the constructor throws when a live '
225
+ + 'call has none — so the tripwire below does not demand it and its absence from '
226
+ + 'that computed set is expected. Registered because it is the code that puts the '
227
+ + 'credential on the wire.',
228
+ },
177
229
  ];
178
230
 
179
231
  // Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
@@ -303,6 +355,212 @@ test('every execution root in the closure is a protected surface (MIT1 drift tri
303
355
  }
304
356
  });
305
357
 
358
+ // ---------- the credential tripwire: every credential READER is a protected surface ----------
359
+ //
360
+ // task 1002891, from idea 1000692 (the grader's hacker worker, on the 1002690 re-grade).
361
+ // Group 'box-access' protected the files that decide WHO may provision a box, but nothing
362
+ // that HOLDS the DigitalOcean / Cloudflare credentials those decisions spend.
363
+ //
364
+ // WHY THIS IS COMPUTED AND NOT LISTED. The task offered two options, both resting on a
365
+ // named file: "scripts/gds/provision.js is the sole holder of the DO and CF tokens". By
366
+ // the time it was claimed that sentence was false in both halves — provision.js reads
367
+ // NEITHER token (the code moved), and the readers are spread across five files. Taking the
368
+ // prose at its word would have registered a file holding no credential and recorded the
369
+ // gap as closed. That is the failure shape of task 1003366, where an audit named four
370
+ // execution roots and the code had eight, and the four missed the earliest vector.
371
+ //
372
+ // So the registry is pinned to what the tree actually does, and this test is the pin: add
373
+ // a process.env read of a credential anywhere in tracked JS and the build reds until the
374
+ // registry grows to match.
375
+ //
376
+ // FAIL CLOSED, DELIBERATELY. The scan is textual, so a `process.env.DO_API_TOKEN` written
377
+ // inside a comment counts as a read. That over-reports rather than under-reports, which is
378
+ // the right direction for a security tripwire: the fix is to register the file or reword
379
+ // the comment, and neither leaves a live credential unprotected.
380
+ const CREDENTIAL_ENV = ['DO_API_TOKEN', 'CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'];
381
+
382
+ // Which CREDENTIAL_ENV names this source reads off process.env. Four spellings, because a
383
+ // tripwire that only knows the idiom in use today goes quiet the first time someone writes
384
+ // the same read differently: `process.env.NAME`, `process.env['NAME']`,
385
+ // `process['env']['NAME']`, and destructuring — `const { NAME } = process.env`, rename and
386
+ // multi-line included.
387
+ //
388
+ // KNOWN LIMIT, stated rather than papered over: a read through an intermediate binding
389
+ // (`const env = process.env; env.DO_API_TOKEN`) or a name assembled at runtime is not
390
+ // textually detectable and is not caught. Closing that needs an AST pass over the tree,
391
+ // which is a different piece of work; nothing in the tree reads a credential that way
392
+ // today, and the registry entries do not depend on the scan to stay correct — the scan is
393
+ // what catches DRIFT.
394
+ //
395
+ // Returns a sorted array; [] means "reads none", which is indistinguishable from "the
396
+ // scanner broke", so every caller also asserts the whole scan is non-vacuous. A scanner
397
+ // that quietly finds nothing looks exactly like a passing test.
398
+ function credentialReadsIn(source) {
399
+ const found = new Set();
400
+ for (const name of CREDENTIAL_ENV) {
401
+ const forms = [
402
+ new RegExp(`process\\.env\\.${name}\\b`),
403
+ new RegExp(`process\\.env\\[\\s*['"\`]${name}['"\`]\\s*\\]`),
404
+ new RegExp(`process\\s*\\[\\s*['"\`]env['"\`]\\s*\\]\\s*\\[\\s*['"\`]${name}['"\`]\\s*\\]`),
405
+ // Destructuring. [^}]* spans newlines inside a character class, so a multi-line
406
+ // destructure matches; the \\b keeps NAME_SUFFIX from counting as NAME.
407
+ new RegExp(`\\{[^}]*\\b${name}\\b[^}]*\\}\\s*=\\s*process\\.env\\b`),
408
+ ];
409
+ if (forms.some((re) => re.test(source))) found.add(name);
410
+ }
411
+ return [...found].sort();
412
+ }
413
+
414
+ // tests/ is out of scope, and the exclusion is load-bearing rather than convenient: the
415
+ // unit tests directly below hold `process.env.DO_API_TOKEN` as fixture text, so without it
416
+ // this scanner reports ITSELF as an unregistered credential holder (it did, on the first
417
+ // run — which is also the evidence that it detects rather than quietly finding nothing).
418
+ // A test fixture is not a credential holder: tests/ is not the provisioning path, holds no
419
+ // live token, and is never what a droplet executes. Every path that DOES reach DigitalOcean
420
+ // or Cloudflare lives under scripts/ or modules/ and stays in scope.
421
+ const SCAN_EXCLUDED = /^tests\//;
422
+
423
+ // The tracked JS tree, scanned for credential reads. git ls-files (not a directory walk)
424
+ // so an untracked scratch file can never add or remove a protected surface.
425
+ function credentialReaders() {
426
+ const tracked = execFileSync('git', ['ls-files', '*.js', '*.mjs', '*.cjs'],
427
+ { cwd: ROOT, encoding: 'utf8' })
428
+ .split('\n').map((s) => s.trim()).filter(Boolean);
429
+ return tracked
430
+ .filter((rel) => !SCAN_EXCLUDED.test(rel))
431
+ .filter((rel) => credentialReadsIn(fs.readFileSync(path.join(ROOT, rel), 'utf8')).length > 0)
432
+ .sort();
433
+ }
434
+
435
+ test('credentialReadsIn detects a read in every form it must', () => {
436
+ assert.deepEqual(credentialReadsIn('const t = process.env.DO_API_TOKEN || null;'),
437
+ ['DO_API_TOKEN']);
438
+ assert.deepEqual(credentialReadsIn("process.env['CLOUDFLARE_API_TOKEN']"),
439
+ ['CLOUDFLARE_API_TOKEN']);
440
+ assert.deepEqual(credentialReadsIn('process.env["CLOUDFLARE_ACCOUNT_ID"]'),
441
+ ['CLOUDFLARE_ACCOUNT_ID']);
442
+ assert.deepEqual(credentialReadsIn('process.env[ `DO_API_TOKEN` ]'), ['DO_API_TOKEN']);
443
+ assert.deepEqual(credentialReadsIn("process['env']['DO_API_TOKEN']"), ['DO_API_TOKEN']);
444
+ assert.deepEqual(credentialReadsIn('const { DO_API_TOKEN } = process.env;'),
445
+ ['DO_API_TOKEN']);
446
+ assert.deepEqual(credentialReadsIn('const { DO_API_TOKEN: tok } = process.env;'),
447
+ ['DO_API_TOKEN'], 'a renamed destructure is still a read');
448
+ assert.deepEqual(
449
+ credentialReadsIn('const {\n PORT,\n CLOUDFLARE_API_TOKEN,\n} = process.env;'),
450
+ ['CLOUDFLARE_API_TOKEN'], 'a multi-line destructure is still a read');
451
+ assert.deepEqual(
452
+ credentialReadsIn('process.env.DO_API_TOKEN; process.env.CLOUDFLARE_API_TOKEN;'),
453
+ ['CLOUDFLARE_API_TOKEN', 'DO_API_TOKEN']);
454
+ });
455
+
456
+ test('credentialReadsIn does not fire on a near-miss', () => {
457
+ // The guard that matters: a scanner matching too loosely reds honest files until
458
+ // someone loosens it, and a loosened scanner protects nothing.
459
+ assert.deepEqual(credentialReadsIn(''), []);
460
+ assert.deepEqual(credentialReadsIn('export DO_API_TOKEN or run without --apply'), []);
461
+ assert.deepEqual(credentialReadsIn('opts.token || cfg.DO_API_TOKEN'), []);
462
+ assert.deepEqual(credentialReadsIn('process.env.DO_API_TOKEN_FILE'), [],
463
+ 'a longer env name that merely starts with a credential name is a different variable');
464
+ assert.deepEqual(credentialReadsIn('process.env.SOME_OTHER_TOKEN'), []);
465
+ assert.deepEqual(credentialReadsIn('const { DO_API_TOKEN_FILE } = process.env;'), [],
466
+ 'the destructure form must respect the word boundary too');
467
+ assert.deepEqual(credentialReadsIn('const { DO_API_TOKEN } = someOtherObject;'), [],
468
+ 'destructuring a credential-shaped name off something else is not an env read');
469
+ });
470
+
471
+ test('every credential reader in the tree is a protected surface (1002891 tripwire)', () => {
472
+ const readers = credentialReaders();
473
+ assert.ok(readers.length >= 5,
474
+ 'fixture sanity: the scan found '
475
+ + `${readers.length} credential reader(s), and the tree has had at least five since `
476
+ + 'task 1002891. A scan that finds nothing passes every assertion below without '
477
+ + 'checking anything — treat this as the scanner being broken, not as good news.');
478
+ for (const rel of readers) {
479
+ const surface = ppc.surfaceFor(rel);
480
+ assert.ok(surface,
481
+ `${rel} READS an infrastructure credential off process.env `
482
+ + `(${credentialReadsIn(fs.readFileSync(path.join(ROOT, rel), 'utf8')).join(', ')}) `
483
+ + 'but no protected surface covers it. Add it to '
484
+ + 'modules/government/protected-surfaces.json (group "box-access") and declare it '
485
+ + 'in ADDED_SINCE_R101 above.');
486
+ assert.ok(ppc.RANK_TIER[surface.floor] >= ppc.RANK_TIER.metic,
487
+ `${rel} holds an infrastructure credential but is floored at '${surface.floor}' — `
488
+ + 'below Metic it stays writable by the ranks ADR 0043 keeps out of the trust core.');
489
+ }
490
+ });
491
+
492
+ // ---------- the hard-floor subset: gate-review's second list may narrow, never diverge ----------
493
+ //
494
+ // task 1002875, from idea 1000620 (raised as a major by the BV1.R101 ship grader).
495
+ // scripts/gds/gate-review.js keeps its own HARD_FLOOR_GLOBS — the rank-INDEPENDENT
496
+ // escalate-to-owner set — and the comment above it declares the array to be "A subset of
497
+ // PROTECTED_GLOBS". That was a checkable invariant stated in prose and enforced by nothing.
498
+ //
499
+ // TWO LISTS IS THE DESIGN, NOT THE BUG, which is why this is a subset test and not a
500
+ // de-duplication. The registry answers "who may EDIT this path" (a rank floor, re-mappable
501
+ // by government). The hard floor answers "may CI auto-merge an edit to this path at all"
502
+ // — no, escalate to the owner, whatever the author's rank. The second is deliberately
503
+ // narrower: every hard-floor surface must be protected, but most protected surfaces are
504
+ // ordinary Metic work that should not ping the owner.
505
+ //
506
+ // Checked at BOTH altitudes on purpose. The source literal is what a reviewer reads and
507
+ // what the decision asked be parsed; the exported array is what compileGlobMatcher actually
508
+ // compiles at gate-review.js:102. Asserting they agree means a future refactor that
509
+ // computes or filters the list cannot leave the visible source saying one thing while the
510
+ // gate enforces another.
511
+ function hardFloorGlobsFromSource() {
512
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'gate-review.js'), 'utf8');
513
+ const block = src.match(/const HARD_FLOOR_GLOBS = \[([\s\S]*?)\n\];/);
514
+ assert.ok(block, 'could not find the HARD_FLOOR_GLOBS array literal in gate-review.js — '
515
+ + 'if it was renamed or restructured, this test must be taught the new shape rather '
516
+ + 'than deleted: it is the only thing holding the declared subset honest.');
517
+ return [...block[1].matchAll(/'([^']+)'/g)].map((m) => m[1]);
518
+ }
519
+
520
+ test('every hard-floor glob is a protected surface (1002875 subset tripwire)', () => {
521
+ const fromSource = hardFloorGlobsFromSource();
522
+ assert.ok(fromSource.length >= 20,
523
+ `fixture sanity: parsed ${fromSource.length} hard-floor globs out of gate-review.js, `
524
+ + 'and the array has held about two dozen since it was written. A parse that returns '
525
+ + 'few or none passes the subset check vacuously.');
526
+
527
+ const gr = require(path.join(ROOT, 'scripts', 'gds', 'gate-review.js'));
528
+ assert.deepEqual(fromSource, gr.HARD_FLOOR_GLOBS,
529
+ 'the HARD_FLOOR_GLOBS literal in gate-review.js no longer matches the array the module '
530
+ + 'exports and compiles into its matcher. The source and the enforced list must be the '
531
+ + 'same thing, or a reviewer reading the file is reading a list the gate does not use.');
532
+
533
+ const registry = new Set(ppc.PROTECTED_GLOBS);
534
+ for (const glob of gr.HARD_FLOOR_GLOBS) {
535
+ assert.ok(registry.has(glob),
536
+ `'${glob}' is a HARD FLOOR surface in scripts/gds/gate-review.js — CI escalates any `
537
+ + 'edit to it to the owner regardless of rank — but it is not in '
538
+ + 'modules/government/protected-surfaces.json, so no rank floor covers it at all. '
539
+ + 'The hard floor is declared to be a SUBSET of the protected set: it may be '
540
+ + 'narrower (most protected paths are ordinary Metic work and should not ping the '
541
+ + 'owner), never wider. Either add the glob to the registry, or remove it from '
542
+ + 'HARD_FLOOR_GLOBS — do not delete this assertion.');
543
+ }
544
+ });
545
+
546
+ test('registering the credential readers widened no module wall', () => {
547
+ // The second-order effect task 1002891 was explicitly warned about, asserted rather
548
+ // than reasoned about. src/bongos/module-scope-map.js DERIVES a module's `protected`
549
+ // flag from overlap with this registry, and a protected module makes its goals
550
+ // Archon-mediated-join. The six surfaces added for 1002891 are file-level for exactly
551
+ // this reason: the modules/provisioning/ subtree (the task's option (b)) would have
552
+ // flipped `provisioning` and walled goals 1000032, 1000046 and 1000059.
553
+ const map = require(path.join(ROOT, 'src', 'bongos', 'module-scope-map.js'));
554
+ const protectedNow = map.protectedModules();
555
+ assert.ok(!protectedNow.includes('provisioning'),
556
+ 'the provisioning module derived protected:true — a credential-reader entry reached '
557
+ + 'modules/provisioning/ or scripts/gds/provision.js, which walls goals 1000032, '
558
+ + '1000046 and 1000059 behind an Archon join. Keep the entries file-level.');
559
+ assert.ok(protectedNow.includes('dev-box'),
560
+ 'dev-box was already protected via its modules/dev-box/ globs before 1002891; if it '
561
+ + 'is not, the premise of the box.js entry has changed.');
562
+ });
563
+
306
564
  // Metic is a FLOOR, not a fixed value (task 1003191). Pinning every surface to
307
565
  // exactly 'metic' made the registry's own contract untestable and, worse,
308
566
  // illegal: governance owns this data and raising one surface to 'archon' is a