@bongos/core 1.19.875 → 1.19.877

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.
Files changed (48) hide show
  1. package/.bongos-core.json +103 -43
  2. package/.gitattributes +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -0
  5. package/clients/bongos-client/index.cjs +4 -0
  6. package/clients/bongos-client/index.d.ts +7 -0
  7. package/clients/bongos-client/index.mjs +4 -0
  8. package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -1
  9. package/docs/api/openapi.json +149 -3
  10. package/docs/api-reference.md +4 -2
  11. package/docs/copy-inventory.md +7 -6
  12. package/docs/copy-registry.json +16 -7
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/recipes/autobongos-windows-host.md +138 -0
  15. package/docs/recipes/instance-deploy.md +29 -3
  16. package/modules/autonomy/cadence.js +276 -0
  17. package/modules/autonomy/db.js +47 -0
  18. package/modules/autonomy/migrations/autonomy_002_runner_heartbeat.sql +93 -0
  19. package/modules/autonomy/routes/autonomy.js +69 -0
  20. package/modules/autonomy/runner-health.js +97 -0
  21. package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
  22. package/modules/hall-ui/public/gate.html +4 -1
  23. package/modules/hall-ui/public/gate.js +51 -2
  24. package/modules/security/tests/artifact_scan.mjs +32 -9
  25. package/modules/security/tests/mirror.mjs +26 -4
  26. package/modules/security/tests/sanitize_rebuild.mjs +25 -4
  27. package/package-lock.json +2 -2
  28. package/package.json +1 -1
  29. package/scripts/gds/autobongos-run.js +368 -11
  30. package/scripts/gds/autobongos-service.cmd +38 -0
  31. package/scripts/gds/autobongos-service.ps1 +129 -0
  32. package/scripts/gds/deploy-main-audit-gate.sh +90 -0
  33. package/scripts/gds/main-audit.js +26 -4
  34. package/scripts/gds/run-unit-tests.js +46 -5
  35. package/src/module-api.js +1 -1
  36. package/tests/autobongos_cadence.mjs +602 -0
  37. package/tests/autonomy_runner_health.mjs +135 -0
  38. package/tests/bump_version.mjs +12 -1
  39. package/tests/cli_package.mjs +11 -1
  40. package/tests/deploy_audit_gate.mjs +154 -0
  41. package/tests/deploy_divergence_line.mjs +47 -1
  42. package/tests/helpers.mjs +61 -0
  43. package/tests/main_audit_db_roster.mjs +97 -0
  44. package/tests/publish_watch.mjs +9 -0
  45. package/tests/setup_device_flow_fallback.mjs +2 -1
  46. package/tests/skip_is_not_pass.mjs +121 -0
  47. package/tests/tool_scan_engine.mjs +25 -4
  48. package/tests/unit_runner_teardown_abort.mjs +37 -0
@@ -254,7 +254,7 @@
254
254
  const el = document.getElementById('fence-state');
255
255
  el.innerHTML = `
256
256
  <div class="ov-foot" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
257
- <strong>${on ? 'Running' : 'Stopped'}</strong>
257
+ <span class="fact-pill ${on ? 'fact-pill--ok' : 'fact-pill--warn'}">${on ? 'Allowed to work' : 'Stopped'}</span>
258
258
  <span>${on ? 'The runner may claim and work allowlisted goals.' : escapeHtml(fence.paused_reason || 'The runner is doing nothing.')}</span>
259
259
  <button type="button" id="fence-toggle" class="btn">${on ? 'Stop the runner' : 'Start the runner'}</button>
260
260
  </div>`;
@@ -317,6 +317,54 @@
317
317
  }
318
318
  }
319
319
 
320
+ // The runner's liveness (task 1003905). Rendered ABOVE the switch on purpose:
321
+ // "is it running at all" is the first question, and an owner who has just
322
+ // flipped the switch on wants to see the machine notice.
323
+ //
324
+ // The verdict is the SERVER's — modules/autonomy/runner-health.js decides what
325
+ // dead means and the route attaches it — so this page and any future alert
326
+ // cannot drift into two definitions of the same word.
327
+ // This page's OWN class vocabulary (style.css `.fact-pill--{ok,warn,danger}`),
328
+ // the same one renderRow spends for a PR's verdict. The first cut reached for
329
+ // inline `--color-text-*` tokens from a different design system: they do not
330
+ // exist here, resolved to inherited grey, and drew a dead runner in exactly the
331
+ // same colour as a healthy one. Named classes cannot fail that way silently —
332
+ // a missing class is visible in the stylesheet, a missing var is not.
333
+ const HEALTH_CLASS = { alive: 'fact-pill--ok', late: 'fact-pill--warn', dead: 'fact-pill--danger', never_seen: 'fact-pill--info' };
334
+
335
+ async function loadRunners() {
336
+ const el = document.getElementById('fence-runner');
337
+ if (!el) return;
338
+ let resp;
339
+ try {
340
+ resp = await fetchJson('/autonomy/runners');
341
+ } catch (err) {
342
+ if (err && err.kind === '401') return;
343
+ el.innerHTML = emptyStateHtml('Could not read the runner\u2019s check-ins just now.');
344
+ return;
345
+ }
346
+ const runners = resp.runners || [];
347
+ if (!runners.length) {
348
+ // never_seen, and the server said so rather than this page guessing. Not an
349
+ // alarm: nobody has set the host up yet.
350
+ el.innerHTML = `<p class="ov-foot">${escapeHtml(resp.none ? resp.none.detail : 'No runner has ever checked in.')}</p>`;
351
+ return;
352
+ }
353
+ el.innerHTML = runners.map((r) => {
354
+ const h = r.health || {};
355
+ const cls = HEALTH_CLASS[h.state] || 'fact-pill--info';
356
+ // NOT the word "running": the switch below already uses it for a different
357
+ // thing (allowed to work), and the two lines sit one above the other. A
358
+ // reader should never have to work out which sense is meant.
359
+ const label = { dead: 'Not checking in', late: 'Quiet', alive: 'Checking in' }[h.state] || 'Unknown';
360
+ return `<p class="ov-foot" style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;">
361
+ <span class="fact-pill ${cls}">${escapeHtml(label)}</span>
362
+ <span>${escapeHtml(h.detail || '')}</span>
363
+ <span class="ov-fact">last check-in ${escapeHtml(h.age || 'unknown')} ago</span>
364
+ </p>`;
365
+ }).join('');
366
+ }
367
+
320
368
  async function loadFence() {
321
369
  let fence;
322
370
  try {
@@ -356,7 +404,8 @@
356
404
  document.getElementById('gate-body').hidden = false;
357
405
  if (rank === 'archon') {
358
406
  document.getElementById('fence-panel').hidden = false;
359
- await loadFence();
407
+ // Independent reads, so one round trip rather than two.
408
+ await Promise.all([loadRunners(), loadFence()]);
360
409
  }
361
410
  await load();
362
411
  }
@@ -31,10 +31,35 @@ const { createQuarantine, siteRefusal, isSyncBacked, isInside, REPO_ROOT, CAPS }
31
31
  const { buildManifest, verifyUnchanged, CapExceeded, KIND } = manifest;
32
32
  const { validateTargetUrl, hardeningConfig, hardenedEnv, parseLsFiles, fetchArtifact, blockedAddress, assertHostResolvesPublic } = fetchMod;
33
33
 
34
+
35
+ // Test hook (task 1003993): forces the platform-cannot-symlink branch so the skip path is
36
+ // reachable on a box where symlinks work. Throws exactly where fs.symlinkSync would.
37
+ function symlinkOrRefuse(target, link) {
38
+ if (process.env.BONGOS_TEST_FORCE_NO_SYMLINK === '1') throw new Error('EPERM (forced by BONGOS_TEST_FORCE_NO_SYMLINK)');
39
+ fs.symlinkSync(target, link);
40
+ }
41
+
34
42
  let passed = 0;
35
43
  let failed = 0;
44
+ let skipped = 0;
45
+
46
+ // A case that did not run is NOT a case that passed (task 1003993). A bare `return` from a
47
+ // case that found its platform unable to do the thing left the harness seeing a callback that
48
+ // did not throw — so it printed `ok` and counted a pass for work never attempted. Returning
49
+ // skipCase(why) instead is the same one-line exit, and it tells the truth.
50
+ const SKIP = Symbol('skip');
51
+ const skipCase = (why) => ({ [SKIP]: why });
52
+ function noteSkip(name, why) {
53
+ skipped++;
54
+ console.log(` SKIP ${name}`);
55
+ console.log(` (not run: ${why})`);
56
+ }
36
57
  async function test(name, fn) {
37
- try { await fn(); passed++; console.log(` ok ${name}`); }
58
+ try {
59
+ const r = await fn();
60
+ if (r && r[SKIP]) { noteSkip(name, r[SKIP]); return; }
61
+ passed++; console.log(` ok ${name}`);
62
+ }
38
63
  catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
39
64
  }
40
65
 
@@ -182,10 +207,9 @@ await test('a symlink is recorded by its link text and its target is NEVER opene
182
207
  fs.writeFileSync(secret, 'AKIA-SUPER-SECRET-TOKEN\n');
183
208
  fs.writeFileSync(path.join(root, 'README.md'), 'hi\n');
184
209
  try {
185
- fs.symlinkSync(secret, path.join(root, 'stolen'));
210
+ symlinkOrRefuse(secret, path.join(root, 'stolen'));
186
211
  } catch {
187
- console.log(' (skipped: this platform/account cannot create symlinks)');
188
- return;
212
+ return skipCase('this platform/account cannot create symlinks');
189
213
  }
190
214
  const m = await buildManifest(root, { maxFiles: 100, maxBytes: 1e6 });
191
215
  const link = m.entries.find((e) => e.path === 'stolen');
@@ -542,11 +566,10 @@ await test('symlinks count against maxFiles too — a cap on regular files only
542
566
  const root = mkTmp();
543
567
  fs.writeFileSync(path.join(root, 'a.txt'), 'a');
544
568
  try {
545
- fs.symlinkSync(path.join(root, 'a.txt'), path.join(root, 'l1'));
546
- fs.symlinkSync(path.join(root, 'a.txt'), path.join(root, 'l2'));
569
+ symlinkOrRefuse(path.join(root, 'a.txt'), path.join(root, 'l1'));
570
+ symlinkOrRefuse(path.join(root, 'a.txt'), path.join(root, 'l2'));
547
571
  } catch {
548
- console.log(' (skipped: this platform/account cannot create symlinks)');
549
- return;
572
+ return skipCase('this platform/account cannot create symlinks');
550
573
  }
551
574
  await assert.rejects(() => buildManifest(root, { maxFiles: 2, maxBytes: 1e6 }), /entry cap/);
552
575
  });
@@ -588,5 +611,5 @@ await test('CAPS are finite — no accidental unlimited default', async () => {
588
611
 
589
612
  for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* best effort */ } }
590
613
 
591
- console.log(`\nartifact_scan: ${passed} passed, ${failed} failed`);
614
+ console.log(`\nartifact_scan: ${passed} passed, ${failed} failed, ${skipped} skipped`);
592
615
  if (failed > 0) process.exit(1);
@@ -35,14 +35,35 @@ const { scrubUntrusted, newFenceDelim, FENCE_MARKER, DANGEROUS_CHARS } = fenceLo
35
35
 
36
36
  let passed = 0;
37
37
  let failed = 0;
38
+ let skipped = 0;
39
+
40
+ // A case that did not run is NOT a case that passed (task 1003993). A bare `return` from a
41
+ // case that found its platform unable to do the thing left the harness seeing a callback that
42
+ // did not throw — so it printed `ok` and counted a pass for work never attempted. Returning
43
+ // skipCase(why) instead is the same one-line exit, and it tells the truth.
44
+ const SKIP = Symbol('skip');
45
+ const skipCase = (why) => ({ [SKIP]: why });
46
+ function noteSkip(name, why) {
47
+ skipped++;
48
+ console.log(` SKIP ${name}`);
49
+ console.log(` (not run: ${why})`);
50
+ }
38
51
  async function test(name, fn) {
39
- try { await fn(); passed++; console.log(` ok ${name}`); }
52
+ try {
53
+ const r = await fn();
54
+ if (r && r[SKIP]) { noteSkip(name, r[SKIP]); return; }
55
+ passed++; console.log(` ok ${name}`);
56
+ }
40
57
  catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
41
58
  }
42
59
 
43
60
  // Symlink creation needs privileges on some Windows accounts — same skip idiom
44
61
  // as artifact_scan.mjs so the suite stays green everywhere.
45
62
  function canSymlink(dir) {
63
+ // Test hook (task 1003993): the skip branch is unreachable on any box that CAN symlink,
64
+ // which is every CI runner — exactly why the old fake-pass went unnoticed for so long.
65
+ // tests/skip_is_not_pass.mjs sets this to exercise the skip path anywhere.
66
+ if (process.env.BONGOS_TEST_FORCE_NO_SYMLINK === '1') return false;
46
67
  const target = path.join(dir, 'symlink-probe-target');
47
68
  const link = path.join(dir, 'symlink-probe-link');
48
69
  try {
@@ -176,7 +197,7 @@ async function run() {
176
197
  const rel = `docs/${rlo}evil.md`;
177
198
  let wrote = true;
178
199
  try { writePristine(pen, rel, 'body'); } catch { wrote = false; }
179
- if (!wrote) { console.log(' (skipped: this filesystem refuses bidi filenames)'); return; }
200
+ if (!wrote) return skipCase('this filesystem refuses bidi filenames');
180
201
  const res = await buildMirror({
181
202
  pristineDir: pen.pristineDir, mirrorDir: pen.mirrorDir, quarantineRoot: pen.root,
182
203
  entries: [fileEntry(rel, 4)],
@@ -313,7 +334,8 @@ async function run() {
313
334
  } finally { pen.cleanup(); }
314
335
  });
315
336
  } else {
316
- console.log(' (skipped: this platform/account cannot create symlinks)');
337
+ noteSkip('a REAL on-disk symlink whose realpath escapes the pen is refused (defense in depth)',
338
+ 'this platform/account cannot create symlinks');
317
339
  }
318
340
 
319
341
  // ---------- the index: the record downstream stages reduce over ----------
@@ -352,5 +374,5 @@ function* walkFiles(dir) {
352
374
  }
353
375
 
354
376
  await run();
355
- console.log(`\nmirror: ${passed} passed, ${failed} failed`);
377
+ console.log(`\nmirror: ${passed} passed, ${failed} failed, ${skipped} skipped`);
356
378
  if (failed > 0) process.exit(1);
@@ -25,11 +25,23 @@ const { excerpt } = require('../artifact-scan/detectors/shared.js');
25
25
 
26
26
  let passed = 0;
27
27
  let failed = 0;
28
+ let skipped = 0;
28
29
  async function test(name, fn) {
29
30
  try { await fn(); passed++; console.log(` ok ${name}`); }
30
31
  catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
31
32
  }
32
33
 
34
+ // A case that did not run is NOT a case that passed (task 1003993). This prints SKIP rather
35
+ // than `ok` and counts separately, so the difference between "verified" and "not attempted"
36
+ // survives into the summary line and into run-unit-tests.js's roll-up. It takes the same
37
+ // `name` the real case would have used, because "which coverage did I lose?" is the only
38
+ // question worth answering here.
39
+ function skip(name, why) {
40
+ skipped++;
41
+ console.log(` SKIP ${name}`);
42
+ console.log(` (not run: ${why})`);
43
+ }
44
+
33
45
  const RAW_DANGEROUS_RE = new RegExp(
34
46
  '[\\u200b-\\u200f\\u2028\\u2029\\u202a-\\u202e\\u2060\\u2066-\\u2069\\ufeff]|[\\u{e0000}-\\u{e007f}]', 'u');
35
47
 
@@ -384,6 +396,11 @@ await test('a failed build leaves NO partial sanitized/ tree, and a retry is sti
384
396
  // Symlink creation needs privileges on some Windows accounts — same skip idiom
385
397
  // as mirror.mjs / artifact_scan.mjs so the suite stays green everywhere.
386
398
  function canSymlink(dir) {
399
+ // A test hook, because the branch below is unreachable on any box that CAN symlink — which
400
+ // is every CI runner and most dev machines, and is precisely why the old fake-pass survived
401
+ // so long unnoticed. tests/skip_is_not_pass.mjs sets this to exercise the skip path on a
402
+ // platform where the real probe succeeds.
403
+ if (process.env.BONGOS_TEST_FORCE_NO_SYMLINK === '1') return false;
387
404
  const target = path.join(dir, 'probe-target');
388
405
  const link = path.join(dir, 'probe-link');
389
406
  try {
@@ -434,9 +451,13 @@ function canSymlink(dir) {
434
451
  } finally { pen.cleanup(); fs.rmSync(farAway, { recursive: true, force: true }); }
435
452
  });
436
453
  } else {
437
- console.log(' ok physical-confinement symlink cases');
438
- console.log(' (skipped: this platform/account cannot create symlinks)');
439
- passed += 2;
454
+ // These two are the ONLY coverage of physical confinement, so losing them silently is
455
+ // how task 1003848 reached CI red while this box read 760/760: a change to isInside()
456
+ // collapsed the lexical/physical staging in sanitize.js, and the cases that would have
457
+ // caught it had been reporting `ok` without running for as long as anyone could remember.
458
+ const why = 'this platform/account cannot create symlinks';
459
+ skip('physical confinement: a real symlink the manifest calls a "file" is refused, and its target never reaches sanitized/', why);
460
+ skip('physical confinement: a symlink whose realpath escapes the QUARANTINE entirely is refused', why);
440
461
  }
441
462
  }
442
463
 
@@ -495,5 +516,5 @@ await test('an artifact with no docs still yields an honest spec — the no-docs
495
516
  });
496
517
 
497
518
  console.log('');
498
- console.log(`${passed} passed, ${failed} failed`);
519
+ console.log(`${passed} passed, ${failed} failed, ${skipped} skipped`);
499
520
  if (failed > 0) process.exit(1);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.875",
3
+ "version": "1.19.877",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.875",
9
+ "version": "1.19.877",
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.875",
3
+ "version": "1.19.877",
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",