@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.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.703",
6
- "core_contract": "1.19.703",
7
- "source_commit": "f85beb07ce74ab0f248774c68e6d16453aa964c1",
5
+ "core_version": "1.19.705",
6
+ "core_contract": "1.19.705",
7
+ "source_commit": "ee446c148a7c0bc6e13028a538e32f2288b74802",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-13T03:53:32.093Z",
9
+ "built_at": "2026-09-13T05:14:27.400Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 477,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2132,
14
+ "functional_verbatim": 2134,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2633,
20
- "tree_sha256": "4890a8f49b9e7da75dd0e91d122be5cc3c64802bca863b7eae48b237cab5e9b3",
19
+ "file_count": 2635,
20
+ "tree_sha256": "b975cdb11e18889435478808545314cf6b5f9d2a3f17411eb4cd63d3d294e047",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2802,7 +2802,7 @@
2802
2802
  {
2803
2803
  "path": "docs/module-api-changelog.md",
2804
2804
  "mode": "0000644",
2805
- "sha256": "f777f91ad88b47ff271da5bbc1670507ed77fa105df07e3d15dca6912617baef"
2805
+ "sha256": "64a2ef09f0be002b9c3af200c125b35b09775adb665a273b44e235a79b7265b9"
2806
2806
  },
2807
2807
  {
2808
2808
  "path": "docs/modules-contract.md",
@@ -3067,7 +3067,7 @@
3067
3067
  {
3068
3068
  "path": "docs/recipes/windows-builders.md",
3069
3069
  "mode": "0000644",
3070
- "sha256": "e5bcc0b4da0755546cb10870052099def2745c890cd8157d36d997e58a0399b3"
3070
+ "sha256": "6760a34d404aa68c61896983c8aa621bd26244e5c98ed056045b3467359c4b53"
3071
3071
  },
3072
3072
  {
3073
3073
  "path": "docs/recipes/work-on-a-task-from-your-dev-box.md",
@@ -3862,7 +3862,7 @@
3862
3862
  {
3863
3863
  "path": "modules/agents/lib/validate.js",
3864
3864
  "mode": "0000644",
3865
- "sha256": "5cff70fc4e280c49d33dfb8f60ddb01181decd1645625b0bb5e7c2eb97335b81"
3865
+ "sha256": "cd3659ad5831c767fe6600fabbe883f33b3429107a0b999092158dfa7c95b4e7"
3866
3866
  },
3867
3867
  {
3868
3868
  "path": "modules/agents/migrations/agents_001_definitions.sql",
@@ -4642,7 +4642,7 @@
4642
4642
  {
4643
4643
  "path": "modules/government/protected-surfaces.json",
4644
4644
  "mode": "0000644",
4645
- "sha256": "6d9a7520b2565b34bf002df016598543101639ccc384d81a622d313ed688acbc"
4645
+ "sha256": "2a6c392d4499498f0e0fb80c80002ba9d82a3f5e66e48cce9802c21c276af2a0"
4646
4646
  },
4647
4647
  {
4648
4648
  "path": "modules/government/ranks.js",
@@ -7782,12 +7782,12 @@
7782
7782
  {
7783
7783
  "path": "package-lock.json",
7784
7784
  "mode": "0000644",
7785
- "sha256": "d63e44b7d4f919455708ad0fd9041ef90552639733c506b1fdc06fe2e2ccd12b"
7785
+ "sha256": "b74ccfc9d9bcd1da47c4bdf0010e1516b48e5213cc435f10069bb30454b6e4fc"
7786
7786
  },
7787
7787
  {
7788
7788
  "path": "package.json",
7789
7789
  "mode": "0000644",
7790
- "sha256": "ff9eefa1e4c9f66ddd29b8a077ef567602ca79e69ab569b13f72e9221289b2ed"
7790
+ "sha256": "59663e88e9591b8ef999f69c31757eac4275b7aa1376b9497ad6467223a21b3a"
7791
7791
  },
7792
7792
  {
7793
7793
  "path": "public-docs/index.html",
@@ -7862,7 +7862,7 @@
7862
7862
  {
7863
7863
  "path": "scripts/gds/agents-sync.js",
7864
7864
  "mode": "0000644",
7865
- "sha256": "2e1a55ae7187a8ee970873a108a7989fed121f1ac1f92361d7b9bc61987056a2"
7865
+ "sha256": "7a639c73f8d2f4f523899ab737641174ca7b5ed5409427238d7fc397cdba65ed"
7866
7866
  },
7867
7867
  {
7868
7868
  "path": "scripts/gds/api.js",
@@ -8199,6 +8199,11 @@
8199
8199
  "mode": "0000644",
8200
8200
  "sha256": "2fcc3f97558ed6a653638df96d585dc7a1ef7dd884657a63d3a9545dbdb03e92"
8201
8201
  },
8202
+ {
8203
+ "path": "scripts/gds/exec-path-guard.js",
8204
+ "mode": "0000644",
8205
+ "sha256": "e568575a922de8c4538f11fa0e67ca0a97ab75317ff59b6aea27e6020a87313c"
8206
+ },
8202
8207
  {
8203
8208
  "path": "scripts/gds/feedback-latest.js",
8204
8209
  "mode": "0000644",
@@ -8252,7 +8257,7 @@
8252
8257
  {
8253
8258
  "path": "scripts/gds/fitness.js",
8254
8259
  "mode": "0000644",
8255
- "sha256": "d6120a5427f9053529639cf15a59bad0afae9602d5a1e8301017f3321493038b"
8260
+ "sha256": "418376cea128d2b1683e65017d74c453eb170e163bd131c1252ec79c6baf0920"
8256
8261
  },
8257
8262
  {
8258
8263
  "path": "scripts/gds/gate-review.js",
@@ -9557,7 +9562,7 @@
9557
9562
  {
9558
9563
  "path": "src/module-api.js",
9559
9564
  "mode": "0000644",
9560
- "sha256": "6cb88cea4e61987151e1872011b8667189ef59d03f3999be5950ba716ad9b05e"
9565
+ "sha256": "d5b14e265925245fcd8ff4c280c68d0162a3bec6d58ee5358047e43708bdeaed"
9561
9566
  },
9562
9567
  {
9563
9568
  "path": "src/module-loader/catalog.js",
@@ -9672,12 +9677,12 @@
9672
9677
  {
9673
9678
  "path": "tests/agents_sync.mjs",
9674
9679
  "mode": "0000644",
9675
- "sha256": "96197b8e25d9a8620cd3e02b7c96137d7118543662555c73d74e22a6b6161953"
9680
+ "sha256": "f7f6d88fd31a2eb1a81678d883706d2716e6a2ae20159599d3dbe7ed4c267f2e"
9676
9681
  },
9677
9682
  {
9678
9683
  "path": "tests/agents_validate.mjs",
9679
9684
  "mode": "0000644",
9680
- "sha256": "c698b80ab8d64421b5c81e91172e1f219c41924af19721106f1899cd29c2fabc"
9685
+ "sha256": "82c7bcef316f5c0068cb53a43b2ef4a01367ff285939f132f01b0d8633d36353"
9681
9686
  },
9682
9687
  {
9683
9688
  "path": "tests/api_cli_flags.mjs",
@@ -10574,6 +10579,11 @@
10574
10579
  "mode": "0000644",
10575
10580
  "sha256": "d6a78357533fdeea3bc33fe97cfcd27b1d04747c600d0a7800ddfca5199dbb1f"
10576
10581
  },
10582
+ {
10583
+ "path": "tests/exec_path_guard.mjs",
10584
+ "mode": "0000644",
10585
+ "sha256": "478075d19af9592d87ef9419c06496e72c132f4c045e19a1ebf821ab6ff7074b"
10586
+ },
10577
10587
  {
10578
10588
  "path": "tests/executed_closure.mjs",
10579
10589
  "mode": "0000644",
@@ -10992,7 +11002,7 @@
10992
11002
  {
10993
11003
  "path": "tests/government_protected_surfaces.mjs",
10994
11004
  "mode": "0000644",
10995
- "sha256": "3517e05c1264fbdbeadc533884942926689952c8995548863ca1cfbfc365428e"
11005
+ "sha256": "d11562d59861a5f5d294c072b3aa15f03703ddbb2102210ff92f7ccbcff5b5f6"
10996
11006
  },
10997
11007
  {
10998
11008
  "path": "tests/government_rank_sync.mjs",
@@ -1865,5 +1865,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1865
1865
  landed since 1.19.701 with no explicit bump. run 34735775228. (task 1002620)
1866
1866
  1.19.703 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1867
1867
  landed since 1.19.702 with no explicit bump. run 34736566183. (task 1002620)
1868
+ 1.19.704 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1869
+ landed since 1.19.703 with no explicit bump. run 34738864605. (task 1002620)
1870
+ 1.19.705 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1871
+ landed since 1.19.704 with no explicit bump. run 34739785273. (task 1002620)
1868
1872
  ---------------------------------------------------------------------------
1869
1873
  ```
@@ -88,30 +88,57 @@ DB-backed tests with no local Postgres — the count drifts and is not worth rec
88
88
  **`node scripts/gds/run-unit-tests.js`** instead: it is the DB-free gate CI actually runs, so its
89
89
  result means something locally.
90
90
 
91
- **That gate's Windows baseline as of task 1003529 (2026-09-12): 4 suites** — `sound_player`
92
- (task 1003628), `session_caps` (1003827), `skill_preflight` (1003754), `task_recommendations`
93
- (1003759) — **plus the 6 carried by task 1003848**: `cli_surface`, `module_api_lazy`,
94
- `search_index_poller`, `search_isolation`, `tool_scan_engine`, `security/mirror`. Everything else
95
- is green, and a red suite outside those ten is yours.
96
-
97
- `logger` is **not** on that list and needs no work: it was the whole of the old "missing deps in
98
- the worktree" family and task 1003616 fixed it on 2026-09-07see the walk-up section below.
99
-
100
- **Two of the ten are INTERMITTENT, so a green run does not mean fixed.** `session_caps` and
101
- `search_isolation` exit `3221226505` (`0xC0000409`, STATUS_STACK_BUFFER_OVERRUN) the process
102
- dies during teardown *after* its assertions have all passed, so whether the suite counts them as
103
- failures depends on timing. Measured in task 1003529: `session_caps` passed inside a full
104
- `run-unit-tests.js` sweep and failed twice in a row when run on its own, minutes apart, with task
105
- 1003827 still open and nothing on main having touched it. This is the `cliExit` class documented in
91
+ **That gate's Windows baseline, re-measured for task 1003860 on 2026-09-12 in a deps-installed
92
+ `origin/main` worktree (core 1.19.703): 9 suites** — `session_caps` (task 1003827),
93
+ `skill_preflight` (1003754), `task_recommendations` (1003759) — **plus the 6 carried by task
94
+ 1003848**: `cli_surface`, `module_api_lazy`, `search_index_poller`, `search_isolation`,
95
+ `tool_scan_engine`, `security/mirror`. Everything else is green, and a red suite outside those
96
+ nine is yours.
97
+
98
+ **`sound_player` LEFT this list on 2026-09-12**task 1003628 fixed the real bug behind it (the
99
+ sound-prefs case read the builder's own settings instead of a fixture), and it is now green in two
100
+ full sweeps and two standalone runs. It is called out rather than quietly deleted because it was
101
+ listed as expected-red for a matter of hours: the list was written the same day the fix shipped.
102
+ `logger` is likewise **not** on the list and needs no work — it was the whole of the old "missing
103
+ deps in the worktree" family and task 1003616 fixed it on 2026-09-07 (see the walk-up section
104
+ below).
105
+
106
+ **THREE suites are INTERMITTENT on a Windows box, so neither a green run nor a red one settles
107
+ anything on its own.** Two of the three are on the baseline above; the third is NOT, and keeping
108
+ that distinction straight is the whole point of this paragraph — "intermittent" is a property of a
109
+ suite, not a licence to add it to the expected-red list.
110
+
111
+ *Intermittent AND on the list:* `session_caps` and `search_isolation` exit `3221226505`
112
+ (`0xC0000409`, STATUS_STACK_BUFFER_OVERRUN) — the process dies during teardown *after* its
113
+ assertions have all passed, so whether the suite counts them as failures depends on timing. Both
114
+ directions are now on record. Task 1003529: `session_caps` passed inside a full
115
+ `run-unit-tests.js` sweep and failed twice in a row when run on its own, minutes apart. Task
116
+ 1003860, two sweeps minutes apart on one unchanged tree: `session_caps` was RED in the first and
117
+ GREEN in the second, plus green twice standalone — so the same tree answers 9 failures or 8
118
+ depending on nothing you did, and `search_isolation` was red in both. Task 1003827 is still open
119
+ and nothing on main has touched either. This is the `cliExit` class documented in
106
120
  [scripts/gds/CLAUDE.md](../../scripts/gds/CLAUDE.md) — a hard exit while handles are still closing.
107
121
  Do not mark either fixed on the strength of one green sweep, and do not let their noise cover a
108
122
  real regression: diff by name AND re-run the individual suite.
109
123
 
124
+ *Intermittent but NOT on the list:* `<redacted>` (task 1003860). It failed inside one
125
+ full sweep on 2026-09-12 and has not reproduced since — green standalone 7-of-7 then, and green in
126
+ both full sweeps and three more standalone runs when the baseline was re-measured. It binds an
127
+ ephemeral socket (`app.listen(0)`) and calls `server.close()` without awaiting it, so it is the
128
+ same teardown-timing class as the two above. Know the name, so a red here does not send you
129
+ hunting your own diff — but it stays OFF the expected-red list, because it is green on
130
+ `origin/main`. **A suite that reds once and then will not reproduce is not a baseline entry.**
131
+ Promoting a one-off red to a known name buys nothing and gives a genuine regression somewhere to
132
+ hide; `git_merge_regen` below is the case that cost a whole session. Re-run this one alone before
133
+ you touch anything.
134
+
110
135
  Two warnings about that list, both paid for:
111
136
 
112
137
  - **It drifts in BOTH directions.** The measurement before it (2026-08-27, 25 suites) had 7
113
138
  entries already fixed and was missing 6 that had since broken — so more than half of it was
114
- wrong within a fortnight. Re-measure; never inherit a list.
139
+ wrong within a fortnight. And the one after THAT went stale in hours rather than weeks:
140
+ `sound_player` was fixed by task 1003628 the same day it was written down as expected-red
141
+ (task 1003860). Re-measure; never inherit a list.
115
142
  - **Compare NAMES, never counts**, and never bank a local red as "environmental" without a
116
143
  by-name baseline run in a deps-installed `origin/main` worktree. A count that matches can hide
117
144
  one suite fixed and another broken.
@@ -143,6 +143,11 @@ function validateAgentDefinition(obj, opts = {}) {
143
143
  const rawProtected = safeGet(options, 'protectedModules');
144
144
  const allowedModules = Array.isArray(rawAllowed) ? rawAllowed : [];
145
145
  const protectedModules = Array.isArray(rawProtected) ? rawProtected : [];
146
+ // (path) => boolean — "does this repo-relative path reach a protected
147
+ // surface?". Supplied by the caller from the protected-surface registry
148
+ // (task 1002492); see the scope_paths wall below for why it is passed in and
149
+ // why its absence refuses rather than admits.
150
+ const isProtectedPath = safeGet(options, 'isProtectedPath');
146
151
 
147
152
  // --- name ----------------------------------------------------------------
148
153
  const name = safeGet(obj, 'name');
@@ -276,6 +281,57 @@ function validateAgentDefinition(obj, opts = {}) {
276
281
  }
277
282
  }
278
283
 
284
+ // --- scope: THE OTHER HALF OF THE SAME WALL (task 1002492) -----------------
285
+ //
286
+ // Until now the rank wall ran over `scope_modules` only, and `scope_paths` was
287
+ // checked for tree-escape and nothing else. So one wall had two halves that
288
+ // disagreed: `scope_modules: ['government']` was refused below Metic, while
289
+ // `scope_paths: ['modules/government/']` reached the same code and passed at
290
+ // any rank. A definition could scope itself onto the authority surface by
291
+ // spelling it as a path — which is the one thing this check exists to stop.
292
+ //
293
+ // The matcher is PASSED IN rather than required, for the same reason
294
+ // `protectedModules` is: this module may not require core (ADR 0083), and the
295
+ // protected-surface registry must stay readable with no DB and no server so
296
+ // the out-of-band enforcers keep working (ADR 0043). The caller owns both.
297
+ if (scopePaths && scopePaths.length > 0) {
298
+ if (typeof isProtectedPath !== 'function') {
299
+ // FAIL-CLOSED, and deliberately not a default of "nothing is protected":
300
+ // scope_paths has no allowlist counterpart the way scope_modules has
301
+ // `allowedModules`, so an absent matcher would be a silent hole rather
302
+ // than a narrow scope. Declaring paths REQUIRES the caller to supply the
303
+ // means to check them.
304
+ E('scope_paths_uncheckable', 'scope_paths',
305
+ 'scope_paths: declared, but no protected-path matcher was supplied, so they cannot be checked against the protected-surface registry. Refused rather than admitted — a path scope that nothing verified is indistinguishable from one nobody walled.');
306
+ } else {
307
+ const hits = [];
308
+ for (const p of scopePaths) {
309
+ let protectedPath;
310
+ try {
311
+ // ONLY an explicit `false` clears a path. Not `=== true`: that reads
312
+ // every ambiguous answer — undefined from a matcher that forgot a
313
+ // return, a truthy string, null — as "not protected", which is
314
+ // fail-OPEN on the exact question the wall exists to ask. Reading
315
+ // ambiguity as protected is loud and gets fixed; reading it as clean
316
+ // is silent and does not.
317
+ protectedPath = isProtectedPath(p) !== false;
318
+ } catch {
319
+ // A throwing matcher is treated as a HIT for the same reason: the
320
+ // question going unanswered must not read as no.
321
+ protectedPath = true;
322
+ }
323
+ if (protectedPath) hits.push(p);
324
+ }
325
+ if (hits.length > 0) {
326
+ const rankIdx = typeof authorRank === 'string' ? RANK_ORDER.indexOf(authorRank) : -1;
327
+ if (rankIdx < RANK_ORDER.indexOf(SCOPE_RANK_FLOOR)) {
328
+ E('scope_protected_rank', 'scope_paths',
329
+ `scope_paths: ${hits.map((p) => JSON.stringify(p)).join(', ')} ${hits.length === 1 ? 'reaches a protected surface' : 'reach protected surfaces'} and require a ${SCOPE_RANK_FLOOR}+ author; the author's resolved rank is ${JSON.stringify(authorRank ?? null)} (ADR 0016 / ADR 0043). A path scope is walled exactly as a module scope is — spelling the surface as a path is not a way around it.`);
330
+ }
331
+ }
332
+ }
333
+ }
334
+
279
335
  if (errors.length > 0) return { ok: false, errors, value: null };
280
336
 
281
337
  // The normalized row, built fresh — the caller's object is never mutated, and
@@ -55,6 +55,14 @@
55
55
  { "glob": "scripts/gds/install-git-hooks.js", "group": "pipeline", "why": "the hook installer (self-protect)" },
56
56
  { "glob": "scripts/gds/hooks/", "group": "pipeline", "why": "the git hooks themselves (self-protect)" },
57
57
  { "glob": "scripts/gds/cli-lib.js", "group": "pipeline", "why": "session/token + apiCall the gate relies on" },
58
+ { "glob": "scripts/gds/git-merge-regen.js", "group": "pipeline", "why": "task 1003366, audit ref MIT1. EXECUTION ROOT: registerMergeDriver writes 'node scripts/gds/git-merge-regen.js' as a RELATIVE command with cwd at the clone root, so git merge runs the BRANCH's copy, with the live server's DATABASE_URL and BUILDER_SECRET_KEY, before any post-merge step. This and the six generators below are the ENTRYPOINTS in modules/lifecycle/executed-closure.js; tests/government_protected_surfaces.mjs fails the build if that list ever grows past this registry." },
59
+ { "glob": "scripts/gds/gen-repo-map.js", "group": "pipeline", "why": "task 1003366, audit ref MIT1. EXECUTION ROOT: run by the merge resolver as execFile(node, [<clone>/...]) after the merge — the branch's copy, the server's environment." },
60
+ { "glob": "scripts/gds/gen-file-map.js", "group": "pipeline", "why": "task 1003366, audit ref MIT1. EXECUTION ROOT: the same post-merge generator path as gen-repo-map.js." },
61
+ { "glob": "scripts/gds/gen-session-index.js", "group": "pipeline", "why": "task 1003366, audit ref MIT1. EXECUTION ROOT: the same post-merge generator path as gen-repo-map.js." },
62
+ { "glob": "scripts/gds/copy-inventory.js", "group": "pipeline", "why": "task 1003366, audit ref MIT1. EXECUTION ROOT: a generator the resolver runs out of the clone. NOT named by the 2026-08-29 audit, which said 'the gen-* trio' — that trio was already stale against GENERATOR_SCRIPTS, which is why this registry is pinned to the closure's ENTRYPOINTS rather than to the audit's prose." },
63
+ { "glob": "scripts/gds/gen-api-docs.js", "group": "pipeline", "why": "task 1003366, audit ref MIT1. EXECUTION ROOT: a generator the resolver runs out of the clone, and the widest one — it require()s src/bongos/route-rank-check, api-prefix and routes/_helpers." },
64
+ { "glob": "scripts/gds/gen-api-client.js", "group": "pipeline", "why": "task 1003366, audit ref MIT1. EXECUTION ROOT: a generator the resolver runs out of the clone." },
65
+ { "glob": ".gitattributes", "group": "pipeline", "why": "task 1003366, audit ref MIT1. Not code and never require()d, but it NAMES which paths git hands the otb-regen driver — so editing it changes how much of the tree that driver rewrites during a merge. modules/lifecycle/executed-closure.js carries it as TRUSTED_NON_CODE for exactly this reason (ADR 0043)." },
58
66
  { "glob": "scripts/migrate.sh", "group": "pipeline", "why": "applies migrations on deploy" },
59
67
  { "glob": "scripts/deploy/", "group": "pipeline", "why": "the deploy scripts run on the droplet at deploy (ADR 0058)" },
60
68
  { "glob": "modules/lifecycle/merge-lock.js", "group": "pipeline", "why": "BV1.R86: serializes a ship" },
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.703",
3
+ "version": "1.19.705",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.703",
9
+ "version": "1.19.705",
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.703",
3
+ "version": "1.19.705",
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",
@@ -64,14 +64,35 @@ const validate = require(path.join(REPO_ROOT, 'modules/agents/lib/validate.js'))
64
64
  // not a definition". The distinction is the whole of rule 3: a scope refusal is
65
65
  // imported as a flagged row so somebody can SEE it, while a shape error has no
66
66
  // row to import — there is no definition there to put in the registry.
67
- const SCOPE_CODES = Object.freeze(['scope_protected_rank', 'scope_out_of_bounds']);
68
-
69
- // Does this definition reach a PROTECTED module? If so, the git-derived author
70
- // identity is not consulted at all (see below) — which is what makes rule 2 hold
71
- // against a value the author controls.
72
- function declaresProtectedScope(candidate, protectedModules = []) {
73
- const declared = Array.isArray(candidate?.scope_modules) ? candidate.scope_modules : [];
74
- return declared.some((m) => protectedModules.includes(m));
67
+ const SCOPE_CODES = Object.freeze([
68
+ 'scope_protected_rank',
69
+ 'scope_out_of_bounds',
70
+ // task 1002492. The definition is well-formed; what failed is that its paths
71
+ // could not be checked. That is importable-but-flagged (visible, disabled),
72
+ // never a silent skip — "nobody checked" must surface as loudly as "refused".
73
+ 'scope_paths_uncheckable',
74
+ ]);
75
+
76
+ // Does this definition reach a PROTECTED surface — by module OR by path? If so,
77
+ // the git-derived author identity is not consulted at all (see below), which is
78
+ // what makes rule 2 hold against a value the author controls.
79
+ //
80
+ // The path half arrived with task 1002492. Before it, this asked about modules
81
+ // only, which left the withholding rule with the same asymmetry the scope wall
82
+ // itself had: naming `modules/government/` as a PATH reached neither check, so a
83
+ // spoofed identity could still have stamped a rank onto a definition pointed at
84
+ // the authority surface. Both halves, or neither is a wall.
85
+ function declaresProtectedScope(candidate, protectedModules = [], isProtectedPath = null) {
86
+ const modules = Array.isArray(candidate?.scope_modules) ? candidate.scope_modules : [];
87
+ if (modules.some((m) => protectedModules.includes(m))) return true;
88
+ const paths = Array.isArray(candidate?.scope_paths) ? candidate.scope_paths : [];
89
+ if (paths.length === 0) return false;
90
+ // No matcher and declared paths: unanswerable, so treated as protected — the
91
+ // same fail-closed reading the validator applies (scope_paths_uncheckable).
92
+ if (typeof isProtectedPath !== 'function') return true;
93
+ return paths.some((p) => {
94
+ try { return isProtectedPath(p) !== false; } catch { return true; }
95
+ });
75
96
  }
76
97
 
77
98
  // THE RANK THAT ACTUALLY GETS STAMPED, and the answer to the spoofable-identity
@@ -95,8 +116,8 @@ function declaresProtectedScope(candidate, protectedModules = []) {
95
116
  //
96
117
  // The result is that spoofing the git identity buys exactly nothing: on the only
97
118
  // path where rank matters, no git-derived value is read.
98
- function stampableAuthorRank(candidate, { gitRank = null, protectedModules = [] } = {}) {
99
- if (declaresProtectedScope(candidate, protectedModules)) return null;
119
+ function stampableAuthorRank(candidate, { gitRank = null, protectedModules = [], isProtectedPath = null } = {}) {
120
+ if (declaresProtectedScope(candidate, protectedModules, isProtectedPath)) return null;
100
121
  return gitRank;
101
122
  }
102
123
 
@@ -203,6 +224,7 @@ function planOne(candidate, {
203
224
  authorRank: gitRank = null,
204
225
  allowedModules = [],
205
226
  protectedModules = [],
227
+ isProtectedPath = null,
206
228
  existing = null,
207
229
  } = {}) {
208
230
  const name = typeof candidate?.name === 'string' ? candidate.name : null;
@@ -210,7 +232,7 @@ function planOne(candidate, {
210
232
  // survives stampableAuthorRank, which withholds it entirely on any definition
211
233
  // that reaches a protected module. Everything below uses `authorRank`, so the
212
234
  // spoofable value cannot reach the wall by a path that forgets to launder it.
213
- const authorRank = stampableAuthorRank(candidate, { gitRank, protectedModules });
235
+ const authorRank = stampableAuthorRank(candidate, { gitRank, protectedModules, isProtectedPath });
214
236
 
215
237
  // Rule 1, checked before anything else: an instance-authored row owns its
216
238
  // name. A file that happens to share it does not get to overwrite it, and
@@ -220,7 +242,7 @@ function planOne(candidate, {
220
242
  }
221
243
 
222
244
  const verdict = validate.validateAgentDefinition(candidate, {
223
- authorRank, allowedModules, protectedModules,
245
+ authorRank, allowedModules, protectedModules, isProtectedPath,
224
246
  });
225
247
 
226
248
  if (verdict.ok) {
@@ -256,6 +278,11 @@ function planOne(candidate, {
256
278
  authorRank: validate.RANK_ORDER[validate.RANK_ORDER.length - 1],
257
279
  allowedModules: [...new Set([...allowedModules, ...declaredModules])],
258
280
  protectedModules: [],
281
+ // The re-run clears the MODULE wall to recover the normalized shape. The
282
+ // path matcher is deliberately left in place: a matcher that says a path is
283
+ // protected is a fact about the path, not about the author, so laundering
284
+ // it here would store a row whose scope no longer matches its flag.
285
+ isProtectedPath: () => false,
259
286
  });
260
287
  if (!shapeOnly.ok) {
261
288
  // Defensive: the two runs disagreeing means an error that is scope-coded but
@@ -275,13 +302,14 @@ function planOne(candidate, {
275
302
  // The whole plan, for a list of { name, candidate } and a lookup of existing
276
303
  // rows by name. Pure, so the reconcile's DECISIONS are unit-testable with no
277
304
  // database standing up.
278
- function planSync(files, { existingByName = new Map(), authorRankFor = () => null, allowedModules = [], protectedModules = [] } = {}) {
305
+ function planSync(files, { existingByName = new Map(), authorRankFor = () => null, allowedModules = [], protectedModules = [], isProtectedPath = null } = {}) {
279
306
  return files.map((f) => ({
280
307
  file: f.file || null,
281
308
  ...planOne(f.candidate, {
282
309
  authorRank: authorRankFor(f.file),
283
310
  allowedModules,
284
311
  protectedModules,
312
+ isProtectedPath,
285
313
  existing: existingByName.get(f.candidate?.name) || null,
286
314
  }),
287
315
  }));
@@ -370,6 +398,12 @@ async function main() {
370
398
  const scopeMap = require(path.join(REPO_ROOT, 'src/bongos/module-scope-map.js'));
371
399
  const allowedModules = scopeMap.moduleKeys();
372
400
  const protectedModules = scopeMap.protectedModules();
401
+ // The protected-surface registry, read the way every other enforcer reads it:
402
+ // synchronously from the checkout, no DB and no server (ADR 0043). surfaceFor
403
+ // returns the governing registry entry or null, so this is the boolean the
404
+ // validator's scope_paths wall asks for.
405
+ const permissionPaths = require(path.join(REPO_ROOT, 'src/bongos/permission-path-check.js'));
406
+ const isProtectedPath = (p) => permissionPaths.surfaceFor(p) !== null;
373
407
 
374
408
  const { rows: existingRows } = await pool.query(
375
409
  'SELECT name, source, enabled FROM agents_definitions'
@@ -397,7 +431,7 @@ async function main() {
397
431
  return login ? (rankByLogin.get(login.toLowerCase()) || null) : null;
398
432
  };
399
433
 
400
- const plan = planSync(files, { existingByName, authorRankFor, allowedModules, protectedModules });
434
+ const plan = planSync(files, { existingByName, authorRankFor, allowedModules, protectedModules, isProtectedPath });
401
435
 
402
436
  if (dryRun) {
403
437
  if (asJson) console.log(JSON.stringify({ ok: true, scanned: files.length, plan }, null, 2));