@bongos/core 1.19.876 → 1.19.878

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 (47) hide show
  1. package/.bongos-core.json +110 -40
  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 +163 -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/digest.js +344 -0
  19. package/modules/autonomy/migrations/autonomy_002_runner_heartbeat.sql +93 -0
  20. package/modules/autonomy/routes/autonomy.js +69 -0
  21. package/modules/autonomy/runner-health.js +97 -0
  22. package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
  23. package/modules/hall-ui/public/gate.html +4 -1
  24. package/modules/hall-ui/public/gate.js +51 -2
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/scripts/gds/autobongos-digest.js +123 -0
  28. package/scripts/gds/autobongos-run.js +375 -11
  29. package/scripts/gds/autobongos-service.cmd +38 -0
  30. package/scripts/gds/autobongos-service.ps1 +135 -0
  31. package/scripts/gds/deploy-main-audit-gate.sh +90 -0
  32. package/scripts/gds/main-audit.js +26 -4
  33. package/scripts/gds/provision-core-upgrade.js +23 -3
  34. package/scripts/gds/run-unit-tests.js +16 -1
  35. package/src/module-api.js +1 -1
  36. package/tests/autobongos_cadence.mjs +602 -0
  37. package/tests/autonomy_digest.mjs +302 -0
  38. package/tests/autonomy_runner_health.mjs +135 -0
  39. package/tests/bump_version.mjs +12 -1
  40. package/tests/cli_package.mjs +11 -1
  41. package/tests/core_upgrade_runner.mjs +43 -1
  42. package/tests/deploy_audit_gate.mjs +154 -0
  43. package/tests/deploy_divergence_line.mjs +47 -1
  44. package/tests/helpers.mjs +61 -0
  45. package/tests/main_audit_db_roster.mjs +97 -0
  46. package/tests/publish_watch.mjs +9 -0
  47. package/tests/setup_device_flow_fallback.mjs +2 -1
@@ -0,0 +1,90 @@
1
+ #!/bin/sh
2
+ # scripts/gds/deploy-main-audit-gate.sh — the SEC #863 / ADR 0043 deploy-side gate.
3
+ #
4
+ # Refuses a deploy when a builder BELOW Metic modified a permission-sensitive path in the
5
+ # delta this deploy introduces. Run from the instance checkout root, AFTER the working tree
6
+ # has been moved to the new commit and BEFORE anything is migrated or restarted.
7
+ #
8
+ # OTB_DEPLOY_PREV_SHA="$(git rev-parse HEAD)"; export OTB_DEPLOY_PREV_SHA # BEFORE the pull
9
+ # git reset --hard origin/main
10
+ # sh node_modules/@bongos/core/scripts/gds/deploy-main-audit-gate.sh || exit 1
11
+ # ...migrate, restart...
12
+ #
13
+ # WHY THIS LIVES IN THE CORE (task 1001426). It used to live in the control-plane repo's
14
+ # infra/, where a standalone instance checkout cannot reach it — and on cloudbongos.com it
15
+ # was never wired into ~/deploy.sh at all, so the "poison never reaches prod" check had
16
+ # never once run. Shipping it beside main-audit.js means every instance inherits the gate
17
+ # with its pinned core, and there is no per-instance copy to drift.
18
+ #
19
+ # THE MARKER IS THE CHECKED-OUT HEAD, AND IT ADVANCES ONLY ON SUCCESS.
20
+ # That is the substance of this task and the reason the restore below is in the GATE rather
21
+ # than in each caller's deploy script. The gate can only audit what was pulled, so it runs
22
+ # after the tree has already moved. Exiting non-zero and leaving it moved would mean the
23
+ # next deploy computes OTB_DEPLOY_PREV_SHA from the POISONED commit, never re-audits that
24
+ # delta, and finds the poison already checked out — the marker-bypass this gate exists to
25
+ # close, reintroduced one layer up. So a refusal puts the tree back where it started.
26
+ # Set OTB_GATE_NO_RESTORE=1 only when the caller restores it itself.
27
+ #
28
+ # STRICT IS OFF BY DEFAULT, AND THAT IS NOT LAZINESS. Rank resolution keys on github_login
29
+ # only (#863-followup finding #6: display_name is builder-controlled, so keying it let one
30
+ # builder alias another's rank). A deploy lane whose commits are authored under a human
31
+ # display name ("Firstname Lastname") rather than the builder's github_login therefore
32
+ # resolves to nothing, and OTB_AUDIT_STRICT=1 would block every deploy forever.
33
+ # Non-strict still hard-blocks the incident class this was built for: a RESOLVED below-Metic
34
+ # author touching a protected path. Turn strict on once the lane commits under logins.
35
+ #
36
+ # KNOWN LIMIT, stated because a gate nobody distrusts is worse than no gate: if the roster
37
+ # cannot be read at all, every author is unresolved and non-strict passes. main-audit.js
38
+ # announces that loudly on stderr (task 1001426 gave it a fallback and a voice — it used to
39
+ # fail silently and return an empty map, so the gate reported a clean audit that never ran).
40
+ # Availability is chosen over a hard stop deliberately: a database blip must not freeze
41
+ # deploys. Read the stderr.
42
+
43
+ set -eu
44
+
45
+ if [ -n "${OTB_DEPLOY_PREV_SHA:-}" ]; then
46
+ OTB_AUDIT_RANGE="${OTB_DEPLOY_PREV_SHA}..HEAD"; export OTB_AUDIT_RANGE
47
+ echo "main audit: range ${OTB_AUDIT_RANGE} (exact deploy delta)"
48
+ else
49
+ echo "main audit: OTB_DEPLOY_PREV_SHA not set — falling back to the wide window."
50
+ echo " (Recommended: capture OTB_DEPLOY_PREV_SHA=\$(git rev-parse HEAD) BEFORE the pull;"
51
+ echo " without it a poisoned commit buried under the window can escape, and a refusal"
52
+ echo " cannot put the tree back.)"
53
+ fi
54
+
55
+ STRICT=""
56
+ [ "${OTB_AUDIT_STRICT:-0}" = "1" ] && STRICT="--strict"
57
+
58
+ # Prefer the installed core; fall back to an in-tree checkout (the core repo itself).
59
+ MAIN_AUDIT="node_modules/@bongos/core/scripts/gds/main-audit.js"
60
+ [ -f "$MAIN_AUDIT" ] || MAIN_AUDIT="scripts/gds/main-audit.js"
61
+ if [ ! -f "$MAIN_AUDIT" ]; then
62
+ echo "X DEPLOY ABORTED: main-audit.js not found (looked in node_modules/@bongos/core and in-tree)."
63
+ echo " A gate that cannot find its auditor must fail closed, not wave the deploy through."
64
+ exit 1
65
+ fi
66
+
67
+ if node "$MAIN_AUDIT" --verify-head --db ${STRICT}; then
68
+ echo "OK main audit clean${STRICT:+ (strict)} — proceeding with deploy."
69
+ exit 0
70
+ fi
71
+
72
+ echo ""
73
+ echo "X DEPLOY ABORTED (SEC #863): a below-Metic${STRICT:+ or unresolved} author modified a"
74
+ echo " permission-sensitive path in this deploy. Investigate and revert before deploying:"
75
+ echo " node $MAIN_AUDIT --range \"\${OTB_AUDIT_RANGE:-HEAD~200..HEAD}\" --revert"
76
+
77
+ if [ -n "${OTB_DEPLOY_PREV_SHA:-}" ] && [ "${OTB_GATE_NO_RESTORE:-0}" != "1" ]; then
78
+ if git rev-parse --verify --quiet "${OTB_DEPLOY_PREV_SHA}^{commit}" >/dev/null; then
79
+ echo ""
80
+ echo " restoring the checkout to ${OTB_DEPLOY_PREV_SHA} — the deployed marker advances only on success,"
81
+ echo " so the NEXT deploy re-audits this same delta instead of treating it as already-reviewed."
82
+ git reset --hard "${OTB_DEPLOY_PREV_SHA}" >/dev/null
83
+ else
84
+ echo ""
85
+ echo " ::WARNING:: cannot restore — ${OTB_DEPLOY_PREV_SHA} is not a commit in this checkout."
86
+ echo " The tree is left at the REFUSED commit; the next deploy will not re-audit this delta."
87
+ fi
88
+ fi
89
+
90
+ exit 1
@@ -251,6 +251,13 @@ function rowsToMap(rows) {
251
251
  return map;
252
252
  }
253
253
 
254
+ // Where getBuildersRoster lives. It was carved out of src/bongos/db.js into the
255
+ // lifecycle module (goal 1000010's per-domain db.js split) and this file was not
256
+ // updated with it, so `--db` threw `db.getBuildersRoster is not a function` on every
257
+ // run — for however long that carve has been landed. Kept as a named constant so the
258
+ // next carve breaks a test (tests/main_audit_db_roster.mjs) rather than a deploy gate.
259
+ const ROSTER_DB_MODULE = '../../modules/lifecycle/db.js';
260
+
254
261
  // API mode (default): for an Archon session / operator laptop. DB mode (--db):
255
262
  // for the droplet deploy gate, which has DB access but no GDS session — it reads
256
263
  // the roster straight from Postgres. db.js is lazy-required so importing this
@@ -258,13 +265,24 @@ function rowsToMap(rows) {
258
265
  async function fetchRankMap({ useDb = false } = {}) {
259
266
  if (useDb) {
260
267
  try {
261
- const db = require('../../src/bongos/db.js');
268
+ const db = require(ROSTER_DB_MODULE);
269
+ if (typeof db.getBuildersRoster !== 'function') {
270
+ // The exact failure this file already shipped once, named rather than caught as a
271
+ // generic TypeError: a moved export reads identically to "no database" otherwise.
272
+ throw new Error(`${ROSTER_DB_MODULE} exports no getBuildersRoster — it has moved again`);
273
+ }
262
274
  const roster = await db.getBuildersRoster({ staleHours: 24 });
263
- return rowsToMap(roster.builders || roster);
275
+ const map = rowsToMap(roster.builders || roster);
276
+ if (Object.keys(map).length) return map;
277
+ console.error('main-audit: --db roster read returned NO builders — falling through to the API/public roster.');
264
278
  } catch (e) {
265
- console.error(`main-audit: --db roster read failed (${e.message})`);
266
- return {};
279
+ console.error(`main-audit: --db roster read failed (${e.message}) — falling through to the API/public roster.`);
267
280
  }
281
+ // FALL THROUGH, do not return {}. Returning an empty map here is the vacuous green
282
+ // this file's own comment warns about twelve lines down: with no ranks, every author
283
+ // resolves to null, no blocker can ever fire, and the gate reports success for the
284
+ // reason it was built to prevent. A `--db` gate that cannot reach Postgres is better
285
+ // served by the public leaderboard than by silence.
268
286
  }
269
287
  try {
270
288
  const api = await cliClient({ allowUnauthenticated: true });
@@ -394,4 +412,8 @@ if (require.main === module) {
394
412
  module.exports = {
395
413
  analyzeCommits, resolveRank, parseCommits, rowsToMap,
396
414
  clampWindow, defaultRange, firstParentDepth, DEFAULT_WINDOW,
415
+ // Exported so a test can assert the address is LIVE without a database. The --db
416
+ // path was dead for as long as the db.js carve has been landed precisely because
417
+ // nothing checked that this require target still exports what it is called for.
418
+ ROSTER_DB_MODULE, fetchRankMap,
397
419
  };
@@ -187,6 +187,25 @@ async function coreUpgradeInstance(inst, deps, intent) {
187
187
  const healthUrl = `http://127.0.0.1:${inst.port}/healthz`;
188
188
  const versionUrl = `http://127.0.0.1:${inst.port}/version`;
189
189
  const sudoP = privileged ? 'sudo ' : '';
190
+ // WHERE THE CLI LIVES AND WHERE THE TARGET LIVES ARE DIFFERENT PLACES (task 1004053).
191
+ //
192
+ // `scripts/gds/upgrade.js` ships in the CORE checkout. A provisioned instance root holds
193
+ // no scripts/ directory at all — its core sits under node_modules/@bongos/core — so
194
+ // running the CLI with cwd = the instance root resolved nothing, and EVERY standalone
195
+ // upgrade died before it began:
196
+ //
197
+ // Error: Cannot find module '/srv/<base>/<slug>/scripts/gds/upgrade.js'
198
+ //
199
+ // It worked only where instanceDir HAPPENED to be this checkout — the control-plane self
200
+ // row and a co-tenant — which is why it survived to production: the shapes that are
201
+ // upgraded by hand are the shapes the relative path fits. Every ACTIVE tenant is
202
+ // standalone, so the deploy door could not move a single one of them.
203
+ //
204
+ // upgrade.js's own header states the contract this restores: "Runs from the consumer
205
+ // instance root (default) or `--instance <dir>`. This script ships IN the core … against
206
+ // `--instance <consumer>`." So: run it where it exists, and NAME the target.
207
+ const runDir = REPO_ROOT;
208
+ const targetArg = instanceDir === REPO_ROOT ? '' : `--instance ${instanceDir}`;
190
209
  const cli = path.posix.join('scripts', 'gds', 'upgrade.js');
191
210
  const requestedBy = intent.requested_by && /^api:archon:(\d+)$/.test(intent.requested_by)
192
211
  ? intent.requested_by.replace(/^api:archon:/, '')
@@ -195,6 +214,7 @@ async function coreUpgradeInstance(inst, deps, intent) {
195
214
  `${sudoP}node ${cli}`,
196
215
  '--registry',
197
216
  `--to ${to}`,
217
+ targetArg,
198
218
  `--service ${inst.slug}`,
199
219
  `--health-url ${healthUrl}`,
200
220
  `--version-url ${versionUrl}`,
@@ -204,13 +224,13 @@ async function coreUpgradeInstance(inst, deps, intent) {
204
224
  ].filter(Boolean).join(' ');
205
225
 
206
226
  if (!apply) {
207
- log(` ✓ would run: ${cmd} (dry-run — nothing ran)`);
227
+ log(` ✓ would run (in ${runDir}): ${cmd} (dry-run — nothing ran)`);
208
228
  log(` ✓ would then read ${versionUrl} on the box and require it to say ${to}`);
209
229
  return { ok: true, served: null };
210
230
  }
211
231
  try {
212
- log(` [upgrade] ${cmd}`);
213
- boxExec(cmd, { cwd: instanceDir });
232
+ log(` [upgrade] (in ${runDir}) ${cmd}`);
233
+ boxExec(cmd, { cwd: runDir });
214
234
  } catch (e) {
215
235
  // upgrade.js rolls itself back on a failed health check or an unchanged served
216
236
  // version, so by the time this throws the instance is back on the core it had.
@@ -464,7 +464,12 @@ function main() {
464
464
  if (n) skipped.push({ name: displayName, n });
465
465
  console.log(` ok ${displayName}${n ? ` (${n} SKIPPED — not run on this platform)` : ''}`);
466
466
  } catch (err) {
467
- const why = err.killed ? 'TIMEOUT' : `exit ${err.status}`;
467
+ // `exit null` on its own is unreadable, and it is what a HANG looks like here: the
468
+ // signal that ended the process is the whole diagnosis, so say it (task 1004052 —
469
+ // a test that hung for the full budget reported `exit null` and nothing else).
470
+ const why = err.killed
471
+ ? `TIMEOUT after ${perTestTimeout / 1000}s${err.signal ? `, ${err.signal}` : ''}`
472
+ : `exit ${err.status}${err.signal ? ` (${err.signal})` : ''}`;
468
473
  const stdoutText = (err.stdout ? err.stdout.toString() : '').trim();
469
474
  if (!err.killed && isCleanTeardownAbort(stdoutText, err.stderr ? err.stderr.toString() : '')) {
470
475
  console.log(` ok ${displayName} (clean verdict, then libuv teardown abort, ${why})`);
@@ -474,6 +479,16 @@ function main() {
474
479
  console.log(` FAIL ${displayName} (${why})`);
475
480
  // The actual failures first: the file's own FAIL lines, then its stdout
476
481
  // tail (the last failure's assertion detail + summary), both bounded.
482
+ // EMPTY STDOUT IS ITSELF THE CLUE, and it used to print as a blank gap. node:test
483
+ // buffers a file's whole report until the file finishes, so a suite that hangs
484
+ // mid-run emits NOTHING — which reads as "no detail available" when it actually
485
+ // means "it never got to the end". Name that, and name how to find the case.
486
+ if (!stdoutText) {
487
+ console.log(' (no output at all — node:test buffers a file\'s report until it finishes,');
488
+ console.log(' so this suite did not reach the end. To find which case: run it on the same');
489
+ console.log(' platform with each case writing its name to STDERR before it runs; stderr is');
490
+ console.log(' unbuffered, so the last name with no completion is the one that hung.)');
491
+ }
477
492
  const failLines = stdoutText ? stdoutText.split('\n').filter((l) => /\bFAIL\b/.test(l)).slice(0, 5) : [];
478
493
  for (const l of failLines) console.log(` ${l.trim()}`);
479
494
  if (stdoutText) {
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.876'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.878'; // 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');