@bongos/core 1.19.696 → 1.19.698
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 +22 -17
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/box-access.js +18 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/context-pack.js +62 -20
- package/scripts/gds/run-unit-tests.js +20 -0
- package/src/module-api.js +1 -1
- package/tests/box_access.mjs +6 -1
- package/tests/context_pack.mjs +80 -0
- package/tests/secret_box.mjs +83 -9
- package/tests/unit_runner_serial.mjs +72 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.698",
|
|
6
|
+
"core_contract": "1.19.698",
|
|
7
|
+
"source_commit": "8fb86a2225d9ed2a99fe253ea3886c7b2e7bcfc9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-13T02:
|
|
9
|
+
"built_at": "2026-09-13T02:48:21.644Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 477,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2126,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2627,
|
|
20
|
+
"tree_sha256": "53de7b22ab854dd4a788f5ce3827fca83328bb1f43e0415eae9528923e68596b",
|
|
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": "5c48de96a5d2d3ed794c4f503a665f0c4defc7d06f384ba4012eeb02cf3036aa"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -4267,7 +4267,7 @@
|
|
|
4267
4267
|
{
|
|
4268
4268
|
"path": "modules/dev-box/box-access.js",
|
|
4269
4269
|
"mode": "0000644",
|
|
4270
|
-
"sha256": "
|
|
4270
|
+
"sha256": "7271dc2c3eab6c8ed274dc90f855ec813eef4e401180a300e31de31dce30aa6c"
|
|
4271
4271
|
},
|
|
4272
4272
|
{
|
|
4273
4273
|
"path": "modules/dev-box/box-credential.js",
|
|
@@ -7772,12 +7772,12 @@
|
|
|
7772
7772
|
{
|
|
7773
7773
|
"path": "package-lock.json",
|
|
7774
7774
|
"mode": "0000644",
|
|
7775
|
-
"sha256": "
|
|
7775
|
+
"sha256": "0faabe7f40491c6ab374488d6ae4a244694e1fbf40c280ad98ef0a249c207b80"
|
|
7776
7776
|
},
|
|
7777
7777
|
{
|
|
7778
7778
|
"path": "package.json",
|
|
7779
7779
|
"mode": "0000644",
|
|
7780
|
-
"sha256": "
|
|
7780
|
+
"sha256": "f623a28e95671e15f28bc19bc1b29f26f00312c5f0dc5e8761b872dd42e85923"
|
|
7781
7781
|
},
|
|
7782
7782
|
{
|
|
7783
7783
|
"path": "public-docs/index.html",
|
|
@@ -8072,7 +8072,7 @@
|
|
|
8072
8072
|
{
|
|
8073
8073
|
"path": "scripts/gds/context-pack.js",
|
|
8074
8074
|
"mode": "0000644",
|
|
8075
|
-
"sha256": "
|
|
8075
|
+
"sha256": "7863284f9d81a9cf77a5d01cff82d119207637090f9ef14012c8689625e93a49"
|
|
8076
8076
|
},
|
|
8077
8077
|
{
|
|
8078
8078
|
"path": "scripts/gds/control-manifest.js",
|
|
@@ -8697,7 +8697,7 @@
|
|
|
8697
8697
|
{
|
|
8698
8698
|
"path": "scripts/gds/run-unit-tests.js",
|
|
8699
8699
|
"mode": "0000644",
|
|
8700
|
-
"sha256": "
|
|
8700
|
+
"sha256": "4ba19aaa0106bafafd6369b761460e0a1fccac3045f191c3f7835bc250623f40"
|
|
8701
8701
|
},
|
|
8702
8702
|
{
|
|
8703
8703
|
"path": "scripts/gds/sandbox-stage.js",
|
|
@@ -9542,7 +9542,7 @@
|
|
|
9542
9542
|
{
|
|
9543
9543
|
"path": "src/module-api.js",
|
|
9544
9544
|
"mode": "0000644",
|
|
9545
|
-
"sha256": "
|
|
9545
|
+
"sha256": "f03b116eece1a5517cda1bf812627f7148aaf2827806d53d70f3b67d343026ab"
|
|
9546
9546
|
},
|
|
9547
9547
|
{
|
|
9548
9548
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9922,7 +9922,7 @@
|
|
|
9922
9922
|
{
|
|
9923
9923
|
"path": "tests/box_access.mjs",
|
|
9924
9924
|
"mode": "0000644",
|
|
9925
|
-
"sha256": "
|
|
9925
|
+
"sha256": "85b0f479d16d53eea0442cab9ea8aa2dd09d95926765b47a142c609af22f6927"
|
|
9926
9926
|
},
|
|
9927
9927
|
{
|
|
9928
9928
|
"path": "tests/box_auth_check.mjs",
|
|
@@ -10232,7 +10232,7 @@
|
|
|
10232
10232
|
{
|
|
10233
10233
|
"path": "tests/context_pack.mjs",
|
|
10234
10234
|
"mode": "0000644",
|
|
10235
|
-
"sha256": "
|
|
10235
|
+
"sha256": "159661396a274740d9164dce8532b54e283ead733301c841078334033784d04f"
|
|
10236
10236
|
},
|
|
10237
10237
|
{
|
|
10238
10238
|
"path": "tests/control_manifest.mjs",
|
|
@@ -12447,7 +12447,7 @@
|
|
|
12447
12447
|
{
|
|
12448
12448
|
"path": "tests/secret_box.mjs",
|
|
12449
12449
|
"mode": "0000644",
|
|
12450
|
-
"sha256": "
|
|
12450
|
+
"sha256": "b50f33fe20bfa93f97c1250083bf0b4d36c72be75ddeffeef6ff7c2c0fe1cc62"
|
|
12451
12451
|
},
|
|
12452
12452
|
{
|
|
12453
12453
|
"path": "tests/secret_gate.mjs",
|
|
@@ -12984,6 +12984,11 @@
|
|
|
12984
12984
|
"mode": "0000644",
|
|
12985
12985
|
"sha256": "a2ab80b7fb38695bb65c617eff1c39cfbe17403b1586cb1b20e3dee50e5a23fa"
|
|
12986
12986
|
},
|
|
12987
|
+
{
|
|
12988
|
+
"path": "tests/unit_runner_serial.mjs",
|
|
12989
|
+
"mode": "0000644",
|
|
12990
|
+
"sha256": "024c65f27b684bc3a00181cd72df7e7152af3372c9338d9a38f0a1200c25b678"
|
|
12991
|
+
},
|
|
12987
12992
|
{
|
|
12988
12993
|
"path": "tests/update_channel.mjs",
|
|
12989
12994
|
"mode": "0000644",
|
|
@@ -1851,5 +1851,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1851
1851
|
landed since 1.19.694 with no explicit bump. run 34732266878. (task 1002620)
|
|
1852
1852
|
1.19.696 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1853
1853
|
landed since 1.19.695 with no explicit bump. run 34733145358. (task 1002620)
|
|
1854
|
+
1.19.697 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1855
|
+
landed since 1.19.696 with no explicit bump. run 34733301156. (task 1002620)
|
|
1856
|
+
1.19.698 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1857
|
+
landed since 1.19.697 with no explicit bump. run 34733917938. (task 1002620)
|
|
1854
1858
|
---------------------------------------------------------------------------
|
|
1855
1859
|
```
|
|
@@ -178,6 +178,24 @@ const BASE_SPARSE_PATHS = [
|
|
|
178
178
|
// config/government.json) is host content the box's own builder authors — no
|
|
179
179
|
// credentials live here (ADR 0022); those are in /etc and the box env.
|
|
180
180
|
'config',
|
|
181
|
+
// modules/lifecycle — task 1003085. BASE's whole charter is "enough to run the
|
|
182
|
+
// /builder-* CLI", and the CLI does not run without this module:
|
|
183
|
+
// scripts/gds/context-pack.js reads the task enums from
|
|
184
|
+
// modules/lifecycle/task-classifier, and context-pack is on the /builder-start
|
|
185
|
+
// AND /builder-claim paths. On a task-scoped box with NO active claim nothing
|
|
186
|
+
// put lifecycle in scope, so both commands died at import with MODULE_NOT_FOUND
|
|
187
|
+
// — and neither could be used to fix it, because a CLAIM is what fetches the
|
|
188
|
+
// module. The only way out was POST /claims by hand.
|
|
189
|
+
//
|
|
190
|
+
// This is the structural half; context-pack.js also degrades now if the module
|
|
191
|
+
// is absent for any OTHER reason. Both, deliberately: the degrade keeps the CLI
|
|
192
|
+
// alive against a future module gap, and this line is what makes the pack
|
|
193
|
+
// actually COMPLETE on every box rather than quietly reduced.
|
|
194
|
+
//
|
|
195
|
+
// Same posture as the entries above — core-domain work-tracking code, which is
|
|
196
|
+
// methodology and not product runtime, so it belongs in BASE by the same reading
|
|
197
|
+
// that put scripts/ and src/bongos/ here. No secrets; a cone-mode directory.
|
|
198
|
+
'modules/lifecycle',
|
|
181
199
|
];
|
|
182
200
|
|
|
183
201
|
// sparsePathsForScopeKeys — PURE. Given the module KEYS a builder's active claims
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.698",
|
|
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.698",
|
|
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.698",
|
|
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",
|
|
@@ -30,11 +30,30 @@ const path = require('node:path');
|
|
|
30
30
|
// The live validators. task-classifier is a pure module (no DB, no network),
|
|
31
31
|
// so importing it from a CLI is safe — same precedent as next-number.js reading
|
|
32
32
|
// the repo tree directly. This is the "not hand-maintained" guarantee.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
//
|
|
34
|
+
// OPTIONAL, because the file may not be ON DISK (task 1003085). A task-scoped box
|
|
35
|
+
// (ADR 0148) materializes modules/<key> only for the modules the CLAIM puts in
|
|
36
|
+
// scope, so on a box with NO active claim modules/lifecycle is absent and this
|
|
37
|
+
// require threw MODULE_NOT_FOUND at import time. That killed /builder-start AND
|
|
38
|
+
// /builder-claim — the exact two commands needed to escape the state, since the
|
|
39
|
+
// claim is what would fetch the module. A deadlock, escapable only by calling
|
|
40
|
+
// POST /claims with curl. The header's FAIL-OPEN promise ("must NEVER throw or
|
|
41
|
+
// block a claim") was already the right rule; this require was the line that did
|
|
42
|
+
// not keep it.
|
|
43
|
+
//
|
|
44
|
+
// Degrade by OMITTING the enums, NEVER by hard-coding a copy. A local list of
|
|
45
|
+
// kinds is precisely the drift the "generated from the live validators" design
|
|
46
|
+
// exists to prevent, and it would be WORSE than absence: absence costs the
|
|
47
|
+
// planner one /docs lookup, while a stale list reads as authoritative and is
|
|
48
|
+
// discovered only when POST /tasks rejects it — the guess-and-retry turn this
|
|
49
|
+
// whole pack was built to save.
|
|
50
|
+
let classifier = null;
|
|
51
|
+
try {
|
|
52
|
+
// eslint-disable-next-line global-require
|
|
53
|
+
classifier = require('../../modules/lifecycle/task-classifier');
|
|
54
|
+
} catch (_) {
|
|
55
|
+
// Not materialized on this box. apiContract() reports it honestly below.
|
|
56
|
+
}
|
|
38
57
|
|
|
39
58
|
// Single source of truth for the API prefix (src/bongos/api-prefix.js) so the URL
|
|
40
59
|
// rename (Part B) flips one constant and this pack follows. Importing a pure src
|
|
@@ -46,17 +65,31 @@ const { API_PREFIX } = require('../../src/bongos/api-prefix');
|
|
|
46
65
|
// The API contract a planner needs to file/claim without guessing. 'unclassified'
|
|
47
66
|
// is dropped from both enums: it's the classifier's fallback bucket, never a
|
|
48
67
|
// value a human should pick.
|
|
68
|
+
//
|
|
69
|
+
// The SHAPE is the same either way — empty arrays and null bounds, never missing
|
|
70
|
+
// keys — because every consumer here and in start.js reads these fields
|
|
71
|
+
// positionally. Returning a short object instead would move the crash from the
|
|
72
|
+
// require to the formatter and fix nothing. `enums_available` is what the
|
|
73
|
+
// formatters branch on, so "we don't know" is said once, in words, rather than
|
|
74
|
+
// rendered as an empty list that looks like "there are none".
|
|
49
75
|
function apiContract() {
|
|
50
|
-
const
|
|
51
|
-
const disciplines = [...VALID_DISCIPLINES].filter((d) => d !== 'unclassified');
|
|
52
|
-
return {
|
|
76
|
+
const base = {
|
|
53
77
|
api_prefix: API_PREFIX,
|
|
54
78
|
docs_url: '/docs',
|
|
55
79
|
openapi_url: '/docs/openapi.json',
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
enums_available: Boolean(classifier),
|
|
81
|
+
task_kinds: [],
|
|
82
|
+
disciplines: [],
|
|
83
|
+
priority: null,
|
|
84
|
+
manual_degree: null,
|
|
85
|
+
};
|
|
86
|
+
if (!classifier) return base;
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
task_kinds: [...classifier.VALID_KINDS].filter((k) => k !== 'unclassified'),
|
|
90
|
+
disciplines: [...classifier.VALID_DISCIPLINES].filter((d) => d !== 'unclassified'),
|
|
91
|
+
priority: classifier.TASK_FIELD_BOUNDS.priority,
|
|
92
|
+
manual_degree: classifier.TASK_FIELD_BOUNDS.manual_degree,
|
|
60
93
|
};
|
|
61
94
|
}
|
|
62
95
|
|
|
@@ -212,13 +245,20 @@ function formatContextPack(pack) {
|
|
|
212
245
|
lines.push(` Held claims: ${formatClaims(claims)}`);
|
|
213
246
|
lines.push(` API prefix: ${api.api_prefix} (e.g. POST ${api.api_prefix}/tasks, GET ${api.api_prefix}/me)`);
|
|
214
247
|
lines.push(` API reference: ${api.docs_url} (rendered) · ${api.openapi_url} (OpenAPI 3.1)`);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
248
|
+
if (api.enums_available) {
|
|
249
|
+
lines.push(` Task kinds: ${api.task_kinds.join(', ')}`);
|
|
250
|
+
lines.push(` Disciplines: ${api.disciplines.join(', ')}`);
|
|
251
|
+
const conv = api.priority.convention;
|
|
252
|
+
lines.push(
|
|
253
|
+
` Numeric fields: priority int ${api.priority.min}–${api.priority.max} ` +
|
|
254
|
+
`(use ${conv.label}) · manual_degree int ${api.manual_degree.min}–${api.manual_degree.max}`
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
// Say WHY and WHERE to look. The one thing this line must not do is imply the
|
|
258
|
+
// enums are empty — a planner who reads "kinds:" with nothing after it will
|
|
259
|
+
// invent one.
|
|
260
|
+
lines.push(` Task kinds: unavailable here — modules/lifecycle is not on this box; see ${api.docs_url}`);
|
|
261
|
+
}
|
|
222
262
|
lines.push(` OS: ${formatOs(os)}`);
|
|
223
263
|
return lines.join('\n');
|
|
224
264
|
}
|
|
@@ -230,7 +270,9 @@ function formatContextPack(pack) {
|
|
|
230
270
|
// noise beyond the shared compass.
|
|
231
271
|
function formatContextPackCompact(pack) {
|
|
232
272
|
const { worktree, api } = pack;
|
|
233
|
-
const kinds = api.
|
|
273
|
+
const kinds = api.enums_available
|
|
274
|
+
? api.task_kinds.slice(0, 4).join(', ') + (api.task_kinds.length > 4 ? ', …' : '')
|
|
275
|
+
: `see ${api.docs_url}`;
|
|
234
276
|
return [
|
|
235
277
|
`🧭 Worktree: ${worktree.root} (edit + git here, not the main checkout)`,
|
|
236
278
|
` API ${api.api_prefix} · kinds: ${kinds} · full pack on /builder-claim`,
|
|
@@ -284,6 +284,26 @@ function main() {
|
|
|
284
284
|
}
|
|
285
285
|
};
|
|
286
286
|
|
|
287
|
+
// EXECUTION IS SERIAL, ONE PROCESS PER TEST — and that is load-bearing, not
|
|
288
|
+
// incidental (task 1001435). runOne uses execFileSync, which blocks, and these
|
|
289
|
+
// are plain sequential loops: no Promise.all, no worker pool, no concurrency
|
|
290
|
+
// anywhere in this file. Pinned by tests/unit_runner_serial.mjs.
|
|
291
|
+
//
|
|
292
|
+
// Why it is written down. A flaky-`unit` report (this task, from idea #353)
|
|
293
|
+
// diagnosed "parallel execution racing on shared global state" and asked whether
|
|
294
|
+
// the runner fans tests out concurrently in one process. It does not, and never
|
|
295
|
+
// has — so an in-process race is ruled out as a cause by construction, and any
|
|
296
|
+
// future flake investigation should start somewhere else. The three suites that
|
|
297
|
+
// report named were audited then:
|
|
298
|
+
// - fitness.mjs — writes only under mkdtempSync(os.tmpdir()); no repo
|
|
299
|
+
// writes, no env mutation. Clean.
|
|
300
|
+
// - session_index.mjs — mutates no global state at all. Clean.
|
|
301
|
+
// - secret_box.mjs — DID have a real hole, but not this one: it redirected
|
|
302
|
+
// only HOME, which os.homedir() ignores on Windows, so
|
|
303
|
+
// its sandbox was a no-op there. Fixed under this task.
|
|
304
|
+
// Collisions between separate processes remain possible through state OUTSIDE
|
|
305
|
+
// them — the real home directory, generated files in the repo, a second run on
|
|
306
|
+
// the same machine — which is where to look next, not here.
|
|
287
307
|
for (const name of files) runOne(name, path.join(TESTS_DIR, `${name}.mjs`));
|
|
288
308
|
for (const { key, name, file } of moduleTests) runOne(`${key}/${name}`, file);
|
|
289
309
|
|
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.698'; // 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/box_access.mjs
CHANGED
|
@@ -196,7 +196,12 @@ console.log('\nBASE_SPARSE_PATHS — the non-product base a task-scoped box laye
|
|
|
196
196
|
t('BASE carries the methodology + tooling, NOT the product/game', () => {
|
|
197
197
|
// clients/ is load-bearing: cli-lib.js hard-imports clients/bongos-client, so a
|
|
198
198
|
// cone without it has a fully dead builder CLI on the box (task 1002745).
|
|
199
|
-
|
|
199
|
+
// modules/lifecycle is load-bearing for the same reason (task 1003085):
|
|
200
|
+
// scripts/gds/context-pack.js reads the task enums from its task-classifier,
|
|
201
|
+
// and context-pack sits on BOTH /builder-start and /builder-claim. A box with
|
|
202
|
+
// no active claim put nothing in scope, so both commands died at import — and
|
|
203
|
+
// neither could fix it, because a CLAIM is what fetches the module.
|
|
204
|
+
for (const p of ['.devcontainer', '.claude', 'scripts', 'src/bongos', 'docs', 'infra', 'tests', 'clients', 'modules/lifecycle']) {
|
|
200
205
|
assert.ok(BASE.includes(p), `BASE must include ${p}`);
|
|
201
206
|
}
|
|
202
207
|
for (const p of ['src/world', 'src/rooms', 'public', 'art', 'modules/status-ui']) {
|
package/tests/context_pack.mjs
CHANGED
|
@@ -339,5 +339,85 @@ t('formatContextPackCompact: two lines, worktree + prefix, no undefined', () =>
|
|
|
339
339
|
assert.ok(!/undefined/.test(out));
|
|
340
340
|
});
|
|
341
341
|
|
|
342
|
+
// ---- the task-scoped box with no claim (task 1003085) ----------------------
|
|
343
|
+
//
|
|
344
|
+
// A box scoped to a claim materializes modules/<key> only for the modules that
|
|
345
|
+
// claim put in scope (ADR 0148). With NO active claim, modules/lifecycle is not
|
|
346
|
+
// on disk — and this file's hard `require` of task-classifier threw
|
|
347
|
+
// MODULE_NOT_FOUND at IMPORT time, which killed /builder-start and
|
|
348
|
+
// /builder-claim: the two commands needed to escape the state, because a claim
|
|
349
|
+
// is what fetches the module. The live escape was POST /claims by curl.
|
|
350
|
+
//
|
|
351
|
+
// This reproduces the condition for real rather than asserting on source text:
|
|
352
|
+
// a temp tree holding context-pack.js and the one other thing it imports, with
|
|
353
|
+
// NO modules/ directory at all. A regression re-introducing a bare top-level
|
|
354
|
+
// require fails here at `require(...)`, before any assertion runs.
|
|
355
|
+
const fs = require('node:fs');
|
|
356
|
+
const os = require('node:os');
|
|
357
|
+
const path = require('node:path');
|
|
358
|
+
const url = require('node:url');
|
|
359
|
+
|
|
360
|
+
function boxWithoutLifecycle() {
|
|
361
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
362
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ctxpack-nolifecycle-'));
|
|
363
|
+
fs.mkdirSync(path.join(root, 'scripts', 'gds'), { recursive: true });
|
|
364
|
+
fs.mkdirSync(path.join(root, 'src', 'bongos'), { recursive: true });
|
|
365
|
+
fs.copyFileSync(path.join(here, '..', 'scripts', 'gds', 'context-pack.js'),
|
|
366
|
+
path.join(root, 'scripts', 'gds', 'context-pack.js'));
|
|
367
|
+
// The ONLY other import. Deliberately copied rather than stubbed: if
|
|
368
|
+
// context-pack grows a second hard dependency, this test fails and that new
|
|
369
|
+
// coupling has to be looked at — which is the point.
|
|
370
|
+
fs.copyFileSync(path.join(here, '..', 'src', 'bongos', 'api-prefix.js'),
|
|
371
|
+
path.join(root, 'src', 'bongos', 'api-prefix.js'));
|
|
372
|
+
// No modules/ — that IS the box condition.
|
|
373
|
+
assert.ok(!fs.existsSync(path.join(root, 'modules')), 'the fixture must not carry modules/');
|
|
374
|
+
return { root, mod: require(path.join(root, 'scripts', 'gds', 'context-pack.js')) };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
t('a box with no modules/lifecycle can still LOAD the pack (the deadlock)', () => {
|
|
378
|
+
const { mod } = boxWithoutLifecycle();
|
|
379
|
+
const api = mod.apiContract();
|
|
380
|
+
assert.equal(api.enums_available, false, 'it must say so, not pretend');
|
|
381
|
+
assert.equal(api.api_prefix, '/api/bongos', 'everything not behind the module still works');
|
|
382
|
+
// Shape preserved, so every positional consumer survives.
|
|
383
|
+
assert.deepEqual(api.task_kinds, []);
|
|
384
|
+
assert.deepEqual(api.disciplines, []);
|
|
385
|
+
assert.equal(api.priority, null);
|
|
386
|
+
assert.equal(api.manual_degree, null);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
t('both formatters render on that box instead of throwing', () => {
|
|
390
|
+
const { mod } = boxWithoutLifecycle();
|
|
391
|
+
const pack = mod.buildContextPack({
|
|
392
|
+
activeClaims: [], cwd: '/fallback', platform: 'linux',
|
|
393
|
+
runGit: () => '/work/tree\n', runWhich: () => '',
|
|
394
|
+
});
|
|
395
|
+
// The crash this moves is subtle: omitting the keys instead of emptying them
|
|
396
|
+
// would just relocate the failure from the require to `api.priority.convention`
|
|
397
|
+
// here — still a dead CLI, one stack frame later.
|
|
398
|
+
const full = mod.formatContextPack(pack);
|
|
399
|
+
assert.ok(!/undefined/.test(full), 'no undefined leaks into the pack');
|
|
400
|
+
assert.match(full, /unavailable here/, 'it names the gap');
|
|
401
|
+
assert.match(full, /\/docs/, 'and says where the real answer is');
|
|
402
|
+
assert.doesNotMatch(full, /Task kinds: {5}$/m, 'never an EMPTY kinds line — that reads as "there are none"');
|
|
403
|
+
const compact = mod.formatContextPackCompact(pack);
|
|
404
|
+
assert.equal(compact.split('\n').length, 2);
|
|
405
|
+
assert.ok(!/undefined/.test(compact));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
t('with the module present the enums are still the LIVE ones, not a local copy', () => {
|
|
409
|
+
// The guard on the fix: degrading must not have introduced a hand-maintained
|
|
410
|
+
// fallback list. This is the original design promise, re-pinned against the
|
|
411
|
+
// new code path.
|
|
412
|
+
const api = apiContract();
|
|
413
|
+
assert.equal(api.enums_available, true);
|
|
414
|
+
const live = require('../modules/lifecycle/task-classifier');
|
|
415
|
+
assert.deepEqual(api.task_kinds, [...live.VALID_KINDS].filter((k) => k !== 'unclassified'));
|
|
416
|
+
assert.deepEqual(api.disciplines, [...live.VALID_DISCIPLINES].filter((d) => d !== 'unclassified'));
|
|
417
|
+
const src = fs.readFileSync(new URL('../scripts/gds/context-pack.js', import.meta.url), 'utf8');
|
|
418
|
+
assert.doesNotMatch(src, /'(bug|feature|chore|cleanup|verify)'\s*,\s*'/,
|
|
419
|
+
'a literal kind list in this file is the drift the generated design exists to prevent');
|
|
420
|
+
});
|
|
421
|
+
|
|
342
422
|
console.log(`\ncontext_pack: ${passed} passed, ${failed} failed`);
|
|
343
423
|
process.exit(failed ? 1 : 0);
|
package/tests/secret_box.mjs
CHANGED
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
// tamper detection (a flipped byte fails the auth tag), wrong-key failure, and
|
|
7
7
|
// the fail-closed isConfigured() behaviour.
|
|
8
8
|
//
|
|
9
|
-
// Hermetic: forces BUILDER_SECRET_KEY
|
|
10
|
-
//
|
|
9
|
+
// Hermetic: forces BUILDER_SECRET_KEY and redirects the HOME DIRECTORY — every
|
|
10
|
+
// variable a platform might read for it, not HOME alone — so the
|
|
11
|
+
// ~/.config/<instance>/builder-secret.key fallback cannot resolve to a real
|
|
12
|
+
// operator file on the test machine. withKey then PROVES the redirect landed
|
|
13
|
+
// (os.homedir() must be inside the sandbox) before running any body, and refuses
|
|
14
|
+
// loudly if it did not. HOME alone was a no-op on Windows; see withKey (task 1001435).
|
|
11
15
|
//
|
|
12
16
|
// Run: node tests/secret_box.mjs
|
|
13
17
|
|
|
14
18
|
import { strict as assert } from 'node:assert';
|
|
15
19
|
import { createRequire } from 'node:module';
|
|
16
20
|
import { mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
17
|
-
import { tmpdir } from 'node:os';
|
|
18
|
-
import { join } from 'node:path';
|
|
21
|
+
import os, { tmpdir } from 'node:os';
|
|
22
|
+
import path, { join } from 'node:path';
|
|
19
23
|
import crypto from 'node:crypto';
|
|
20
24
|
|
|
21
25
|
const require = createRequire(import.meta.url);
|
|
@@ -38,18 +42,69 @@ function t(name, fn) {
|
|
|
38
42
|
const KEY_HEX = crypto.randomBytes(32).toString('hex');
|
|
39
43
|
const OTHER_HEX = crypto.randomBytes(32).toString('hex');
|
|
40
44
|
|
|
41
|
-
// Run a thunk with BUILDER_SECRET_KEY + an empty
|
|
45
|
+
// Run a thunk with BUILDER_SECRET_KEY + an empty home, restoring everything after.
|
|
46
|
+
//
|
|
47
|
+
// SETTING HOME ALONE IS NOT ISOLATION (task 1001435, applying task 1003760's
|
|
48
|
+
// contract). This file's header promises the real ~/.config/<instance>/
|
|
49
|
+
// builder-secret.key "is never touched", and secret-box.js finds that file via
|
|
50
|
+
// instance-config configHome() -> os.homedir(). On Windows os.homedir() reads
|
|
51
|
+
// USERPROFILE and NEVER consults HOME, so the override below isolated nothing
|
|
52
|
+
// there: the promise held on POSIX and was silently false on every Windows
|
|
53
|
+
// checkout. Exactly the miss that cost real credentials in task 1003760, which
|
|
54
|
+
// found ten files using this pattern and left the stragglers as task 1003763.
|
|
55
|
+
//
|
|
56
|
+
// No key file was in fact being written to the real config dir — measured before
|
|
57
|
+
// changing this, by running the suite and checking the path — because every
|
|
58
|
+
// provisioning case passes an explicit `file`. So this closes a LATENT hole, not
|
|
59
|
+
// an active leak. It is still worth closing: the next case added here would
|
|
60
|
+
// reasonably trust the header's promise, and the failure mode is destroying a
|
|
61
|
+
// builder's own master key.
|
|
62
|
+
// `home` is always an mkdtempSync(os.tmpdir()) path we just created, so the
|
|
63
|
+
// HOMEDRIVE/HOMEPATH split below is plain string work on a value of our own making
|
|
64
|
+
// — never attacker-influenced. If this shape is ever reused on an untrusted path,
|
|
65
|
+
// split with path.win32 helpers instead of slicing.
|
|
66
|
+
function sandboxHomeEnv(home) {
|
|
67
|
+
const { root } = path.parse(home);
|
|
68
|
+
return {
|
|
69
|
+
HOME: home,
|
|
70
|
+
USERPROFILE: home, // Windows: what os.homedir() actually reads
|
|
71
|
+
HOMEDRIVE: root.replace(/[\\/]+$/, ''), // legacy Windows fallback pair
|
|
72
|
+
HOMEPATH: home.slice(root.length - 1) || '\\',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
42
76
|
function withKey(keyValue, fn) {
|
|
43
77
|
const origKey = process.env.BUILDER_SECRET_KEY;
|
|
44
|
-
const origHome = process.env.HOME;
|
|
45
78
|
if (keyValue === null) delete process.env.BUILDER_SECRET_KEY;
|
|
46
79
|
else process.env.BUILDER_SECRET_KEY = keyValue;
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
|
|
81
|
+
const home = mkdtempSync(join(tmpdir(), 'otb-secretbox-'));
|
|
82
|
+
const sandbox = sandboxHomeEnv(home);
|
|
83
|
+
const orig = {};
|
|
84
|
+
for (const k of Object.keys(sandbox)) orig[k] = process.env[k];
|
|
85
|
+
Object.assign(process.env, sandbox);
|
|
86
|
+
try {
|
|
87
|
+
// Layer 2 of the same contract: PROVE the override landed before running a
|
|
88
|
+
// body that can write a master key. os.homedir() re-reads the environment on
|
|
89
|
+
// every call (measured on win32: it follows USERPROFILE mid-process), so this
|
|
90
|
+
// is a real check, not a tautology. A future platform that reads some third
|
|
91
|
+
// variable fails here LOUDLY instead of quietly writing to the real config dir
|
|
92
|
+
// — the only acceptable outcome for a test whose subject is a secret file.
|
|
93
|
+
if (os.homedir() !== home) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`secret_box sandbox escape: os.homedir() is ${os.homedir()} but the sandbox is ${home} — `
|
|
96
|
+
+ 'refusing to run, this would touch the real builder-secret.key.'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
fn();
|
|
100
|
+
}
|
|
49
101
|
finally {
|
|
50
102
|
if (origKey === undefined) delete process.env.BUILDER_SECRET_KEY;
|
|
51
103
|
else process.env.BUILDER_SECRET_KEY = origKey;
|
|
52
|
-
|
|
104
|
+
for (const [k, v] of Object.entries(orig)) {
|
|
105
|
+
if (v === undefined) delete process.env[k];
|
|
106
|
+
else process.env[k] = v;
|
|
107
|
+
}
|
|
53
108
|
}
|
|
54
109
|
}
|
|
55
110
|
|
|
@@ -146,6 +201,25 @@ t('decrypt rejects a malformed blob', () => {
|
|
|
146
201
|
});
|
|
147
202
|
});
|
|
148
203
|
|
|
204
|
+
// task 1001435: the isolation this file's header PROMISES, asserted instead of
|
|
205
|
+
// assumed. Before this, withKey set HOME only — which on Windows os.homedir()
|
|
206
|
+
// never reads — so the promise that the real builder-secret.key "is never
|
|
207
|
+
// touched" was true on POSIX and false here, and nothing would have said so.
|
|
208
|
+
t('withKey actually isolates the home dir — the header\'s promise, checked', () => {
|
|
209
|
+
const realHome = os.homedir();
|
|
210
|
+
let insideHome = null;
|
|
211
|
+
let insideConfig = null;
|
|
212
|
+
withKey(KEY_HEX, () => {
|
|
213
|
+
insideHome = os.homedir();
|
|
214
|
+
// The path secret-box.js would resolve for the key file, via the same
|
|
215
|
+
// instance-config chain it uses at runtime.
|
|
216
|
+
insideConfig = join(os.homedir(), '.config');
|
|
217
|
+
});
|
|
218
|
+
assert.notEqual(insideHome, realHome, 'the body must not see the builder\'s real home');
|
|
219
|
+
assert.ok(insideConfig.startsWith(insideHome), 'and the config dir must sit inside the sandbox');
|
|
220
|
+
assert.equal(os.homedir(), realHome, 'and the real home is restored afterwards');
|
|
221
|
+
});
|
|
222
|
+
|
|
149
223
|
console.log('\nprovisionMasterKey (task 1398 — productized, idempotent, never-overwrite):');
|
|
150
224
|
|
|
151
225
|
// Split in two (task 1003529): the creation claim holds on every platform, the MODE
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// tests/unit_runner_serial.mjs
|
|
2
|
+
//
|
|
3
|
+
// The `unit` gate runs its tests SERIALLY, one child process each — pinned here
|
|
4
|
+
// because that property is now load-bearing, not incidental (task 1001435).
|
|
5
|
+
//
|
|
6
|
+
// A flaky-`unit` report (idea #353) diagnosed "parallel execution racing on shared
|
|
7
|
+
// global state" and asked, correctly, whether run-unit-tests fans tests out
|
|
8
|
+
// concurrently in one process. It does not: runOne calls execFileSync, which
|
|
9
|
+
// BLOCKS, from plain sequential for-loops. That rules an in-process race out by
|
|
10
|
+
// construction, and scripts/gds/run-unit-tests.js now records the audit that
|
|
11
|
+
// followed from it — including that two of the three suites the report named
|
|
12
|
+
// (fitness, session_index) mutate no global state at all.
|
|
13
|
+
//
|
|
14
|
+
// All of that reasoning is only true while execution stays serial. Make the runner
|
|
15
|
+
// concurrent and every word of it silently becomes wrong, with no test to say so —
|
|
16
|
+
// so this suite fails if that happens, and whoever does it has to revisit the note
|
|
17
|
+
// rather than leave a stale explanation behind.
|
|
18
|
+
//
|
|
19
|
+
// Static source assertions on purpose: actually observing concurrency would mean
|
|
20
|
+
// running the whole gate from inside the gate.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/unit_runner_serial.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const RUNNER = path.join(ROOT, 'scripts', 'gds', 'run-unit-tests.js');
|
|
31
|
+
const src = fs.readFileSync(RUNNER, 'utf8');
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
function t(name, fn) {
|
|
36
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
37
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
t('each test runs in its OWN process, spawned synchronously', () => {
|
|
41
|
+
assert.match(src, /execFileSync\(\s*process\.execPath/,
|
|
42
|
+
'the runner must spawn each test with execFileSync (blocking, one process per test)');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
t('the run loops are sequential — no concurrency primitive in the runner', () => {
|
|
46
|
+
// Promise.all / allSettled over the file list, a worker pool, or the async
|
|
47
|
+
// execFile would each make the suites overlap and reintroduce the shared-state
|
|
48
|
+
// class the recorded audit rules out.
|
|
49
|
+
assert.doesNotMatch(src, /Promise\.(all|allSettled|race)\s*\(/,
|
|
50
|
+
'a Promise.all over the test list would run suites concurrently');
|
|
51
|
+
assert.doesNotMatch(src, /require\(['"]node:worker_threads['"]\)|from ['"]node:worker_threads['"]/,
|
|
52
|
+
'a worker pool would run suites concurrently');
|
|
53
|
+
assert.doesNotMatch(src, /\bexecFile\s*\(/,
|
|
54
|
+
'the ASYNC execFile would overlap suites; the runner uses execFileSync');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
t('the loop over the discovered test files is a plain for-of', () => {
|
|
58
|
+
assert.match(src, /for \(const name of files\) runOne\(/,
|
|
59
|
+
'sequential iteration is the mechanism — not a map() returning promises');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
t('the serial guarantee is EXPLAINED where it is relied on, not just true by accident', () => {
|
|
63
|
+
// The audit recorded beside the loop is what a future flake investigation reads
|
|
64
|
+
// instead of re-deriving it. If the loop is rewritten the note must be revisited,
|
|
65
|
+
// which is the whole point of pinning it next to the mechanism.
|
|
66
|
+
assert.match(src, /EXECUTION IS SERIAL, ONE PROCESS PER TEST/,
|
|
67
|
+
'run-unit-tests.js must carry the note explaining why serialism is relied upon');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(`unit_runner_serial: ${passed} passed, ${failed} failed`);
|
|
72
|
+
process.exit(failed ? 1 : 0);
|