@bongos/core 1.19.703 → 1.19.704
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 +16 -16
- package/docs/module-api-changelog.md +2 -0
- package/docs/recipes/windows-builders.md +43 -16
- package/modules/agents/lib/validate.js +56 -0
- package/modules/government/protected-surfaces.json +8 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/agents-sync.js +48 -14
- package/src/module-api.js +1 -1
- package/tests/agents_sync.mjs +63 -0
- package/tests/agents_validate.mjs +118 -1
- package/tests/government_protected_surfaces.mjs +132 -1
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.704",
|
|
6
|
+
"core_contract": "1.19.704",
|
|
7
|
+
"source_commit": "86a7901eed7ffd687d8366c14b70b6dbce1a36f8",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-13T04:51:44.117Z",
|
|
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": 2633,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "6a38a87e7ab24df8fa7020189772ed67bc02d6271601eda7a94aeddf84bfc12d",
|
|
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": "
|
|
2805
|
+
"sha256": "339e97fa485b7a1b3090c5964a23d6d30d3863781e549e30af68610fb3eedb91"
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
7785
|
+
"sha256": "8eb4b18a9a0ee8ec64bab2712698fca515ff92277ebeed12adbb7126fe30e3e2"
|
|
7786
7786
|
},
|
|
7787
7787
|
{
|
|
7788
7788
|
"path": "package.json",
|
|
7789
7789
|
"mode": "0000644",
|
|
7790
|
-
"sha256": "
|
|
7790
|
+
"sha256": "6c55beca8912927a2d103ef0efee212341daa09be17e346d4d298377e3958d19"
|
|
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": "
|
|
7865
|
+
"sha256": "7a639c73f8d2f4f523899ab737641174ca7b5ed5409427238d7fc397cdba65ed"
|
|
7866
7866
|
},
|
|
7867
7867
|
{
|
|
7868
7868
|
"path": "scripts/gds/api.js",
|
|
@@ -9557,7 +9557,7 @@
|
|
|
9557
9557
|
{
|
|
9558
9558
|
"path": "src/module-api.js",
|
|
9559
9559
|
"mode": "0000644",
|
|
9560
|
-
"sha256": "
|
|
9560
|
+
"sha256": "0954b91956641ff68caa5c9373be6d33d3ac3fdacda102846de75a339326fc86"
|
|
9561
9561
|
},
|
|
9562
9562
|
{
|
|
9563
9563
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9672,12 +9672,12 @@
|
|
|
9672
9672
|
{
|
|
9673
9673
|
"path": "tests/agents_sync.mjs",
|
|
9674
9674
|
"mode": "0000644",
|
|
9675
|
-
"sha256": "
|
|
9675
|
+
"sha256": "f7f6d88fd31a2eb1a81678d883706d2716e6a2ae20159599d3dbe7ed4c267f2e"
|
|
9676
9676
|
},
|
|
9677
9677
|
{
|
|
9678
9678
|
"path": "tests/agents_validate.mjs",
|
|
9679
9679
|
"mode": "0000644",
|
|
9680
|
-
"sha256": "
|
|
9680
|
+
"sha256": "82c7bcef316f5c0068cb53a43b2ef4a01367ff285939f132f01b0d8633d36353"
|
|
9681
9681
|
},
|
|
9682
9682
|
{
|
|
9683
9683
|
"path": "tests/api_cli_flags.mjs",
|
|
@@ -10992,7 +10992,7 @@
|
|
|
10992
10992
|
{
|
|
10993
10993
|
"path": "tests/government_protected_surfaces.mjs",
|
|
10994
10994
|
"mode": "0000644",
|
|
10995
|
-
"sha256": "
|
|
10995
|
+
"sha256": "d11562d59861a5f5d294c072b3aa15f03703ddbb2102210ff92f7ccbcff5b5f6"
|
|
10996
10996
|
},
|
|
10997
10997
|
{
|
|
10998
10998
|
"path": "tests/government_rank_sync.mjs",
|
|
@@ -1865,5 +1865,7 @@ 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)
|
|
1868
1870
|
---------------------------------------------------------------------------
|
|
1869
1871
|
```
|
|
@@ -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
|
|
92
|
-
(
|
|
93
|
-
(1003759) — **plus the 6 carried by task
|
|
94
|
-
`
|
|
95
|
-
is green, and a red suite outside those
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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.
|
|
3
|
+
"version": "1.19.704",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.704",
|
|
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.
|
|
3
|
+
"version": "1.19.704",
|
|
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([
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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));
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.704'; // 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');
|
package/tests/agents_sync.mjs
CHANGED
|
@@ -316,6 +316,69 @@ t('a GitHub noreply address yields the login; a local name is only a fallback',
|
|
|
316
316
|
'an unreadable history resolves to null, which rule 2 treats as insufficient');
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
+
console.log('\nthe path wall is wired to the REAL registry (task 1002492):');
|
|
320
|
+
|
|
321
|
+
const permissionPaths = require('../src/bongos/permission-path-check.js');
|
|
322
|
+
const realMatcher = (p) => permissionPaths.surfaceFor(p) !== null;
|
|
323
|
+
|
|
324
|
+
t('the wall is wired to the REAL protected-surface registry', () => {
|
|
325
|
+
// Not a stub: the same synchronously-loaded registry the pre-push hook, the
|
|
326
|
+
// grader pre-pass and main-audit.js read (ADR 0043). If a governance re-map
|
|
327
|
+
// narrows it, this fails rather than silently widening what an agent may reach.
|
|
328
|
+
assert.equal(realMatcher('modules/government/catalog.js'), true, 'the authority surface');
|
|
329
|
+
assert.equal(realMatcher('migrations/001_init.sql'), true, 'schema');
|
|
330
|
+
assert.equal(realMatcher('.claude/hooks/pre-push.js'), true, 'local automation');
|
|
331
|
+
assert.equal(realMatcher('modules/agents/lib/validate.js'), false, "a module's own code is not protected");
|
|
332
|
+
// `.claude/agents/` itself is deliberately NOT in the registry — see the note
|
|
333
|
+
// on ADDED_SINCE_R101 in tests/government_protected_surfaces.mjs. Protecting
|
|
334
|
+
// it would rank-wall the whole agents module by derivation, which is a
|
|
335
|
+
// separate decision from the one this wall makes.
|
|
336
|
+
assert.equal(realMatcher('.claude/agents/historian.md'), false,
|
|
337
|
+
'not protected — and that is recorded, not accidental');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
t('a path scope onto the authority surface is withheld a rank AND flagged', () => {
|
|
341
|
+
// Both halves of task 1002492 meeting the rule from 1002490: the definition
|
|
342
|
+
// reaches a protected surface BY PATH, so (a) no git-derived rank is stamped
|
|
343
|
+
// and (b) the scope wall refuses it — imported, disabled, visible.
|
|
344
|
+
const d = sync.parseAgentFile(
|
|
345
|
+
'---\nname: snoop\ntrigger: on-demand\nscope_paths: [modules/government/catalog.js]\n---\nbody', {});
|
|
346
|
+
assert.equal(sync.declaresProtectedScope(d, [], realMatcher), true);
|
|
347
|
+
assert.equal(sync.stampableAuthorRank(d, { gitRank: 'archon', isProtectedPath: realMatcher }), null,
|
|
348
|
+
'a claimed archon rank must not survive a PATH-spelled protected scope either');
|
|
349
|
+
const p = sync.planOne(d, {
|
|
350
|
+
authorRank: 'archon', allowedModules: ['agents'], isProtectedPath: realMatcher,
|
|
351
|
+
});
|
|
352
|
+
assert.equal(p.action, 'flagged');
|
|
353
|
+
assert.equal(p.authorRank, null);
|
|
354
|
+
assert.equal(p.enabled, false);
|
|
355
|
+
assert.match(p.scopeViolation, /scope_paths|protected surface/i);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
t('an unprotected path scope still reconciles normally', () => {
|
|
359
|
+
// The wall is a floor, not a ban — a tightening that refused every path scope
|
|
360
|
+
// would pass the test above and break the feature.
|
|
361
|
+
const d = sync.parseAgentFile(
|
|
362
|
+
'---\nname: reader\ntrigger: on-demand\nscope_paths: [modules/agents/lib/]\n---\nbody', {});
|
|
363
|
+
assert.equal(sync.declaresProtectedScope(d, [], realMatcher), false);
|
|
364
|
+
const p = sync.planOne(d, {
|
|
365
|
+
authorRank: 'metic', allowedModules: ['agents'], isProtectedPath: realMatcher,
|
|
366
|
+
});
|
|
367
|
+
assert.equal(p.action, 'upsert');
|
|
368
|
+
assert.equal(p.authorRank, 'metic', 'rank gates nothing here, so the provenance hint is kept');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
t('declaring paths with NO matcher flags rather than importing clean', () => {
|
|
372
|
+
// The uncheckable case end to end: the plan must not treat "nobody checked"
|
|
373
|
+
// as "nothing was protected".
|
|
374
|
+
const d = sync.parseAgentFile(
|
|
375
|
+
'---\nname: x\ntrigger: on-demand\nscope_paths: [anything]\n---\nbody', {});
|
|
376
|
+
assert.equal(sync.declaresProtectedScope(d, [], null), true, 'unanswerable reads as protected');
|
|
377
|
+
const p = sync.planOne(d, { authorRank: 'archon', allowedModules: ['agents'] });
|
|
378
|
+
assert.equal(p.action, 'flagged', 'imported and visible, never silently clean');
|
|
379
|
+
assert.equal(p.enabled, false);
|
|
380
|
+
});
|
|
381
|
+
|
|
319
382
|
console.log('\nthe reconcile actually RUNS at deploy:');
|
|
320
383
|
|
|
321
384
|
t('migrate.sh invokes agents-sync, module-gated and fail-open', () => {
|
|
@@ -41,7 +41,15 @@ const OK_DEF = Object.freeze({
|
|
|
41
41
|
model_tier: 'default',
|
|
42
42
|
scope_modules: ['agents'],
|
|
43
43
|
});
|
|
44
|
-
|
|
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', () => {
|
|
@@ -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
|
-
|
|
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
|