@bongos/core 1.19.1073 → 1.19.1075

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 (377) hide show
  1. package/.bongos-core.json +404 -549
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/bin/bongos.js +1 -3
  4. package/clients/bongos-client/README.md +1 -1
  5. package/clients/bongos-client/bongos-client.global.js +4 -60
  6. package/clients/bongos-client/index.cjs +4 -60
  7. package/clients/bongos-client/index.d.ts +5 -90
  8. package/clients/bongos-client/index.mjs +4 -60
  9. package/config/branding.neutral.json +1 -5
  10. package/config/modules.neutral.json +2 -3
  11. package/config/scheduled-routines.json +0 -2
  12. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  13. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  14. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  15. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  16. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
  17. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  18. package/docs/adr/README.md +1 -0
  19. package/docs/api/openapi.json +175 -1665
  20. package/docs/api-reference.md +9 -41
  21. package/docs/architecture.md +16 -1
  22. package/docs/branding-contract.md +0 -3
  23. package/docs/copy-inventory.md +454 -479
  24. package/docs/copy-registry.json +746 -978
  25. package/docs/file-map.md +5 -34
  26. package/docs/module-api-changelog.md +4 -0
  27. package/docs/modules-contract.md +13 -1
  28. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  29. package/docs/onboarding/diagrams/README.md +3 -3
  30. package/docs/onboarding/diagrams/assertions.json +2 -22
  31. package/docs/onboarding/primer.md +9 -28
  32. package/docs/page-inventory.json +4 -1
  33. package/docs/page-readings.json +1032 -1077
  34. package/docs/recipes/render-pilot.md +29 -10
  35. package/migrations/core_257_module_store_registry.sql +87 -0
  36. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  37. package/modules/agents/module.json +1 -0
  38. package/modules/autonomy/module.json +1 -0
  39. package/modules/builder-settings/module.json +1 -0
  40. package/modules/builder-settings/render-prefs.js +4 -5
  41. package/modules/copy-desk/docx.js +78 -5
  42. package/modules/copy-desk/module.json +1 -0
  43. package/modules/copy-desk/routes/copy-desk.js +10 -10
  44. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  45. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  46. package/modules/discord/module.json +1 -0
  47. package/modules/discord/ship-broadcast.js +3 -3
  48. package/modules/economy/credits.js +2 -2
  49. package/modules/economy/module.json +1 -0
  50. package/modules/economy/routes/credits.js +1 -2
  51. package/modules/government/catalog.js +13 -17
  52. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  53. package/modules/government/module.json +1 -0
  54. package/modules/government/protected-surfaces.json +3 -11
  55. package/modules/government/resolver.js +3 -46
  56. package/modules/government/routes/government.js +0 -2
  57. package/modules/government/session-scopes.js +43 -83
  58. package/modules/government/session-scopes.json +8 -18
  59. package/modules/grading/module.json +1 -0
  60. package/modules/hall-ui/module.json +1 -0
  61. package/modules/hall-ui/public/board-lib.js +2 -2
  62. package/modules/hall-ui/public/builders.js +29 -40
  63. package/modules/hall-ui/public/collab.html +1 -1
  64. package/modules/hall-ui/public/collab.js +1 -1
  65. package/modules/hall-ui/public/diagrams.html +3 -3
  66. package/modules/hall-ui/public/dom-utils.js +1 -1
  67. package/modules/hall-ui/public/gate.js +2 -2
  68. package/modules/hall-ui/public/gate.states.json +1 -1
  69. package/modules/hall-ui/public/hall-render.js +43 -79
  70. package/modules/hall-ui/public/index.html +5 -1
  71. package/modules/hall-ui/public/modules.js +1 -0
  72. package/modules/hall-ui/public/oversight.css +5 -5
  73. package/modules/hall-ui/public/palette.js +0 -11
  74. package/modules/hall-ui/public/primer.js +1 -1
  75. package/modules/hall-ui/public/sessions.html +1 -1
  76. package/modules/hall-ui/public/settings-sessions.js +1 -1
  77. package/modules/hall-ui/public/settings.css +0 -1
  78. package/modules/hall-ui/public/settings.html +4 -33
  79. package/modules/hall-ui/public/settings.js +3 -33
  80. package/modules/hall-ui/public/settings.states.json +2 -2
  81. package/modules/hall-ui/public/studio.html +1 -1
  82. package/modules/hall-ui/public/style.css +10 -13
  83. package/modules/hall-ui/public/task.html +4 -1
  84. package/modules/hall-ui/public/task.js +36 -1
  85. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  86. package/modules/hall-ui/public/tweak-editor.css +16 -0
  87. package/modules/hall-ui/public/tweak-editor.html +26 -3
  88. package/modules/hall-ui/public/tweak-editor.js +32 -1
  89. package/modules/hall-ui/public/work.js +1 -1
  90. package/modules/hall-ui/records/tweak-editor.md +1 -0
  91. package/modules/ideas/module.json +1 -0
  92. package/modules/ideas/projection.js +1 -1
  93. package/modules/lifecycle/db-claim-reads.js +1 -58
  94. package/modules/lifecycle/db-ship.js +4 -9
  95. package/modules/lifecycle/db.js +0 -4
  96. package/modules/lifecycle/github-push.js +12 -14
  97. package/modules/lifecycle/kickoff-checklist.js +0 -4
  98. package/modules/lifecycle/lifecycle.js +0 -8
  99. package/modules/lifecycle/module.json +1 -0
  100. package/modules/lifecycle/publish-reconciler.js +10 -10
  101. package/modules/lifecycle/routes/tasks.js +13 -15
  102. package/modules/lifecycle/ship-card.js +1 -1
  103. package/modules/lifecycle/task-visuals.js +2 -2
  104. package/modules/memory/module.json +1 -0
  105. package/modules/memory/routes/memory.js +2 -2
  106. package/modules/npm-release/module.json +1 -0
  107. package/modules/onboarding/module.json +1 -0
  108. package/modules/onboarding/onboarding-state.js +17 -36
  109. package/modules/onboarding/routes/access-requests.js +3 -3
  110. package/modules/platform-identity/module.json +1 -0
  111. package/modules/platform-identity/platform-identity.js +13 -5
  112. package/modules/platform-identity/routes/sso.js +5 -4
  113. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  114. package/modules/provisioning/capacity.js +1 -1
  115. package/modules/provisioning/module.json +1 -0
  116. package/modules/provisioning/provisioning.js +6 -5
  117. package/modules/provisioning/routes/provisioning.js +4 -4
  118. package/modules/provisioning/starter-bundles.js +5 -25
  119. package/modules/public-landing/module.json +1 -0
  120. package/modules/public-landing/public/assets/cosmos.css +1 -1
  121. package/modules/public-landing/public/contact.html +1 -1
  122. package/modules/public-landing/public/privacy.html +1 -1
  123. package/modules/public-landing/public/projects.html +101 -32
  124. package/modules/public-landing/public/projects.probes.json +2 -2
  125. package/modules/public-landing/public/projects.states.json +4 -3
  126. package/modules/public-landing/public/terms.html +1 -1
  127. package/modules/security/module.json +1 -0
  128. package/modules/sessions/db.js +1 -1
  129. package/modules/sessions/module.json +1 -0
  130. package/modules/specialities/module.json +1 -0
  131. package/modules/status-ui/module.json +1 -0
  132. package/modules/ui-design/module.json +1 -0
  133. package/package-lock.json +2 -2
  134. package/package.json +1 -1
  135. package/release-notes.json +84 -0
  136. package/scripts/gds/artifact-format.js +203 -0
  137. package/scripts/gds/artifact-staleness.js +2 -2
  138. package/scripts/gds/audit-authorship.sh +1 -13
  139. package/scripts/gds/backfill-task-rewards.js +5 -5
  140. package/scripts/gds/claim.js +0 -15
  141. package/scripts/gds/claude-materialize.js +2 -2
  142. package/scripts/gds/cli-lib.js +15 -15
  143. package/scripts/gds/codemod-rename-src-gds.js +1 -1
  144. package/scripts/gds/context-pack.js +5 -7
  145. package/scripts/gds/diagram-facts.js +12 -23
  146. package/scripts/gds/do-api.js +13 -80
  147. package/scripts/gds/doc-cli-guard.js +2 -0
  148. package/scripts/gds/doctor.js +4 -4
  149. package/scripts/gds/feedback-latest.js +139 -9
  150. package/scripts/gds/fitness-checks-identity.js +2 -7
  151. package/scripts/gds/fitness-checks-packaging.js +4 -4
  152. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  153. package/scripts/gds/fitness-ratchets.js +5 -5
  154. package/scripts/gds/fitness.js +10 -10
  155. package/scripts/gds/gen-api-client.js +4 -4
  156. package/scripts/gds/gen-api-docs.js +1 -1
  157. package/scripts/gds/gen-diagrams.js +49 -71
  158. package/scripts/gds/gen-repo-map.js +13 -14
  159. package/scripts/gds/gen-session-index.js +8 -8
  160. package/scripts/gds/http-api-client.js +12 -19
  161. package/scripts/gds/init.js +1 -1
  162. package/scripts/gds/leak-scan-allowlist.js +1 -1
  163. package/scripts/gds/lint-infra-exec.js +1 -1
  164. package/scripts/gds/local-preview-lib.js +8 -39
  165. package/scripts/gds/local-preview.js +2 -15
  166. package/scripts/gds/migration-namespace.js +3 -3
  167. package/scripts/gds/module-artifact.js +212 -0
  168. package/scripts/gds/module.js +95 -6
  169. package/scripts/gds/package-core.js +5 -116
  170. package/scripts/gds/plain-cards.js +1 -1
  171. package/scripts/gds/provision-config.js +2 -2
  172. package/scripts/gds/provision-net.js +2 -3
  173. package/scripts/gds/provision.js +9 -9
  174. package/scripts/gds/publish-manifest.js +1 -2
  175. package/scripts/gds/push-path-brief.js +10 -10
  176. package/scripts/gds/regen-instance-docs.js +1 -1
  177. package/scripts/gds/rename-history-check.js +1 -1
  178. package/scripts/gds/run-unit-tests.js +2 -16
  179. package/scripts/gds/sandbox-stage.js +39 -341
  180. package/scripts/gds/session-digest-build.js +2 -2
  181. package/scripts/gds/ship-deploy-target.js +13 -13
  182. package/scripts/gds/ship-flow.js +7 -7
  183. package/scripts/gds/ship-land.js +9 -10
  184. package/scripts/gds/ship-merge.js +4 -4
  185. package/scripts/gds/ship-preflight-steps.js +4 -4
  186. package/scripts/gds/ship-regen.js +15 -49
  187. package/scripts/gds/ship.js +13 -33
  188. package/scripts/gds/skill-preflight.js +9 -8
  189. package/scripts/gds/start.js +0 -15
  190. package/scripts/hall-preview/README.md +5 -5
  191. package/scripts/hall-preview/server.js +6 -6
  192. package/scripts/render-diagrams.sh +1 -1
  193. package/src/bongos/api-errors.js +1 -1
  194. package/src/bongos/api-prefix.js +4 -4
  195. package/src/bongos/auth-github.js +2 -3
  196. package/src/bongos/auth.js +51 -126
  197. package/src/bongos/db-kernel.js +2 -3
  198. package/src/bongos/db.js +5 -28
  199. package/src/bongos/module-scope-map.js +14 -29
  200. package/src/bongos/module-store.js +141 -0
  201. package/src/bongos/platform-visibility-gate.js +1 -1
  202. package/src/bongos/route-rank-check.js +3 -16
  203. package/src/bongos/routes/auth.js +0 -116
  204. package/src/bongos/routes/builders.js +5 -15
  205. package/src/bongos/routes/instance.js +8 -14
  206. package/src/bongos/routes/me.js +7 -23
  207. package/src/bongos/routes/modules.js +83 -0
  208. package/src/bongos/routes/security.js +3 -3
  209. package/src/bongos/routes.js +7 -2
  210. package/src/bongos/serve-internal.js +5 -6
  211. package/src/bongos-downloads.js +1 -6
  212. package/src/branding.js +2 -2
  213. package/src/build-info.js +4 -6
  214. package/src/instance-config.js +2 -2
  215. package/src/module-api.js +13 -6
  216. package/src/module-loader/catalog.js +3 -0
  217. package/src/module-loader/loader.js +4 -4
  218. package/src/module-loader/manifest-schema.js +16 -8
  219. package/src/module-seams.js +2 -3
  220. package/src/modules.js +42 -14
  221. package/tests/agents_spend_guard.mjs +1 -1
  222. package/tests/api_alias_caller_ratchet.mjs +1 -1
  223. package/tests/api_alias_exceptions.mjs +1 -1
  224. package/tests/api_client.mjs +1 -16
  225. package/tests/api_path_404.mjs +1 -1
  226. package/tests/auth_page_gate.mjs +10 -10
  227. package/tests/backfill_task_rewards.mjs +1 -1
  228. package/tests/canonical_profile_url.mjs +1 -3
  229. package/tests/claim_action.mjs +4 -1
  230. package/tests/claim_from_task_and_home.mjs +127 -0
  231. package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
  232. package/tests/cli_sessions.mjs +1 -1
  233. package/tests/consumer_layout_boot.mjs +2 -0
  234. package/tests/context_pack.mjs +13 -13
  235. package/tests/copy_desk_page_docx.mjs +33 -2
  236. package/tests/copy_inventory.mjs +7 -7
  237. package/tests/core_upgrade_runner.mjs +6 -6
  238. package/tests/credit_grant.mjs +2 -2
  239. package/tests/criterion_suggest.mjs +1 -1
  240. package/tests/deploy_divergence_line.mjs +11 -11
  241. package/tests/design_tokens_sync.mjs +3 -2
  242. package/tests/diagram_facts_offset.mjs +1 -1
  243. package/tests/do_api.mjs +106 -0
  244. package/tests/feedback_latest.mjs +122 -0
  245. package/tests/fitness.mjs +44 -42
  246. package/tests/fitness_ratchets.mjs +2 -2
  247. package/tests/gate_approvals.mjs +0 -1
  248. package/tests/github_push_land_proof.mjs +1 -1
  249. package/tests/go_live.mjs +3 -3
  250. package/tests/goal_advisory.mjs +3 -3
  251. package/tests/goal_suggest.mjs +3 -3
  252. package/tests/government_abuse_matrix.mjs +21 -19
  253. package/tests/government_ownership_scope.mjs +33 -35
  254. package/tests/government_parity.mjs +2 -2
  255. package/tests/government_protected_surfaces.mjs +56 -37
  256. package/tests/government_require_permission.mjs +3 -3
  257. package/tests/government_seed.mjs +37 -4
  258. package/tests/government_session_scope.mjs +92 -366
  259. package/tests/hall_audit.mjs +1 -1
  260. package/tests/hall_error_envelope.mjs +4 -0
  261. package/tests/hall_landing_boot.mjs +4 -1
  262. package/tests/hall_palette.mjs +1 -16
  263. package/tests/hall_record_world.mjs +6 -0
  264. package/tests/hall_tweak_editor.mjs +52 -1
  265. package/tests/helpers.mjs +1 -1
  266. package/tests/host_topology_skips.mjs +3 -4
  267. package/tests/html_comment_nesting.mjs +137 -0
  268. package/tests/infra_exec_paths.mjs +9 -10
  269. package/tests/init.mjs +4 -5
  270. package/tests/instance_manifest.mjs +18 -25
  271. package/tests/kickoff_checklist.mjs +1 -1
  272. package/tests/lib_sh_resolution.mjs +0 -221
  273. package/tests/local_preview.mjs +0 -12
  274. package/tests/migration_namespace.mjs +6 -6
  275. package/tests/module-scope-map.mjs +9 -9
  276. package/tests/module_api.mjs +2 -2
  277. package/tests/module_catalog.mjs +1 -1
  278. package/tests/module_cli.mjs +20 -20
  279. package/tests/module_contributions.mjs +6 -6
  280. package/tests/module_loader.mjs +6 -6
  281. package/tests/module_manifest.mjs +26 -17
  282. package/tests/module_route_rank.mjs +2 -2
  283. package/tests/module_store_publish.mjs +307 -0
  284. package/tests/module_store_publish_route.mjs +121 -0
  285. package/tests/module_store_registry_migration.mjs +70 -0
  286. package/tests/modules.mjs +56 -26
  287. package/tests/modules_route_wiring.mjs +1 -1
  288. package/tests/onboarding_route_signals.mjs +21 -31
  289. package/tests/onboarding_state.mjs +35 -49
  290. package/tests/paste_token_session_store.mjs +1 -1
  291. package/tests/permission_path.mjs +0 -3
  292. package/tests/platform_boot.mjs +1 -2
  293. package/tests/platform_visibility_gate.mjs +1 -1
  294. package/tests/profile_route.mjs +1 -6
  295. package/tests/project_modules_ui.mjs +17 -17
  296. package/tests/projects_hub.mjs +5 -5
  297. package/tests/projects_hub_app_step.mjs +138 -0
  298. package/tests/projects_hub_module_picker.mjs +77 -76
  299. package/tests/projects_hub_pre_uat.mjs +4 -5
  300. package/tests/provision.mjs +7 -7
  301. package/tests/provisioning_capacity.mjs +1 -1
  302. package/tests/provisioning_recommendations.mjs +2 -1
  303. package/tests/provisioning_starter_bundles.mjs +0 -22
  304. package/tests/publish_branch_route.mjs +11 -19
  305. package/tests/publish_manifest.mjs +2 -3
  306. package/tests/publish_reconciler.mjs +8 -8
  307. package/tests/push_path_brief.mjs +10 -9
  308. package/tests/rank_tier_single_source.mjs +1 -13
  309. package/tests/regrade_eligibility.mjs +1 -1
  310. package/tests/rename_history_restraint.mjs +1 -1
  311. package/tests/repo_map.mjs +12 -13
  312. package/tests/runner_drift.mjs +3 -21
  313. package/tests/sandbox_stage.mjs +57 -390
  314. package/tests/seam_wiring_guard.mjs +11 -11
  315. package/tests/search_isolation.mjs +3 -3
  316. package/tests/session_records.mjs +1 -1
  317. package/tests/session_rename_fallback.mjs +1 -22
  318. package/tests/session_start_freshness.mjs +3 -3
  319. package/tests/session_token_hash_db.mjs +1 -1
  320. package/tests/ship_card.mjs +1 -1
  321. package/tests/ship_ci_deploy.mjs +17 -17
  322. package/tests/ship_error_shape.mjs +1 -1
  323. package/tests/ship_premerge.mjs +1 -1
  324. package/tests/ship_resume.mjs +1 -1
  325. package/tests/skill_preflight.mjs +2 -2
  326. package/tests/skip_is_not_pass.mjs +1 -1
  327. package/tests/task_visual_slots.mjs +31 -0
  328. package/tests/terms_acceptance.mjs +2 -2
  329. package/tests/upgrade.mjs +4 -4
  330. package/tests/watch_sealed_floor.mjs +0 -1
  331. package/tests/wizard_draft_resume.mjs +2 -2
  332. package/tests/wizard_intent_resume.mjs +21 -10
  333. package/tests/wizard_preselect_why.mjs +18 -18
  334. package/docs/onboarding/browser-terminal-guide.md +0 -73
  335. package/docs/recipes/managed-settings-remote-control.md +0 -416
  336. package/modules/dev-box/CLAUDE.md +0 -15
  337. package/modules/dev-box/box-access.js +0 -623
  338. package/modules/dev-box/box-credential.js +0 -94
  339. package/modules/dev-box/box-onboard.js +0 -486
  340. package/modules/dev-box/boxes.js +0 -1038
  341. package/modules/dev-box/db.js +0 -39
  342. package/modules/dev-box/module.json +0 -19
  343. package/modules/dev-box/routes/box.js +0 -1223
  344. package/scripts/gds/box-auth-check.js +0 -160
  345. package/scripts/gds/box-infra.js +0 -321
  346. package/scripts/gds/box-sync.js +0 -216
  347. package/scripts/gds/box.js +0 -1332
  348. package/scripts/gds/cf-tunnel.js +0 -558
  349. package/scripts/gds/smoke-box.sh +0 -150
  350. package/scripts/gds/tree-preflight.js +0 -241
  351. package/src/bongos/app-pair.js +0 -172
  352. package/tests/app_pair.mjs +0 -168
  353. package/tests/box_access.mjs +0 -578
  354. package/tests/box_auth_check.mjs +0 -114
  355. package/tests/box_code_staleness.mjs +0 -129
  356. package/tests/box_connect_e2e.mjs +0 -196
  357. package/tests/box_cost.mjs +0 -70
  358. package/tests/box_credential.mjs +0 -125
  359. package/tests/box_dns_repoint.mjs +0 -153
  360. package/tests/box_env.mjs +0 -64
  361. package/tests/box_host_keys.mjs +0 -126
  362. package/tests/box_infra_resolve.mjs +0 -246
  363. package/tests/box_onboard.mjs +0 -317
  364. package/tests/box_scope_session.mjs +0 -119
  365. package/tests/box_sweep_docs.mjs +0 -119
  366. package/tests/box_sync.mjs +0 -55
  367. package/tests/box_sync_scope_report.mjs +0 -126
  368. package/tests/box_task_scope.mjs +0 -169
  369. package/tests/box_task_scope_pause.mjs +0 -66
  370. package/tests/box_terminal_ssrf.mjs +0 -96
  371. package/tests/boxes.mjs +0 -1585
  372. package/tests/cf_ruleset_preflight.mjs +0 -142
  373. package/tests/cf_tunnel.mjs +0 -496
  374. package/tests/docs_scrubber_damage.mjs +0 -111
  375. package/tests/module_scope_active_claims.mjs +0 -94
  376. package/tests/ship_api_client_pathspec.mjs +0 -100
  377. package/tests/tree_preflight.mjs +0 -243
@@ -61,7 +61,7 @@ const OVERRIDES = Object.freeze({
61
61
  unauthenticated: 'unauthenticated',
62
62
  cookie_required: 'unauthenticated',
63
63
  unknown_or_expired: 'unauthenticated',
64
- box_scope: 'forbidden',
64
+ session_scope: 'forbidden',
65
65
 
66
66
  // --- forbidden: 403 (rank/ownership, not "missing" or "bad input") ---
67
67
  forbidden: 'forbidden',
@@ -9,10 +9,10 @@
9
9
  //
10
10
  // The URL rename (Part B / B1, task 1919) flipped the canonical here: the API is
11
11
  // now served at '/api/bongos', and '/api/gds' stays mounted FOREVER as a silent
12
- // alias (LEGACY_API_PREFIXES). "Forever" is deliberate — shipped Dev Box desktop
13
- // binaries, the separately-hosted status-dashboard mirror, and live dev boxes all
14
- // call '/api/gds' and can't be force-updated. serve-internal.js mounts the router
15
- // + discovery index at API_PREFIX and every LEGACY_API_PREFIXES entry.
12
+ // alias (LEGACY_API_PREFIXES). "Forever" is deliberate — shipped older CLI
13
+ // releases and the separately-hosted status-dashboard mirror all call '/api/gds'
14
+ // and can't be force-updated. serve-internal.js mounts the router + discovery
15
+ // index at API_PREFIX and every LEGACY_API_PREFIXES entry.
16
16
  const API_PREFIX = '/api/bongos';
17
17
 
18
18
  // Prefixes the server ALSO answers on, for callers pinned to the previous path.
@@ -253,9 +253,8 @@ async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken
253
253
  // Approval gate (2026-05-30): the web hall sign-in no longer admits anyone
254
254
  // with a GitHub account. A first-time visitor must have been approved by an
255
255
  // Archon (their access_request marked 'invited') before a builder row is
256
- // created. This Archon approval is the ONLY gate — newcomers build on hosted
257
- // dev boxes, so there is no GitHub repo-access / collaborator check anywhere
258
- // (2026-06-07). Established builders (a row already exists for this github_id)
256
+ // created. This Archon approval is the ONLY gate sign-in applies — there is
257
+ // no GitHub repo-access / collaborator check in it (2026-06-07). Established builders (a row already exists for this github_id)
259
258
  // always pass — this only blocks NEW first-time entries.
260
259
  //
261
260
  // SR-19 / task #1000 (2026-06-13): the CLI device flow (pollDeviceFlow,
@@ -128,47 +128,35 @@ async function resolveSession(req) {
128
128
  return resolved;
129
129
  }
130
130
 
131
- // ── SESSION-SCOPES — capability attached to the TOKEN (BV1.R99, ADR 0151 §3) ──
132
- //
133
- // A session's SCOPE is orthogonal to its builder's roles: a dev-box session
134
- // (source='box') is baked onto a machine OUTSIDE the ADR 0016 trust boundary
135
- // (#919/SR#2, ADR 0053), so it must stay limited even when its builder is an
136
- // Archon. Before R99 that was an ad-hoc boolean (`req.allowBoxScope`) plus a
137
- // hardcoded `source === 'box'` test; it is now an explicit, declared model.
138
- //
139
- // A SESSION-SCOPE CAN ONLY EVER NARROW. Effective authority is (what the builder's
140
- // roles confer) ∩ (what this session's scope allows), with the allow-list
141
- // DENY-BY-DEFAULT. The governance resolver COMPOSES that intersection
142
- // (resolveSessionPermissions), but nothing on the request path reads it — it is
143
- // exported on the `government` port and exercised by
144
- // tests/government_session_scope.mjs, and that is the whole of its live use. The
145
- // ENFORCEMENT is the kernel's own route-reachability projection below: "is this
146
- // route inside the session's scope at all", run by requireBuilder and by
147
- // runPageGate. This comment used to say the permission gates read the resolver's
148
- // composed answer; they never have, and the next reviewer should not lose an hour
149
- // to that ghost (task 1003197 (B)). The projection is deliberately the ROLE-FREE
150
- // half — folding the builder's roles in here would change who can reach the
151
- // bootstrap routes (a builder on a rank with no seeded governance role resolves to
152
- // zero permissions by design — see tests/government_parity.mjs Part D), and R99 is
153
- // a behaviour-identical remodelling of the #919 gate, not a re-gating of it.
154
- //
155
- // GOVERNANCE OWNS THE POLICY, THE KERNEL STAYS THE ENFORCER. The declaration is
156
- // governance-owned data in modules/government/session-scopes.json, validated
157
- // against the permission catalog at load by modules/government/session-scopes.js.
158
- // Core reads it as DATA with fs — never `require` (ADR 0083's one-way rule: a
159
- // module's code and enable/disable state must never be something the kernel
160
- // depends on). The sibling trust primitive permission-path-check.js reads the
161
- // protected-surface registry exactly this way (R101). Reading it statically rather
162
- // than through the `governance` kernel port is the deliberate call: a request must
163
- // not need a module to be mounted to stay deny-by-default, and the alternative
164
- // fails badly in both directions — denying on an absent port would take every
165
- // cli/web session down, admitting would hand a box token full authority.
131
+ // ── SESSION SOURCES — which kinds of token may authenticate at all (BV1.R99) ──
132
+ //
133
+ // Every builder_sessions row carries a `source` saying where it was minted. The
134
+ // session-scope registry (modules/government/session-scopes.json) DECLARES the
135
+ // sources this instance accepts; a session whose source it does not declare is
136
+ // DENIED — by requireBuilder on every route and by runPageGate on every page —
137
+ // rather than defaulting to its builder's full authority. A null/absent source is
138
+ // a DECLARED row (sessions minted before migration 090 added the column), not an
139
+ // implicit default.
140
+ //
141
+ // Every declared source is UNSCOPED: it carries its builder's full rank authority.
142
+ // The registry used to be able to declare a NARROWING scope too — a
143
+ // deny-by-default allow-list for a token living outside the ADR 0016 trust
144
+ // boundary — and its only user was a machine-baked token (ADR 0053) that goal
145
+ // 1000120 retired. With no other narrowing scope the allow-list machinery went
146
+ // with it (task 1003894). The parser therefore REFUSES a row that declares an
147
+ // allow-list: without the enforcement behind it, such a row would read as a
148
+ // restriction while granting full authority. A session row still carrying a
149
+ // retired source simply fails the "declared?" test and is denied.
150
+ //
151
+ // GOVERNANCE OWNS THE LIST, THE KERNEL STAYS THE ENFORCER. Core reads the registry
152
+ // as DATA with fs — never `require` (ADR 0083's one-way rule: a module's code and
153
+ // enable/disable state must never be something the kernel depends on), exactly as
154
+ // permission-path-check.js reads the protected-surface registry (R101). A request
155
+ // must not need a module mounted to stay fail-closed: denying on an absent port
156
+ // would take every cli/web session down.
166
157
  //
167
158
  // FAIL-CLOSED both ways: an unreadable or malformed registry THROWS (it never
168
- // degrades to "every session is unscoped"), and a session whose `source` the
169
- // registry does not declare is DENIED rather than defaulting to full authority. A
170
- // null/absent source is a DECLARED row (rows minted before migration 090 added the
171
- // column, which predate the 'box' source existing), not an implicit default.
159
+ // degrades to "every source is accepted"), and an undeclared source is denied.
172
160
  const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules', 'government', 'session-scopes.json');
173
161
 
174
162
  // Pure: validate + normalize the parsed registry. Split from the file read so the
@@ -177,31 +165,21 @@ const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules',
177
165
  function parseSessionScopes(raw, label = 'session-scope registry') {
178
166
  const rows = Array.isArray(raw && raw.scopes) ? raw.scopes : null;
179
167
  if (!rows || rows.length === 0) {
180
- throw new Error(`${label} declares no session scopes. Refusing to run with no session-scope model (ADR 0053/0151).`);
168
+ throw new Error(`${label} declares no session sources. Refusing to run with no session-source model (ADR 0151).`);
181
169
  }
182
170
  const bySource = new Map();
183
171
  for (const row of rows) {
184
172
  const source = row && row.source !== undefined ? row.source : null;
185
173
  if (source !== null && (typeof source !== 'string' || !source)) {
186
- throw new Error(`${label}: every scope needs a non-empty session source string, or null for the legacy sourceless row`);
174
+ throw new Error(`${label}: every row needs a non-empty session source string, or null for the legacy sourceless row`);
187
175
  }
188
176
  if (bySource.has(source)) throw new Error(`${label}: duplicate session source "${source}"`);
189
- const unscoped = !!(row && row.unscoped === true);
190
- const permissions = row && Array.isArray(row.permissions) ? [...row.permissions] : [];
191
- // unscoped XOR an allow-list. Both would be ambiguous (does it narrow?);
192
- // neither would be a scope that can never admit anything — an authoring slip
193
- // either way, and both must fail loud rather than pick a default.
194
- if (unscoped === (permissions.length > 0)) {
195
- throw new Error(`${label}: "${source}" must declare EITHER unscoped:true OR a non-empty permissions allow-list, never both/neither`);
196
- }
197
- if (permissions.some((key) => typeof key !== 'string' || !key)) {
198
- throw new Error(`${label}: "${source}" lists a non-string permission key`);
177
+ if (row && (row.permissions !== undefined || row.unscoped !== undefined)) {
178
+ throw new Error(`${label}: "${source}" declares a scope (permissions/unscoped). Narrowing session scopes were removed (task 1003894); a row may only declare a source that carries its builder's full authority.`);
199
179
  }
200
180
  bySource.set(source, Object.freeze({
201
181
  source,
202
182
  label: row && typeof row.label === 'string' ? row.label : null,
203
- unscoped,
204
- permissions: Object.freeze(permissions),
205
183
  }));
206
184
  }
207
185
  return bySource;
@@ -212,7 +190,7 @@ function loadSessionScopes(file) {
212
190
  try {
213
191
  raw = JSON.parse(fs.readFileSync(file, 'utf8'));
214
192
  } catch (e) {
215
- throw new Error(`session-scope registry unreadable at ${file}: ${e.message}. Refusing to run with no session-scope model (ADR 0053/0151).`);
193
+ throw new Error(`session-scope registry unreadable at ${file}: ${e.message}. Refusing to run with no session-source model (ADR 0151).`);
216
194
  }
217
195
  return parseSessionScopes(raw, `session-scope registry ${file}`);
218
196
  }
@@ -220,7 +198,7 @@ function loadSessionScopes(file) {
220
198
  const _sessionScopes = loadSessionScopes(SESSION_SCOPE_REGISTRY_PATH);
221
199
  const SESSION_SCOPES = Object.freeze([..._sessionScopes.values()]);
222
200
 
223
- // The scope a session's `source` declares, or null when the registry does not
201
+ // The declared row for a session's `source`, or null when the registry does not
224
202
  // declare it (⇒ deny; never "full").
225
203
  function sessionScopeFor(source) {
226
204
  const key = source === undefined ? null : source;
@@ -228,39 +206,6 @@ function sessionScopeFor(source) {
228
206
  return _sessionScopes.get(key) || null;
229
207
  }
230
208
 
231
- // Route reachability, deny-by-default. An unscoped session reaches everything its
232
- // builder's gates admit; a SCOPED session reaches only a route that declared
233
- // capability its allow-list names — so a route that declares nothing (every route
234
- // that has not opted in, including any added later) is out of reach.
235
- function scopeCovers(scope, declared) {
236
- if (!scope) return false; // unrecognised source
237
- if (scope.unscoped) return true;
238
- const needs = Array.isArray(declared) ? declared : [];
239
- return needs.length > 0 && needs.every((key) => scope.permissions.includes(key));
240
- }
241
- function sessionScopeAdmits(source, declared) {
242
- return scopeCovers(sessionScopeFor(source), declared);
243
- }
244
-
245
- // The route-side DECLARATION: "reaching this route needs these capabilities from
246
- // the session's scope." Mount it BEFORE requireBuilder. Inert for an unscoped
247
- // session — requireBuilder compares the declaration only against a narrowing scope.
248
- function sessionScope(...keys) {
249
- const needs = Object.freeze([...keys]);
250
- return function declareSessionScope(req, _res, next) {
251
- req.sessionScopeNeeds = needs;
252
- next();
253
- };
254
- }
255
-
256
- // The dev-box bootstrap declaration (#919) — the name modules/dev-box mounts
257
- // through the plugin API (docs/modules-contract.md 1.2.0), kept as the module-
258
- // facing spelling. It is no longer a bare boolean: it declares the box scope's OWN
259
- // allow-list, read from the registry, so the declaration cannot drift from the
260
- // scope it declares. If the registry stopped declaring a box scope it would
261
- // declare nothing and every box session would be denied — fail closed.
262
- const allowBoxScope = sessionScope(...((sessionScopeFor('box') || { permissions: [] }).permissions));
263
-
264
209
  async function requireBuilder(req, res, next) {
265
210
  let session;
266
211
  try {
@@ -295,24 +240,13 @@ async function requireBuilder(req, res, next) {
295
240
  res.fail('builder_inactive', 403);
296
241
  return;
297
242
  }
298
- // SECURITY (#919, SR#2): SESSION-SCOPE, deny-by-default. The dev-box provisioner
299
- // (scripts/gds/box.js) bakes a session into cloud-init so the box can bootstrap
300
- // (#818 — box-source-fetch clones the rank-scoped repo before /builder-setup
301
- // exists). That token lives ON the box, which is OUTSIDE the ADR 0016 trust
302
- // boundary, so it must NOT carry the builder's full authority. Since R99 the rule
303
- // is DATA (the session-scope registry above), not a hardcoded source test: a
304
- // narrowing session reaches ONLY a route that declared capability its allow-list
305
- // names, so EVERY other route (all of /tasks, /me, the rest of /box …) fails
306
- // closed here, including any route added later; and a source the registry does not
307
- // declare at all is denied rather than treated as unscoped. A builder who wants to
308
- // do real work FROM the box runs /builder-reauth, minting a normal cli/web session.
309
- const scope = sessionScopeFor(session.source);
310
- if (!scopeCovers(scope, req.sessionScopeNeeds)) {
311
- res.fail('box_scope', {
243
+ // SECURITY: an UNDECLARED session source is denied (see the registry notes
244
+ // above). This is what keeps a token of a retired source out of every route, and
245
+ // any source added later out until the registry declares it.
246
+ if (!sessionScopeFor(session.source)) {
247
+ res.fail('session_scope', {
312
248
  status: 403,
313
- message: scope
314
- ? 'This is a box-scoped session; it may only call dev-box bootstrap endpoints.'
315
- : 'This session carries an unrecognised scope and may not call authenticated endpoints.',
249
+ message: 'This session carries an unrecognised source and may not call authenticated endpoints. Sign in again to mint a normal session.',
316
250
  });
317
251
  return;
318
252
  }
@@ -382,9 +316,9 @@ function signInWithReturn(req) {
382
316
  // 1. IDENTITY — is there a live session? (token from the gds_session cookie /
383
317
  // legacy pms_session / Bearer, looked up fresh against builder_sessions).
384
318
  // No → 302 into the web sign-in flow.
385
- // 2. SCOPE — is a page inside this SESSION's scope at all? A narrowing scope
386
- // (a box-baked token) reaches no page, whatever its builder's rank, exactly as
387
- // requireBuilder denies it every route that declared nothing (task 1003197).
319
+ // 2. SOURCE — does the session-scope registry declare this session's source?
320
+ // An undeclared one reaches no page, whatever its builder's rank, exactly as
321
+ // requireBuilder denies it every route (task 1003197).
388
322
  // No → 302 into the web sign-in flow: the remedy is a different session.
389
323
  // 3. AUTHORITY — does the builder HOLD the page's catalog permission, resolved
390
324
  // per request through the `governance` port exactly as requirePermission
@@ -557,21 +491,15 @@ async function runPageGate(name, req, res, next) {
557
491
  res.redirect(302, signInWithReturn(req));
558
492
  return;
559
493
  }
560
- // SECURITY (task 1003197 (A)): SESSION-SCOPE, the same deny-by-default check
561
- // requireBuilder runs above — the two auth paths must not diverge. resolveSession
562
- // accepts a Bearer token with NO source filter, so before this a box-baked token
563
- // could fetch a gated page SHELL (/builders, /watch, the tab shells) whenever its
564
- // builder's rank held the page's permission. Every data endpoint underneath
565
- // re-gates through requireBuilder and 403s box_scope, so the exposure was shells
566
- // only — but a shell is still a surface a token living outside the ADR 0016 trust
567
- // boundary was never meant to reach. Page routes declare no capability, so
568
- // scopeCovers refuses EVERY narrowing scope here (and an unrecognised source with
569
- // it), while an unscoped cli/web session passes untouched.
494
+ // SECURITY (task 1003197 (A)): the same undeclared-source check requireBuilder
495
+ // runs above — the two auth paths must not diverge. resolveSession accepts a
496
+ // Bearer token with NO source filter, so without this a token of an undeclared
497
+ // source could fetch a gated page SHELL whenever its builder's rank held the
498
+ // page's permission.
570
499
  //
571
500
  // Sign-in, not `deniedTo`: unlike the authority branch below, the remedy is a
572
- // DIFFERENT SESSION — /builder-reauth mints a normal one — not a different page,
573
- // and a box session has no browser cookie to loop the sign-in flow with.
574
- if (!scopeCovers(sessionScopeFor(session.source), req.sessionScopeNeeds)) {
501
+ // DIFFERENT SESSION, not a different page.
502
+ if (!sessionScopeFor(session.source)) {
575
503
  res.redirect(302, signInWithReturn(req));
576
504
  return;
577
505
  }
@@ -598,14 +526,14 @@ async function runPageGate(name, req, res, next) {
598
526
  }
599
527
 
600
528
  // The authenticated /builders HTML surface — the Work Board, Roadmap, Goals, the
601
- // task-detail page, the Repo Atlas, the Dev Box pair page and Settings. A Xenos IS
529
+ // task-detail page, the Repo Atlas and Settings. A Xenos IS
602
530
  // allowed (page.view.public floors at xenos); an unauthenticated visitor is bounced
603
531
  // into sign-in rather than served the page shell.
604
532
  async function requireBuilderPage(req, res, next) {
605
533
  return runPageGate('requireBuilderPage', req, res, next);
606
534
  }
607
535
 
608
- // The /watch monitoring surface, the /harbor dev-box page and the /gate page
536
+ // The /watch monitoring surface and the /gate page
609
537
  // (#726, idea 257, task 1179) — Metic and up, per `page.view.government`'s catalog
610
538
  // floor. Named `requireArchonPage` until ADR 0157 lowered that floor archon→metic
611
539
  // (older ADRs still use the old name; this line is the trail). Defense-in-depth:
@@ -917,10 +845,7 @@ module.exports = {
917
845
  extractTokens,
918
846
  SESSION_COOKIE_NAMES,
919
847
  resolveSession,
920
- allowBoxScope,
921
- sessionScope,
922
848
  sessionScopeFor,
923
- sessionScopeAdmits,
924
849
  parseSessionScopes,
925
850
  SESSION_SCOPES,
926
851
  requireBuilder,
@@ -45,7 +45,7 @@ async function withTx(fn, { pool: p = pool } = {}) {
45
45
  }
46
46
 
47
47
  // getBuilderById — the canonical builder-identity read (id, rank, status, the
48
- // economy counters, box-block state). The kernel's identity primitive; auth and
48
+ // economy counters). The kernel's identity primitive; auth and
49
49
  // every rank gate ultimately resolve a builder through this shape.
50
50
  async function getBuilderById(id) {
51
51
  // karma was added in migration 040 (V3.R24 / #244) as a separate
@@ -55,8 +55,7 @@ async function getBuilderById(id) {
55
55
  const { rows } = await pool.query(
56
56
  `SELECT id, github_id, github_login, display_name, avatar_url, total_credits, karma, created_at, rank,
57
57
  status, deactivated_at, reactivated_at, deactivated_reason, preferred_disciplines, system_role,
58
- anthropic_email, anthropic_user_id, monthly_budget_usd,
59
- box_blocked, box_blocked_reason, box_blocked_at, box_blocked_by
58
+ anthropic_email, anthropic_user_id, monthly_budget_usd
60
59
  FROM builders WHERE id = $1`,
61
60
  [id]
62
61
  );
package/src/bongos/db.js CHANGED
@@ -26,10 +26,9 @@
26
26
 
27
27
  const { pool } = require('./pool');
28
28
  const secretBox = require('./secret-box');
29
- // Module seam (ADR 0083 / BV1.R43). The rank/status-change paths EMIT
30
- // `builder.box-reconcile-needed` AFTER COMMIT (best-effort) so the dev-box module
31
- // can reconcile a droplet a demotion/graduation implies — never a direct require
32
- // of the module.
29
+ // Module seam (ADR 0083). The builder-exit path EMITS `builder.secrets-scrub-needed`
30
+ // AFTER COMMIT (best-effort) so a module that stores a builder-keyed secret can
31
+ // clear its own store — never a direct require of the module.
33
32
  const seams = require('../module-seams');
34
33
 
35
34
  // Kernel db slice (BV1.R69 / ADR 0091 §2): builder IDENTITY (getBuilderById),
@@ -123,11 +122,6 @@ async function linkAnthropicAccount(builderId, { email, userId }) {
123
122
  // setBuilderMonthlyBudget() carved into modules/economy/cost.js (BV1.R77 / ADR
124
123
  // 0093 §1-2). routes/builders.js resolves it via the `reward` kernel port.
125
124
 
126
- // setBuilderBoxBlocked + its box_blocked UPDATE RELOCATED to modules/dev-box/db.js
127
- // (BV1.R68 / ADR 0091 §3): box-blocked is cleanly dev-box-owned — the module is
128
- // its only writer AND reader (core never reads box_blocked) — so the query left
129
- // the core monolith with the doorway re-export when the slice pattern landed.
130
-
131
125
  // Approval-gated entry (2026-05-30). The web hall sign-in no longer auto-creates
132
126
  // an account for any GitHub user — a first-time visitor must be approved first
133
127
  // (an Archon marks their access_request 'invited'). These three small lookups
@@ -446,14 +440,6 @@ async function setBuilderRank({ builderId, newRank, actorBuilderId }) {
446
440
  const result = await withTx(async (client) => {
447
441
  return _applyRankChangeTx(client, { builderId, newRank, actorBuilderId, enforceSingleRungPromotion: true });
448
442
  });
449
- // Source-access clawback (ADR 0031 §6 / #600). Post-commit, own connection,
450
- // never throws — records the box reconcile this rank change implies (a demotion
451
- // below the floor → deprovision; a graduation up → rescope). The CREDENTIAL
452
- // layer is already cut off live on the next /box/source-access fetch; this
453
- // just signals + audits the slower droplet-level reconcile.
454
- await seams.emitAsync('builder.box-reconcile-needed', {
455
- builderId, rank: newRank, status: result.status, actor: `rank-change:builder#${actorBuilderId}`,
456
- });
457
443
  return {
458
444
  builder: result.builder,
459
445
  previous_rank: result.previous_rank,
@@ -613,20 +599,11 @@ async function deactivateBuilder({ builderId, reason, actorBuilderId }) {
613
599
  await deleteGithubToken({ builderId, p: client });
614
600
  return { updated, activeClaims };
615
601
  });
616
- // Source-access clawback (ADR 0031 §6 / #600): a deactivated builder loses
617
- // their box → loses source. Post-commit, own connection, never throws. The
618
- // builder is now status='inactive' (rank forced to xenos above), so the
619
- // reconcile resolves to 'deprovision'. Their next /box/source-access fetch is
620
- // already 403'd (inactive) — this signals the droplet teardown for `box.js
621
- // reconcile` / the dormant sweep.
622
- await seams.emitAsync('builder.box-reconcile-needed', {
623
- builderId, rank: 'xenos', status: 'inactive', actor: `builder-exit:builder#${actorBuilderId}`,
624
- });
625
602
  // PRIVACY (task 1003338 / ADR 0332): teardown port for a MODULE-owned
626
603
  // builder-keyed secret (today: art-pipeline's Gemini BYOK key,
627
604
  // gemini_key_enc/last4/set_at — CRUD moved out of core under ADR 0102, so
628
- // core does not touch those columns directly). Best-effort, same as
629
- // box-reconcile-needed above: a module that stores such a secret listens
605
+ // core does not touch those columns directly). Best-effort, post-commit:
606
+ // a module that stores such a secret listens
630
607
  // for this event and clears its own store; on a vanilla instance (no
631
608
  // listener registered) it is a no-op.
632
609
  await seams.emitAsync('builder.secrets-scrub-needed', {
@@ -20,7 +20,7 @@
20
20
  // regex, so wildcards would never match. Every PROTECTED_GLOBS entry is likewise
21
21
  // an exact path or dir-prefix, so the overlap is exact.
22
22
  //
23
- // FIRST CUT, auto-tightening. We are mid-strangler (ADR 0083): only dev-box has
23
+ // FIRST CUT, auto-tightening. We are mid-strangler (ADR 0083): some domains have
24
24
  // moved into modules/<key>/; the rest still live fused in src/bongos/. So a module's
25
25
  // globs list WHERE ITS CODE LIVES TODAY plus its modules/<key>/ home, and the
26
26
  // `protected` result reflects the current tree (most core domains are protected
@@ -30,7 +30,7 @@
30
30
  // modules + kernel from docs/design/modular-architecture/02-module-map.md §6.
31
31
  //
32
32
  // RELATION to src/modules.js: that registry tracks only the OPTIONAL feature
33
- // modules (game/art-pipeline/discord/dev-box) an instance toggles. This map is a
33
+ // modules (game/art-pipeline/discord) an instance toggles. This map is a
34
34
  // SUPERSET — every module including the always-on core domains — because the goal
35
35
  // scope wall must place ALL work, not just the optional features.
36
36
 
@@ -316,7 +316,7 @@ const MODULE_GLOBS = {
316
316
  // (files-are-source — the committed definitions ARE the input agents-sync
317
317
  // upserts), listed alongside it on the autonomy precedent, which likewise
318
318
  // owns its '.claude/scheduled-tasks/' definition dir. The sync CLI travels
319
- // with the domain but stays under scripts/gds/ (the dev-box/provisioning
319
+ // with the domain but stays under scripts/gds/ (the provisioning
320
320
  // precedent). Without this key the three Agents goals (1000038/39/40) could
321
321
  // declare no wall at all (task 1002736).
322
322
  agents: [
@@ -342,7 +342,7 @@ const MODULE_GLOBS = {
342
342
  'npm-release': ['modules/npm-release/', 'tests/npm_release_*.mjs'],
343
343
  // --- instance provisioning: the web-tier REQUEST + STATE surface (module home)
344
344
  // plus the control-plane runner CLI — a command travels with its domain (the
345
- // lifecycle/dev-box precedent). Covers the projects-surface work (goal
345
+ // lifecycle precedent). Covers the projects-surface work (goal
346
346
  // 1000059); previously NO key resolved these paths, so no goal could wall
347
347
  // them (task 1002690). ---
348
348
  provisioning: [
@@ -355,32 +355,17 @@ const MODULE_GLOBS = {
355
355
  // core/CLI-side under scripts/discord/.
356
356
  'scripts/discord/', 'modules/discord/',
357
357
  ],
358
- // --- per-builder cloud dev boxes (DO droplets + Cloudflare tunnels), the sandbox
359
- // preview, server-mediated branch publish. First module carved to modules/dev-box/
360
- // (BV1.R43 — ADR 0083). Like the lifecycle/onboarding CLIs, the dev-box OPERATOR +
361
- // connect commands travel with the domain but stay under scripts/gds/: box.js (the
362
- // DO/CF provisioning control plane), box-auth-check, box-connect-lib, and the box
363
- // smoke test. Listing them here so a `dev-box`-scoped goal's wall covers the box
364
- // provisioning / bring-up work (BV1.R91, goal 1000051 C2), not just modules/dev-box/.
365
- // The box BRING-UP scripts (infra/box-*.sh — box-source-fetch, heartbeat, terminal
366
- // units) are INSTANCE-VENDORED (ADR 0144; not tracked in core), so they are not
367
- // listed here yet — they come under this key when de-vendored. (ADR 0150 /
368
- // task 1002386 made box.js RESOLVE infra/ from the instance root and fail
369
- // loudly when it is missing; de-vendoring infra/ into core is still open.)
370
- 'dev-box': [
371
- 'modules/dev-box/', 'devbox-app/',
372
- 'scripts/gds/box.js', 'scripts/gds/box-auth-check.js', 'scripts/gds/box-connect-lib.js',
373
- 'scripts/gds/smoke-box.sh',
374
- 'scripts/gds/box-sync.js', // task 1004024: split out of box.js, now protected
375
-
376
- // ADOPTED (task 1004111): the box BRING-UP surface. box.js (already here)
377
- // orchestrates these five provisioning CLIs, and infra/ is the shell-script
378
- // tree it resolves from the instance root — the comment above has described
379
- // infra/ as dev-box's since task 1002386 without the glob saying so.
358
+ // --- the operator CONTROL PLANE: the provisioning CLIs that hold the DigitalOcean /
359
+ // Cloudflare credentials, and an instance's host topology under infra/. Not a
360
+ // module (nothing under modules/ carries it) — a scope like kernel or devsecops.
361
+ // Its own key rather than provisioning's, deliberately: every path here is a
362
+ // PROTECTED surface, so listing them under provisioning would make that module
363
+ // derive protected:true and put its open goals (1000106) behind an Archon join —
364
+ // the effect tests/government_protected_surfaces.mjs pins against. Given this key
365
+ // by task 1003892 (their previous owner was retired).
366
+ 'control-plane': [
380
367
  'scripts/gds/provision-config.js',
381
368
  'scripts/gds/provision-net.js',
382
- 'scripts/gds/box-infra.js',
383
- 'scripts/gds/cf-tunnel.js',
384
369
  'scripts/gds/do-api.js',
385
370
  'infra/',
386
371
  ],
@@ -709,7 +694,7 @@ function scopeIncludesProtected(keys) {
709
694
  //
710
695
  // WHAT THIS IS AND IS NOT FOR. This narrows ONE consumer: the ship-time module-
711
696
  // drift advisory (moduleDriftFiles below), whose question is "you said game, did
712
- // you touch dev-box?" — module-to-module confusion. It does NOT touch the goal
697
+ // you touch discord?" — module-to-module confusion. It does NOT touch the goal
713
698
  // scope wall, claim collisions (touchesOverlap), or the protected-path wall.
714
699
  // Those keep counting methodology's territory as methodology's.
715
700
  //
@@ -0,0 +1,141 @@
1
+ // src/bongos/module-store.js — the module store's write side: keep a published
2
+ // module version's tarball and record it in the registry (task 1004271, ADR 0338 D1).
3
+ //
4
+ // The route (POST /store/modules/:key/versions in routes/modules.js) verifies the
5
+ // upload with scripts/gds/module-artifact.js first; this file only persists what has
6
+ // already passed. Two halves, in this order:
7
+ //
8
+ // 1. stageArtifact — write the tarball under the store dir (a temp name first;
9
+ // commitArtifact moves it into place inside step 2's transaction). ADR 0338 lets the
10
+ // store keep tarballs on the control plane's disk while volume is small, so it
11
+ // does: <MODULE_STORE_DIR or var/module-store>/<key>/<key>-<version>.tgz. Outside
12
+ // the git tree, like var/task-visuals, so a deploy's `git reset --hard` never
13
+ // wipes it.
14
+ // 2. publishVersion — one transaction over the registry tables (core_257): create
15
+ // the store_modules row on a key's first publish (the publisher becomes its
16
+ // author), refuse anyone else's key, refuse a delisted key (ADR 0338 D2: delisting
17
+ // stops what comes next), refuse a version that already exists or is not newer
18
+ // than the latest, then INSERT the version row. Rows are only ever INSERTed.
19
+ //
20
+ // If step 2 refuses, the route removes the temp file step 1 wrote, so a refused
21
+ // publish leaves nothing behind.
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const crypto = require('node:crypto');
26
+ const { parseVersion, cmp } = require('../module-loader/semver');
27
+ const { KEY_RE } = require('../module-loader/manifest-schema');
28
+
29
+
30
+ function storeDir() {
31
+ return process.env.MODULE_STORE_DIR || path.join(__dirname, '..', '..', 'var', 'module-store');
32
+ }
33
+
34
+ // Where one version's tarball lives. Key and version are re-validated here so this
35
+ // is the path-traversal defense on its own, whatever the caller checked.
36
+ function artifactPath(key, version, { dir = storeDir() } = {}) {
37
+ if (!KEY_RE.test(String(key)) || !parseVersion(String(version))) {
38
+ throw new Error('artifactPath: a bad module key or version');
39
+ }
40
+ return path.join(dir, key, `${key}-${version}.tgz`);
41
+ }
42
+
43
+ // Write the tarball to a private temp name beside its final place and return both
44
+ // paths. It becomes the real file only through commitArtifact, which publishVersion
45
+ // runs just before COMMIT — so a crash anywhere leaves at most a temp file (or a
46
+ // final file with no row, which the next publish of that version simply replaces),
47
+ // never a row pointing at nothing and never a stray file that blocks a retry.
48
+ // Async throughout so a multi-MB write never holds the web process's event loop.
49
+ async function stageArtifact(buf, { key, version, dir = storeDir() } = {}) {
50
+ const final = artifactPath(key, version, { dir });
51
+ await fs.promises.mkdir(path.dirname(final), { recursive: true });
52
+ const tmp = `${final}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.partial`;
53
+ await fs.promises.writeFile(tmp, buf, { flag: 'wx', mode: 0o640 });
54
+ return { tmp, final };
55
+ }
56
+
57
+ async function commitArtifact({ tmp, final }) {
58
+ await fs.promises.rename(tmp, final);
59
+ }
60
+
61
+ async function removeArtifact(file) {
62
+ try { await fs.promises.unlink(file); } catch { /* already gone — nothing to undo */ }
63
+ }
64
+
65
+ // The store keeps artifact_path relative to the store dir, so moving the dir (or
66
+ // moving to object storage later) is a config change, not a data migration.
67
+ function relativeArtifactPath(file, { dir = storeDir() } = {}) {
68
+ return path.relative(dir, file).split(path.sep).join('/');
69
+ }
70
+
71
+ // One transaction. `db` is a pg Pool (anything with connect()). Returns
72
+ // { ok: true, version: row, created: bool } or { ok: false, code, message, status }.
73
+ async function publishVersion(input, { db, beforeCommit } = {}) {
74
+ const {
75
+ key, title, description, version, coreVersion, manifest,
76
+ treeSha256, tarballSha256, bytes, artifactPath: relPath, builderId,
77
+ } = input;
78
+ const pool = db || require('./pool').pool;
79
+ const client = await pool.connect();
80
+ const refuse = async (status, code, message) => {
81
+ await client.query('ROLLBACK');
82
+ return { ok: false, status, code, message };
83
+ };
84
+ try {
85
+ await client.query('BEGIN');
86
+ const { rows: [mod] } = await client.query(
87
+ 'SELECT module_key, author_id, status FROM store_modules WHERE module_key = $1 FOR UPDATE', [key]);
88
+ let created = false;
89
+ if (!mod) {
90
+ await client.query(
91
+ 'INSERT INTO store_modules (module_key, title, description, author_id) VALUES ($1, $2, $3, $4)',
92
+ [key, title, description || '', builderId]);
93
+ created = true;
94
+ } else {
95
+ if (String(mod.author_id) !== String(builderId)) {
96
+ return refuse(403, 'not_module_author', `"${key}" belongs to another author — only its author can publish new versions`);
97
+ }
98
+ if (mod.status === 'delisted') {
99
+ return refuse(409, 'module_delisted', `"${key}" is delisted — it takes no new versions (ADR 0338 D2)`);
100
+ }
101
+ }
102
+
103
+ // Every version of the key, compared in JS: semver order is numeric per part, so
104
+ // a text ORDER BY would rank 1.10.0 below 1.9.0. A module's version count is small.
105
+ const { rows: existing } = await client.query(
106
+ 'SELECT version FROM store_module_versions WHERE module_key = $1', [key]);
107
+ const wanted = parseVersion(version);
108
+ if (existing.some((r) => r.version === version)) {
109
+ return refuse(409, 'version_exists', `${key} ${version} is already published — a version never changes; bump module.json's version`);
110
+ }
111
+ const newest = existing.map((r) => parseVersion(r.version)).filter(Boolean).sort(cmp).pop();
112
+ if (newest && cmp(wanted, newest) <= 0) {
113
+ return refuse(409, 'version_not_newer', `${key} ${version} is not newer than the latest published ${newest.join('.')}`);
114
+ }
115
+
116
+ const { rows: [row] } = await client.query(
117
+ `INSERT INTO store_module_versions
118
+ (module_key, version, core_version, manifest, tree_sha256, tarball_sha256,
119
+ tarball_bytes, artifact_path, published_by)
120
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
121
+ RETURNING id, module_key, version, core_version, tree_sha256, tarball_sha256,
122
+ tarball_bytes, published_by, published_at`,
123
+ [key, version, coreVersion, manifest, treeSha256, tarballSha256, bytes, relPath, builderId]);
124
+ // Put the tarball in place while the row is still uncommitted: if this throws,
125
+ // the catch below rolls the row back, so the two can never disagree.
126
+ if (beforeCommit) await beforeCommit();
127
+ await client.query('COMMIT');
128
+ return { ok: true, created, version: row };
129
+ } catch (e) {
130
+ try { await client.query('ROLLBACK'); } catch { /* the connection may be gone */ }
131
+ // Two publishes of the same version racing past the check: the UNIQUE wins.
132
+ if (e && e.code === '23505') return { ok: false, status: 409, code: 'version_exists', message: `${key} ${version} is already published` };
133
+ throw e;
134
+ } finally {
135
+ client.release();
136
+ }
137
+ }
138
+
139
+ module.exports = {
140
+ stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
141
+ };