@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.
- package/.bongos-core.json +103 -43
- package/.gitattributes +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -1
- package/docs/api/openapi.json +149 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +7 -6
- package/docs/copy-registry.json +16 -7
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +138 -0
- package/docs/recipes/instance-deploy.md +29 -3
- package/modules/autonomy/cadence.js +276 -0
- package/modules/autonomy/db.js +47 -0
- package/modules/autonomy/migrations/autonomy_002_runner_heartbeat.sql +93 -0
- package/modules/autonomy/routes/autonomy.js +69 -0
- package/modules/autonomy/runner-health.js +97 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/gate.html +4 -1
- package/modules/hall-ui/public/gate.js +51 -2
- package/modules/security/tests/artifact_scan.mjs +32 -9
- package/modules/security/tests/mirror.mjs +26 -4
- package/modules/security/tests/sanitize_rebuild.mjs +25 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-run.js +368 -11
- package/scripts/gds/autobongos-service.cmd +38 -0
- package/scripts/gds/autobongos-service.ps1 +129 -0
- package/scripts/gds/deploy-main-audit-gate.sh +90 -0
- package/scripts/gds/main-audit.js +26 -4
- package/scripts/gds/run-unit-tests.js +46 -5
- package/src/module-api.js +1 -1
- package/tests/autobongos_cadence.mjs +602 -0
- package/tests/autonomy_runner_health.mjs +135 -0
- package/tests/bump_version.mjs +12 -1
- package/tests/cli_package.mjs +11 -1
- package/tests/deploy_audit_gate.mjs +154 -0
- package/tests/deploy_divergence_line.mjs +47 -1
- package/tests/helpers.mjs +61 -0
- package/tests/main_audit_db_roster.mjs +97 -0
- package/tests/publish_watch.mjs +9 -0
- package/tests/setup_device_flow_fallback.mjs +2 -1
- package/tests/skip_is_not_pass.mjs +121 -0
- package/tests/tool_scan_engine.mjs +25 -4
- 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
|
-
<
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
210
|
+
symlinkOrRefuse(secret, path.join(root, 'stolen'));
|
|
186
211
|
} catch {
|
|
187
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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 {
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|