@bongos/core 1.19.883 → 1.19.885
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 +31 -291
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +4 -2
- package/docs/architecture.md +0 -1
- package/docs/file-map.md +0 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/routes/tasks.js +18 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-land.js +103 -14
- package/scripts/gds/ship-merge.js +28 -0
- package/scripts/gds/ship.js +8 -0
- package/src/bongos/serve-internal.js +10 -56
- package/src/module-api.js +1 -1
- package/src/platform-server.js +0 -1
- package/tests/host_topology_skips.mjs +0 -1
- package/tests/instance_release_config.mjs +27 -26
- package/tests/publish_branch_route.mjs +70 -2
- package/tests/session_rename_fallback.mjs +8 -90
- package/tests/ship_land_proof.mjs +218 -0
- package/modules/dev-box/app/README.md +0 -74
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +0 -95
- package/modules/dev-box/app/electron-builder.yml +0 -95
- package/modules/dev-box/app/package-lock.json +0 -5291
- package/modules/dev-box/app/package.json +0 -30
- package/modules/dev-box/app/renderer/app.js +0 -319
- package/modules/dev-box/app/renderer/index.html +0 -45
- package/modules/dev-box/app/renderer/styles.css +0 -248
- package/modules/dev-box/app/renderer/theme.generated.css +0 -16
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +0 -82
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +0 -39
- package/modules/dev-box/app/scripts/gen-brand.js +0 -138
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +0 -323
- package/modules/dev-box/app/scripts/gen-icons.js +0 -286
- package/modules/dev-box/app/scripts/vendor-client.js +0 -23
- package/modules/dev-box/app/src/box-controller.js +0 -287
- package/modules/dev-box/app/src/brand.generated.json +0 -13
- package/modules/dev-box/app/src/brand.js +0 -16
- package/modules/dev-box/app/src/config.js +0 -31
- package/modules/dev-box/app/src/gds-client.js +0 -96
- package/modules/dev-box/app/src/main.js +0 -610
- package/modules/dev-box/app/src/pairing.js +0 -57
- package/modules/dev-box/app/src/preload.js +0 -29
- package/modules/dev-box/app/src/session-store.js +0 -82
- package/modules/dev-box/app/src/ssh-setup.js +0 -569
- package/modules/dev-box/app/src/task-picker.js +0 -70
- package/modules/dev-box/app/src/tray.js +0 -99
- package/modules/dev-box/app/src/updater.js +0 -55
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +0 -1014
- package/modules/dev-box/app/test/unit.mjs +0 -526
- package/src/devbox-downloads.js +0 -67
- package/tests/devbox_artifact_gate.mjs +0 -75
- package/tests/devbox_dmg_backdrop.mjs +0 -137
- package/tests/devbox_downloads.mjs +0 -140
- package/tests/devbox_icons.mjs +0 -304
- package/tests/devbox_installer_brand.mjs +0 -232
- package/tests/devbox_key_register.mjs +0 -234
- package/tests/devbox_update_path.mjs +0 -84
package/scripts/gds/ship-land.js
CHANGED
|
@@ -438,24 +438,100 @@ async function pollServerPublish(taskId, branch) {
|
|
|
438
438
|
// Best-effort + never throws: a non-Metic author, an unconfigured server, or a
|
|
439
439
|
// transient failure just leaves the PR un-attested → it escalates (the safe
|
|
440
440
|
// direction). A non-gate PR is unaffected by the attestation either way.
|
|
441
|
+
//
|
|
442
|
+
// task 1004055 — the endpoint does TWO things, and only one of them was ever
|
|
443
|
+
// reported. Besides the attestation (best-effort, and declining is a legitimate
|
|
444
|
+
// answer) it records claims.published_head_sha, the tip that makes the ship
|
|
445
|
+
// PROVABLE — and that half is load-bearing, not best-effort: without it POST /ship
|
|
446
|
+
// refuses the confirmed→shipped flip. So this returns { recorded, attested, reason }
|
|
447
|
+
// rather than undefined, and callers act on `recorded`. It still never throws.
|
|
441
448
|
async function attestGateTrust(taskId, headSha) {
|
|
442
|
-
if (!headSha) return;
|
|
449
|
+
if (!headSha) return { recorded: false, attested: false, reason: 'no head SHA to attest' };
|
|
450
|
+
let r;
|
|
443
451
|
try {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
452
|
+
r = await (await cliClient()).tasks.postTasksIdAttestGate({ id: taskId, body: { head_sha: headSha } });
|
|
453
|
+
} catch (e) {
|
|
454
|
+
// task 1004055: a bare `catch (_) {}` here meant a thrown request error left NO
|
|
455
|
+
// trace at all — the quietest of the failure shapes, and the one that reached
|
|
456
|
+
// the builder as an unexplained ship_land_not_proven.
|
|
457
|
+
console.log(` ci-land: gate attestation request failed (${(e && e.message) || 'unknown error'}).`);
|
|
458
|
+
return { recorded: false, attested: false, reason: `request failed (${(e && e.message) || 'unknown error'})` };
|
|
459
|
+
}
|
|
460
|
+
const recorded = landProofRecorded(r);
|
|
461
|
+
if (r.ok && r.data) {
|
|
462
|
+
if (r.data.attested) {
|
|
463
|
+
console.log(' ci-land: gate-author-trust attested on the head SHA (Metic+) — a gate-surface PR auto-passes the ADR-0058 gatekeeper.');
|
|
453
464
|
} else {
|
|
454
|
-
console.log(` ci-land: gate
|
|
465
|
+
console.log(` ci-land: gate-author-trust NOT attested (${r.data.reason || 'rank below Metic'}) — a gate-surface PR will need owner approval; a non-gate PR is unaffected.`);
|
|
455
466
|
}
|
|
456
|
-
|
|
457
|
-
|
|
467
|
+
return { recorded, attested: !!r.data.attested, reason: r.data.reason || null };
|
|
468
|
+
}
|
|
469
|
+
if (r.status === 503) {
|
|
470
|
+
console.log(' ci-land: the server has no push credential to post the gate attestation (operator: GITHUB_APP_* / GITHUB_PUSH_TOKEN). A gate-surface PR will need owner approval.');
|
|
471
|
+
return { recorded, attested: false, reason: 'the server has no push credential' };
|
|
458
472
|
}
|
|
473
|
+
console.log(` ci-land: gate attestation request failed (HTTP ${r.status}); a gate-surface PR may need a manual re-run / owner approval.`);
|
|
474
|
+
return { recorded, attested: false, reason: `HTTP ${r.status}` };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// landProofRecorded — PURE. Did an attest-gate response actually record
|
|
478
|
+
// claims.published_head_sha (task 1004055)? That row is the durable per-task land
|
|
479
|
+
// proof; without it POST /tasks/:id/ship refuses with ship_land_not_proven and the
|
|
480
|
+
// task strands at 'confirmed' with its work already on main.
|
|
481
|
+
//
|
|
482
|
+
// The server SAYS so in `tip_recorded` — on the 200 body and in the 503's details,
|
|
483
|
+
// because the write deliberately sits BEFORE the credential check. Prefer that.
|
|
484
|
+
// A server that predates the field sends neither, which is the NORMAL state here:
|
|
485
|
+
// an instance runs a PINNED core, so it lags main by however long the pin sits
|
|
486
|
+
// still. For those, fall back to the route's contract — the write is above the
|
|
487
|
+
// credential 503, so 200 and 503 both mean the tip landed and nothing else does.
|
|
488
|
+
// Exported so the truth table is pinned with no server.
|
|
489
|
+
function landProofRecorded(r) {
|
|
490
|
+
if (!r || typeof r.status !== 'number') return false;
|
|
491
|
+
const body = r.data || {};
|
|
492
|
+
const explicit = body.tip_recorded !== undefined
|
|
493
|
+
? body.tip_recorded
|
|
494
|
+
: (body.error && body.error.details ? body.error.details.tip_recorded : undefined);
|
|
495
|
+
if (explicit !== undefined) return explicit === true;
|
|
496
|
+
return r.status === 200 || r.status === 503;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// landProofWarningLines — PURE. What to print when the proof did NOT get recorded
|
|
500
|
+
// (task 1004055). Without it POST /ship refuses, the task parks at 'confirmed' —
|
|
501
|
+
// which gates the builder's WHOLE queue — and the publish-reconciler eventually
|
|
502
|
+
// writes strand:land_not_proven, after which nothing heals it. Say it HERE, while
|
|
503
|
+
// the sha is still on screen, and hand over the calls that fix it, rather than
|
|
504
|
+
// letting it surface as an opaque 409 later.
|
|
505
|
+
//
|
|
506
|
+
// `merged` is the tense, and it is the whole difference between the two callers:
|
|
507
|
+
// the local-merge path has ALREADY put the work on main and needs the full
|
|
508
|
+
// two-call recovery now; the PR path has only pushed, so the land may still be
|
|
509
|
+
// minutes away and the same recovery would be premature. Exported so both
|
|
510
|
+
// wordings are pinned.
|
|
511
|
+
function landProofWarningLines(taskId, sha, reason, { merged = true } = {}) {
|
|
512
|
+
const head = sha || '<the merge commit on main>';
|
|
513
|
+
const why = ` WARNING: the land proof for task ${taskId} was NOT recorded (${reason || 'unknown'}).`;
|
|
514
|
+
const fix = ` node scripts/gds/api.js POST /tasks/${taskId}/attest-gate --body '{"head_sha":"${head}"}'`;
|
|
515
|
+
if (!merged) {
|
|
516
|
+
return [
|
|
517
|
+
'',
|
|
518
|
+
why,
|
|
519
|
+
' Nothing is lost yet — the branch is pushed and the PR still lands normally.',
|
|
520
|
+
' But if the ship then refuses with ship_land_not_proven, re-record the tip',
|
|
521
|
+
' against the merge commit and re-run the ship:',
|
|
522
|
+
` node scripts/gds/api.js POST /tasks/${taskId}/attest-gate --body '{"head_sha":"<the merge commit on main>"}'`,
|
|
523
|
+
` node scripts/gds/ship.js ${taskId}`,
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
return [
|
|
527
|
+
'',
|
|
528
|
+
why,
|
|
529
|
+
' The merge IS on main, but the server cannot prove the land is this task\'s, so',
|
|
530
|
+
' the confirmed -> shipped flip will be refused (ship_land_not_proven) and the',
|
|
531
|
+
' task will park at confirmed — which blocks every new claim you make. Fix it:',
|
|
532
|
+
fix,
|
|
533
|
+
` node scripts/gds/api.js POST /tasks/${taskId}/ship --body '{}'`,
|
|
534
|
+
];
|
|
459
535
|
}
|
|
460
536
|
|
|
461
537
|
// pickStaleGateReviewRun — PURE. Among `gh run list --json
|
|
@@ -596,7 +672,14 @@ async function ciLandLocal(taskId, branch, valueSummary) {
|
|
|
596
672
|
// and a Metic+ gate-surface PR auto-passes — race-free for a fresh PR, since the
|
|
597
673
|
// `gh pr create` below is what fires gate-review and it runs only after this
|
|
598
674
|
// returns. (A reused open PR is handled by the re-run nudge after auto-merge.)
|
|
599
|
-
|
|
675
|
+
// task 1004055: the same call also records the land proof, so a miss here is not
|
|
676
|
+
// cosmetic — check it rather than firing and forgetting. Not fatal on this path:
|
|
677
|
+
// the branch is pushed and the PR lands regardless, so say what to do IF the ship
|
|
678
|
+
// is later refused, and carry on.
|
|
679
|
+
const gateProof = await attestGateTrust(taskId, headSha);
|
|
680
|
+
if (!gateProof.recorded) {
|
|
681
|
+
for (const line of landProofWarningLines(taskId, headSha, gateProof.reason, { merged: false })) console.log(line);
|
|
682
|
+
}
|
|
600
683
|
|
|
601
684
|
// Ensure exactly one PR for this branch. gh pr create is idempotent-ish: it
|
|
602
685
|
// errors if a PR already exists, so check first and reuse it.
|
|
@@ -723,6 +806,12 @@ module.exports = {
|
|
|
723
806
|
ciLand,
|
|
724
807
|
classifyLandOutcome,
|
|
725
808
|
formatPublishFailure,
|
|
809
|
+
// task 1004055: the land-proof half of attest-gate. attestGateTrust is exported
|
|
810
|
+
// so the LOCAL-merge path can record a proof it previously never recorded at all;
|
|
811
|
+
// the two pure halves so the truth table + the wording are pinned without a server.
|
|
812
|
+
attestGateTrust,
|
|
813
|
+
landProofRecorded,
|
|
814
|
+
landProofWarningLines,
|
|
726
815
|
// task 1002572 (PART 2): the bail ids + the pure "can a PR still land this?"
|
|
727
816
|
// decision, so the single-checkout fallback is opt-in per bail and unit-tested.
|
|
728
817
|
LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH,
|
|
@@ -20,9 +20,11 @@ const { mainIsProtected, pushVia, readDeployConfigRaw, resolveDeployMode, resolv
|
|
|
20
20
|
const {
|
|
21
21
|
LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH,
|
|
22
22
|
LAND_BAIL_NO_TASK_BRANCH,
|
|
23
|
+
attestGateTrust,
|
|
23
24
|
ciLand,
|
|
24
25
|
landBailRecoverableByPr,
|
|
25
26
|
landBailed,
|
|
27
|
+
landProofWarningLines,
|
|
26
28
|
landed,
|
|
27
29
|
} = require('./ship-land.js');
|
|
28
30
|
const { runSmoke } = require('./ship-grade.js');
|
|
@@ -301,6 +303,32 @@ async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
301
303
|
return landBailed('could not push the merged main to origin', '/merge-mode');
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
// task 1004055: RECORD THE LAND PROOF — the step this path never had.
|
|
307
|
+
//
|
|
308
|
+
// The two OTHER land paths each tell the server which commit is this task's:
|
|
309
|
+
// ciLandLocal posts attest-gate after its push, ciLandServer gets it from
|
|
310
|
+
// publish-branch. This path merges and pushes main ITSELF, so nothing ever did,
|
|
311
|
+
// and claims.published_head_sha stayed NULL. It also opens no PR and deletes the
|
|
312
|
+
// branch below, so getPublishStatus can find nothing by head ref either — which
|
|
313
|
+
// left deriveShipAssurance with neither of its two proofs and made the POST /ship
|
|
314
|
+
// three lines later refuse with ship_land_not_proven, EVERY time, with the work
|
|
315
|
+
// already merged. The task then parked at 'confirmed', gating its builder's whole
|
|
316
|
+
// queue, until someone ran the recovery by hand (five consecutive ships,
|
|
317
|
+
// 2026-09-19; twelve refusals in one day's audit log).
|
|
318
|
+
//
|
|
319
|
+
// The merge commit is the sha to record: the push above just put it on main, so
|
|
320
|
+
// the ancestry check cannot fail, and it is what the hand-recovery always used.
|
|
321
|
+
// It goes HERE, immediately after the push and BEFORE both the deploy step (which
|
|
322
|
+
// can bail) and the branch deletion (which destroys the only other evidence) —
|
|
323
|
+
// the proof must exist from the first moment the work is on main.
|
|
324
|
+
const mergeSha = gitOk('git rev-parse HEAD', { cwd: REPO_ROOT });
|
|
325
|
+
const proof = await attestGateTrust(taskId, mergeSha);
|
|
326
|
+
if (proof.recorded) {
|
|
327
|
+
console.log(` auto-merge: land proof recorded (merge ${String(mergeSha).slice(0, 8)} on main).`);
|
|
328
|
+
} else {
|
|
329
|
+
for (const line of landProofWarningLines(taskId, mergeSha, proof.reason)) console.log(line);
|
|
330
|
+
}
|
|
331
|
+
|
|
304
332
|
// Deploy step. The core carries NO deploy target of its own — under the
|
|
305
333
|
// standalone model a core change reaches a live instance via package-core +
|
|
306
334
|
// `bongos upgrade` (pin-bump), NOT an SSH deploy (ADR 0108). So unless an
|
package/scripts/gds/ship.js
CHANGED
|
@@ -127,6 +127,8 @@ const {
|
|
|
127
127
|
landPending,
|
|
128
128
|
landPendingLines,
|
|
129
129
|
landPollAfterAutoMerge,
|
|
130
|
+
landProofRecorded,
|
|
131
|
+
landProofWarningLines,
|
|
130
132
|
landed,
|
|
131
133
|
pickStaleGateReviewRun,
|
|
132
134
|
reconcileBlockedLines,
|
|
@@ -266,6 +268,12 @@ module.exports = {
|
|
|
266
268
|
// object, so a consumer MUST test `.ok`; the old `if (!merged)` would sail past
|
|
267
269
|
// a bail. Also pins pending (a poll timeout) as distinct from bailed.
|
|
268
270
|
landed, landPending, landBailed, strandCommand,
|
|
271
|
+
// task 1004055: the land proof the LOCAL-merge path never recorded. landProofRecorded
|
|
272
|
+
// is the "did the tip actually land?" truth table (explicit tip_recorded wins; a
|
|
273
|
+
// pinned older core falls back to the route's before-the-credential-check contract);
|
|
274
|
+
// landProofWarningLines is the recovery the builder gets when it did not, in the
|
|
275
|
+
// tense of whichever path is speaking. Both pure — exported so they pin without a server.
|
|
276
|
+
landProofRecorded, landProofWarningLines,
|
|
269
277
|
// task 1001916: the publish-branch failure formatter — exported pure so both
|
|
270
278
|
// wire shapes (envelope + legacy flat), the step/reason surfacing, and the
|
|
271
279
|
// non-fast-forward remedy hint are pinned without a server.
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// Mounts every non-game surface the platform serves onto an Express app:
|
|
4
4
|
// * /healthz + /version — liveness + deployed-build info
|
|
5
5
|
// * /api/bongos/* — the GDS API (buildGdsRouter)
|
|
6
|
-
// * /downloads/devbox/* — Dev Box installer redirect surface (#904)
|
|
7
6
|
// * generic module web-surface — each enabled module's contributes.webSurfaces
|
|
8
7
|
// hosts served on its Host prefix by ONE generic static
|
|
9
8
|
// loop (the status dashboard at status.<apex>,
|
|
@@ -28,9 +27,7 @@
|
|
|
28
27
|
// import boundary (scripts/gds/fitness.js §1) — it imports nothing from the host
|
|
29
28
|
// game runtime (no src/db.js, no src/rooms/, no src/world/, no Colyseus). It
|
|
30
29
|
// serves only static files + the GDS router, so a non-game Cloud Bongos instance
|
|
31
|
-
// runs it unchanged.
|
|
32
|
-
// R54's module system, task 1193, will gate per-instance; it is benign on a
|
|
33
|
-
// vanilla boot — it only redirects to whatever config/devbox-app.json names.)
|
|
30
|
+
// runs it unchanged.
|
|
34
31
|
|
|
35
32
|
const path = require('path');
|
|
36
33
|
const fs = require('fs');
|
|
@@ -44,7 +41,6 @@ const gdsAuth = require('./auth');
|
|
|
44
41
|
const { loadIdpConfig } = require('./auth-config');
|
|
45
42
|
const { buildStampedHtml } = require('../asset-version');
|
|
46
43
|
const { buildInfo } = require('../build-info');
|
|
47
|
-
const { buildDevboxDownloads } = require('../devbox-downloads');
|
|
48
44
|
const { buildBongosDownloads } = require('../bongos-downloads');
|
|
49
45
|
const { clientBranding, branding, applyBrandConditionals } = require('../branding');
|
|
50
46
|
const { prelaunchGate } = require('./prelaunch-gate');
|
|
@@ -59,7 +55,7 @@ const { API_PREFIX, LEGACY_API_PREFIXES, API_VERSION, VERSIONED_API_PREFIX, ALL_
|
|
|
59
55
|
// is byte-identical for the live instance.
|
|
60
56
|
const ROOT = resolveCoreRoot();
|
|
61
57
|
|
|
62
|
-
// Instance-only RELEASE config (config/
|
|
58
|
+
// Instance-only RELEASE config (config/bongos-cli.json) is
|
|
63
59
|
// boundary-excluded from the portable core (publish-manifest INSTANCE_EXCLUDE), so it is
|
|
64
60
|
// HOST content: it lives at the INSTANCE root, which on a vendored instance is NOT the
|
|
65
61
|
// package dir this file sits under. A core-relative require therefore looked in
|
|
@@ -532,38 +528,6 @@ function isApexOnlyPath(p) {
|
|
|
532
528
|
// Call this ONCE, right after app setup (trust proxy etc.) and BEFORE any
|
|
533
529
|
// entrypoint-specific tail (the game static + themed 404 in server.js; the plain
|
|
534
530
|
// 404 in platform-server.js).
|
|
535
|
-
// The Dev Box download/manifest mini-router (#904, ADR 0045), extracted so the
|
|
536
|
-
// ACTUAL route handlers are hermetically testable (tests/devbox_update_path.mjs
|
|
537
|
-
// mounts them on a throwaway app — the docs_openapi_route pattern) instead of
|
|
538
|
-
// only their pure builder. task 1002649: the manifest is served under BOTH
|
|
539
|
-
// /latest.json and an extensionless /latest twin — a front proxy with a
|
|
540
|
-
// file-matcher can eat *.json paths before Node (the live pre-ADR-0152 vhost
|
|
541
|
-
// 404'd latest.json empty while extensionless siblings worked), so every
|
|
542
|
-
// manifest consumer has a path that survives that class. Registration order
|
|
543
|
-
// matters: the literal routes must precede the `:target` catch-all.
|
|
544
|
-
function mountDevboxDownloads(app, devboxDownloads) {
|
|
545
|
-
const manifest = (_req, res) => {
|
|
546
|
-
// Short TTL: a release bump (config change + deploy) is visible within a
|
|
547
|
-
// minute, same posture as the hall's HTML.
|
|
548
|
-
res.set('Cache-Control', 'public, max-age=60');
|
|
549
|
-
res.json(devboxDownloads.latestJson());
|
|
550
|
-
};
|
|
551
|
-
app.get('/downloads/devbox/latest.json', manifest);
|
|
552
|
-
app.get('/downloads/devbox/latest', manifest);
|
|
553
|
-
// #924: the lightweight update-check endpoint the Dev Box app polls + the hall
|
|
554
|
-
// /settings courtesy line reads.
|
|
555
|
-
app.get('/downloads/devbox/version', (_req, res) => {
|
|
556
|
-
res.set('Cache-Control', 'public, max-age=60');
|
|
557
|
-
res.json(devboxDownloads.versionJson());
|
|
558
|
-
});
|
|
559
|
-
app.get('/downloads/devbox/:target', (req, res) => {
|
|
560
|
-
const url = devboxDownloads.redirectFor(req.params.target);
|
|
561
|
-
if (!url) return res.fail('unknown_target', 404, { targets: Object.keys(devboxDownloads.latestJson().assets) });
|
|
562
|
-
res.set('Cache-Control', 'public, max-age=60');
|
|
563
|
-
res.redirect(302, url);
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
567
531
|
function mountInternalSurfaces(app) {
|
|
568
532
|
// Prometheus instrumentation (task #1680, V4.R28a). The counting middleware
|
|
569
533
|
// mounts FIRST so it wraps every surface registered below (and, since
|
|
@@ -691,24 +655,14 @@ function mountInternalSurfaces(app) {
|
|
|
691
655
|
app.use(prefix, gdsRouter);
|
|
692
656
|
}
|
|
693
657
|
|
|
694
|
-
// Dev Box desktop app downloads (#904, ADR 0045). Branded redirect surface:
|
|
695
|
-
// the installers live as release assets on the public releases repo (they
|
|
696
|
-
// can't ride the git deploy — see src/devbox-downloads.js), and these routes
|
|
697
|
-
// keep every builder-facing URL on the apex. Mounted right after the API and
|
|
698
|
-
// BEFORE the host shims so the same paths work on the apex AND on
|
|
699
|
-
// builders.<apex> (the builders rewrite shim below only prefixes non-/api
|
|
700
|
-
// paths, so without this placement /downloads/* would be rewritten to
|
|
701
|
-
// /builders/downloads/* and 404).
|
|
702
|
-
const devboxCfg = tryLoadInstanceConfig('devbox-app.json');
|
|
703
|
-
if (devboxCfg) {
|
|
704
|
-
mountDevboxDownloads(app, buildDevboxDownloads(devboxCfg));
|
|
705
|
-
}
|
|
706
|
-
|
|
707
658
|
// Standalone `bongos` CLI downloads + the one-line installer (task 1248,
|
|
708
|
-
// V4.R81).
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
659
|
+
// V4.R81). Off-repo release pattern: the compiled per-platform binaries live
|
|
660
|
+
// as release assets on the public releases repo (config/bongos-cli.json →
|
|
661
|
+
// src/bongos-downloads.js), and these routes keep every user-facing URL on the
|
|
662
|
+
// apex. Mounted right after the API and BEFORE the host shims so the same
|
|
663
|
+
// paths work on the apex AND on builders.<apex> (the builders rewrite shim
|
|
664
|
+
// below only prefixes non-/api paths, so without this placement /downloads/*
|
|
665
|
+
// would be rewritten to /builders/downloads/* and 404). The installer scripts are served with the
|
|
712
666
|
// server's PINNED origin substituted for __GDS_API_BASE__ — never a request
|
|
713
667
|
// header, so a spoofed Host can't redirect a `curl | sh` (the box-connect.sh rule).
|
|
714
668
|
const bongosCfg = tryLoadInstanceConfig('bongos-cli.json');
|
|
@@ -1357,4 +1311,4 @@ function mountInternalSurfaces(app) {
|
|
|
1357
1311
|
return app;
|
|
1358
1312
|
}
|
|
1359
1313
|
|
|
1360
|
-
module.exports = { mountInternalSurfaces,
|
|
1314
|
+
module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
|
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.885'; // 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/src/platform-server.js
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// * the builders' hall host — modules/hall-ui/public/ (the hall-ui module's
|
|
14
14
|
// web surface, BV1.R85 — served by its dedicated
|
|
15
15
|
// block in serve-internal)
|
|
16
|
-
// * /downloads/devbox/* — the Dev Box installer redirects
|
|
17
16
|
//
|
|
18
17
|
// WHAT IT DELIBERATELY DOES NOT CONSTRUCT — the host game runtime:
|
|
19
18
|
// * NO Colyseus server, NO WebSocketTransport
|
|
@@ -37,7 +37,6 @@ const ALLOWED = {
|
|
|
37
37
|
box_ship_permissions: '.claude/settings.json (package-excluded)',
|
|
38
38
|
conductor: '.claude/hooks/conductor.js (package-excluded)',
|
|
39
39
|
deploy_instance: 'scripts/deploy/deploy-instance.sh (host topology)',
|
|
40
|
-
devbox_downloads: 'config/devbox-app.json (instance content)',
|
|
41
40
|
eviction_policy: '.claude/hooks/eviction-policy.js (package-excluded)',
|
|
42
41
|
first_run_engine_check: '.claude/hooks/session-start.js (package-excluded)',
|
|
43
42
|
hook_core_resolve: '.claude/hooks/_core-resolve.js (package-excluded)',
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// tests/instance_release_config.mjs
|
|
2
2
|
//
|
|
3
|
-
// serve-internal mounts /downloads/
|
|
4
|
-
//
|
|
5
|
-
// config/bongos-cli.json). Those configs are boundary-EXCLUDED from the packaged core,
|
|
3
|
+
// serve-internal mounts /downloads/bongos/* only when the instance ships the matching
|
|
4
|
+
// RELEASE config (config/bongos-cli.json). That config is boundary-EXCLUDED from the packaged core,
|
|
6
5
|
// so they live at the INSTANCE root — which on a vendored instance (@bongos/core in
|
|
7
6
|
// node_modules, ADR 0108) is a DIFFERENT directory from the core package dir
|
|
8
7
|
// serve-internal.js sits in. The old core-relative `require('../../config/…')` looked
|
|
9
|
-
// only in node_modules/@bongos/core/config/, so a vendored instance's
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// only in node_modules/@bongos/core/config/, so a vendored instance's download routes
|
|
9
|
+
// never mounted (task 1002371; ADR 0145 papered over it by copying the config into
|
|
10
|
+
// node_modules — lost on the next npm ci). Same class as ADR 0144's vendored infra/
|
|
11
|
+
// path bug. This test was written against the /downloads/devbox/* twin, which task
|
|
12
|
+
// 1003891 deleted with the Dev Box app; it now proves the SAME ladder against
|
|
13
|
+
// /downloads/bongos/*, the surviving surface that still depends on it.
|
|
13
14
|
//
|
|
14
15
|
// The first test is THE regression: a config present ONLY at the instance root must be
|
|
15
16
|
// found. It fails against the old resolution (which could not see the instance root at
|
|
@@ -29,12 +30,12 @@ import path from 'node:path';
|
|
|
29
30
|
|
|
30
31
|
const require = createRequire(import.meta.url);
|
|
31
32
|
|
|
32
|
-
// A minimal but VALID release config —
|
|
33
|
+
// A minimal but VALID release config — buildBongosDownloads rejects a bad
|
|
33
34
|
// releases_repo/version at boot, so the end-to-end mount below proves real parsing.
|
|
34
|
-
const
|
|
35
|
-
releases_repo: 'example-owner/example-
|
|
35
|
+
const CLI_CFG = {
|
|
36
|
+
releases_repo: 'example-owner/example-cli',
|
|
36
37
|
version: '9.9.9',
|
|
37
|
-
assets: { 'mac-arm64': '
|
|
38
|
+
assets: { 'mac-arm64': 'bongos-9.9.9-macos-arm64' },
|
|
38
39
|
aliases: { mac: 'mac-arm64' },
|
|
39
40
|
};
|
|
40
41
|
|
|
@@ -50,23 +51,23 @@ function makeRoot(name, contents) {
|
|
|
50
51
|
|
|
51
52
|
// Point the process at a temp INSTANCE root BEFORE serve-internal loads, so the default
|
|
52
53
|
// (uninjected) resolution runs the vendored shape: config at the instance root, absent
|
|
53
|
-
// from the core package dir. This checkout ships no config/
|
|
54
|
+
// from the core package dir. This checkout ships no config/bongos-cli.json — it is
|
|
54
55
|
// instance-only — so the core tier genuinely has nothing to find, exactly like a
|
|
55
56
|
// freshly-vendored box.
|
|
56
|
-
const INSTANCE_ROOT = makeRoot('
|
|
57
|
+
const INSTANCE_ROOT = makeRoot('bongos-cli.json', JSON.stringify(CLI_CFG));
|
|
57
58
|
process.env.CLOUDBONGOS_INSTANCE_ROOT = INSTANCE_ROOT;
|
|
58
59
|
const { tryLoadInstanceConfig, mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
59
60
|
|
|
60
61
|
test('finds a release config that exists ONLY at the instance root (the vendored bug)', () => {
|
|
61
|
-
const instance = makeRoot('
|
|
62
|
+
const instance = makeRoot('bongos-cli.json', '{"version":"1.0.0"}');
|
|
62
63
|
const core = makeRoot(null);
|
|
63
|
-
assert.deepEqual(tryLoadInstanceConfig('
|
|
64
|
+
assert.deepEqual(tryLoadInstanceConfig('bongos-cli.json', [instance, core]), { version: '1.0.0' });
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
test('the instance root wins over the core package dir when both carry a copy', () => {
|
|
67
|
-
const instance = makeRoot('
|
|
68
|
-
const core = makeRoot('
|
|
69
|
-
assert.equal(tryLoadInstanceConfig('
|
|
68
|
+
const instance = makeRoot('bongos-cli.json', '{"from":"instance"}');
|
|
69
|
+
const core = makeRoot('bongos-cli.json', '{"from":"core"}');
|
|
70
|
+
assert.equal(tryLoadInstanceConfig('bongos-cli.json', [instance, core]).from, 'instance');
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
test('falls back to the core package dir (single checkout + the copied-in workaround)', () => {
|
|
@@ -76,16 +77,16 @@ test('falls back to the core package dir (single checkout + the copied-in workar
|
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
test('absent from every root → null, so the download surface just does not mount', () => {
|
|
79
|
-
assert.equal(tryLoadInstanceConfig('
|
|
80
|
-
assert.equal(tryLoadInstanceConfig('
|
|
80
|
+
assert.equal(tryLoadInstanceConfig('bongos-cli.json', [makeRoot(null), makeRoot(null)]), null);
|
|
81
|
+
assert.equal(tryLoadInstanceConfig('bongos-cli.json', []), null);
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
test('a MALFORMED config still throws — only a MISSING file is tolerated', () => {
|
|
84
|
-
const bad = makeRoot('
|
|
85
|
-
assert.throws(() => tryLoadInstanceConfig('
|
|
85
|
+
const bad = makeRoot('bongos-cli.json', '{ this is not valid json ');
|
|
86
|
+
assert.throws(() => tryLoadInstanceConfig('bongos-cli.json', [bad]), SyntaxError);
|
|
86
87
|
});
|
|
87
88
|
|
|
88
|
-
test('/downloads/
|
|
89
|
+
test('/downloads/bongos/version mounts from an INSTANCE-root config (end to end)', async () => {
|
|
89
90
|
const express = require('express');
|
|
90
91
|
const app = express();
|
|
91
92
|
mountInternalSurfaces(app);
|
|
@@ -93,11 +94,11 @@ test('/downloads/devbox/version mounts from an INSTANCE-root config (end to end)
|
|
|
93
94
|
const s = app.listen(0, () => resolve(s));
|
|
94
95
|
});
|
|
95
96
|
try {
|
|
96
|
-
const res = await fetch(`http://127.0.0.1:${server.address().port}/downloads/
|
|
97
|
+
const res = await fetch(`http://127.0.0.1:${server.address().port}/downloads/bongos/version`);
|
|
97
98
|
assert.equal(res.status, 200, 'the route must mount when only the INSTANCE root has the config');
|
|
98
99
|
const body = await res.json();
|
|
99
|
-
assert.equal(body.version,
|
|
100
|
-
assert.match(body.assets['mac-arm64'], /^https:\/\/github\.com\/example-owner\/example-
|
|
100
|
+
assert.equal(body.version, CLI_CFG.version);
|
|
101
|
+
assert.match(body.assets['mac-arm64'], /^https:\/\/github\.com\/example-owner\/example-cli\//);
|
|
101
102
|
} finally {
|
|
102
103
|
await new Promise((r) => server.close(r));
|
|
103
104
|
}
|
|
@@ -471,11 +471,11 @@ await t('POST /tasks/:id/attest-gate records the pushed tip on the claim, even w
|
|
|
471
471
|
const router = buildTasksRouter();
|
|
472
472
|
const layer = findRouteLayer(router, '/tasks/:id/attest-gate', 'post');
|
|
473
473
|
const handler = layer.route.stack[layer.route.stack.length - 1].handle;
|
|
474
|
-
const out = { failCode: null, jsonBody: null };
|
|
474
|
+
const out = { failCode: null, failOpts: null, jsonBody: null };
|
|
475
475
|
const res = {
|
|
476
476
|
status() { return this; },
|
|
477
477
|
json(b) { out.jsonBody = b; return this; },
|
|
478
|
-
fail(code) { out.failCode = code; return this; },
|
|
478
|
+
fail(code, opts) { out.failCode = code; out.failOpts = opts; return this; },
|
|
479
479
|
};
|
|
480
480
|
// rank archon → gateTaskOwnership passes with no DB lookup, keeping the test hermetic.
|
|
481
481
|
const req = { params: { id: '77' }, body: { head_sha: 'a'.repeat(40) }, builder: { id: 95, rank: 'archon' } };
|
|
@@ -483,9 +483,77 @@ await t('POST /tasks/:id/attest-gate records the pushed tip on the claim, even w
|
|
|
483
483
|
assert.equal(out.failCode, 'push_unconfigured', 'no credential → still the usual 503 to the caller');
|
|
484
484
|
assert.deepEqual(recorded, [{ taskId: 77, builderId: 95, sha: 'a'.repeat(40) }],
|
|
485
485
|
'the tip is recorded BEFORE the credential check — the wave-strand cure');
|
|
486
|
+
// task 1004055: and the 503 must SAY the tip landed. This exit is the one a
|
|
487
|
+
// caller is most likely to misread as total failure; ship.js decides whether
|
|
488
|
+
// to warn the builder off this flag, so it has to travel on the error too.
|
|
489
|
+
assert.equal(out.failOpts && out.failOpts.details && out.failOpts.details.tip_recorded, true,
|
|
490
|
+
'the 503 must carry tip_recorded:true — the write happened before it');
|
|
491
|
+
});
|
|
492
|
+
} finally {
|
|
493
|
+
dbMod.setClaimPublishedSha = savedSet;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// task 1004055: the SILENT MISS. setClaimPublishedSha returns null when the UPDATE
|
|
498
|
+
// matched no claim row, and the old route discarded that return value — so a caller
|
|
499
|
+
// that had already merged to main was told nothing was wrong, and only found out one
|
|
500
|
+
// call later as an opaque ship_land_not_proven 409, with the task parked at
|
|
501
|
+
// 'confirmed' gating its builder's whole queue. `tip_recorded` is what makes that
|
|
502
|
+
// answerable at the moment it happens.
|
|
503
|
+
await t('POST /tasks/:id/attest-gate reports tip_recorded:false when no claim row matched', async () => {
|
|
504
|
+
const dbMod = require('../modules/lifecycle/db.js');
|
|
505
|
+
const savedSet = dbMod.setClaimPublishedSha;
|
|
506
|
+
const savedHolder = dbMod.getLastClaimHolder;
|
|
507
|
+
// null = "the UPDATE matched nothing", the shape db-ship.js really returns.
|
|
508
|
+
dbMod.setClaimPublishedSha = async () => null;
|
|
509
|
+
dbMod.getLastClaimHolder = async () => ({ builder_id: 95 });
|
|
510
|
+
try {
|
|
511
|
+
// A credential IS configured, so the handler reaches res.json; a below-Metic rank
|
|
512
|
+
// means postGateAuthorTrust declines on the pure rank check and never fetches.
|
|
513
|
+
await withEnv({ GITHUB_APP_ID: undefined, GITHUB_APP_INSTALLATION_ID: undefined, GITHUB_APP_PRIVATE_KEY: undefined, GITHUB_PUSH_TOKEN: 'ghp_fake_for_isConfigured' }, async () => {
|
|
514
|
+
const router = buildTasksRouter();
|
|
515
|
+
const layer = findRouteLayer(router, '/tasks/:id/attest-gate', 'post');
|
|
516
|
+
const handler = layer.route.stack[layer.route.stack.length - 1].handle;
|
|
517
|
+
const out = { failCode: null, jsonBody: null };
|
|
518
|
+
const res = {
|
|
519
|
+
status() { return this; },
|
|
520
|
+
json(b) { out.jsonBody = b; return this; },
|
|
521
|
+
fail(code) { out.failCode = code; return this; },
|
|
522
|
+
};
|
|
523
|
+
const req = { params: { id: '77' }, body: { head_sha: 'b'.repeat(40) }, builder: { id: 95, rank: 'thetes' } };
|
|
524
|
+
await handler(req, res);
|
|
525
|
+
assert.equal(out.failCode, null, 'a below-Metic attest is a 200, not a failure');
|
|
526
|
+
assert.equal(out.jsonBody.ok, true);
|
|
527
|
+
assert.equal(out.jsonBody.attested, false, 'below Metic → no attestation, as before');
|
|
528
|
+
assert.equal(out.jsonBody.tip_recorded, false,
|
|
529
|
+
'nothing was recorded, and the response must say so rather than reading as success');
|
|
530
|
+
});
|
|
531
|
+
} finally {
|
|
532
|
+
dbMod.setClaimPublishedSha = savedSet;
|
|
533
|
+
dbMod.getLastClaimHolder = savedHolder;
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await t('POST /tasks/:id/attest-gate reports tip_recorded:true on the happy path', async () => {
|
|
538
|
+
const dbMod = require('../modules/lifecycle/db.js');
|
|
539
|
+
const savedSet = dbMod.setClaimPublishedSha;
|
|
540
|
+
const savedHolder = dbMod.getLastClaimHolder;
|
|
541
|
+
dbMod.setClaimPublishedSha = async () => 4242; // a claim id = the UPDATE matched
|
|
542
|
+
dbMod.getLastClaimHolder = async () => ({ builder_id: 95 });
|
|
543
|
+
try {
|
|
544
|
+
await withEnv({ GITHUB_APP_ID: undefined, GITHUB_APP_INSTALLATION_ID: undefined, GITHUB_APP_PRIVATE_KEY: undefined, GITHUB_PUSH_TOKEN: 'ghp_fake_for_isConfigured' }, async () => {
|
|
545
|
+
const router = buildTasksRouter();
|
|
546
|
+
const layer = findRouteLayer(router, '/tasks/:id/attest-gate', 'post');
|
|
547
|
+
const handler = layer.route.stack[layer.route.stack.length - 1].handle;
|
|
548
|
+
const out = { jsonBody: null };
|
|
549
|
+
const res = { status() { return this; }, json(b) { out.jsonBody = b; return this; }, fail() { return this; } };
|
|
550
|
+
const req = { params: { id: '77' }, body: { head_sha: 'c'.repeat(40) }, builder: { id: 95, rank: 'thetes' } };
|
|
551
|
+
await handler(req, res);
|
|
552
|
+
assert.equal(out.jsonBody.tip_recorded, true);
|
|
486
553
|
});
|
|
487
554
|
} finally {
|
|
488
555
|
dbMod.setClaimPublishedSha = savedSet;
|
|
556
|
+
dbMod.getLastClaimHolder = savedHolder;
|
|
489
557
|
}
|
|
490
558
|
});
|
|
491
559
|
|