@bongos/core 1.21.125 → 1.21.127
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 +106 -51
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +6 -0
- package/docs/adr/0354-a-builder-hosts-one-project-free-and-more-takes-the-fleet-permission.md +1 -1
- package/docs/api/openapi.json +4 -4
- package/docs/copy-inventory.md +128 -126
- package/docs/copy-registry.json +188 -170
- package/docs/module-api-changelog.md +5 -1
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +152 -143
- package/modules/hall-ui/public/goal-forest-lib.js +36 -0
- package/modules/hall-ui/public/goal-forest-render.js +50 -10
- package/modules/hall-ui/public/goals-page.js +19 -22
- package/modules/hall-ui/public/goals-render.js +6 -16
- package/modules/hall-ui/public/goals.css +13 -4
- package/modules/hall-ui/public/goals.html +22 -24
- package/modules/hall-ui/public/goals.states.json +19 -1
- package/modules/hall-ui/records/goal-dependency-forest.md +4 -3
- package/modules/platform-identity/invite-authz.js +1 -1
- package/modules/provisioning/catalog-bridge.js +1 -1
- package/modules/provisioning/provisioning.js +1 -1
- package/modules/provisioning/route-helpers.js +50 -0
- package/modules/provisioning/routes/disconnect.js +6 -5
- package/modules/provisioning/routes/env-manifest.js +11 -27
- package/modules/provisioning/routes/provisioning.js +58 -75
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/gds/exec-path-guard.js +1 -1
- package/scripts/gds/fitness.js +1 -0
- package/scripts/gds/module-assess-tests.js +40 -11
- package/scripts/gds/module-assess-version.js +10 -2
- package/scripts/gds/module-error-report.js +148 -0
- package/scripts/gds/module-test-runner/README.md +44 -0
- package/scripts/gds/module-test-runner/module-tests.yml +87 -0
- package/scripts/gds/module-test-runner.js +358 -0
- package/scripts/gds/module.js +1 -0
- package/scripts/gds/provision-caps.js +63 -0
- package/scripts/gds/provision-process.js +3 -0
- package/scripts/gds/provision-units.js +3 -0
- package/scripts/gds/provision.js +3 -1
- package/src/module-api.js +1 -1
- package/src/module-loader/error-tally.js +188 -0
- package/src/module-loader/loader.js +52 -4
- package/src/platform-server.js +7 -0
- package/tests/goal_forest_lib.mjs +28 -0
- package/tests/goal_forest_page.mjs +106 -11
- package/tests/module_assess_price_parity.mjs +2 -2
- package/tests/module_assess_score.mjs +13 -3
- package/tests/module_error_counts.mjs +265 -0
- package/tests/module_test_runner.mjs +294 -0
- package/tests/project_catalog_routes.mjs +1 -1
- package/tests/provision.mjs +2 -0
- package/tests/provision_resource_caps.mjs +117 -0
- package/tests/provisioning_module_selection.mjs +1 -1
- package/tests/provisioning_project_owner_gate.mjs +271 -0
|
@@ -67,10 +67,12 @@ const { callbackPage } = require('./callback-page'); const githubSetup = require
|
|
|
67
67
|
const { badProjectDetail, badModuleSelection, badProjectType, screenedOut } = require('./body-validators'); const demo = require('../demo'); const birth = require('../birth');
|
|
68
68
|
const { failFrom } = require('../public-refusal'); // every catch: a declared refusal reaches the owner, anything else stays opaque (task 1004126)
|
|
69
69
|
const { settingsPush, failPushConflict } = require('../settings-push'); // a settings save's push + receipt, shared with routes/look.js (task 1004421)
|
|
70
|
+
const { requireProjectOwner, projectActor, isProjectOwner } = require('../route-helpers'); // the owner-or-archon gate (task 1004159, ADR 0328)
|
|
70
71
|
// the kernel ports this module provides (moved out at the size ratchet, task 1003578)
|
|
71
72
|
const { registerProvisioningSeams } = require('../seams');
|
|
72
73
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
73
74
|
const log = api.logger('provisioning');
|
|
75
|
+
const loadInstance = (id) => provisioning.getInstanceById(pool, id);
|
|
74
76
|
|
|
75
77
|
// How long a GitHub-App-Manifest `state` token is valid — the owner must click the
|
|
76
78
|
// pre-filled link + approve on GitHub within this window (task 2080). Comfortably
|
|
@@ -634,27 +636,18 @@ module.exports = function provisioningRoutes() {
|
|
|
634
636
|
}
|
|
635
637
|
});
|
|
636
638
|
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
// settings routes (task 1003040).
|
|
641
|
-
function instanceAccessDenied(inst, builder) {
|
|
642
|
-
if (!inst) return true;
|
|
643
|
-
if (builder.rank === 'archon') return false;
|
|
644
|
-
return String(inst.owner_builder_id) !== String(builder.id);
|
|
645
|
-
}
|
|
639
|
+
// Every own-scoped /provisioning/instances/:id route below mounts requireProjectOwner
|
|
640
|
+
// (../route-helpers.js): the owner, or an Archon on any project (fleet support), and the
|
|
641
|
+
// same 404 for everyone else — no existence leak. The row rides on req.project.
|
|
646
642
|
|
|
647
643
|
// GET /provisioning/instances/:id — read one instance (the manage page's read).
|
|
648
644
|
// rank: any authenticated builder (own resource; archon may read any — task
|
|
649
645
|
// 1003041). 404 otherwise — does not leak another owner's instance existence.
|
|
650
|
-
router.get('/provisioning/instances/:id', auth.requireBuilder, async (req, res) => {
|
|
646
|
+
router.get('/provisioning/instances/:id', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
651
647
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
652
648
|
if (!id) return;
|
|
653
649
|
try {
|
|
654
|
-
const inst =
|
|
655
|
-
if (instanceAccessDenied(inst, req.builder)) {
|
|
656
|
-
return res.fail('instance_not_found', 404);
|
|
657
|
-
}
|
|
650
|
+
const inst = req.project;
|
|
658
651
|
// SELF-HEAL THE CATALOG PROJECTION (task 1003581). ADR 0246's projection is
|
|
659
652
|
// write-triggered, so an instance older than it has no catalog row and its owner's
|
|
660
653
|
// invites 404 until they happen to save something. This own-scoped read — the manage
|
|
@@ -730,7 +723,7 @@ module.exports = function provisioningRoutes() {
|
|
|
730
723
|
// the answer moved moves back. Only null does it — an empty string or any other
|
|
731
724
|
// value is still refused as off-vocabulary, and `detail: {}` still changes nothing,
|
|
732
725
|
// because a form that collected no answers is not asking for any to be erased.
|
|
733
|
-
router.patch('/provisioning/instances/:id/detail', auth.requireBuilder, async (req, res) => {
|
|
726
|
+
router.patch('/provisioning/instances/:id/detail', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
734
727
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
735
728
|
if (!id) return;
|
|
736
729
|
if (validateOrRespond(req, res, {
|
|
@@ -757,9 +750,8 @@ module.exports = function provisioningRoutes() {
|
|
|
757
750
|
message: 'The body carries nothing to change — send description, detail, modules, type, or any mix.' });
|
|
758
751
|
}
|
|
759
752
|
try {
|
|
760
|
-
const inst =
|
|
761
|
-
|
|
762
|
-
const actor = String(inst.owner_builder_id) === String(req.builder.id) ? 'api:self' : `api:archon:${req.builder.id}`;
|
|
753
|
+
const inst = req.project;
|
|
754
|
+
const actor = projectActor(req);
|
|
763
755
|
// An empty description is CLEARING it, not a no-op string: normalize here so
|
|
764
756
|
// the column never holds '' — a blank string would read as answered to a
|
|
765
757
|
// `description IS NOT NULL` publish check and put an empty husk on the map.
|
|
@@ -810,7 +802,7 @@ module.exports = function provisioningRoutes() {
|
|
|
810
802
|
// purpose: ranking builders is a directory read the pure engine may not run.
|
|
811
803
|
// This is the resolution, and ADR 0251 is why it looks like this.
|
|
812
804
|
//
|
|
813
|
-
// rank: any authenticated builder, own resource — `
|
|
805
|
+
// rank: any authenticated builder, own resource — `requireProjectOwner`, the
|
|
814
806
|
// same gate and the same 404 as the manage read above. That is deliberate and
|
|
815
807
|
// not a new authorization idea: REACHING this route IS the ownership answer
|
|
816
808
|
// (ADR 0249 §4), so the surface never re-derives "is this theirs" and can never
|
|
@@ -821,12 +813,9 @@ module.exports = function provisioningRoutes() {
|
|
|
821
813
|
// nobody on purpose (ADR 0239 §6), which is a different statement from an empty
|
|
822
814
|
// list nobody filled — so that answer returns its own reason and never touches
|
|
823
815
|
// the directory at all.
|
|
824
|
-
router.get('/provisioning/instances/:id/invite-suggestions', auth.requireBuilder, async (req, res) => {
|
|
825
|
-
const id = parseId(req, res);
|
|
826
|
-
if (!id) return;
|
|
816
|
+
router.get('/provisioning/instances/:id/invite-suggestions', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
827
817
|
try {
|
|
828
|
-
const inst =
|
|
829
|
-
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
818
|
+
const inst = req.project;
|
|
830
819
|
|
|
831
820
|
const detail = provisioning.projectDetail(inst) || {};
|
|
832
821
|
const { invites } = recommendationsForDetails({ type: inst.type, ...detail });
|
|
@@ -862,18 +851,15 @@ module.exports = function provisioningRoutes() {
|
|
|
862
851
|
// ── Per-project settings (task 1003040, goal 1000058) ─────────────────────
|
|
863
852
|
// The foundation the manage-page wave hangs off: effective settings are the
|
|
864
853
|
// DEFAULTS (today's behavior — public surfaces, apply-to-join) overlaid with
|
|
865
|
-
// the instance's stored keys. Access:
|
|
854
|
+
// the instance's stored keys. Access: requireProjectOwner (own-scoped,
|
|
866
855
|
// Archon override, 404 otherwise — no existence leak).
|
|
867
856
|
|
|
868
857
|
// GET /provisioning/instances/:id/settings — read a project's effective
|
|
869
858
|
// settings + the allowed values (so a form can render its choices).
|
|
870
859
|
// rank: any authenticated builder (own resource; archon may read any).
|
|
871
|
-
router.get('/provisioning/instances/:id/settings', auth.requireBuilder, async (req, res) => {
|
|
872
|
-
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
873
|
-
if (!id) return;
|
|
860
|
+
router.get('/provisioning/instances/:id/settings', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
874
861
|
try {
|
|
875
|
-
const inst =
|
|
876
|
-
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
862
|
+
const inst = req.project;
|
|
877
863
|
// `planet` rides beside `settings` (task 1003347): the same row, read by a
|
|
878
864
|
// different rule — no defaults, null = the platform's own pick.
|
|
879
865
|
res.json({ ok: true, settings: provisioning.effectiveSettings(inst), planet: provisioning.planetChoice(inst),
|
|
@@ -902,7 +888,7 @@ module.exports = function provisioningRoutes() {
|
|
|
902
888
|
// did not get carried (that 409, or an enqueue that threw) marks the push OWED,
|
|
903
889
|
// which is what makes the "send it again" both of those name actually re-queue
|
|
904
890
|
// instead of hitting the no-op "No change" branch (task 1003524).
|
|
905
|
-
router.patch('/provisioning/instances/:id/settings', auth.requireBuilder, async (req, res) => {
|
|
891
|
+
router.patch('/provisioning/instances/:id/settings', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
906
892
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
907
893
|
if (!id) return;
|
|
908
894
|
// The schema IS the vocabularies, not a copy of them (task 1003352): strict
|
|
@@ -927,11 +913,10 @@ module.exports = function provisioningRoutes() {
|
|
|
927
913
|
message: `The body carries no settings to change — send any of: ${settingKeys.join(', ')}.` });
|
|
928
914
|
}
|
|
929
915
|
try {
|
|
930
|
-
const inst =
|
|
931
|
-
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
916
|
+
const inst = req.project;
|
|
932
917
|
// The archon override carries WHICH archon (the force-teardown convention) on
|
|
933
918
|
// both the event and the intent it may enqueue below.
|
|
934
|
-
const actor =
|
|
919
|
+
const actor = projectActor(req);
|
|
935
920
|
// A save that changes nothing writes nothing and restarts nothing — a
|
|
936
921
|
// whole-form save or a retried request must not bounce a live project. The
|
|
937
922
|
// one exception is the owner re-kick (the domain route's F19 shape): the same
|
|
@@ -983,32 +968,32 @@ module.exports = function provisioningRoutes() {
|
|
|
983
968
|
});
|
|
984
969
|
|
|
985
970
|
// POST /provisioning/instances/:id/teardown — request teardown of the caller's
|
|
986
|
-
// OWN instance. rank: any authenticated builder (own resource
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
|
|
971
|
+
// OWN instance. rank: any authenticated builder (own resource; archon may tear down
|
|
972
|
+
// any — the override is audited via the actor). Enqueues a 'teardown' intent; the
|
|
973
|
+
// runner executes it (web tier holds no tokens). 404 when the id isn't the caller's;
|
|
974
|
+
// 409 when the instance isn't in a teardownable state.
|
|
975
|
+
router.post('/provisioning/instances/:id/teardown', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
990
976
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
991
977
|
if (!id) return;
|
|
992
978
|
try {
|
|
993
|
-
const inst =
|
|
994
|
-
|
|
995
|
-
return res.fail('instance_not_found', 404);
|
|
996
|
-
}
|
|
979
|
+
const inst = req.project;
|
|
980
|
+
const actor = projectActor(req);
|
|
997
981
|
if (refusedAsTornDown(inst, res)) return;
|
|
998
982
|
// enqueueTeardown supersedes a stale pending provision intent and reports each
|
|
999
983
|
// outcome distinctly, so "already queued" is only ever said when it is TRUE
|
|
1000
984
|
// (task 1002691 — the old blind enqueue lied here and the instance leaked).
|
|
1001
|
-
const r = await provisioning.enqueueTeardown(pool, id,
|
|
985
|
+
const r = await provisioning.enqueueTeardown(pool, id, actor);
|
|
1002
986
|
if (r.conflict) {
|
|
1003
987
|
return res.fail('intent_conflict', { status: 409,
|
|
1004
988
|
message: `A '${r.intent.action}' run is currently ${r.intent.state} for this instance — the teardown could not be queued; retry in a moment.`,
|
|
1005
989
|
details: { action: r.intent.action, state: r.intent.state } });
|
|
1006
990
|
}
|
|
1007
991
|
if (r.created) {
|
|
992
|
+
const who = isProjectOwner(inst, req.builder) ? 'owner' : `archon ${req.builder.github_login || req.builder.id}`;
|
|
1008
993
|
await provisioning.recordEvent(pool, {
|
|
1009
|
-
instanceId: id, ownerBuilderId:
|
|
1010
|
-
detail: r.superseded ? '
|
|
1011
|
-
actor
|
|
994
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'teardown',
|
|
995
|
+
detail: `teardown requested by ${who}${r.superseded ? ' (superseded a stale provision intent)' : ''}`,
|
|
996
|
+
actor,
|
|
1012
997
|
}).catch(() => {});
|
|
1013
998
|
}
|
|
1014
999
|
res.json({ ok: true, queued: r.created, action: 'teardown', status: inst.status, superseded: r.superseded,
|
|
@@ -1034,14 +1019,13 @@ module.exports = function provisioningRoutes() {
|
|
|
1034
1019
|
// Every refusal names the action that IS available in that state rather than
|
|
1035
1020
|
// hiding behind a code, and no branch promises a duration — a restart's downtime
|
|
1036
1021
|
// is exactly what this route cannot know (the F16 rule, ADR 0190 §7).
|
|
1037
|
-
router.post('/provisioning/instances/:id/restart', auth.requireBuilder, async (req, res) => {
|
|
1022
|
+
router.post('/provisioning/instances/:id/restart', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
1038
1023
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
1039
1024
|
if (!id) return;
|
|
1040
1025
|
try {
|
|
1041
|
-
const inst =
|
|
1042
|
-
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
1026
|
+
const inst = req.project;
|
|
1043
1027
|
// The archon override carries WHICH archon (the force-teardown convention).
|
|
1044
|
-
const actor =
|
|
1028
|
+
const actor = projectActor(req);
|
|
1045
1029
|
if (!provisioning.RESTART_FROM.has(inst.status)) {
|
|
1046
1030
|
const offline = inst.status === 'torn_down' || inst.status === 'tearing_down';
|
|
1047
1031
|
return res.fail('not_restartable', { status: 409, details: { status: inst.status }, message: offline
|
|
@@ -1086,8 +1070,9 @@ module.exports = function provisioningRoutes() {
|
|
|
1086
1070
|
// (re)provision run wires the domain itself. A bring-your-own address may be
|
|
1087
1071
|
// RE-POINTED (task 1003237, ADR 0200): the same guards the first attach applies
|
|
1088
1072
|
// decide it, and the address it moved off is queued for release.
|
|
1089
|
-
// Body: { domain (required) }.
|
|
1090
|
-
|
|
1073
|
+
// Body: { domain (required) }. An archon may set any project's address; the
|
|
1074
|
+
// override is audited via the actor.
|
|
1075
|
+
router.post('/provisioning/instances/:id/domain', auth.requireBuilder, domainWriteRateLimit, requireProjectOwner(loadInstance), async (req, res) => {
|
|
1091
1076
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
1092
1077
|
if (!id) return;
|
|
1093
1078
|
if (validateOrRespond(req, res, { domain: { required: true, type: 'string', maxLength: 253 } })) return;
|
|
@@ -1104,10 +1089,8 @@ module.exports = function provisioningRoutes() {
|
|
|
1104
1089
|
message: `${domain} is a platform-reserved address — bring a domain of your own instead.`,
|
|
1105
1090
|
});
|
|
1106
1091
|
}
|
|
1107
|
-
const inst =
|
|
1108
|
-
|
|
1109
|
-
return res.fail('instance_not_found', 404);
|
|
1110
|
-
}
|
|
1092
|
+
const inst = req.project;
|
|
1093
|
+
const actor = projectActor(req);
|
|
1111
1094
|
// Same rule as create: inside the platform apex the only address an instance
|
|
1112
1095
|
// can hold is its own <slug>.<apex> — anything else is a squat (task 1002704).
|
|
1113
1096
|
const apexZone = provisioning.normalizeHostname(instanceApex());
|
|
@@ -1129,11 +1112,11 @@ module.exports = function provisioningRoutes() {
|
|
|
1129
1112
|
// idempotent no-op below.
|
|
1130
1113
|
const needsRewire = provisioning.ownsErrorNote(inst, 'domain-attach') || inst.last_verified_up === false;
|
|
1131
1114
|
if (inst.status === 'active' && needsRewire && !(await provisioning.getOpenIntent(pool, id))) {
|
|
1132
|
-
const { created } = await provisioning.enqueueIntent(pool, id, 'domain-attach',
|
|
1115
|
+
const { created } = await provisioning.enqueueIntent(pool, id, 'domain-attach', actor);
|
|
1133
1116
|
if (created) {
|
|
1134
1117
|
await provisioning.recordEvent(pool, {
|
|
1135
|
-
instanceId: id, ownerBuilderId:
|
|
1136
|
-
detail: `domain ${domain} re-attach queued (owner re-kick)`, actor
|
|
1118
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'domain',
|
|
1119
|
+
detail: `domain ${domain} re-attach queued (${isProjectOwner(inst, req.builder) ? 'owner' : 'archon'} re-kick)`, actor,
|
|
1137
1120
|
}).catch(() => {});
|
|
1138
1121
|
}
|
|
1139
1122
|
// created:false = a racing enqueue got there first — still truthful.
|
|
@@ -1176,7 +1159,7 @@ module.exports = function provisioningRoutes() {
|
|
|
1176
1159
|
await catalogBridge.publishInstanceToCatalog(pool, rewired, { retireOrigin: current }).catch(() => {});
|
|
1177
1160
|
let queued = false;
|
|
1178
1161
|
if (inst.status === 'active') {
|
|
1179
|
-
const { intent, created } = await provisioning.enqueueIntent(pool, id, 'domain-attach',
|
|
1162
|
+
const { intent, created } = await provisioning.enqueueIntent(pool, id, 'domain-attach', actor);
|
|
1180
1163
|
if (!created && intent.action !== 'domain-attach') {
|
|
1181
1164
|
// The open slot is held by other work (e.g. a teardown). The address IS
|
|
1182
1165
|
// recorded — say exactly what happened and what to do.
|
|
@@ -1188,8 +1171,8 @@ module.exports = function provisioningRoutes() {
|
|
|
1188
1171
|
if (!created) {
|
|
1189
1172
|
// an identical attach is already open — idempotent
|
|
1190
1173
|
await provisioning.recordEvent(pool, {
|
|
1191
|
-
instanceId: id, ownerBuilderId:
|
|
1192
|
-
detail: `domain ${domain} recorded (attach already queued)`, actor
|
|
1174
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'domain',
|
|
1175
|
+
detail: `domain ${domain} recorded (attach already queued)`, actor,
|
|
1193
1176
|
}).catch(() => {});
|
|
1194
1177
|
return res.json({ ok: true, changed: true, queued: false, domain, replaced: releasing,
|
|
1195
1178
|
message: 'That address is already being wired for this instance.' });
|
|
@@ -1197,9 +1180,9 @@ module.exports = function provisioningRoutes() {
|
|
|
1197
1180
|
}
|
|
1198
1181
|
const moved = releasing ? ` (moved off ${releasing}, queued for release)` : '';
|
|
1199
1182
|
await provisioning.recordEvent(pool, {
|
|
1200
|
-
instanceId: id, ownerBuilderId:
|
|
1183
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'domain',
|
|
1201
1184
|
detail: (queued ? `domain ${domain} recorded + attach queued` : `domain ${domain} recorded (wired at provision)`) + moved,
|
|
1202
|
-
actor
|
|
1185
|
+
actor,
|
|
1203
1186
|
}).catch(() => {});
|
|
1204
1187
|
// State-only sentences (task 1002720 / F16): the web tier holds no cloud
|
|
1205
1188
|
// tokens and cannot know whether a DNS hook exists on the runner, so "it
|
|
@@ -1225,7 +1208,7 @@ module.exports = function provisioningRoutes() {
|
|
|
1225
1208
|
|
|
1226
1209
|
// POST /provisioning/instances/:id/github-app — mint a GitHub-App-Manifest onboarding
|
|
1227
1210
|
// link for the caller's OWN instance (task 2080, ADR 0133). rank: any authenticated
|
|
1228
|
-
// builder (own resource). Returns the pre-filled manifest + the GitHub form-POST target
|
|
1211
|
+
// builder (own resource; archon may mint for any — audited via the actor). Returns the pre-filled manifest + the GitHub form-POST target
|
|
1229
1212
|
// (personal + org variants) + a one-time `state`; the browser POSTs the manifest to
|
|
1230
1213
|
// GitHub, the owner approves, and GitHub redirects back to the public callback below.
|
|
1231
1214
|
// The web tier only RECORDS the attempt (state token) — the control-plane runner does
|
|
@@ -1237,7 +1220,7 @@ module.exports = function provisioningRoutes() {
|
|
|
1237
1220
|
// whether the founder may make an app there is GitHub's to enforce on its own page
|
|
1238
1221
|
// (this module holds no GitHub token to ask with, ADR 0111 §2). Anything other than
|
|
1239
1222
|
// account 'org' is the founder's personal account, exactly as before.
|
|
1240
|
-
router.post('/provisioning/instances/:id/github-app', auth.requireBuilder, async (req, res) => {
|
|
1223
|
+
router.post('/provisioning/instances/:id/github-app', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
1241
1224
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
1242
1225
|
if (!id) return;
|
|
1243
1226
|
if (validateOrRespond(req, res, { account: { type: 'string', maxLength: 12 }, org: { type: 'string', maxLength: 39 } })) return;
|
|
@@ -1247,10 +1230,8 @@ module.exports = function provisioningRoutes() {
|
|
|
1247
1230
|
return res.fail('bad_org', { status: 400, message: 'That isn’t a GitHub organisation name: letters, numbers and single dashes, up to 39.' });
|
|
1248
1231
|
}
|
|
1249
1232
|
try {
|
|
1250
|
-
const inst =
|
|
1251
|
-
|
|
1252
|
-
return res.fail('instance_not_found', 404);
|
|
1253
|
-
}
|
|
1233
|
+
const inst = req.project;
|
|
1234
|
+
const actor = projectActor(req);
|
|
1254
1235
|
// A domain is required: it is the App's homepage + the sign-in callback host. No
|
|
1255
1236
|
// domain ⇒ there is nothing to point "Login with GitHub" at yet.
|
|
1256
1237
|
if (!inst.domain) {
|
|
@@ -1274,17 +1255,19 @@ module.exports = function provisioningRoutes() {
|
|
|
1274
1255
|
if (guard && guard.refuse) return res.fail(guard.refuse, { status: 409, message: guard.message });
|
|
1275
1256
|
const state = guard ? guard.reuse : crypto.randomBytes(32).toString('hex');
|
|
1276
1257
|
const expiresAt = guard ? guard.expiresAt : new Date(Date.now() + MANIFEST_STATE_TTL_MS).toISOString();
|
|
1277
|
-
|
|
1258
|
+
// requested_by is read downstream as the project's owner (every later manifest event's
|
|
1259
|
+
// ownerBuilderId); an archon's mint is named by the actor on the event below instead
|
|
1260
|
+
if (!guard) await provisioning.createOAuthManifest(pool, { instanceId: id, state, requestedBy: inst.owner_builder_id, expiresAt });
|
|
1278
1261
|
await provisioning.recordEvent(pool, {
|
|
1279
|
-
instanceId: id, ownerBuilderId:
|
|
1280
|
-
detail: guard ? 'GitHub App Manifest link reopened (same attempt)' : 'GitHub App Manifest link minted', actor
|
|
1262
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'oauth-manifest-start',
|
|
1263
|
+
detail: guard ? 'GitHub App Manifest link reopened (same attempt)' : 'GitHub App Manifest link minted', actor,
|
|
1281
1264
|
}).catch(() => {});
|
|
1282
1265
|
// which account owns this attempt's app, so the page can link its settings there
|
|
1283
1266
|
// from any tab or device (github-setup.js reads it back as app_org)
|
|
1284
1267
|
if (account === 'org') {
|
|
1285
1268
|
await provisioning.recordEvent(pool, {
|
|
1286
|
-
instanceId: id, ownerBuilderId:
|
|
1287
|
-
detail: orgLogin, actor
|
|
1269
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: githubSetup.SETUP_EVENTS.appOrg,
|
|
1270
|
+
detail: orgLogin, actor,
|
|
1288
1271
|
}).catch(() => {});
|
|
1289
1272
|
}
|
|
1290
1273
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.127",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.127",
|
|
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.21.
|
|
3
|
+
"version": "1.21.127",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9728,6 +9728,14 @@
|
|
|
9728
9728
|
}
|
|
9729
9729
|
],
|
|
9730
9730
|
"1.21.125": [
|
|
9731
|
+
{
|
|
9732
|
+
"id": "1004460",
|
|
9733
|
+
"text": "Store modules' own tests can now run on GitHub's free test machines, away from the server holding our secrets. It switches on once the owner does three short GitHub setup steps."
|
|
9734
|
+
},
|
|
9735
|
+
{
|
|
9736
|
+
"id": "1004159",
|
|
9737
|
+
"text": "Project owners' controls now share one access check, and Archons can help with teardown, address and GitHub setup on any project — every such action is logged under the Archon's name."
|
|
9738
|
+
},
|
|
9731
9739
|
{
|
|
9732
9740
|
"id": "1004632",
|
|
9733
9741
|
"text": "When a project updates its Bongos core, the next task's ship now uses the new core automatically instead of the old one, so it no longer fails for no visible reason."
|
|
@@ -9736,5 +9744,21 @@
|
|
|
9736
9744
|
"id": "1004627",
|
|
9737
9745
|
"text": "Bongos now has a 27-step plain-language how-to guide that an AI can walk a new person through inside their session, from where to start to their first ship."
|
|
9738
9746
|
}
|
|
9747
|
+
],
|
|
9748
|
+
"1.21.126": [
|
|
9749
|
+
{
|
|
9750
|
+
"id": "1004738",
|
|
9751
|
+
"text": "A goal's page is much shorter now: task dependencies sit in a small card at the top where Conflicts used to be, and the full dependency picture and conflict list are one click away."
|
|
9752
|
+
}
|
|
9753
|
+
],
|
|
9754
|
+
"1.21.127": [
|
|
9755
|
+
{
|
|
9756
|
+
"id": "1004637",
|
|
9757
|
+
"text": "Projects now count each store module's errors per day and send the count to the store, so a module's reliability score comes from real use. It sends with a builder's sign-in on that machine and respects the reporting switch in"
|
|
9758
|
+
},
|
|
9759
|
+
{
|
|
9760
|
+
"id": "1004414",
|
|
9761
|
+
"text": "Each hosted project now has a memory and CPU limit, so one runaway project can no longer slow down every other project or the hub."
|
|
9762
|
+
}
|
|
9739
9763
|
]
|
|
9740
9764
|
}
|
|
@@ -103,7 +103,7 @@ const EXEMPTIONS = [
|
|
|
103
103
|
+ 'absorbed into this check.',
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
|
-
at: 'src/module-loader/loader.js:
|
|
106
|
+
at: 'src/module-loader/loader.js:350',
|
|
107
107
|
why: 'The poller twin of the route require at :196 — same DEFAULT_ROOTS bound, same plugin-by-'
|
|
108
108
|
+ 'design rationale, same ADR 0107 / task 1003400 follow-on. Listed on its own line so a '
|
|
109
109
|
+ 'change to either call re-opens only that one.',
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -524,6 +524,7 @@ const KERNEL_FILES = [
|
|
|
524
524
|
'src/module-loader/loader.js', // the disk loader (discover → validate → mount)
|
|
525
525
|
'src/module-loader/manifest-schema.js', // the manifest validator
|
|
526
526
|
'src/module-loader/semver.js', // coreVersion compatibility
|
|
527
|
+
'src/module-loader/error-tally.js', // per-module runtime-error counts the loader feeds (task 1004637)
|
|
527
528
|
'src/bongos/module-scope-map.js', // the module→path registry (it NAMES every module — exempt below)
|
|
528
529
|
// trust primitives (ADR 0091 §1) — the authz boundary + data substrate
|
|
529
530
|
'src/bongos/auth.js', // authz gates (requireBuilder/requireRank, ADR 0016)
|
|
@@ -33,13 +33,16 @@
|
|
|
33
33
|
// (owner, 2026-09-30): never on the control plane, where the secrets live. The
|
|
34
34
|
// store path (recordTestsSignal) refuses unless the process declares it is that
|
|
35
35
|
// environment with MODULE_TEST_SANDBOX=1 — set only on the isolated test box, no
|
|
36
|
-
// secrets on disk,
|
|
36
|
+
// secrets on disk: GitHub's free runners, driven by module-test-runner.js (task 1004460). `--dir` stays open: it runs a module
|
|
37
37
|
// from your own checkout, i.e. code you already chose to run.
|
|
38
38
|
//
|
|
39
39
|
// node scripts/gds/module-assess-tests.js <store_module_versions.id> run + record
|
|
40
40
|
// node scripts/gds/module-assess-tests.js --dir modules/<key> dry run on a
|
|
41
41
|
// local module
|
|
42
42
|
// (prints; no DB)
|
|
43
|
+
// node scripts/gds/module-assess-tests.js --tarball <f> --key <k> --version-id N --nonce X --json
|
|
44
|
+
// the testing environment's mode: no DB, prints the
|
|
45
|
+
// signal JSON (module-test-runner.js; task 1004460)
|
|
43
46
|
|
|
44
47
|
const fs = require('node:fs');
|
|
45
48
|
const path = require('node:path');
|
|
@@ -177,6 +180,18 @@ async function insertSignal(versionId, s, { db }) {
|
|
|
177
180
|
return row;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
const notScored = (e) => ({ outcome: 'not_scored', score: null, sample_size: null, reason: `the tests could not run (${e.code || 'error'})`, detail: {} });
|
|
184
|
+
|
|
185
|
+
// Verify a tarball and run its tests: the signal fields, with a tarball that fails
|
|
186
|
+
// verification as not_scored. No DB — this is what the separate testing
|
|
187
|
+
// environment runs (`--tarball`, module-test-runner.js); assessTests keeps the gate.
|
|
188
|
+
async function assessTarball(tgz, key, opts = {}) {
|
|
189
|
+
const { verifyModuleArtifact } = require('./module-artifact');
|
|
190
|
+
const v = await verifyModuleArtifact(tgz, { key, includeFiles: true });
|
|
191
|
+
if (!v.ok) return { outcome: 'not_scored', score: null, sample_size: null, reason: `the tarball failed verification (${v.code}): ${v.message}`, detail: {} };
|
|
192
|
+
return assessTests(key, v.files, opts);
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
// Read, verify and test one published version, then record the result. A tarball
|
|
181
196
|
// that is missing or fails verification is recorded as not_scored — the part could
|
|
182
197
|
// not be measured, which is not the same as the module failing its tests.
|
|
@@ -185,21 +200,16 @@ async function recordTestsSignal(versionId, { db, storeDir, root, timeoutMs, exe
|
|
|
185
200
|
if (refused) return { ok: false, ...refused };
|
|
186
201
|
const pool = db || require('../../src/bongos/pool').pool;
|
|
187
202
|
const { versionArtifactFile } = require('../../src/bongos/module-store');
|
|
188
|
-
const { verifyModuleArtifact } = require('./module-artifact');
|
|
189
203
|
const { rows: [ver] } = await pool.query(
|
|
190
204
|
'SELECT id, module_key, version, artifact_path FROM store_module_versions WHERE id = $1', [versionId]);
|
|
191
205
|
if (!ver) return { ok: false, code: 'version_not_found', message: `no store_module_versions row ${versionId}` };
|
|
192
206
|
|
|
193
207
|
let signal;
|
|
194
208
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const v = await verifyModuleArtifact(tgz, { key: ver.module_key, includeFiles: true });
|
|
198
|
-
signal = v.ok
|
|
199
|
-
? await assessTests(ver.module_key, v.files, { root, timeoutMs, exec, env })
|
|
200
|
-
: { outcome: 'not_scored', score: null, sample_size: null, reason: `the tarball failed verification (${v.code}): ${v.message}`, detail: {} };
|
|
209
|
+
const tgz = await fs.promises.readFile(versionArtifactFile(ver, storeDir ? { dir: storeDir } : {}));
|
|
210
|
+
signal = await assessTarball(tgz, ver.module_key, { root, timeoutMs, exec, env });
|
|
201
211
|
} catch (e) {
|
|
202
|
-
signal =
|
|
212
|
+
signal = notScored(e);
|
|
203
213
|
}
|
|
204
214
|
const row = await insertSignal(ver.id, signal, { db: pool });
|
|
205
215
|
return { ok: true, version: ver, signal: row };
|
|
@@ -220,7 +230,26 @@ function readModuleDir(dir) {
|
|
|
220
230
|
return out;
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
// The value after a flag, or null.
|
|
234
|
+
const flag = (argv, name) => { const i = argv.indexOf(name); return i === -1 ? null : (argv[i + 1] || ''); };
|
|
235
|
+
|
|
223
236
|
async function main(argv) {
|
|
237
|
+
// DB-free, for the separate testing environment: verify + run + print the signal
|
|
238
|
+
// JSON (with the run's version id and nonce, which the control plane checks when
|
|
239
|
+
// it pulls the result). The gate still applies: without MODULE_TEST_SANDBOX=1 and
|
|
240
|
+
// a secret-free environment this refuses before anything is read.
|
|
241
|
+
const tarball = flag(argv, '--tarball');
|
|
242
|
+
if (tarball !== null) {
|
|
243
|
+
const key = flag(argv, '--key');
|
|
244
|
+
if (!tarball || !key) { console.error('usage: --tarball <file> --key <module key> [--version-id N --nonce X] --json'); return 2; }
|
|
245
|
+
const refused = runRefusal();
|
|
246
|
+
if (refused) { console.error(refused.message); return 1; }
|
|
247
|
+
let s;
|
|
248
|
+
try { s = await assessTarball(fs.readFileSync(tarball), key); }
|
|
249
|
+
catch (e) { s = notScored(e); }
|
|
250
|
+
console.log(JSON.stringify({ version_id: flag(argv, '--version-id'), nonce: flag(argv, '--nonce'), ...s }));
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
224
253
|
const i = argv.indexOf('--dir');
|
|
225
254
|
if (i !== -1) {
|
|
226
255
|
const dir = path.resolve(argv[i + 1] || '');
|
|
@@ -231,7 +260,7 @@ async function main(argv) {
|
|
|
231
260
|
}
|
|
232
261
|
const id = argv[0];
|
|
233
262
|
if (!/^\d+$/.test(String(id || ''))) {
|
|
234
|
-
console.error('usage: node scripts/gds/module-assess-tests.js <store_module_versions.id> | --dir modules/<key>');
|
|
263
|
+
console.error('usage: node scripts/gds/module-assess-tests.js <store_module_versions.id> | --dir modules/<key> | --tarball <file> --key <k> --json');
|
|
235
264
|
return 2;
|
|
236
265
|
}
|
|
237
266
|
const res = await recordTestsSignal(id);
|
|
@@ -246,4 +275,4 @@ if (require.main === module) {
|
|
|
246
275
|
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => { console.error(e.stack || e.message); process.exit(1); });
|
|
247
276
|
}
|
|
248
277
|
|
|
249
|
-
module.exports = { discoverTestFiles, assessTests, recordTestsSignal, insertSignal, runRefusal, testEnv, SANDBOX_PREFIX };
|
|
278
|
+
module.exports = { discoverTestFiles, assessTests, assessTarball, recordTestsSignal, insertSignal, runRefusal, testEnv, SANDBOX_PREFIX };
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// this after it answers the author. Tests is recorded `pending`, and only when the
|
|
8
8
|
// version has no Tests result yet: a published module's tests run only in the
|
|
9
9
|
// separate test environment, never on the control plane (module-assess-tests.js).
|
|
10
|
+
// When the runner is configured (module-test-runner.js) the tests are dispatched to
|
|
11
|
+
// it here, and its result is pulled back into the row later.
|
|
10
12
|
// The Security gate reads the tarball and asks npm about the declared dependencies
|
|
11
13
|
// — it never runs the module's code, so it is safe here. Docs is one Sonnet call over
|
|
12
14
|
// the HOWTO.md text (module-assess-docs.js) — also no module code. Then the score is
|
|
@@ -27,12 +29,18 @@ const { recomposeScore } = require('./module-assess-score');
|
|
|
27
29
|
// the score. `recordSecurity` / `recordDocs` are injectable so tests need no npm
|
|
28
30
|
// and no model. Never throws for a failed measurement — that is recorded as the signal's outcome — and resolves
|
|
29
31
|
// { ok: false, code } only when the version does not exist.
|
|
30
|
-
async function assessVersion(versionId, { db, recordSecurity, recordDocs } = {}) {
|
|
32
|
+
async function assessVersion(versionId, { db, recordSecurity, recordDocs, dispatchTests } = {}) {
|
|
31
33
|
const pool = db || require('../../src/bongos/pool').pool;
|
|
32
34
|
const { rows: [ver] } = await pool.query(
|
|
33
|
-
'SELECT id, module_key, version FROM store_module_versions WHERE id = $1', [versionId]);
|
|
35
|
+
'SELECT id, module_key, version, artifact_path FROM store_module_versions WHERE id = $1', [versionId]);
|
|
34
36
|
if (!ver) return { ok: false, code: 'version_not_found', message: `no store_module_versions row ${versionId}` };
|
|
35
37
|
|
|
38
|
+
// When the separate test environment is configured, send the run there first
|
|
39
|
+
// (module-test-runner.js; task 1004460): it records a pending row carrying the
|
|
40
|
+
// run's nonce, so the plain pending below then has nothing to add. Unconfigured
|
|
41
|
+
// (or a failed dispatch), nothing is sent and Tests stays plain pending.
|
|
42
|
+
await (dispatchTests || require('./module-test-runner').maybeDispatch)(ver, { db: pool });
|
|
43
|
+
|
|
36
44
|
// Only a version with no Tests result yet: a re-assess must never hide a real
|
|
37
45
|
// score behind a newer "pending".
|
|
38
46
|
await pool.query(
|