@bongos/core 1.19.696 → 1.19.697

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.696",
6
- "core_contract": "1.19.696",
7
- "source_commit": "999c5518a84488c7cc2a016c7ca06b9331ee83fc",
5
+ "core_version": "1.19.697",
6
+ "core_contract": "1.19.697",
7
+ "source_commit": "bfb9b4492d0f0057725db35914055204f148c032",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-13T02:29:16.607Z",
9
+ "built_at": "2026-09-13T02:32:59.182Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 477,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2125,
14
+ "functional_verbatim": 2126,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2626,
20
- "tree_sha256": "ef4cf6428521a6e96ab9c4568a06c28a2be79efe59d11cb876ff225ed3a88021",
19
+ "file_count": 2627,
20
+ "tree_sha256": "aecd13ccb73fd1082fe45b3aa23bab026afc2c358c584c4e188cb8caafcc1199",
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": "37285cc9f4e80f241192e4bd438b870f641b834bfac40011de3dca5560b5300a"
2805
+ "sha256": "8862b2feb53917b00597c8a2a32d75fedbbe24cdb78c12998209f9de39395c15"
2806
2806
  },
2807
2807
  {
2808
2808
  "path": "docs/modules-contract.md",
@@ -7772,12 +7772,12 @@
7772
7772
  {
7773
7773
  "path": "package-lock.json",
7774
7774
  "mode": "0000644",
7775
- "sha256": "cc9097410d73f9a032acff4b32a2b99196204ac471a5bf4ae8b88fa47a04b1d2"
7775
+ "sha256": "b8fa92bf72071648e7a68331fcd48c316d5f42b4dd22ac58de8c69fa91cdbefb"
7776
7776
  },
7777
7777
  {
7778
7778
  "path": "package.json",
7779
7779
  "mode": "0000644",
7780
- "sha256": "4ec1ea048ca3d3900d16773d1c62663fc61d4292670da335cab8c014cdba60cd"
7780
+ "sha256": "1c2dd79228c1d4d99779ed0526734e6f75a68e40026db6665e800862b95cccc5"
7781
7781
  },
7782
7782
  {
7783
7783
  "path": "public-docs/index.html",
@@ -8697,7 +8697,7 @@
8697
8697
  {
8698
8698
  "path": "scripts/gds/run-unit-tests.js",
8699
8699
  "mode": "0000644",
8700
- "sha256": "e29e5f883205a94499ef0770ac71d517302c2e7079e48e1c9566d805dce6a245"
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": "5f34fdf0e3931cb1bdce807abf20c9086f5a6795c5370a7e8911363f6cb36dd3"
9545
+ "sha256": "aceceda426abd1aa9dd04fdfc69a225e7b33a91b790a823cf10d206698f24c1e"
9546
9546
  },
9547
9547
  {
9548
9548
  "path": "src/module-loader/catalog.js",
@@ -12447,7 +12447,7 @@
12447
12447
  {
12448
12448
  "path": "tests/secret_box.mjs",
12449
12449
  "mode": "0000644",
12450
- "sha256": "1eb2218f483f63f28af363fb1f79cff6c79f3b83d5f152c004798e4fd3c5035d"
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,7 @@ 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)
1854
1856
  ---------------------------------------------------------------------------
1855
1857
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.696",
3
+ "version": "1.19.697",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.696",
9
+ "version": "1.19.697",
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.696",
3
+ "version": "1.19.697",
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",
@@ -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.696'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.697'; // 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');
@@ -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 + HOME so the ~/.config/otb/builder-secret.key
10
- // fallback can't resolve to a real operator file on the test machine.
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 HOME, restoring both after.
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
- process.env.HOME = mkdtempSync(join(tmpdir(), 'otb-secretbox-'));
48
- try { fn(); }
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
- process.env.HOME = origHome;
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);