@bongos/core 1.19.1072 → 1.19.1074

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +2 -1
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +84 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
@@ -1,160 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/gds/box-auth-check.js — dev-box session-start auth gate (task 1712).
3
- //
4
- // THE FIRST THING a dev-box session should confirm: that its GDS CLI session is
5
- // a REAL, full-authority session — not the box-scoped bootstrap token, and not
6
- // an expired one. A box's baked session is `source:'box'` (ADR 0053 / #919): it
7
- // may call the on-box bootstrap endpoints but is 403'd (`box_scope`) on
8
- // claim / ship / publish. Nothing used to surface that until a ship hit the
9
- // publish-branch wall (the 2026-07-02 incident: a doc couldn't be pushed and the
10
- // session burned time on raw `git push`). This check makes the session say, up
11
- // front, "re-auth first" so no work starts on a token that can't finish it.
12
- //
13
- // Self-gating + fail-open, by design:
14
- // * NOT a dev box (no /etc/otb/box.env) → silent no-op. Laptops handle auth at
15
- // point-of-use and can't be box-scoped, so this stays out of their way.
16
- // * a subagent (OTB_SUBAGENT) → silent (grader/worker context stays clean).
17
- // * box-scoped or missing session → the directive comes from the LOCAL file
18
- // alone (no network) — instant, and exactly the incident case.
19
- // * a real token present → ONE bounded, fail-open GET /me to also catch an
20
- // EXPIRED/revoked token; a quiet '✓ authed' on success; silent when the
21
- // network is flaky (point-of-use still catches a genuine problem — never
22
- // cry wolf and never delay session start).
23
- //
24
- // Printed to stdout so the SessionStart hook forwards it into session context.
25
- // Wired FIRST (right after the identity card) in .claude/hooks/session-start.js.
26
-
27
- const fs = require('node:fs');
28
-
29
- const BOX_ENV_PATH = '/etc/otb/box.env';
30
- const ME_TIMEOUT_MS = 3000;
31
-
32
- // Pure decision — given the auth facts, what should the card do? Exported +
33
- // unit-tested (tests/box_auth_check.mjs). Never does I/O.
34
- // onBox: provisioned dev box (/etc/otb/box.env present)
35
- // hasToken: a local session file with a token exists
36
- // isBoxScoped: that session is the baked box token (source === 'box')
37
- // meStatus: HTTP status of a bounded GET /me, or null (not attempted / network failed)
38
- // → { action: 'skip' | 'setup' | 'reauth' | 'ok', reason }
39
- //
40
- // Ordering matters: isBoxScoped is decided from the local file BEFORE any
41
- // meStatus, so the incident case never needs (or waits on) the network.
42
- function decideBoxAuth({ onBox, hasToken, isBoxScoped, meStatus }) {
43
- if (!onBox) return { action: 'skip', reason: 'not-a-box' };
44
- if (!hasToken) return { action: 'setup', reason: 'no-session' };
45
- if (isBoxScoped) return { action: 'reauth', reason: 'box-scoped' };
46
- if (meStatus === 200) return { action: 'ok', reason: 'validated' };
47
- if (meStatus === 401 || meStatus === 403) return { action: 'reauth', reason: 'expired-or-revoked' };
48
- // Couldn't definitively verify (no network / fetch unavailable / timeout /
49
- // 5xx). A locally valid-looking real token is trusted; stay silent rather
50
- // than warn falsely — the point-of-use guard (cli-lib) still catches a real
51
- // 401/403 the moment a command runs.
52
- return { action: 'skip', reason: 'unverified' };
53
- }
54
-
55
- function onBox() {
56
- try {
57
- return fs.existsSync(BOX_ENV_PATH);
58
- } catch (_) {
59
- return false;
60
- }
61
- }
62
-
63
- // Bounded, fail-open GET /me. Returns { status, login }; status is null and
64
- // login is null on ANY failure (no global fetch, network error, abort/timeout).
65
- // Never throws.
66
- async function probeMe(tokenInfo) {
67
- if (!tokenInfo || !tokenInfo.token || typeof fetch !== 'function') {
68
- return { status: null, login: null };
69
- }
70
- const ctl = new AbortController();
71
- const timer = setTimeout(() => ctl.abort(), ME_TIMEOUT_MS);
72
- try {
73
- const res = await fetch(`${tokenInfo.apiBase}/api/bongos/me`, {
74
- method: 'GET',
75
- headers: { Authorization: `Bearer ${tokenInfo.token}`, 'User-Agent': 'box-auth-check/1.0' },
76
- signal: ctl.signal,
77
- });
78
- let login = null;
79
- if (res.ok) {
80
- try {
81
- const j = await res.json();
82
- login = (j && j.builder && j.builder.github_login) || null;
83
- } catch (_) { /* body parse is cosmetic only */ }
84
- }
85
- return { status: res.status, login };
86
- } catch (_) {
87
- return { status: null, login: null };
88
- } finally {
89
- clearTimeout(timer);
90
- }
91
- }
92
-
93
- function printReauth({ boxScoped }) {
94
- const why = boxScoped
95
- ? 'This box holds the BOX-SCOPED bootstrap token (source=box, ADR 0053) — it can bootstrap the box but is refused (403) on claim / ship / publish.'
96
- : 'This session token is REVOKED or otherwise rejected (CLI tokens do not expire on their own — ADR 0294).';
97
- console.log('[otb] ── dev box: RE-AUTH BEFORE YOU WORK ──');
98
- console.log(`[otb] ${why}`);
99
- console.log('[otb] ⚠ FIRST STEP this session: get the builder onto a real CLI session BEFORE claiming or shipping —');
100
- console.log('[otb] run /builder-reauth (UI-first: opens Settings → "Re-issue CLI token" → paste the one-liner back).');
101
- console.log('[otb] Work claimed on this token cannot be shipped/published — it will strand at the publish 403 wall.');
102
- }
103
-
104
- function printSetup() {
105
- console.log('[otb] ── dev box: NO GDS SESSION — SIGN IN BEFORE YOU WORK ──');
106
- console.log('[otb] ⚠ No CLI session was found on this box. FIRST STEP this session: sign in before claiming or');
107
- console.log('[otb] shipping any work — run /builder-reauth (UI-first), or /builder-setup for a brand-new machine.');
108
- }
109
-
110
- async function main() {
111
- if (process.env.OTB_SUBAGENT) return;
112
- if (!onBox()) return;
113
-
114
- let lib;
115
- let session = null;
116
- let tokenInfo = null;
117
- try {
118
- lib = require('./cli-lib');
119
- session = lib.loadSessionSync();
120
- tokenInfo = lib.readSessionToken();
121
- } catch (_) {
122
- // cli-lib unreadable → treat as no-session (the setup path), which is the
123
- // safe, actionable default rather than silence.
124
- }
125
-
126
- const hasToken = !!(session && session.token);
127
- const isBoxScoped = !!(session && session.source === 'box');
128
-
129
- // Only pay a network round-trip when a real (non-box) token is present — the
130
- // box-scoped and no-session cases are already decided from the file alone.
131
- let meStatus = null;
132
- let login = null;
133
- if (hasToken && !isBoxScoped) {
134
- ({ status: meStatus, login } = await probeMe(tokenInfo));
135
- }
136
-
137
- const { action } = decideBoxAuth({ onBox: true, hasToken, isBoxScoped, meStatus });
138
- if (action === 'reauth') {
139
- printReauth({ boxScoped: isBoxScoped });
140
- } else if (action === 'setup') {
141
- printSetup();
142
- } else if (action === 'ok') {
143
- console.log(`[otb] ✓ dev box: GDS session is a real CLI session${login ? ` (signed in as ${login})` : ''} — clear to claim + ship.`);
144
- }
145
- // action === 'skip' → say nothing (off-box handled above; 'unverified' is silent).
146
- }
147
-
148
- // Direct run (the SessionStart hook spawns `node .../box-auth-check.js`) — run
149
- // the check and exit fast. A require() of this file (the unit test importing
150
- // decideBoxAuth) must NOT run main, so guard on require.main. The watchdog is
151
- // belt-and-suspenders: the SessionStart hook spawns this synchronously with no
152
- // timeout of its own, so guarantee a bounded exit even if something wedges (the
153
- // fetch already self-aborts at ME_TIMEOUT_MS).
154
- if (require.main === module) {
155
- const watchdog = setTimeout(() => process.exit(0), ME_TIMEOUT_MS + 1500);
156
- watchdog.unref();
157
- main().then(() => process.exit(0)).catch(() => process.exit(0));
158
- }
159
-
160
- module.exports = { decideBoxAuth, onBox, printReauth, printSetup };
@@ -1,321 +0,0 @@
1
- // scripts/gds/box-infra.js — the dev-box CONFIG block plus everything that
2
- // turns it into a bootable droplet: infra/ resolution, naming, the DNS hook,
3
- // and cloud-init rendering.
4
- //
5
- // Carved out of scripts/gds/box.js by task 1003206 (goal 1000079, the R03
6
- // de-monolith plan) to bring that file under the 1,500-line ratchet. A
7
- // STRUCTURAL move: the lines below are box.js's originals unchanged, and box.js
8
- // requires every name straight back, so the operator CLI is untouched.
9
-
10
- 'use strict';
11
-
12
- const fs = require('node:fs');
13
- const path = require('node:path');
14
- const zlib = require('node:zlib');
15
- const { execFileSync } = require('node:child_process');
16
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
17
- const instanceConfig = require(path.join(REPO_ROOT, 'src/instance-config'));
18
- const BOX_INFRA_TEMPLATE = 'builder-box-cloud-init.yaml';
19
-
20
- function boxInfraCandidates({ env = process.env, repoRoot = REPO_ROOT } = {}) {
21
- const out = [];
22
- const seen = new Set();
23
- const add = (d) => { if (d && !seen.has(d)) { seen.add(d); out.push(d); } };
24
- // 1. explicit override: <PREFIX>_BOX_INFRA_DIR (operator escape hatch).
25
- const override = instanceConfig.resolveEnv('BOX_INFRA_DIR', { env });
26
- if (override) add(path.resolve(override));
27
- // 2. the instance root via an EXPLICIT pointer (<PREFIX>_INSTANCE_ROOT env or a
28
- // bongos.config.json {"instanceRoot":"."}) — where a vendored instance keeps infra/.
29
- const inst = instanceConfig.resolveInstanceRootExplicit({ env });
30
- if (inst) add(path.join(inst, 'infra'));
31
- // 3. REPO_ROOT/infra — the dev single-checkout (and the ADR 0144 node_modules workaround).
32
- add(path.join(repoRoot, 'infra'));
33
- // 4. walk up out of node_modules from the core root, so a vendored
34
- // …/node_modules/@bongos/core still finds its host instance's infra/ with no pointer set.
35
- let dir = repoRoot;
36
- for (let i = 0; i < 6; i += 1) {
37
- const parent = path.dirname(dir);
38
- if (parent === dir) break;
39
- dir = parent;
40
- add(path.join(dir, 'infra'));
41
- }
42
- return out;
43
- }
44
-
45
- // The resolved infra/ dir (first candidate holding the template), or null when the
46
- // box cloud-init cannot be located anywhere — the caller MUST fail loud on null
47
- // rather than provision a box with empty user-data (ADR 0150).
48
- function resolveBoxInfraDir(opts = {}) {
49
- for (const c of boxInfraCandidates(opts)) {
50
- try { if (fs.existsSync(path.join(c, BOX_INFRA_TEMPLATE))) return c; }
51
- catch { /* unreadable candidate → try the next */ }
52
- }
53
- return null;
54
- }
55
-
56
- // task 1002757 — the instance's own apex; null when unconfigured (callers refuse).
57
- const { instanceApex } = require('./instance-apex');
58
-
59
- const CONFIG = {
60
- floor: process.env.BOX_PROVISION_MIN_RANK || 'xenos',
61
- region: process.env.BOX_REGION || 'nyc3',
62
- size: process.env.BOX_SIZE || 's-2vcpu-4gb',
63
- baseImage: process.env.BOX_BASE_IMAGE || 'ubuntu-24-04-x64',
64
- idleMinutes: numEnv('BOX_IDLE_MINUTES', 10),
65
- // task 1003507 — the longest a box may stay active without evidence that a
66
- // HUMAN was attached to it. One number bounds both proxies that used to keep a
67
- // box alive on their own (a running `claude` process; load above the floor), so
68
- // an abandoned session can no longer pin a droplet forever. 12h is deliberately
69
- // generous — longer than any interactive session, long enough for a real
70
- // overnight autonomous run — because being wrong costs one wake, while having
71
- // no cap at all cost $20.21 on a single box.
72
- unattendedMaxHours: numEnv('BOX_UNATTENDED_MAX_HOURS', 12),
73
- dormantDays: numEnv('BOX_DORMANT_DAYS', 14),
74
- // task 1002727 — how many days BEFORE the reclaim a builder gets warned.
75
- dormantWarnLeadDays: numEnv('BOX_DORMANT_WARN_LEAD_DAYS', 3),
76
- sshKeyIds: (process.env.BOX_SSH_KEY_IDS || '').split(',').map((s) => s.trim()).filter(Boolean),
77
- dnsHook: process.env.BOX_DNS_HOOK || null,
78
- tag: process.env.BOX_TAG || 'otb-builder-box',
79
- // task 1002757 settled this file's old carve-out: an operator-plane DNS zone is
80
- // still a FUNCTIONAL DEFAULT, and a founder literal aimed another operator's
81
- // fleet at the founder's zone. Derived from the instance apex, null when
82
- // unconfigured — hostnameFor() then refuses instead of minting a dead FQDN.
83
- dnsZone: process.env.BOX_DNS_ZONE || (instanceApex() ? `dev.${instanceApex()}` : null),
84
- };
85
-
86
- // Named-tunnel automation for the browser terminal (#771, ADR 0038 Part B).
87
- // OPT-IN, default OFF — an unset BOX_NAMED_TUNNEL leaves the box on the tokenless
88
- // quick tunnel (trycloudflare.com), the zero-secret #760 default. When the operator
89
- // turns it on AND supplies a Cloudflare token with Tunnel + DNS scope (the same
90
- // CLOUDFLARE_API_TOKEN box-dns uses) + the account id, every provision creates a
91
- // stable term-<login>.<terminalZone> tunnel and bakes its connector token into the
92
- // box; deprovision tears it down.
93
- const CF = {
94
- enabled: /^(1|true|yes)$/i.test(process.env.BOX_NAMED_TUNNEL || ''),
95
- apiToken: process.env.CLOUDFLARE_API_TOKEN || null,
96
- accountId: process.env.CLOUDFLARE_ACCOUNT_ID || null,
97
- // The Cloudflare ZONE the record lives in (the registrable apex) — the terminal
98
- // host is a record WITHIN it, matching box-dns-cloudflare.sh. Distinct from
99
- // terminalZone (the host suffix). Both resolve from the instance (task 1002757).
100
- zone: process.env.CLOUDFLARE_ZONE || instanceApex(),
101
- // Host suffix for term-<login>.<terminalZone>. DEFAULTS TO THE APEX, NOT dev.*:
102
- // the terminal rides a PROXIED tunnel, and free Universal SSL covers <apex> +
103
- // *.<apex> (ONE label) but not a TWO-label host like term-x.dev.<apex>, which
104
- // fails TLS at the edge (verified live, #771). An operator holding an Advanced
105
- // Certificate for *.dev.<apex> can override via BOX_TERMINAL_ZONE. (The SSH
106
- // A-record path stays on dev.* — proxied=false, so it needs no edge cert.)
107
- terminalZone: process.env.BOX_TERMINAL_ZONE || instanceApex(),
108
- };
109
-
110
- // Named-tunnel automation is live only when the operator opted in AND the
111
- // credentials are present. Missing creds while enabled is a loud config error at
112
- // provision time (see cmdProvision), not a silent fall-back, so an operator who
113
- // meant to use named tunnels learns immediately.
114
- function numEnv(name, dflt) {
115
- const v = process.env[name];
116
- if (v == null || v === '') return dflt;
117
- const n = Number(v);
118
- return Number.isFinite(n) && n > 0 ? n : dflt;
119
- }
120
-
121
- function hasFlag(args, name) { return args.includes(name); }
122
-
123
- // Lazy + memoized DB + lib load so `--help` and arg errors don't require Postgres
124
- // (this CLI runs on a non-droplet machine for dry-run planning of pure logic too):
125
- // nothing is require()d until the first makeDo/makeCf/IIFE call, and repeat calls
126
- // reuse the first resolution instead of re-running the six requires.
127
- let _deps;
128
- function shortStamp() {
129
- return new Date().toISOString().replace(/[-:T.Z]/g, '').slice(2, 14);
130
- }
131
-
132
- function dropletName(login) { return `otb-box-${login}`; }
133
- // Refuses on an unresolved zone (task 1002757) — `box-x.undefined` would mint a
134
- // DNS record and a box that can never be reached.
135
- function hostnameFor(login) {
136
- if (!CONFIG.dnsZone) throw new Error('box: no DNS zone configured — set BOX_DNS_ZONE, or give this instance a domains.publicOrigin in config/branding.json (task 1002757).');
137
- return `box-${login}.${CONFIG.dnsZone}`;
138
- }
139
-
140
- // Run the optional DNS hook so the per-builder hostname tracks the new IP; with no
141
- // hook configured, print the reminder — the stable-hostname CUTOVER is #599's, #598
142
- // only records IP + hostname and offers the hook so an operator can wire DNS without
143
- // editing this file. → { ok, error?, skipped? }. task 1180: callers MUST act on
144
- // ok:false (box_events row + flag the box), never continue silently — that swallow
145
- // left @kingaonline's A record pinned at a dead IP while provision reported success.
146
- // The hook retries transient Cloudflare blips, so ok:false is a real, exhausted
147
- // failure. `hook` is injectable so the contract is unit-testable (#1034).
148
- function runDnsHook(hostname, ip, hook = CONFIG.dnsHook) {
149
- if (!ip) return { ok: true, skipped: true };
150
- if (!hook) {
151
- console.log(` ↳ DNS: point ${hostname} → ${ip} (no BOX_DNS_HOOK set; #599 owns the cutover)`);
152
- return { ok: true, skipped: true };
153
- }
154
- try {
155
- execFileSync('bash', ['-c', hook], {
156
- cwd: REPO_ROOT, stdio: 'pipe',
157
- env: { ...process.env, BOX_HOSTNAME: hostname, BOX_IP: ip },
158
- });
159
- console.log(` ↳ DNS hook updated ${hostname} → ${ip}`);
160
- return { ok: true };
161
- } catch (e) {
162
- const msg = (e.stderr && e.stderr.toString().trim()) || e.message;
163
- console.log(` ⚠ DNS hook failed: ${msg} (point ${hostname} → ${ip} manually)`);
164
- return { ok: false, error: msg };
165
- }
166
- }
167
-
168
- // Blackhole sentinel for a torn-down box's A record (idea 309). 192.0.2.1 is in
169
- // TEST-NET-1 (RFC 5737) — reserved for documentation and GUARANTEED never to be a
170
- // routable host, so no recycled DigitalOcean IP can ever land here. A perfect
171
- // dead end to park a record at.
172
- const DNS_BLACKHOLE_IP = '192.0.2.1';
173
-
174
- // Park the per-builder A record at the blackhole sentinel on deprovision (idea 309,
175
- // refining task 1180). 1180 DELETED the record so it couldn't dangle at a recycled
176
- // DO IP, but delete-then-recreate across a reap→rebuild left an NXDOMAIN window that
177
- // resolvers negative-cache for the zone's SOA neg-TTL (~30 min, not builder-editable)
178
- // — a client querying then kept getting ENOTFOUND long after the box was back.
179
- // REPOINTING to a non-routable sentinel keeps the name always-resolving (nothing to
180
- // negative-cache) while preserving 1180's guarantee that a torn-down box's name can
181
- // never dial a stranger's recycled IP; the next provision/wake PUTs the live IP
182
- // in-place at the 60s TTL, no gap. Best-effort like the prior delete. Reuses
183
- // runDnsHook's UPSERT path; `hook` injectable for tests. → { ok, error?, skipped? }
184
- function runDnsHookBlackhole(hostname, hook = CONFIG.dnsHook) {
185
- if (!hook || !hostname) return { ok: true, skipped: true };
186
- return runDnsHook(hostname, DNS_BLACKHOLE_IP, hook);
187
- }
188
-
189
- // Read the cloud-init template and substitute the repo URL from BOX_REPO_URL
190
- // (kept out of the repo file so no GitHub identity is hardcoded — V3.R12 rule).
191
- // An unset URL leaves the placeholder; the cloud-init then logs guidance rather
192
- // than cloning, and #600 wires the scoped credential.
193
- // Read + render the cloud-init. `authorizedKeysBlock` is the otb-managed block
194
- // for the builder being provisioned (#685) — base64'd here and baked in so the
195
- // FIRST SSH login works before the box has a GDS token. An empty/absent block
196
- // renders an empty managed block (a keyless box; the operator is warned upstream).
197
- function readCloudInit({ authorizedKeysBlock = '', boxEnv = '', gdsSession = '', infraDir = resolveBoxInfraDir() } = {}) {
198
- // Fail loud, never provision a BARE box (ADR 0150). No infra/ located → there is
199
- // no cloud-init to render, so THROW here rather than the old silent `return null`
200
- // that let cmdProvision ship empty user-data (a box that boots as bare Ubuntu).
201
- if (!infraDir) {
202
- throw new Error(
203
- `box cloud-init not found: no infra/${BOX_INFRA_TEMPLATE} in any candidate location `
204
- + `(set <PREFIX>_BOX_INFRA_DIR or <PREFIX>_INSTANCE_ROOT, or ship infra/). A box must `
205
- + `never be provisioned without user-data — it would boot bare (ADR 0150).`,
206
- );
207
- }
208
- const p = path.join(infraDir, BOX_INFRA_TEMPLATE);
209
- // gzip+base64 a repo file into the cloud-init write_files (`encoding: gz+b64`).
210
- // EVERY inlined infra file goes through this: cloud-init gunzips it back to the
211
- // exact bytes on the box, so it is byte-identical to plain b64 but ~2-3x smaller,
212
- // which keeps the rendered user-data well under DO's 64 KiB cap (#903; the source
213
- // -fetch growth in task 1314 had pushed the all-b64-but-source-fetch render to
214
- // 67 KB — gz+b64-ing the rest brings it back to ~51 KB, task 1321). Only the
215
- // dynamic runtime values (authorized_keys, box.env, gds-session) stay plain b64
216
- // below — they are minted per-provision, not read from infra/. A missing inlined
217
- // file is COLLECTED and fails loud after the render (ADR 0150) — a half-wired box
218
- // (that component silently never boots) is a bug, not something to warn past.
219
- //
220
- // Every name passed to fileGzB64 below is INSTANCE-owned and deliberately absent
221
- // from the neutral core's own infra/ — see infra/README.md (ADR 0150).
222
- const missingInfra = [];
223
- const fileGzB64 = (name) => {
224
- try {
225
- return zlib.gzipSync(fs.readFileSync(path.join(infraDir, name))).toString('base64');
226
- } catch {
227
- missingInfra.push(name);
228
- return '';
229
- }
230
- };
231
- const raw = fs.readFileSync(p, 'utf8');
232
- // Inject the rank-gated source-fetch (#600) + authorized-keys-fetch (#685)
233
- // scripts as gz+b64 (a single YAML-safe token — no indentation/escaping
234
- // pitfalls; cloud-init gunzips encoding: gz+b64 back to the exact file), plus
235
- // the baked authorized_keys block. ONE canonical copy of each lives in infra/.
236
- const rendered = raw
237
- .replace(/__REPO_URL__/g, process.env.BOX_REPO_URL || '')
238
- .replace(/__BOX_SOURCE_FETCH_B64__/g, fileGzB64('box-source-fetch.sh'))
239
- .replace(/__BOX_AUTHKEYS_FETCH_B64__/g, fileGzB64('box-authorized-keys-fetch.sh'))
240
- .replace(/__BUILDER_AUTHORIZED_KEYS_B64__/g, Buffer.from(authorizedKeysBlock || '', 'utf8').toString('base64'))
241
- // /etc/otb/box.env — empty by default (quick tunnel); the named-tunnel
242
- // connector token + stable hostname when BOX_NAMED_TUNNEL is on (#771).
243
- .replace(/__BOX_ENV_B64__/g, Buffer.from(boxEnv || '', 'utf8').toString('base64'))
244
- // /root/.config/otb/bongos-session.json — the builder's Bongos session, minted at
245
- // provision so box-source-fetch (#600) can authenticate on FIRST boot and
246
- // clone the rank-scoped repo. Without it the box never clones (the #818
247
- // chicken-and-egg: the session normally comes from /builder-setup, which
248
- // lives in the un-cloned repo).
249
- .replace(/__GDS_SESSION_B64__/g, Buffer.from(gdsSession || '', 'utf8').toString('base64'))
250
- // #774: the browser-terminal units, inlined so they start at FIRST BOOT
251
- // (independent of the repo clone). ONE canonical copy lives in infra/.
252
- .replace(/__BOX_TERMINAL_SERVICE_B64__/g, fileGzB64('box-terminal.service'))
253
- // #917: credential-drift watcher pair — restarts ttyd when the credential
254
- // file lands/changes after the service started (the wake/first-boot race).
255
- .replace(/__BOX_TERMINAL_CREDSYNC_SERVICE_B64__/g, fileGzB64('box-terminal-credsync.service'))
256
- .replace(/__BOX_TERMINAL_CREDENTIAL_PATH_B64__/g, fileGzB64('box-terminal-credential.path'))
257
- .replace(/__CLOUDFLARED_SERVICE_B64__/g, fileGzB64('cloudflared.service'))
258
- .replace(/__CLOUDFLARED_RUN_B64__/g, fileGzB64('cloudflared-run.sh'))
259
- // #893: the `close-box` teardown command, present at first boot so a bare
260
- // browser-only box has it before any repo clone (box-source-fetch refreshes
261
- // it on existing boxes). ONE canonical copy lives in infra/close-box.sh.
262
- .replace(/__CLOSE_BOX_B64__/g, fileGzB64('close-box.sh'))
263
- // task 986: the browser-terminal auto-launcher — runs `claude` on first
264
- // connect so the builder skips the "type claude" step. box-source-fetch
265
- // refreshes it on existing boxes. ONE canonical copy in infra/otb-box-launch.sh.
266
- .replace(/__OTB_BOX_LAUNCH_B64__/g, fileGzB64('otb-box-launch.sh'));
267
- if (missingInfra.length) {
268
- throw new Error(
269
- `box cloud-init is incomplete under ${infraDir}: missing ${missingInfra.join(', ')} `
270
- + `— a box would boot half-wired (ADR 0150). Fix the instance infra/ before provisioning.`,
271
- );
272
- }
273
- // ADR 0150 prong B named this escalation and never implemented it: "assert no
274
- // __…_B64__ placeholders survive the render, so a *partial* infra/ also surfaces
275
- // rather than half-wiring". The missingInfra check above only catches files THIS
276
- // file knows to ask for. It cannot catch the other direction — an instance
277
- // template carrying a token box-infra.js does not substitute. That renders a
278
- // cloud-init with the literal token in it, and cloud-init happily writes the
279
- // garbage as a file: the box boots half-wired, silently, which is the one
280
- // outcome ADR 0150 exists to prevent.
281
- //
282
- // The stakes are not hypothetical for one of them. __BUILDER_AUTHORIZED_KEYS_B64__
283
- // is the baked SSH key block; if a template misspells it, the substitution never
284
- // happens and the box comes up with a garbage authorized_keys — permanently
285
- // unreachable over SSH, exactly the failure task 1003635 was filed about (it
286
- // guessed the wrong mechanism, but the outcome it named was reachable this way).
287
- //
288
- // Same pattern the happy-path test has always asserted, now enforced in the code
289
- // rather than only in the test.
290
- // The pattern is the placeholder CONVENTION, so this assumes a template never
291
- // carries a literal __UPPER_SNAKE__ it means to keep. Nothing in this tree does,
292
- // and a false positive fails loud and legibly (it names the token) rather than
293
- // shipping a bad box — the safe direction to be wrong in. An instance that ever
294
- // needs a literal one should escape it in the template, not weaken this check.
295
- const survivors = [...new Set(rendered.match(/__[A-Z0-9_]+__/g) || [])];
296
- if (survivors.length) {
297
- throw new Error(
298
- `box cloud-init under ${infraDir} left ${survivors.length} placeholder(s) unsubstituted: `
299
- + `${survivors.join(', ')} — box-infra.js does not know these tokens, so the box would `
300
- + `boot half-wired (ADR 0150). Reconcile the instance template with this renderer.`,
301
- );
302
- }
303
- return rendered;
304
- }
305
-
306
-
307
- module.exports = {
308
- CF,
309
- CONFIG,
310
- boxInfraCandidates,
311
- dropletName,
312
- hasFlag,
313
- hostnameFor,
314
- instanceApex,
315
- numEnv,
316
- readCloudInit,
317
- resolveBoxInfraDir,
318
- runDnsHook,
319
- runDnsHookBlackhole,
320
- shortStamp,
321
- };