@bongos/core 1.19.1072 → 1.19.1074

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +2 -1
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +84 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
@@ -74,15 +74,6 @@ function boundShipDaysToYear(rows, todayStr) {
74
74
  return o != null && o >= cutoff && o <= todayOrd;
75
75
  });
76
76
  }
77
- // The dev-box "has this builder ever connected a box?" signal, read through the
78
- // kernel seam instead of a direct `require('../boxes')` (a forbidden core→module
79
- // edge once dev-box moved to modules/dev-box/, ADR 0083 / BV1.R43). The dev-box
80
- // module provides the `box.hasEverConnected` port when enabled; resolveOptional
81
- // degrades to false on a box-less instance, so onboarding's box_connect step
82
- // simply never auto-clears there. Resolved per call (the provider registers at
83
- // boot, before any request).
84
- const boxHasEverConnected = (builderId) =>
85
- seams.resolveOptional('box.hasEverConnected', async () => false)(builderId);
86
77
  // The art-pipeline "is the shared newcomer key configured?" signal, read through
87
78
  // the kernel seam (ADR 0083 / BV1.R48). The art-pipeline module provides
88
79
  // 'art.sharedKeyConfigured' when enabled; resolveOptional degrades to false on a
@@ -192,7 +183,7 @@ module.exports = function buildMeRouter() {
192
183
  const specialities = seams.resolveOptional('specialities');
193
184
  const [
194
185
  builder, activeClaims, estVsActual, unlocked, lockedWithProgress, streakInfo,
195
- recentGrades, builderAvg, onboardingRow, shipCount, recentKarma, karma30d, discordLink, boxConnected, cliAuthed,
186
+ recentGrades, builderAvg, onboardingRow, shipCount, recentKarma, karma30d, discordLink, cliAuthed,
196
187
  behaviorPrefs, geminiKeyRow, needsRebaseTasks, foundingBuilder, helpRequestCount,
197
188
  taskRecommendationCount, boardVotesPending, socialMode, mingleIntroductionCount,
198
189
  navPermissions,
@@ -227,14 +218,8 @@ module.exports = function buildMeRouter() {
227
218
  // follow-up call. Boolean only — the discord_user_id itself is never sent
228
219
  // to the client (it's PII the UI has no need for).
229
220
  discordIsLinked(req.builder.id),
230
- // #823: whether the builder's dev box has ever reported a live Claude
231
- // process (first_connected_at). Auto-clears the onboarding 'box_connect'
232
- // step — the monotonic "properly connected to the dev box" signal.
233
- boxHasEverConnected(req.builder.id),
234
- // #777: whether the builder has ever authenticated the GDS CLI (ran
235
- // /builder-setup, or the box-connect bootstrap). Auto-clears the
236
- // onboarding 'cli_setup' step — distinct from box_connect (#823: both
237
- // steps have a place — connecting vs. signing the GDS in).
221
+ // #777: whether the builder has ever authenticated the CLI (ran
222
+ // /builder-setup). Auto-clears the onboarding 'cli_setup' step.
238
223
  db.hasCliSession(req.builder.id),
239
224
  // task 1268: the builder's behavior_prefs blob (the 'wandering' + 'render'
240
225
  // knobs), resolved against builder.rank below so /me carries the effective
@@ -431,7 +416,7 @@ module.exports = function buildMeRouter() {
431
416
  // returns a stage-1 "nothing cleared" view so the client renders the
432
417
  // checklist either way. Through the `onboarding` port (BV1.R81); null when
433
418
  // the module is off.
434
- onboarding: onboarding ? onboarding.shapeForApi(onboardingRow, { shipCount, discordLinked: !!discordLink, boxConnected: !!boxConnected, cliAuthed: !!cliAuthed }) : null,
419
+ onboarding: onboarding ? onboarding.shapeForApi(onboardingRow, { shipCount, discordLinked: !!discordLink, cliAuthed: !!cliAuthed }) : null,
435
420
  // F7 (ADR 0033): Discord link state for the hall's Connect/Unlink UI.
436
421
  // `configured` tells the client whether the server even has Discord OAuth
437
422
  // creds — when false the hall renders nothing (the start route 503s).
@@ -570,14 +555,13 @@ module.exports = function buildMeRouter() {
570
555
  }
571
556
  // Echo the onboarding state back so the hall's path checklist can tick
572
557
  // this step off without a follow-up /me round-trip.
573
- const [state, shipCount, discordLink, boxConnected, cliAuthed] = await Promise.all([
558
+ const [state, shipCount, discordLink, cliAuthed] = await Promise.all([
574
559
  onboardingPort ? onboardingPort.getState(pool, req.builder.id) : null,
575
560
  lc().countShippedTasksForBuilder(req.builder.id),
576
561
  discordIsLinked(req.builder.id),
577
- boxHasEverConnected(req.builder.id),
578
562
  db.hasCliSession(req.builder.id),
579
563
  ]);
580
- const onboarding = onboardingPort ? onboardingPort.shapeForApi(state, { shipCount, discordLinked: !!discordLink, boxConnected: !!boxConnected, cliAuthed: !!cliAuthed }) : null;
564
+ const onboarding = onboardingPort ? onboardingPort.shapeForApi(state, { shipCount, discordLinked: !!discordLink, cliAuthed: !!cliAuthed }) : null;
581
565
  res.json({ builder, onboarding });
582
566
  } catch (err) {
583
567
  if (err.code === 'invalid_discipline') {
@@ -17,6 +17,12 @@
17
17
  // authoritative check — `bongos module submit`
18
18
  // is a thin CLI client of this same route).
19
19
  // GET /modules/submissions list queued submissions (task 1771 reads this).
20
+ // POST /store/modules/:key/versions
21
+ // publish one module version to the Bongos-hosted
22
+ // module store (task 1004271, ADR 0338 D1) — the
23
+ // server end of `bongos module publish`. Not the
24
+ // upstreaming door above: submit proposes a module
25
+ // INTO core; publish puts a version in the store.
20
26
  //
21
27
  // Read is open to any signed-in builder (same gate as the atlas/primer pages);
22
28
  // every write (enable/disable, submit) is metic+archon, same rank as
@@ -58,6 +64,9 @@ const modulesLib = require('../../modules');
58
64
  // server process.
59
65
  const moduleCli = require('../../../scripts/gds/module');
60
66
  const moduleSubmissions = require('../module-submissions');
67
+ const moduleArtifact = require('../../../scripts/gds/module-artifact');
68
+ const { stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion } = require('../module-store');
69
+ const { KEY_RE } = require('../../module-loader/manifest-schema');
61
70
  // Named imports, not a namespace object (task 1004052 strand walk): knip cannot follow
62
71
  // property access on a CommonJS namespace, so reaching these through one made them read
63
72
  // as unused exports and tripped the dead-code ratchet that held this branch out of main.
@@ -70,6 +79,21 @@ const { listOverrides, setOverride } = require('../module-overrides');
70
79
  // cap (SECURITY_DOC_CONTENT_MAX) in routes/security.js.
71
80
  const SUBMISSION_MAX_BYTES = 5_000_000;
72
81
 
82
+ // A published version's body is the tarball itself, never JSON (the global parser
83
+ // only reads application/json, so it passes this through). Capped at the parser and
84
+ // never inflated from a Content-Encoding, so the cap is the bytes on the wire.
85
+ const TARBALL_BODY_TYPES = ['application/gzip', 'application/x-gzip', 'application/octet-stream'];
86
+ const rawTarball = express.raw({ type: TARBALL_BODY_TYPES, limit: moduleArtifact.MAX_TARBALL_BYTES, inflate: false });
87
+ function tarballBody(req, res, next) {
88
+ rawTarball(req, res, (err) => {
89
+ if (!err) return next();
90
+ if (err.type === 'entity.too.large') {
91
+ return res.fail('artifact_too_large', { status: 413, message: `The tarball is over the ${moduleArtifact.MAX_TARBALL_BYTES}-byte store cap.` });
92
+ }
93
+ return res.fail('bad_tarball', { status: 400, message: 'The upload could not be read.' });
94
+ });
95
+ }
96
+
73
97
  function ownProjectName() {
74
98
  try { return require('../../branding').branding().identity.productName; } catch { return null; }
75
99
  }
@@ -264,6 +288,65 @@ module.exports = function buildModulesRouter() {
264
288
  res.json({ submissions });
265
289
  }));
266
290
 
291
+ // POST /api/bongos/store/modules/:key/versions — publish one module version to
292
+ // the store (task 1004271, ADR 0338 D1). Body: the gzip tarball
293
+ // `bongos module publish` builds (scripts/gds/module-artifact.js), sent as
294
+ // application/gzip. The route trusts nothing but the bytes: it recomputes every
295
+ // hash, re-runs the publish denylist and validates module.json itself
296
+ // (verifyModuleArtifact), then keeps the tarball on the control plane's disk and
297
+ // INSERTs the version row in one transaction (module-store.js). A key's first
298
+ // publish makes the caller its author; after that only the author may publish,
299
+ // a delisted key takes nothing new, and a version is published once and never
300
+ // changed.
301
+ //
302
+ // rank: metic+archon — it puts code in a shared store other instances install
303
+ // from. Gate: requirePermission('module.submit') — the atom that already guards
304
+ // filing a module into a shared queue; a dedicated `module.publish` atom is a
305
+ // follow-up once who-may-sell is decided.
306
+ router.post('/store/modules/:key/versions', auth.requireBuilder, auth.requirePermission('module.submit'), tarballBody,
307
+ asyncHandler('POST /store/modules/:key/versions', async (req, res) => {
308
+ const { key } = req.params;
309
+ if (!KEY_RE.test(key)) {
310
+ return res.fail('bad_module_key', { status: 400, message: 'A module key is lowercase kebab-case.' });
311
+ }
312
+ const tgz = Buffer.isBuffer(req.body) ? req.body : null;
313
+ // The audit log serializes req.body; a tarball there would be megabytes of
314
+ // byte array. Leave it a one-line marker instead.
315
+ req.body = { tarball_bytes: tgz ? tgz.length : 0 };
316
+ if (!tgz || !tgz.length) {
317
+ return res.fail('empty_artifact', { status: 400, message: 'Send the tarball as the body, Content-Type: application/gzip.' });
318
+ }
319
+
320
+ const v = await moduleArtifact.verifyModuleArtifact(tgz, { key });
321
+ if (!v.ok) {
322
+ return res.fail(v.code, { status: v.code === 'artifact_too_large' ? 413 : 422, message: v.message });
323
+ }
324
+
325
+ const staged = await stageArtifact(tgz, { key, version: v.version });
326
+ let result;
327
+ try {
328
+ result = await publishVersion({
329
+ key,
330
+ title: v.moduleJson.title,
331
+ description: v.moduleJson.description,
332
+ version: v.version,
333
+ coreVersion: v.coreVersion,
334
+ manifest: v.manifest,
335
+ treeSha256: v.treeSha256,
336
+ tarballSha256: v.tarballSha256,
337
+ bytes: v.bytes,
338
+ artifactPath: relativeArtifactPath(staged.final),
339
+ builderId: req.builder.id,
340
+ }, { beforeCommit: () => commitArtifact(staged) });
341
+ } finally {
342
+ await removeArtifact(staged.tmp);
343
+ }
344
+ if (!result.ok) return res.fail(result.code, { status: result.status, message: result.message });
345
+
346
+ console.log(`[gds] module "${key}" ${v.version} published to the store by builder ${req.builder.id}`);
347
+ res.status(201).json({ ok: true, created_module: result.created, version: result.version });
348
+ }));
349
+
267
350
  return router;
268
351
  };
269
352
 
@@ -96,6 +96,11 @@ function buildGdsRouter() {
96
96
  // encoded (4 MB decoded). Same dynamic-:id pattern as publish-branch above;
97
97
  // modules/lifecycle/routes/task-visuals.js mounts its own 8 MB parser.
98
98
  /^\/tasks\/\d+\/visual$/.test(req.path) ||
99
+ // task 1004358: the named-slot sibling (TW11 — the eight page-tweak renders).
100
+ // Same 8 MB parser in modules/lifecycle/routes/visuals.js; a full-page render
101
+ // is ~90-130 KB, so without this every attach 413'd here first. The route
102
+ // validates the slot name itself, so the pattern only needs to be one segment.
103
+ /^\/tasks\/\d+\/visuals\/[^/]+$/.test(req.path) ||
99
104
  // task 1003850: the grade body carries committed_files — EVERY changed path, tens of
100
105
  // KB on its own for a mass change — plus the panel's findings. A 1,141-file rename
101
106
  // (task 1003705) produced a body over the 64 KB cap, so the grade could not be
package/src/module-api.js CHANGED
@@ -71,10 +71,14 @@ 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.1072'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ // The body behind a retired doorway export that must stay callable until the next
75
+ // MAJOR (see allowBoxScope below): passes the request through untouched.
76
+ function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
+
78
+ const CORE_VERSION = '1.19.1074'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
79
 
76
80
  // A namespaced logger so a module's log lines are attributable + consistent.
77
- // Usage: const log = api.logger('dev-box'); log.info('mounted');
81
+ // Usage: const log = api.logger('discord'); log.info('mounted');
78
82
  // task 1003208 (goal 1000079): this USED TO BE `console.log(tag, ...a)`. Every
79
83
  // module that had "adopted the logger" was therefore still writing unstructured
80
84
  // text to stdout with no request correlation and no redaction — the criterion's
@@ -131,10 +135,13 @@ module.exports = {
131
135
  get requirePermission() { return require('./bongos/auth').requirePermission; },
132
136
  get rankMeetsThreshold() { return require('./bongos/auth').rankMeetsThreshold; },
133
137
  get requireBfgPrincipal() { return require('./bongos/auth').requireBfgPrincipal; },
134
- // allowBoxScope — opt a route in to box-scoped (dev-box bootstrap) session
135
- // tokens. A module's own bootstrap routes flag themselves; everything else
136
- // stays closed to box tokens (the ADR 0016 trust boundary).
137
- get allowBoxScope() { return require('./bongos/auth').allowBoxScope; },
138
+ // allowBoxScope — DEPRECATED no-op, kept so the doorway loses no export inside
139
+ // 1.x (removing one is a MAJOR, and every module declares ^1). It used to opt
140
+ // a route in to the narrowing box-scoped session; that scope and its tokens
141
+ // were retired (task 1003894), so an undeclared session source is now denied
142
+ // on every route whatever a route mounts. Passing it changes nothing. Drop it
143
+ // at the next MAJOR.
144
+ get allowBoxScope() { return deprecatedNoopMiddleware; },
138
145
  // resolveSession(req) — resolve the request to a live session row (or null)
139
146
  // WITHOUT the 401-or-pass behaviour of requireBuilder. A module route that
140
147
  // must make its own auth decision — e.g. a browser page that REDIRECTS to
@@ -125,6 +125,9 @@ function buildCatalog({
125
125
  provenance: deriveProvenance(manifest.origin, ownProjectName),
126
126
  enabled: enabled[key] === true,
127
127
  default: manifest.default === true,
128
+ // The module's own version (task 1003781) — null only for a manifest
129
+ // that predates the field (the validator now requires it).
130
+ version: manifest.version || null,
128
131
  coreVersion: manifest.coreVersion || null,
129
132
  contributes: manifest.contributes || {},
130
133
  // DECLARED — exactly what module.json says (null when it says nothing).
@@ -7,7 +7,7 @@
7
7
  // routes.js mounts discovered module routers behind the kernel's auth/audit.
8
8
  //
9
9
  // SAFE-BY-CONSTRUCTION during the strangler migration: with no modules/ dir
10
- // (the state until BV1.R43 moves dev-box), discovery returns EMPTY and every
10
+ // (the state before BV1.R43 moved the first module), discovery returns EMPTY and every
11
11
  // function below is a no-op — OTB behaves byte-identically. As built-ins move
12
12
  // out, they become discovered manifests and the closed registry shrinks.
13
13
  //
@@ -173,7 +173,7 @@ function verifyEnabledPorts(enabled) {
173
173
  }
174
174
 
175
175
  // Mount each discovered + ENABLED module's route factories onto `router`. A
176
- // module declares contributes.routes:["box"] → factory at modules/<key>/routes/box.js
176
+ // module declares contributes.routes:["copy-desk"] → factory at modules/<key>/routes/copy-desk.js
177
177
  // exporting a function returning an express Router. Auth model: audit is composed
178
178
  // at the kernel level in routes.js before this is called; each route self-gates at
179
179
  // its required rank via api.requireBuilder / api.requireRank (public read routes
@@ -309,8 +309,8 @@ function startModulePollers(deps = {}, { isEnabled } = {}, opts) {
309
309
  poller = require(pollerPath);
310
310
  } catch (e) {
311
311
  // No modules/<key>/pollers/<pk>.js → a DECLARATIVE-ONLY / non-web poller
312
- // (e.g. dev-box's box-lifecycle sweep runs on the control plane, not in
313
- // this process). Skip silently — only a web-process poller ships a file
312
+ // (e.g. a sweep that runs on the control plane, not in this process).
313
+ // Skip silently — only a web-process poller ships a file
314
314
  // here. A file that EXISTS but throws is a real error (the catch below).
315
315
  if (e && e.code === 'MODULE_NOT_FOUND' && String(e.message).includes(pollerPath)) continue;
316
316
  console.error(`[module-loader] poller ${key}/${pk} failed to load: ${e && e.message ? e.message : e}`);
@@ -9,9 +9,11 @@
9
9
  //
10
10
  // THE SCHEMA (module.json):
11
11
  // {
12
- // "key": "dev-box", // REQUIRED. ^[a-z][a-z0-9-]*$ ; must match the module's dir name.
12
+ // "key": "art-pipeline", // REQUIRED. ^[a-z][a-z0-9-]*$ ; must match the module's dir name.
13
13
  // "title": "Dev boxes", // REQUIRED. Human label.
14
14
  // "description": "…", // REQUIRED. One sentence.
15
+ // "version": "1.4.2", // REQUIRED. The module's OWN exact version "X.Y.Z" (task 1003781) —
16
+ // // what update, rollout channels and re-assessment name.
15
17
  // "coreVersion": "^1.0.0", // REQUIRED. semver range of src/module-api.js CORE_VERSION it supports.
16
18
  // "default": false, // OPTIONAL (default false). Enablement fail-safe when no config names it.
17
19
  // "author": "…", // OPTIONAL. Attribution (ADR 0107 §2) — portable, not a cross-GDS FK.
@@ -28,12 +30,12 @@
28
30
  // // nobody does. `maintained` needs a named `maintainer`;
29
31
  // // `deprecated`/`orphaned` need a `successor` or a `note`.
30
32
  // "contributes": { // REQUIRED object (may be empty). What the module mounts:
31
- // "routes": ["box"], // route-factory keys (mounted behind kernel-composed auth)
33
+ // "routes": ["stage"], // route-factory keys (mounted behind kernel-composed auth)
32
34
  // "rooms": ["WorldRoom"], // Colyseus room classes (realtime seam)
33
- // "pollers": ["box-lifecycle"],// background pollers to start/stop
35
+ // "pollers": ["nightly-sweep"],// background pollers to start/stop
34
36
  // "disciplines": ["artist"], // disciplines this module adds
35
37
  // "skills": ["otb-…"], // slash-command skills it ships
36
- // "uiSections": ["boxes"], // hall/status UI sections it owns
38
+ // "uiSections": ["gallery"], // hall/status UI sections it owns
37
39
  // "webSurfaces": [{ "host": "status.", "dir": "public" }], // static web surfaces. Each names a Host
38
40
  // // PREFIX (BV1.R84) or, as { "apexRoot": true, "dir": "public" },
39
41
  // // the APEX ROOT — never both (ADR 0218).
@@ -46,7 +48,7 @@
46
48
  // // refuses the enablement when the join door is `open` and no
47
49
  // // payer is configured.
48
50
  // "declarativeSeams": { // OPTIONAL. Per-entry exemptions for the seam-wiring
49
- // "pollers": ["box-lifecycle"] // guard (task 1003819): "this contributes entry ships no
51
+ // "pollers": ["nightly-sweep"] // guard (task 1003819): "this contributes entry ships no
50
52
  // }, // artifact ON PURPOSE". Every key must be a key the module
51
53
  // // actually declares in `contributes`, and every entry must be
52
54
  // // one it actually lists there — a typo'd exemption is an error,
@@ -162,6 +164,12 @@ function validateManifest(obj) {
162
164
  if (!isStr(obj.title)) E('title: required non-empty string');
163
165
  if (!isStr(obj.description)) E('description: required non-empty string');
164
166
 
167
+ // The module's own version (task 1003781) — EXACT, unlike coreVersion's range:
168
+ // it names one published build, so update parity, rollout channels and
169
+ // re-assessment on publish (ADR 0338/0344) all have something to point at.
170
+ if (!isStr(obj.version)) E('version: required exact semver string "X.Y.Z" — the module\'s own version, e.g. "1.0.0"');
171
+ else if (!parseVersion(obj.version)) E(`version: "${obj.version}" must be an exact version "X.Y.Z" (not a range)`);
172
+
165
173
  if (!isStr(obj.coreVersion)) E('coreVersion: required non-empty semver range string (e.g. "^1.0.0")');
166
174
  else if (!isValidRange(obj.coreVersion)) E(`coreVersion: "${obj.coreVersion}" is not a valid semver range`);
167
175
 
@@ -239,8 +247,8 @@ function validateManifest(obj) {
239
247
  // --- optional declarativeSeams (task 1003819) ---
240
248
  // The exemption list for the seam-wiring guard: "this contributes entry ships no
241
249
  // artifact on purpose". Some declarations are legitimately declarative-only —
242
- // dev-box's `box-lifecycle` poller runs on the control plane, not in this process,
243
- // and its `boxes` uiSection ships no hall widget — so the guard needs a way to say
250
+ // a poller that runs on the control plane, not in this process, or a uiSection
251
+ // that ships no hall widget — so the guard needs a way to say
244
252
  // so without a blanket skip that would hide the real cases it exists to catch.
245
253
  //
246
254
  // Validated HERE, against `contributes`, because this is the one place that holds
@@ -179,9 +179,8 @@ function listEvents() {
179
179
  // fire-and-forget reaction; a CONTRIBUTION accumulates — zero-or-more modules
180
180
  // each add a piece to a named extension point, and the kernel hands the whole
181
181
  // set back to a core consumer that renders them together. This is how a module
182
- // adds its own slice to a core surface without core hardcoding it: dev-box adds
183
- // box fields + a box settings panel, discord adds its link state, art adds its
184
- // key surface — each `contribute()`s; core merges. The strangler prerequisite
182
+ // adds its own slice to a core surface without core hardcoding it: discord adds
183
+ // its link state, art adds its key surface — each `contribute()`s; core merges. The strangler prerequisite
185
184
  // that lets those modules leave core (the surfaces in src/bongos/routes/me.js +
186
185
  // modules/hall-ui/public/settings.js stop being hardcoded). With no module enabled the
187
186
  // registry is empty and every consumer renders exactly what it does today.
package/src/modules.js CHANGED
@@ -18,8 +18,8 @@
18
18
  //
19
19
  // This is a config-flag exclusion mechanism, NOT a full plugin registry — the
20
20
  // cheaper near-term shape (ADR 0062 §4). R59 (task 1198) moves game + art-pipeline
21
- // in; R60 (task 1199) moves discord + dev-box in (UI sections / skills / pollers);
22
- // they consume the registry declared here.
21
+ // in; R60 (task 1199) moves discord in (UI sections / skills / pollers); they
22
+ // consume the registry declared here.
23
23
 
24
24
  const fs = require('node:fs');
25
25
  const path = require('node:path');
@@ -45,9 +45,9 @@ const INSTANCE_PATH = path.join(resolveInstanceRoot(), 'config', 'modules.json')
45
45
  // CORE (always on, never a module): auth, builders, tasks, claims, versions,
46
46
  // done-when, credits/cost, inbox, blockers, leaderboard, analytics,
47
47
  // audit-log, security, access-requests, sessions, gate-approvals, healthz,
48
- // public. These mount unconditionally. The four optional feature modules AND the
49
- // first TWO carved CORE-domain modules have graduated to modules/<key>/ (dev-box
50
- // BV1.R43, discord BV1.R47, art-pipeline BV1.R48, game BV1.R49, memory BV1.R72 —
48
+ // public. These mount unconditionally. The optional feature modules AND the
49
+ // first TWO carved CORE-domain modules have graduated to modules/<key>/ (discord
50
+ // BV1.R47, art-pipeline BV1.R48, game BV1.R49, memory BV1.R72 —
51
51
  // memory/recall/search/learnings, ADR 0091 §2 — and grading BV1.R73 — the
52
52
  // adversarial grade panel + rubric, behind the `grade` kernel port, ADR 0091 §4).
53
53
  // A core-domain module like memory or grading is `default: true` (it ships to
@@ -166,6 +166,27 @@ function spendRefusals(resolved, registry, { joinDoorOpen = false, payer = null
166
166
  // something they explicitly asked for, so it has to say what to do about it.
167
167
  // `log` is injectable for the same reason instance-config's legacy-env warning
168
168
  // injects it: a test asserts the warning fired, not only that the value resolved.
169
+ // RETIRED MODULE KEYS (goal 1000120, task 1003892). A module deleted from the core
170
+ // outright, which an instance's committed config/modules.json — written while the
171
+ // module still shipped — may still switch ON. fromConfig below throws on enabling
172
+ // an unknown module (the task-1824 typo guard), so without this a core upgrade
173
+ // would become a boot failure on every instance that ever enabled one:
174
+ // cloudbongos.com's own config still said `"dev-box": true` when the module went.
175
+ // A retired key is DROPPED with a one-line warning instead of thrown on. Keep the
176
+ // list short and say why each key left — an entry is a promise about someone
177
+ // else's config file, not a way to silence a typo.
178
+ const RETIRED_MODULE_KEYS = Object.freeze({
179
+ 'dev-box': 'cloud dev boxes were removed from the core (goal 1000120)',
180
+ });
181
+
182
+ function warnRetiredModule(key, source, log = console) {
183
+ log.warn(
184
+ `[bongos] module "${key}" is switched on in the ${source} config, but it was retired: `
185
+ + `${RETIRED_MODULE_KEYS[key]}. It is ignored — remove the key from that config to `
186
+ + 'silence this line.',
187
+ );
188
+ }
189
+
169
190
  function warnSpendRefusal(key, envPrefix, log = console) {
170
191
  log.warn(
171
192
  `[bongos] module "${key}" is enabled in config but was REFUSED: it spends money on behalf of `
@@ -185,7 +206,7 @@ function resolveModules({
185
206
  // caller that does not know about spend resolves exactly as before; loadModules
186
207
  // is the one that reads the real posture, and the one that fails closed when it
187
208
  // cannot.
188
- spendGuard = NO_SPEND_RISK, warn = warnSpendRefusal,
209
+ spendGuard = NO_SPEND_RISK, warn = warnSpendRefusal, warnRetired = warnRetiredModule,
189
210
  // THE DB OVERRIDE LAYER (task 1002857) — a { <moduleKey>: boolean } snapshot of
190
211
  // module_enablement_overrides for this scope, injected rather than read, so this
191
212
  // resolver stays pure and synchronous. Defaulted to {} so every existing caller
@@ -207,13 +228,19 @@ function resolveModules({
207
228
  for (const [k, v] of Object.entries(m)) {
208
229
  if (known.includes(k)) { out[k] = v; continue; }
209
230
  // Unknown key. A config may name a module this build hasn't installed: a stripped
210
- // CORE has no game/art-pipeline/discord/dev-box, yet config/modules.neutral.json
211
- // (which ships WITH the core) names them so the SAME file is valid in the full repo
212
- // too. Tolerate an unknown module ONLY when it is DISABLED — a disabled-and-absent
213
- // module is a no-op. ENABLING an absent module is a real error (the feature you asked
214
- // for isn't here) and still throws, which also keeps the typo guard for the dangerous
215
- // direction. (task 1824: this is what lets the vanilla core boot instead of crashing
216
- // on `unknown module "game"`.)
231
+ // CORE has no game/art-pipeline/discord, yet config/modules.neutral.json (which
232
+ // ships WITH the core) names them so the SAME file is valid in the full repo too.
233
+ // Tolerate an unknown module ONLY when it is DISABLED — a disabled-and-absent
234
+ // module is a no-op. ENABLING an absent module is a real error (the feature you
235
+ // asked for isn't here) and still throws, which also keeps the typo guard for the
236
+ // dangerous direction. (task 1824: this is what lets the vanilla core boot instead
237
+ // of crashing on `unknown module "game"`.) The one exception is a RETIRED key
238
+ // (RETIRED_MODULE_KEYS above): it can never be installed again, so enabling it is
239
+ // a stale config line, not a request — warn and drop it.
240
+ if (coerceBool(v) === true && Object.prototype.hasOwnProperty.call(RETIRED_MODULE_KEYS, k)) {
241
+ warnRetired(k, source);
242
+ continue;
243
+ }
217
244
  if (coerceBool(v) === true) {
218
245
  throw new Error(`modules: cannot enable unknown module "${k}" in ${source} config — it is not installed (known: ${known.join(', ') || '(none)'}).`);
219
246
  }
@@ -257,7 +284,7 @@ function resolveModules({
257
284
  const merged = Object.assign({}, ...layers.map(([, m]) => m));
258
285
 
259
286
  // Env overrides: <PREFIX>_MODULE_<KEY> where KEY is upper-snake of the module
260
- // name (dev-box -> DEV_BOX). Highest precedence.
287
+ // name (art-pipeline -> ART_PIPELINE). Highest precedence.
261
288
  for (const key of known) {
262
289
  const suffix = `MODULE_${key.toUpperCase().replace(/-/g, '_')}`;
263
290
  const envName = `${envPrefix}_${suffix}`;
@@ -489,6 +516,7 @@ module.exports = {
489
516
  metersSpend,
490
517
  spendRefusals,
491
518
  warnSpendRefusal,
519
+ RETIRED_MODULE_KEYS,
492
520
  resolveModules,
493
521
  resolveInstancePackPath,
494
522
  moduleScopeKey,
@@ -122,7 +122,7 @@ test('spendRefusals never names a module that was already off', () => {
122
122
  });
123
123
 
124
124
  test('the manifest declaration is a CLOSED key set — a typo must not read as "no spend"', () => {
125
- const base = { key: 'x', title: 't', description: 'd', coreVersion: '^1.0.0', contributes: {} };
125
+ const base = { key: 'x', title: 't', description: 'd', version: '1.0.0', coreVersion: '^1.0.0', contributes: {} };
126
126
  assert.equal(validateManifest({ ...base, spend: { requiresPayer: true } }).valid, true);
127
127
  assert.equal(validateManifest({ ...base, spend: { requirespayer: true } }).valid, false,
128
128
  'a misspelled key silently declaring NO spend is the direction that costs money');
@@ -128,21 +128,6 @@ test('every method the README example names actually exists on the client', () =
128
128
  assert.ok(count >= 3, 'README example should exercise several methods');
129
129
  });
130
130
 
131
- test('devbox vendored client is byte-identical to the canonical CJS build (run devbox-app vendor if stale)', () => {
132
- // The devbox Electron app can't require() clients/ at runtime (packaged
133
- // separately, ADR 0045), so scripts/vendor-client.js copies index.cjs into
134
- // devbox-app/src/vendor/. Guard that the vendored copy never drifts from the
135
- // canonical build — CI catches a forgotten `npm run vendor` after a spec change.
136
- // devbox-app/ is instance content the neutral core does not carry (task 1002470).
137
- // Absent APP => nothing to vendor into; absent VENDORED FILE inside a present app
138
- // is the real "forgot to run vendor" drift this guards, and still fails.
139
- if (!fs.existsSync(path.join(ROOT, 'devbox-app'))) return;
140
- const vendored = path.join(ROOT, 'devbox-app/src/vendor/bongos-client.cjs');
141
- if (!fs.existsSync(vendored)) throw new assert.AssertionError({ message: 'devbox-app/src/vendor/bongos-client.cjs missing — run `npm run vendor` in devbox-app/' });
142
- const canonical = fs.readFileSync(path.join(ROOT, 'clients/bongos-client/index.cjs'), 'utf8');
143
- assert.equal(fs.readFileSync(vendored, 'utf8'), canonical, 'devbox vendored client is STALE — run `npm run vendor` in devbox-app/');
144
- });
145
-
146
131
  test('baseUrl:"" is honored (absolute-path passthrough for the hall fetch wrappers)', async () => {
147
132
  const rec = {};
148
133
  const api = createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false, fetch: fakeFetch(rec, { ok: true, status: 200, body: { x: 1 } }) });
@@ -99,18 +99,18 @@ await t('session lookup error → fails CLOSED (redirect, next() NOT called)', a
99
99
  'must redirect to sign-in on a lookup error (fail closed)');
100
100
  });
101
101
 
102
- // ---- SESSION-SCOPE on the page path (task 1003197 (A)) ----------------------
102
+ // ---- SESSION SOURCE on the page path (task 1003197 (A)) ---------------------
103
103
  //
104
104
  // resolveSession accepts a Bearer token with no source filter, so before this the
105
- // page gate never asked the question requireBuilder asks: is a page inside this
106
- // SESSION's scope at all? A box-baked token (which lives on the box, outside the
107
- // ADR 0016 trust boundary) could therefore fetch a gated page SHELL whenever its
108
- // builder's rank held the page permission. The stub above resolves the permission
109
- // for EVERY builder, which is what makes these assertions about scope alone.
105
+ // page gate never asked the question requireBuilder asks: does the session-source
106
+ // registry declare this session's source at all? Without it, a token of an
107
+ // undeclared source could fetch a gated page SHELL whenever its builder's rank held
108
+ // the page permission. The stub above resolves the permission for EVERY builder,
109
+ // which is what makes these assertions about the source alone.
110
110
 
111
111
  console.log('\nrequireBuilderPage — session scope:');
112
112
 
113
- await t('box-scoped session → 302 to sign-in even though its builder holds the permission', async () => {
113
+ await t('a leftover dev-box session (source box, now undeclared) → 302 to sign-in even though its builder holds the permission', async () => {
114
114
  nextSession = { builder_id: 3, github_login: 'boxbot', rank: 'archon', status: 'active', source: 'box' };
115
115
  lookupThrows = false;
116
116
  const req = { headers: { authorization: 'Bearer boxtoken' } }; // a page route declares no scope
@@ -145,9 +145,9 @@ await t('cli and web sessions are untouched by the scope check', async () => {
145
145
  });
146
146
 
147
147
  await t('a legacy session with no source field still reaches the page', async () => {
148
- // The same invariant tests/box_scope_session.mjs pins for requireBuilder: absent
149
- // source resolves to the UNSCOPED scope, so this gate can never lock out a real
150
- // browser session minted before `source` was recorded.
148
+ // The same invariant tests/government_session_scope.mjs pins for requireBuilder:
149
+ // an absent source is the declared legacy row, so this gate can never lock out a
150
+ // real browser session minted before `source` was recorded.
151
151
  nextSession = { builder_id: 2, github_login: 'legacy', rank: 'metic', status: 'active' };
152
152
  const req = { headers: { cookie: 'gds_session=validtoken' } };
153
153
  const res = fakeRes();
@@ -288,8 +288,6 @@ const BUILDER = {
288
288
  anthropic_user_id: 'SENTINEL-anthropic-user',
289
289
  monthly_budget_usd: 999,
290
290
  system_role: 'SENTINEL-system-role',
291
- box_blocked: true,
292
- box_blocked_reason: 'SENTINEL-box-blocked',
293
291
  deactivated_reason: 'SENTINEL-deactivated',
294
292
  };
295
293
 
@@ -380,7 +378,7 @@ test('widening the response did not widen the leak: no private column appears an
380
378
  // The raw serialized text, not the parsed keys — a private value smuggled
381
379
  // under a different key still fails.
382
380
  assert.doesNotMatch(r.text, /SENTINEL-/, 'a private builder column reached the public profile body');
383
- assert.doesNotMatch(r.text, /monthly_budget_usd|anthropic_|system_role|box_blocked|deactivated_reason/);
381
+ assert.doesNotMatch(r.text, /monthly_budget_usd|anthropic_|system_role|deactivated_reason/);
384
382
  assert.deepEqual(Object.keys(r.data.builder).sort(), [
385
383
  'avatar_url', 'created_at', 'display_name', 'github_login', 'id', 'karma',
386
384
  'preferred_disciplines', 'rank', 'total_credits',
@@ -242,7 +242,10 @@ await test('NOBODY posts to /claims except through the shared action', () => {
242
242
  // own claim POST, this fails — which is what "a new claim surface needs no new
243
243
  // refusal handling" has to mean in practice.
244
244
  const offenders = [];
245
- for (const f of ['work.js', 'goals-page.js', 'goals-render.js', 'task.js']) {
245
+ // task 1003459 added two more claim surfaces (task.js's Claim it button,
246
+ // builders.js/hall-render.js's Home Next-up row) — both belong in this scan
247
+ // or a future hand-rolled POST on either trips nothing.
248
+ for (const f of ['work.js', 'goals-page.js', 'goals-render.js', 'task.js', 'builders.js', 'hall-render.js']) {
246
249
  const src = read('modules', 'hall-ui', 'public', f);
247
250
  if (/postJSON\(\s*`\$\{API\}\/claims/.test(src)) offenders.push(f);
248
251
  }