@bongos/core 1.19.1073 → 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 +2 -0
  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 +1 -0
  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 +2 -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 +78 -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
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
+ };
@@ -221,9 +221,7 @@ const EXPECTED_RANKS = {
221
221
  // criterion confirmation, and gate-approvals — moved to MODULE_EXPECTED_RANKS
222
222
  // ['lifecycle'] below in BV1.R86. They live in the lifecycle module now
223
223
  // (modules/lifecycle/routes/…), so they're verified only when that module is in
224
- // scope, not unconditionally from this core list — the pins follow the routes,
225
- // the dev-box precedent.)
226
- // (dev-box fleet pins likewise live in MODULE_EXPECTED_RANKS below — BV1.R43.)
224
+ // scope, not unconditionally from this core list — the pins follow the routes.)
227
225
  // security engagement docs — exploit-detail report store (task 991). Pinned
228
226
  // because a silent downgrade here leaks the attacker cookbook below the
229
227
  // trust line. (security-the-PROPERTY stays kernel — routes/security.js.)
@@ -277,10 +275,8 @@ const GOVERNMENT_PINS = {
277
275
  // when that module is in scope (its routes were discovered in this scan). So an
278
276
  // instance that doesn't ship the module — or a hermetic test isolating a
279
277
  // different module — never trips a false "vanished" alarm, while a real rank
280
- // downgrade or a removal IS still caught wherever the module is present. (dev-box's
281
- // box-fleet routes moved here from EXPECTED_RANKS in BV1.R43 / task 1412 — the pins
282
- // follow the routes; the intent is unchanged: closing/blocking another builder's
283
- // paid infra, and reading the fleet/cost roster, stay archon-only.)
278
+ // downgrade or a removal IS still caught wherever the module is present — the pins
279
+ // follow the routes (BV1.R43 / task 1412 moved the first set here).
284
280
  //
285
281
  // The `ideas` / `memory` / `security` blocks were added in BV1.R109b: their routes are
286
282
  // declared multi-line and so were invisible to this auditor until it learned that form
@@ -293,15 +289,6 @@ const GOVERNMENT_PINS = {
293
289
  // (metic), `security.report.adjudicate` / `idea.ratify` / `memory.read.any` (archon) —
294
290
  // so tightening these to `perm:` pins is a clean follow-up once R104-R107 have landed.
295
291
  const MODULE_EXPECTED_RANKS = {
296
- 'dev-box': {
297
- // ADR 0157 lowered `box.fleet.manage` archon→metic (operating paid infra is admin
298
- // work, not an authority grant). Re-pinned by ATOM so the guarded invariant is
299
- // "these gate on the fleet permission", independent of its floor.
300
- 'GET /boxes': 'perm:box.fleet.manage',
301
- 'GET /boxes/cost-ledger': 'perm:box.fleet.manage',
302
- 'POST /boxes/:builderId/close': 'perm:box.fleet.manage',
303
- 'PATCH /boxes/:builderId/block': 'perm:box.fleet.manage',
304
- },
305
292
  // The lifecycle module's privileged routes (carved BV1.R86 — the pins follow the
306
293
  // routes from EXPECTED_RANKS). Verified only when the lifecycle module is in scope.
307
294
  lifecycle: {
@@ -13,7 +13,6 @@
13
13
  // - /auth/discord/* LINKING an existing builder to a Discord id for role sync.
14
14
  // It is NOT a way to sign in — Discord sits OUTSIDE the trust
15
15
  // boundary (ADR 0032), so it can never mint a session.
16
- // - /auth/app-pair/* the Dev Box desktop app pairing to its builder (ADR 0045).
17
16
  //
18
17
  // TWO INVARIANTS worth keeping in view before editing:
19
18
  // - `/auth/cli-token/issue` mints a CLI bearer from a browser COOKIE, never from a
@@ -36,7 +35,6 @@ const { reportActivityRollupForShippedTask } = require('../auth-admission.js');
36
35
  // endpoint refuses. Same composed answer the gate itself reads.
37
36
  const { effectiveJoinDoor } = require('../project-door.js');
38
37
  const db = require('../db');
39
- const { pairStore } = require('../app-pair');
40
38
  const { publicOrigin, parseCookie, validateOrRespond } = require('./_helpers');
41
39
  const { accountExistenceReadRateLimit } = require('../middleware/rate-limit');
42
40
  const { branding } = require('../../branding');
@@ -1162,120 +1160,6 @@ module.exports = function buildAuthRouter() {
1162
1160
  }
1163
1161
  });
1164
1162
 
1165
- // /auth/app-pair — browser-clicks-only sign-in for the Dev Box desktop app
1166
- // (#904, ADR 0045). The app starts a pairing, opens the browser at
1167
- // /builders/pair?code=…, the signed-in builder clicks ONE Approve button,
1168
- // and the app collects a freshly-minted source='cli' session over its own
1169
- // secret poll channel. Design + threat model in src/bongos/app-pair.js.
1170
- //
1171
- // Route order note: the two PUBLIC routes here sit so no requireBuilder/
1172
- // requireRank from a NEIGHBORING declaration falls inside route-rank-check's
1173
- // 20-line look-ahead — the deterministic auditor must classify them from
1174
- // their own `rank:` annotations, not a false middleware hit.
1175
-
1176
- // rank: public — starts a pairing; returns opaque codes only, no identity.
1177
- router.post('/auth/app-pair/start', (req, res) => {
1178
- if (validateOrRespond(req, res, {
1179
- device_name: { type: 'string', maxLength: 80 },
1180
- platform: { type: 'string', maxLength: 20 },
1181
- })) return;
1182
- const body = req.body || {};
1183
- const out = pairStore.start({ deviceName: body.device_name, platform: body.platform });
1184
- if (out.error === 'busy') {
1185
- return res.fail('pairing_busy', { status: 429, message: 'Too many pairings in flight. Try again in a few minutes.' });
1186
- }
1187
- // The approve-page URL is PINNED to the canonical public origin (same
1188
- // posture as the OAuth redirect_uris above) — never derived from the
1189
- // request host, so a spoofed Host header can't point the app's browser
1190
- // hand-off at a look-alike approve page.
1191
- const verifyPath = `/builders/pair?code=${out.pairCode}`;
1192
- res.status(201).json({
1193
- pair_code: out.pairCode,
1194
- poll_token: out.pollToken,
1195
- verify_path: verifyPath,
1196
- verify_url: `${WEB_REDIRECT_ORIGIN}${verifyPath}`,
1197
- expires_in: out.expiresIn,
1198
- interval: out.interval,
1199
- });
1200
- });
1201
-
1202
- // rank: any-builder — read-only: what the approve page shows BEFORE the
1203
- // click (device name/platform for the given code). Unknown and expired are
1204
- // indistinguishable (404) so the code space can't be probed for liveness.
1205
- router.get('/auth/app-pair/info', auth.requireBuilder, (req, res) => {
1206
- const info = pairStore.info(req.query.code);
1207
- if (!info) return res.fail('unknown_or_expired', 404);
1208
- res.json({
1209
- pair_code: info.pairCode,
1210
- device_name: info.deviceName,
1211
- platform: info.platform,
1212
- status: info.status,
1213
- });
1214
- });
1215
-
1216
- // rank: any-builder (own identity only) — approving signs the app in AS THE
1217
- // APPROVER. Cookie-gated exactly like /auth/cli-token/issue above: a leaked
1218
- // CLI bearer must not be able to approve pairings and mint itself siblings;
1219
- // only the httpOnly browser cookie — the click in the hall — can.
1220
- router.post('/auth/app-pair/approve', auth.requireBuilder, async (req, res) => {
1221
- const cookieToken = sessionCookieToken(req);
1222
- if (!cookieToken) {
1223
- return res.fail('cookie_required', { status: 403, message: 'Pairings are approved from an authenticated browser session in the builders’ hall, not from a bearer token.' });
1224
- }
1225
- if (validateOrRespond(req, res, {
1226
- pair_code: { required: true, type: 'string', maxLength: 32 },
1227
- })) return;
1228
- try {
1229
- const token = crypto.randomBytes(32).toString('hex');
1230
- const row = await db.createSession({
1231
- builderId: req.builder.id,
1232
- token,
1233
- source: 'cli', // 003_pms.sql CHECK; the app is a CLI-class bearer client
1234
- userAgent: `devbox-app pair (${req.headers['user-agent'] ?? 'unknown'})`,
1235
- expiresAt: null, // no expiry — a CLI bearer lasts until revoked (ADR 0294)
1236
- });
1237
- const out = pairStore.approve((req.body || {}).pair_code, {
1238
- token,
1239
- expires_at: row.expires_at,
1240
- builder: {
1241
- id: req.builder.id,
1242
- github_login: req.builder.github_login,
1243
- display_name: req.builder.display_name,
1244
- rank: req.builder.rank,
1245
- },
1246
- });
1247
- if (!out.ok) {
1248
- // The pairing was gone or already approved — revoke the session we
1249
- // just minted so an orphaned token never outlives this branch.
1250
- await db.revokeSession(token);
1251
- return out.error === 'already_approved'
1252
- ? res.fail('already_approved', 409)
1253
- : res.fail('unknown_or_expired', 404);
1254
- }
1255
- res.json({ ok: true });
1256
- } catch (err) {
1257
- log.error('[gds] POST /auth/app-pair/approve', err);
1258
- res.fail('approve_failed', { status: 500, message: 'internal error' });
1259
- }
1260
- });
1261
-
1262
- // rank: public — poll with the secret poll_token; the token IS the bearer
1263
- // (mirrors /auth/device/poll, where the device_code is the bearer). Kept
1264
- // LAST in this router so route-rank-check's look-ahead window stays clear
1265
- // of any neighboring gated declaration (see the block comment above).
1266
- router.post('/auth/app-pair/poll', (req, res) => {
1267
- if (validateOrRespond(req, res, {
1268
- poll_token: { required: true, type: 'string', maxLength: 128 },
1269
- })) return;
1270
- const out = pairStore.poll((req.body || {}).poll_token);
1271
- if (out.status === 'approved') {
1272
- return res.json({ status: 'approved', ...out.grant });
1273
- }
1274
- res.json(out.status === 'pending'
1275
- ? { status: 'pending', interval: out.interval }
1276
- : { status: 'expired' });
1277
- });
1278
-
1279
1163
  return router;
1280
1164
  };
1281
1165
 
@@ -82,14 +82,6 @@ function staleHoursFor(req, res) {
82
82
  // kernel PORT (lc()). The rank/identity mutations (setBuilderRank/deactivate/
83
83
  // reactivate) stay core db (this domain is builder identity, not the lifecycle).
84
84
  const lc = () => seams.resolveOptional('lifecycle');
85
- // Dev-box "has this builder ever connected a box?" read through the kernel seam
86
- // instead of a direct `require('../boxes')` (a forbidden core→module edge once
87
- // dev-box moved to modules/dev-box/, ADR 0083 / BV1.R43). The dev-box module
88
- // provides the `box.hasEverConnected` port when enabled; degrades to false on a
89
- // box-less instance (the box_connect onboarding step just never auto-clears).
90
- const boxHasEverConnected = (builderId) =>
91
- seams.resolveOptional('box.hasEverConnected', async () => false)(builderId);
92
-
93
85
  // The discord "has this builder linked their Discord account?" signal, read
94
86
  // through the kernel seam instead of a direct `db.getDiscordLink` (a forbidden
95
87
  // core→module edge once discord moved to modules/discord/, ADR 0083 / BV1.R47).
@@ -470,22 +462,20 @@ module.exports = function buildBuildersRouter() {
470
462
  // never a direct require of a module. Default-on (present in prod); degrades
471
463
  // to a null state row (shapeForApi synthesizes the "nothing cleared" view).
472
464
  const onboarding = seams.resolveOptional('onboarding');
473
- const [state, shipCount, discordLink, boxConnected, cliAuthed] = await Promise.all([
465
+ const [state, shipCount, discordLink, cliAuthed] = await Promise.all([
474
466
  onboarding ? onboarding.getState(pool, targetId) : null,
475
467
  lc().countShippedTasksForBuilder(targetId),
476
468
  discordIsLinked(targetId),
477
- boxHasEverConnected(targetId),
478
469
  db.hasCliSession(targetId),
479
470
  ]);
480
471
  // shipCount drives the terminal 'ship_three' stage's "N of 3" counter.
481
- // The box/discord/cli steps clear from LIVE signals (not the onboarding_state
472
+ // The discord/cli steps clear from LIVE signals (not the onboarding_state
482
473
  // row), so they must be passed in exactly as GET /me does — otherwise they
483
- // default to false and this route renders box_connect/join_discord/cli_setup
474
+ // default to false and this route renders join_discord/cli_setup
484
475
  // perpetually unchecked even after the builder cleared them (task 1444).
485
476
  const shaped = onboarding ? onboarding.shapeForApi(state, {
486
477
  shipCount,
487
478
  discordLinked: !!discordLink,
488
- boxConnected: !!boxConnected,
489
479
  cliAuthed: !!cliAuthed,
490
480
  }) : null;
491
481
  // Light cache — onboarding state changes a few times per builder, ever.
@@ -510,7 +500,7 @@ module.exports = function buildBuildersRouter() {
510
500
  // shareable without a sign-in wall (that IS the feature). It deliberately
511
501
  // WHITELISTS the public column subset off the full builder row — the private
512
502
  // columns getBuilderById also returns (anthropic_email / anthropic_user_id /
513
- // monthly_budget_usd / system_role / box_blocked*) are NEVER spread into the
503
+ // monthly_budget_usd / system_role) are NEVER spread into the
514
504
  // JSON. A leak-prevention unit test (tests/profile_route.mjs) asserts their
515
505
  // absence + the 404-on-unknown-id behaviour.
516
506
  // THROTTLED since task 1002804. The global limiter meters WRITES only
@@ -554,7 +544,7 @@ module.exports = function buildBuildersRouter() {
554
544
  ]);
555
545
  // WHITELIST — only the leaderboard-equivalent public field set. Never spread
556
546
  // the raw row: it carries anthropic_email / anthropic_user_id /
557
- // monthly_budget_usd / system_role / box_blocked* / deactivated_reason,
547
+ // monthly_budget_usd / system_role / deactivated_reason,
558
548
  // none of which are public.
559
549
  const builder = {
560
550
  id: b.id,
@@ -9,14 +9,12 @@
9
9
  // * modules + disciplines — the R54 feature-module flags
10
10
  // * rank ladder — the canonical live tiers + the instance's labels
11
11
  // * repo binding — the GitHub owner/repo for `--local` clones
12
- // * auth methods — how a stranger signs in (device-flow / web / app-pair)
12
+ // * auth methods — how a stranger signs in (device-flow / web)
13
13
  // * admission policy — invite-only vs open self-enrollment
14
- // * dev-box availability — whether the managed cloud-box backend is on
15
14
  //
16
15
  // WHY THIS EXISTS: GET /healthz returns only { ok, auth_configured } — too thin
17
- // for a CLI to brand + feature-gate itself (e.g. hide the `box` verb on an
18
- // instance that runs no dev-box module). This is the richer public descriptor
19
- // that gap calls for.
16
+ // for a CLI to brand + feature-gate itself against an instance's modules. This is
17
+ // the richer public descriptor that gap calls for.
20
18
  //
21
19
  // SECURITY (no secrets / no topology): the manifest is assembled ONLY from
22
20
  // already-client-safe projections — clientBranding() (the documented allow-list
@@ -35,7 +33,7 @@
35
33
  const express = require('express');
36
34
  const { corsPublicGet, asyncHandler } = require('./_helpers');
37
35
  const { clientBranding } = require('../../branding');
38
- const { clientModules, isModuleEnabled } = require('../../modules');
36
+ const { clientModules } = require('../../modules');
39
37
  const { loadRepoInfo } = require('../repo-info');
40
38
  const { authConfigured, openEnrollmentEnabled, loadIdpConfig, resolveSession } = require('../auth');
41
39
  const { RANK_TIER } = require('../permission-path-check');
@@ -49,15 +47,14 @@ const MANIFEST_SCHEMA = 'instance-manifest/v1';
49
47
 
50
48
  // Supported sign-in methods. All require GitHub auth to be configured (the
51
49
  // server holds the OAuth client creds): device-flow = the CLI Device Flow;
52
- // web = the browsers' hall sign-in (completeWebFlow); app-pair = the Dev Box
53
- // desktop app's browser-clicks-only pairing (/auth/app-pair/*). All three mount
54
- // unconditionally in routes/auth.js, so configured-ness is the only gate.
55
- const AUTH_METHODS = ['device-flow', 'web', 'app-pair'];
50
+ // web = the browsers' hall sign-in (completeWebFlow). Both mount unconditionally
51
+ // in routes/auth.js, so configured-ness is the only gate.
52
+ const AUTH_METHODS = ['device-flow', 'web'];
56
53
 
57
54
  // buildInstanceManifest — PURE assembler. Every input is injected so this is
58
55
  // unit-testable without disk, DB, or git. Inputs are the CLIENT-SAFE projections
59
56
  // (branding = clientBranding() output, modules = clientModules() output), the
60
- // canonical rank-tier map, loadRepoInfo()'s result, and three booleans. Returns
57
+ // canonical rank-tier map, loadRepoInfo()'s result, and two booleans. Returns
61
58
  // the JSON body verbatim.
62
59
  function buildInstanceManifest({
63
60
  branding,
@@ -66,7 +63,6 @@ function buildInstanceManifest({
66
63
  repo,
67
64
  authConfigured: authOk,
68
65
  openEnrollment,
69
- devBoxEnabled,
70
66
  idp = null,
71
67
  } = {}) {
72
68
  const b = branding || {};
@@ -109,7 +105,6 @@ function buildInstanceManifest({
109
105
  idp: idp || null,
110
106
  },
111
107
  admission: { policy: openEnrollment ? 'open' : 'invite-only' },
112
- devBox: { available: !!devBoxEnabled },
113
108
  };
114
109
  }
115
110
 
@@ -144,7 +139,6 @@ function currentManifest() {
144
139
  // what the gate will do — and 'invite-only' already covers every non-open
145
140
  // door, so narrowing it names no state (ADR 0247 D3).
146
141
  openEnrollment: openEnrollmentEnabled(),
147
- devBoxEnabled: isModuleEnabled('dev-box'),
148
142
  // Project OUT the client secret — the manifest is public; only origin +
149
143
  // client_id (both public) advertise federation.
150
144
  idp: idpCfg ? { origin: idpCfg.origin, client_id: idpCfg.clientId } : null,