@bongos/core 1.18.0 → 1.18.18

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 (553) hide show
  1. package/.bongos-core.json +1740 -640
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +5 -3
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +11 -2
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ // modules/governance/rank-roles.js — the PURE rank→role rotation decision
4
+ // (ADR 0151, BV1.R95b).
5
+ //
6
+ // A builder holds exactly ONE seeded rank-role. governance_002 seeds four of them
7
+ // (is_system=true, seeded_rank set) and backfills each builder the one matching
8
+ // their rank — but that backfill is run-once, so nothing kept it true afterwards.
9
+ // This is the decision the writer (db.syncRankRole) executes to keep it true:
10
+ // given a builder's CURRENT assignments and a rank, which seeded role to ADD and
11
+ // which to REMOVE.
12
+ //
13
+ // THE REMOVAL IS THE SECURITY HALF. An additive-only sync would leave a demoted
14
+ // Archon still holding the `archon` role, so resolveBuilderPermissions() would keep
15
+ // returning Archon permissions. That is the exact privilege retention ADR 0016's
16
+ // "a demotion takes effect on the builder's very next request" promise forbids: the
17
+ // resolver is correctly per-request and uncached, but it reads an ASSIGNMENT, and a
18
+ // stale assignment breaks the freshness guarantee one layer beneath the resolver.
19
+ //
20
+ // CUSTOM ROLES ARE NEVER TOUCHED. An Archon-assigned role (is_system=false, the
21
+ // R110 tab's output) must survive a rank change — only rows that are BOTH
22
+ // is_system AND carry a seeded_rank rotate. Keying the removal on "every role this
23
+ // builder holds" would silently revoke delegated authority on every promotion.
24
+ //
25
+ // A rank with NO seeded role — one of the twelve reserved ranks migration 023's
26
+ // CHECK still admits — yields target=null: every seeded role is removed and none is
27
+ // added, so the builder resolves to ZERO permissions. That is governance_002's
28
+ // deliberate fail-closed divergence (ADR 0151), preserved rather than re-derived —
29
+ // never invent a role for a rank the seed does not define.
30
+ //
31
+ // Pure: no DB, no I/O, no module state. The writer reads the two row sets and
32
+ // executes this verdict, so the unit tests exercise production logic directly.
33
+
34
+ // A row (an assignment joined to governance_roles, or a role row) is a SEEDED
35
+ // rank-role only when it is both a system role and names the rank it reproduces.
36
+ // Both halves are required: is_system alone would admit a future non-rank system
37
+ // role, and seeded_rank alone would trust a column a custom role could carry.
38
+ function isSeededRankRole(row) {
39
+ return !!row
40
+ && row.is_system === true
41
+ && typeof row.seeded_rank === 'string'
42
+ && row.seeded_rank.length > 0;
43
+ }
44
+
45
+ // planRankRoleSync({ rank, currentRoles, seededRoles }) → { target, add, remove, next }
46
+ //
47
+ // rank the builder's rank AFTER the write being synced.
48
+ // currentRoles the builder's assignments, each { role_key, is_system, seeded_rank }.
49
+ // seededRoles the seeded rank-roles the DB defines, each { role_key, seeded_rank,
50
+ // is_system }. Passed in rather than hardcoded so the four role keys
51
+ // stay DB truth — this file never re-declares the seed.
52
+ //
53
+ // target the seeded role_key the builder should end up with, or null when the
54
+ // rank has no seeded role (fail-closed divergence, above).
55
+ // add [] or [target] — empty when it is already held (idempotence).
56
+ // remove every OTHER seeded rank-role currently held. Custom roles never appear.
57
+ // next the resulting role_key set, sorted — "what the role set becomes".
58
+ function planRankRoleSync({ rank, currentRoles = [], seededRoles = [] } = {}) {
59
+ const match = seededRoles.find((r) => isSeededRankRole(r) && r.seeded_rank === rank);
60
+ const target = match ? match.role_key : null;
61
+
62
+ const held = currentRoles.filter((r) => r && typeof r.role_key === 'string');
63
+ const remove = held
64
+ .filter((r) => isSeededRankRole(r) && r.role_key !== target)
65
+ .map((r) => r.role_key);
66
+ const add = target && !held.some((r) => r.role_key === target) ? [target] : [];
67
+
68
+ const removed = new Set(remove);
69
+ const next = held
70
+ .map((r) => r.role_key)
71
+ .filter((key) => !removed.has(key))
72
+ .concat(add)
73
+ .sort();
74
+
75
+ return Object.freeze({
76
+ target,
77
+ add: Object.freeze(add),
78
+ remove: Object.freeze(remove),
79
+ next: Object.freeze(next),
80
+ });
81
+ }
82
+
83
+ module.exports = { planRankRoleSync, isSeededRankRole };
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ // modules/governance/resolver.js — the permission resolver (ADR 0151, BV1.R95).
4
+ //
5
+ // Turns a builder's ROLES into their effective PERMISSION SET: the union of every
6
+ // permission granted to every role they hold. This is the heart of the
7
+ // permissions-as-atom model — `requirePermission()` (R97) and every migrated gate
8
+ // (R104-R107) resolve authority through here.
9
+ //
10
+ // TWO AXES, two entry points (R98, ADR 0151 §3). Roles are one axis; a machine
11
+ // PRINCIPAL's capability is the other, resolved from its exact `system_role` and
12
+ // never from a role. They are kept apart on purpose — see resolvePrincipalPermissions
13
+ // below, and the principal keys effectivePermissions drops.
14
+ //
15
+ // It preserves ADR 0016 exactly:
16
+ // • server-side — the answer is computed on the server from DB rows, never from
17
+ // a token claim or anything the client can influence;
18
+ // • per request — resolveBuilderPermissions() is called on the request that
19
+ // needs it, so a revoked role takes effect on the builder's very next request;
20
+ // • UNCACHED — there is no memo, no TTL, no process-local map. Caching authority
21
+ // is the one optimisation this file must never take (a cached grant survives
22
+ // its revocation, which is the failure mode the trust boundary exists to
23
+ // prevent). If a future profile shows the join is hot, the answer is a DB
24
+ // index, not a cache.
25
+ //
26
+ // Fail-closed throughout: a builder with no roles resolves to the EMPTY set, and a
27
+ // grant naming a key the code catalog doesn't define is DROPPED (see
28
+ // effectivePermissions below) rather than admitted on trust.
29
+
30
+ const catalog = require('./catalog');
31
+ const db = require('./db');
32
+ const sessionScopes = require('./session-scopes');
33
+
34
+ // Pure core of the ROLE axis, split out so it is unit-testable without a database.
35
+ //
36
+ // Filters raw grant rows against the code catalog: the catalog is the source of
37
+ // truth for what a permission IS (every key maps to a real route — modules/
38
+ // governance/CLAUDE.md), so a DB row naming an unknown key is stale or hand-
39
+ // inserted and grants NOTHING. Returns a frozen, sorted, de-duplicated array.
40
+ //
41
+ // It also DROPS every principal key (R98). A machine identity's capability is the
42
+ // other axis — it comes from `builders.system_role`, never from a role — so a grant
43
+ // row naming `principal.*` confers nothing here no matter how it got written (a
44
+ // hand-inserted row, a future bug in the governance CRUD's system-permission
45
+ // rejection). That makes "a role cannot confer a principal permission" structural
46
+ // rather than merely policed at the write side.
47
+ function effectivePermissions(rawKeys) {
48
+ const known = new Set();
49
+ for (const key of rawKeys || []) {
50
+ if (!catalog.byKey(key)) continue;
51
+ if (catalog.isPrincipalKey(key)) continue;
52
+ known.add(key);
53
+ }
54
+ return Object.freeze([...known].sort());
55
+ }
56
+
57
+ // The PRINCIPAL axis (R98, ADR 0151 §3) — deliberately a SEPARATE entry point, not
58
+ // a branch inside the role resolve. A builder's authority is the union of their
59
+ // roles; a machine principal's authority is its identity, and collapsing the two
60
+ // into one lookup is exactly the mistake that would let a rank reach a principal
61
+ // capability.
62
+ //
63
+ // Pure and DB-free: the only input is `builders.system_role`, which requireBuilder
64
+ // already reads fresh from the DB per request (alongside rank), and which no API
65
+ // route writes — a principal is provisioned out-of-band by migration, under a DB
66
+ // CHECK allowlist. The match is EXACT string equality, so NULL (every human,
67
+ // including an Archon), a differently-cased value, or an unknown role resolves to
68
+ // the empty set. Mirrors requireBfgPrincipal / requireDiscordBot in src/bongos/
69
+ // auth.js, which stay the HTTP enforcement point.
70
+ function resolvePrincipalPermissions(systemRole) {
71
+ const principal = catalog.principalByRole(systemRole);
72
+ return principal ? Object.freeze([principal.permission]) : Object.freeze([]);
73
+ }
74
+
75
+ // Does this exact machine identity hold this permission? Fail-closed on a blank or
76
+ // unknown identity, and on a key the identity does not own (one principal never
77
+ // reaches another's capability).
78
+ function principalHasPermission(systemRole, key) {
79
+ return resolvePrincipalPermissions(systemRole).includes(key);
80
+ }
81
+
82
+ // The resolve entry point. Returns the builder's effective permission set as a
83
+ // frozen sorted array of catalog keys. A null/invalid builder id resolves to the
84
+ // empty set (fail-closed) without touching the DB.
85
+ async function resolveBuilderPermissions(builderId) {
86
+ if (builderId === null || builderId === undefined || builderId === '') return Object.freeze([]);
87
+ const raw = await db.getPermissionKeysForBuilder(builderId);
88
+ return effectivePermissions(raw);
89
+ }
90
+
91
+ // Convenience for gate code: does this builder hold EVERY one of `required`?
92
+ // An empty `required` list is a programming error, not an open gate — it returns
93
+ // false so a mis-authored gate denies rather than admits (the same posture
94
+ // requireRank takes on an all-unknown allow-list).
95
+ //
96
+ // It also REFUSES ownership-scoped keys (R100). "Do you hold box.manage.own" has
97
+ // no unconditional answer — the permission is parameterized by a resource this
98
+ // function was never given — and returning true would be precisely the collapse
99
+ // of `.own` into an unconditional grant the axis exists to prevent. The caller
100
+ // wants authorizeOwned(), which cannot be called without naming the owner.
101
+ async function builderHasPermissions(builderId, required) {
102
+ if (!Array.isArray(required) || required.length === 0) return false;
103
+ if (required.some((k) => catalog.isOwnershipScoped(k))) return false;
104
+ const held = new Set(await resolveBuilderPermissions(builderId));
105
+ return required.every((k) => held.has(k));
106
+ }
107
+
108
+ // ── the OWNERSHIP axis (R100, ADR 0151 §3) ──────────────────────────────────────
109
+ // The third entry point, and a SEPARATE one for the same reason the principal axis
110
+ // is separate: an ownership-scoped permission is a conjunction of two facts from
111
+ // two different places, and folding it into the plain resolve would silently drop
112
+ // the second one.
113
+ //
114
+ // 1. the ROLE fact — does the builder hold the key? (this file, from the DB)
115
+ // 2. the OWNER fact — is the concrete row theirs? (REQUEST-scoped; only the
116
+ // handler that loaded the row knows it)
117
+ //
118
+ // The resolver structurally cannot know (2): it is handed a builder id, never a
119
+ // box/claim/memory row. So the caller SUPPLIES the owner and the model refuses to
120
+ // answer without it — `ownerId` is not an option, it is the second half of the
121
+ // question. That is what keeps `.own` from degrading into `.any`: there is no code
122
+ // path here that turns an ownership-scoped key into an admit on its own.
123
+ //
124
+ // Pure + DB-free where it can be: the comparison below never touches Postgres, and
125
+ // the role fact reuses resolveBuilderPermissions, so ADR 0016 holds unchanged —
126
+ // per-request, server-side, no stored answer.
127
+
128
+ // The owner comparison, normalized once so ~15 sites stop hand-rolling it. STRICT:
129
+ // only a scalar id (Postgres hands bigints back as strings, JS code carries them as
130
+ // numbers, so 7 and '7' must match) — anything else, including null/undefined, an
131
+ // object, an array, a boolean, NaN or an empty/blank string, has NO identity and
132
+ // therefore matches NOTHING. No trimming and no case-folding: normalizing harder
133
+ // than the data warrants is how a comparison quietly widens.
134
+ function ownerIdentity(value) {
135
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null;
136
+ if (typeof value === 'bigint') return String(value);
137
+ if (typeof value === 'string') return value === '' ? null : value;
138
+ return null;
139
+ }
140
+ function sameOwner(a, b) {
141
+ const left = ownerIdentity(a);
142
+ const right = ownerIdentity(b);
143
+ return left !== null && right !== null && left === right;
144
+ }
145
+
146
+ // Does this builder hold `key` AND own the resource `ownerId` identifies?
147
+ // Fail-closed on every unhappy path — an unknown key, a key that is NOT
148
+ // ownership-scoped (wrong entry point; the plain resolve answers those), a missing
149
+ // or unidentifiable owner, or a builder who simply does not hold the key.
150
+ async function authorizeOwned(builderId, key, context) {
151
+ const permission = catalog.byKey(key);
152
+ if (!permission || permission.scope !== 'own') return false;
153
+ if (!sameOwner(context && context.ownerId, builderId)) return false;
154
+ return (await resolveBuilderPermissions(builderId)).includes(key);
155
+ }
156
+
157
+ // The composition every own-scoped route actually wants: reach this row because it
158
+ // is YOURS, or because you hold the cross-owner permission over its resource
159
+ // family (claim.release.any, memory.read.any, box.fleet.manage). One resolve, both
160
+ // branches. A resource with no cross-owner form by design (credential) simply has
161
+ // no second branch, so the answer stays own-only.
162
+ async function authorizeOwnedOrAny(builderId, ownKey, context) {
163
+ const permission = catalog.byKey(ownKey);
164
+ if (!permission || permission.scope !== 'own') return false;
165
+ const held = await resolveBuilderPermissions(builderId);
166
+ if (catalog.crossOwnerKeysFor(ownKey).some((k) => held.includes(k))) return true;
167
+ return sameOwner(context && context.ownerId, builderId) && held.includes(ownKey);
168
+ }
169
+
170
+ // ── the SESSION-SCOPE axis (R99, ADR 0151 §3) ───────────────────────────────────
171
+ // The FOURTH entry point, separate for the same reason the other three are: it
172
+ // answers a different question with a different input. Roles answer "what may this
173
+ // PERSON do"; a principal answers "what may this MACHINE do"; an ownership scope
174
+ // answers "whose row may it be done to"; a session-scope answers "how much of that
175
+ // does THIS CREDENTIAL carry". A dev-box token is minted onto a machine outside the
176
+ // ADR 0016 trust boundary (#919/SR#2, ADR 0053), so it must stay limited even when
177
+ // its builder is an Archon — which no role-shaped answer can express.
178
+ //
179
+ // A SESSION-SCOPE CAN ONLY EVER NARROW, in both directions:
180
+ // • INTERSECTION, never union. The scope may not GRANT a permission the roles do
181
+ // not confer — so the composition below is written as a FILTER of the ROLE
182
+ // answer. There is no branch here that can add a key, which is what makes
183
+ // "a box token cannot escalate" structural rather than merely intended.
184
+ // • DENY-BY-DEFAULT. A key the allow-list does not name is denied; an
185
+ // unrecognised session source resolves to NOTHING rather than to "full".
186
+ // Widening is therefore not expressible without deleting the filter, and R103's
187
+ // parity is untouched: effectivePermissions is the unchanged ROLE answer this
188
+ // composes ON TOP of, never inside.
189
+ //
190
+ // Pure and DB-free (the DB half is resolveBuilderPermissions'), so an unknown key
191
+ // on an allow-list is inert as well as refused at load: it can never appear in the
192
+ // catalog-filtered role answer, so it can never survive the intersection.
193
+ function narrowToSessionScope(rawKeys, source) {
194
+ const scope = sessionScopes.scopeFor(source);
195
+ if (!scope) return Object.freeze([]); // unrecognised source → no authority at all
196
+ const held = effectivePermissions(rawKeys);
197
+ if (scope.unscoped) return held; // this credential narrows nothing (cli/web)
198
+ const allowed = new Set(scope.permissions);
199
+ return Object.freeze(held.filter((key) => allowed.has(key)));
200
+ }
201
+
202
+ // What can THIS SESSION actually do? The builder's roles ∩ this session's scope,
203
+ // as a frozen sorted array of catalog keys. Fails closed BEFORE touching the DB on
204
+ // a source the registry does not declare.
205
+ async function resolveSessionPermissions(builderId, source) {
206
+ if (!sessionScopes.scopeFor(source)) return Object.freeze([]);
207
+ return narrowToSessionScope(await resolveBuilderPermissions(builderId), source);
208
+ }
209
+
210
+ module.exports = {
211
+ resolveBuilderPermissions,
212
+ builderHasPermissions,
213
+ effectivePermissions,
214
+ resolvePrincipalPermissions,
215
+ principalHasPermission,
216
+ sameOwner,
217
+ authorizeOwned,
218
+ authorizeOwnedOrAny,
219
+ narrowToSessionScope,
220
+ resolveSessionPermissions,
221
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ // The governance module's kernel PORT registration (ADR 0151, BV1.R95).
4
+ //
5
+ // governance exposes the permission RESOLVER as the `governance` kernel PORT.
6
+ // CORE files that must NOT import a module resolve it instead — first among them
7
+ // src/bongos/auth.js, whose `requirePermission()` gate (R97) needs a builder's
8
+ // effective permission set on every gated request. The one-way rule (ADR 0083)
9
+ // forbids core requiring modules/governance/ directly; the port is how the
10
+ // capability crosses that line.
11
+ //
12
+ // Why a route factory for a (currently) routeless capability: `mountModuleRoutes`
13
+ // is the one boot hook that runs BOTH in the live server AND whenever a test
14
+ // rebuilds the GDS router, so registering here keeps the port present for the
15
+ // route-building tests too. Registration is hasProvider-guarded so
16
+ // re-instantiation never trips the seam's single-provider duplicate guard.
17
+ // (Mirrors modules/economy/routes/reward.js.) The governance CRUD endpoints land
18
+ // on this same router in R110.
19
+
20
+ const express = require('express');
21
+ const api = require('../../../src/module-api');
22
+ const resolver = require('../resolver');
23
+ const catalog = require('../catalog');
24
+ const db = require('../db');
25
+ const sessionScopes = require('../session-scopes');
26
+
27
+ // The port surface. Deliberately narrow: resolution + the catalog reads a gate or
28
+ // the Archon tab needs. It grows with R98-R101 (principals, session-scopes,
29
+ // ownership scoping, the protected-surface registry) — one object port rather
30
+ // than a port per capability, so core resolves `governance` once.
31
+ //
32
+ // The principal half (R98) is READ-ONLY by construction: resolution keyed on an
33
+ // exact system_role, plus `listPrincipals` for the tab to render. There is no
34
+ // create/grant/assign counterpart and there must never be one — a principal is
35
+ // provisioned out-of-band in `builders.system_role` (migration + DB CHECK
36
+ // allowlist), so the tab can SHOW a principal but has nothing to call to make one.
37
+ //
38
+ // The ownership half (R100) is why an own-scoped route reaches the port from
39
+ // INSIDE its handler rather than from a middleware: the second half of the
40
+ // decision is the concrete row's owner, which only the handler that loaded the row
41
+ // knows. `authorizeOwned`/`authorizeOwnedOrAny` take that owner as an argument, so
42
+ // there is no arity at which an ownership-scoped key resolves to an admit on its
43
+ // own (ADR 0151 §3's in-handler pure-gate pattern, made a first-class call).
44
+ // Callers must fail CLOSED when the port is absent, exactly as requirePermission
45
+ // does — a missing authority system is a denial, never an admission.
46
+ //
47
+ // The session-scope half (R99) is READ-ONLY and deliberately NOT the enforcement
48
+ // path: the kernel gate (auth.requireBuilder) reads the same registry as static
49
+ // data, so a request never depends on this port being registered to stay
50
+ // deny-by-default. What the port adds is the composed answer — roles ∩ this
51
+ // session's scope — for the governance tab and for any gate that wants the
52
+ // session's real authority rather than the builder's.
53
+ const governancePort = {
54
+ resolveBuilderPermissions: resolver.resolveBuilderPermissions,
55
+ builderHasPermissions: resolver.builderHasPermissions,
56
+ getRoleKeysForBuilder: db.getRoleKeysForBuilder,
57
+ isSystem: catalog.isSystem,
58
+ isGrantable: catalog.isGrantable,
59
+ permissionKeys: catalog.permissionKeys,
60
+ resolvePrincipalPermissions: resolver.resolvePrincipalPermissions,
61
+ principalHasPermission: resolver.principalHasPermission,
62
+ isPrincipalKey: catalog.isPrincipalKey,
63
+ listPrincipals: catalog.principals,
64
+ authorizeOwned: resolver.authorizeOwned,
65
+ authorizeOwnedOrAny: resolver.authorizeOwnedOrAny,
66
+ sameOwner: resolver.sameOwner,
67
+ isOwnershipScoped: catalog.isOwnershipScoped,
68
+ crossOwnerKeysFor: catalog.crossOwnerKeysFor,
69
+ resolveSessionPermissions: resolver.resolveSessionPermissions,
70
+ narrowToSessionScope: resolver.narrowToSessionScope,
71
+ sessionScopeFor: sessionScopes.scopeFor,
72
+ listSessionScopes: () => sessionScopes.SESSION_SCOPES,
73
+ };
74
+
75
+ function registerSeams() {
76
+ if (!api.hasProvider('governance')) {
77
+ api.registerProvider('governance', governancePort);
78
+ }
79
+ }
80
+
81
+ module.exports = function buildGovernanceRouter() {
82
+ registerSeams();
83
+ // No HTTP surface yet — the resolver is a pure kernel port. The empty router is
84
+ // only the loader's contributes.routes mount vehicle; R110 adds the roles /
85
+ // permissions / assignments CRUD here behind `governance.manage`.
86
+ return express.Router();
87
+ };
88
+
89
+ module.exports.governancePort = governancePort;
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ // The governance module's rank-role SYNC port (ADR 0151, BV1.R95b).
4
+ //
5
+ // A second, deliberately tiny kernel port beside `governance` (routes/governance.js).
6
+ // `governance` is the READ surface every gate resolves on every request; this is the
7
+ // single WRITE core needs — "this builder's rank is now X, make their seeded
8
+ // rank-role match". They are kept apart on purpose:
9
+ // • the read port is on the hot path of every permission-gated request and is
10
+ // resolved by requirePermission(); a mutation has no business hanging off it;
11
+ // • this one is resolved by exactly three writers (the two _applyRankChangeTx
12
+ // twins + the sign-in path) and is a TRANSACTION PARTICIPANT — it takes the
13
+ // caller's client so the role rotation commits or rolls back with the rank write.
14
+ // The port name is namespaced (`governance.roleSync`) like the `ideas.capture` /
15
+ // `art.getBuilderGeminiKeyRow` / `grade.builderRollingAvgGrade` ports.
16
+ //
17
+ // Registered from a route factory for the same reason routes/governance.js is:
18
+ // mountModuleRoutes is the one boot hook that runs BOTH in the live server and
19
+ // whenever a test rebuilds the router, and registration is hasProvider-guarded so a
20
+ // rebuild never trips the seam's single-provider duplicate guard.
21
+ //
22
+ // The router is EMPTY and must stay empty. Rank-derived assignment is automatic and
23
+ // has no HTTP surface; the MANUAL assignment CRUD is R110's Archon-gated tab on the
24
+ // governance router. An endpoint here would be a way to hand out a rank-role without
25
+ // changing the rank.
26
+
27
+ const express = require('express');
28
+ const api = require('../../../src/module-api');
29
+ const db = require('../db');
30
+
31
+ // DELEGATE, never spread — a test that stubs db.syncRankRole must still bite at call
32
+ // time (the modules/onboarding/port.js precedent; a spread captures the original
33
+ // reference at module-load).
34
+ const rankRoleSyncPort = {
35
+ syncRankRole: (...a) => db.syncRankRole(...a),
36
+ };
37
+
38
+ function registerSeams() {
39
+ if (!api.hasProvider('governance.roleSync')) {
40
+ api.registerProvider('governance.roleSync', rankRoleSyncPort);
41
+ }
42
+ }
43
+
44
+ module.exports = function buildGovernanceRoleSyncRouter() {
45
+ registerSeams();
46
+ return express.Router();
47
+ };
48
+
49
+ module.exports.rankRoleSyncPort = rankRoleSyncPort;
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ // modules/governance/session-scopes.js — the SESSION-SCOPE axis (ADR 0151 §3, BV1.R99).
4
+ //
5
+ // The FOURTH axis, and the only one attached to the TOKEN rather than to the
6
+ // builder. Roles say what a person may do (R95); a principal's identity says what
7
+ // a machine may do (R98); an ownership scope says whose row it may be done to
8
+ // (R100); a SESSION-SCOPE says how much of that authority this particular
9
+ // credential carries. A dev-box session is minted onto a machine OUTSIDE the ADR
10
+ // 0016 trust boundary (#919/SR#2, ADR 0053), so it must stay limited even when its
11
+ // builder is an Archon.
12
+ //
13
+ // A SESSION-SCOPE CAN ONLY EVER NARROW. That is the whole load-bearing property,
14
+ // and it has two halves that fail in opposite directions:
15
+ // • INTERSECTION, never union — the scope may not GRANT a permission the
16
+ // builder's roles do not already confer, or a box token would escalate;
17
+ // • DENY-BY-DEFAULT — a permission the allow-list does not name is denied, or an
18
+ // unlisted permission leaks through.
19
+ // resolver.narrowToSessionScope() is the one place the two compose, and it is
20
+ // written as a FILTER of the role answer so widening is not expressible there.
21
+ //
22
+ // EXPLICIT DATA, not `if (source === 'box')`. session-scopes.json (next to this
23
+ // file) is the declaration; this module validates it against the permission
24
+ // catalog at load. GOVERNANCE OWNS THE POLICY, THE KERNEL STAYS THE ENFORCER —
25
+ // the same split R101 draws for the protected-surface registry: src/bongos/auth.js
26
+ // reads the same JSON as DATA (fs, never `require` — ADR 0083's one-way rule) and
27
+ // requireBuilder remains the gate every route composes. Nothing here enforces
28
+ // anything; re-mapping a scope here changes what the gate admits, it can never
29
+ // move the gate.
30
+ //
31
+ // FAIL-CLOSED at load: a key the catalog does not define, a `system: true`
32
+ // trust-boundary key, or a machine-principal key on a session-scope allow-list all
33
+ // THROW. Silently dropping the row would quietly shrink (or, for a principal,
34
+ // blur) the surface a token outside the trust boundary may reach — an unreadable
35
+ // policy is a broken install, not an opted-out one.
36
+
37
+ const catalog = require('./catalog');
38
+ const registry = require('./session-scopes.json');
39
+
40
+ // Normalize the declared rows into the same shape the kernel loader produces:
41
+ // `source` null for the legacy sourceless row, `permissions` always an array.
42
+ function normalizeScope(row) {
43
+ return Object.freeze({
44
+ source: row && row.source !== undefined ? row.source : null,
45
+ label: row && typeof row.label === 'string' ? row.label : null,
46
+ unscoped: !!(row && row.unscoped === true),
47
+ permissions: Object.freeze(row && Array.isArray(row.permissions) ? [...row.permissions] : []),
48
+ });
49
+ }
50
+
51
+ // The catalog cross-check. Exported so the shipped assertion itself is what the
52
+ // tests exercise, rather than a re-implementation of it (the fixture would
53
+ // otherwise have to rewrite this file on disk to be loaded).
54
+ function validateAgainstCatalog(scopes) {
55
+ for (const scope of scopes) {
56
+ const where = `session-scope registry: scope "${scope.source}"`;
57
+ // unscoped XOR an allow-list. Both would be ambiguous (does it narrow?);
58
+ // neither would be a scope that can never admit anything.
59
+ if (scope.unscoped === (scope.permissions.length > 0)) {
60
+ throw new Error(`${where} must declare EITHER unscoped:true OR a non-empty permissions allow-list, never both/neither`);
61
+ }
62
+ for (const key of scope.permissions) {
63
+ if (!catalog.byKey(key)) {
64
+ throw new Error(`${where} allows "${key}", which the permission catalog does not define`);
65
+ }
66
+ // R98's rule, kept structural: a machine principal's capability comes from
67
+ // its exact system_role, never from a role — so a box-scoped session
68
+ // carrying one must not be expressible on a scope allow-list either.
69
+ if (catalog.isPrincipalKey(key)) {
70
+ throw new Error(`${where} allows the machine-principal permission "${key}" — a session-scope narrows a builder's ROLE authority and never composes with the identity axis`);
71
+ }
72
+ // A session-scope IS the surface a credential outside the ADR 0016 trust
73
+ // boundary may reach. A trust-boundary permission must never appear on one,
74
+ // even though the intersection would make listing it inert today.
75
+ if (catalog.isSystem(key)) {
76
+ throw new Error(`${where} allows the system:true permission "${key}" — a session-scope may only list operational capability (a scoped token lives outside the trust boundary)`);
77
+ }
78
+ }
79
+ }
80
+ return scopes;
81
+ }
82
+
83
+ const SESSION_SCOPES = Object.freeze(
84
+ validateAgainstCatalog(Object.freeze((registry.scopes || []).map(normalizeScope))),
85
+ );
86
+
87
+ const _bySource = new Map(SESSION_SCOPES.map((s) => [s.source, s]));
88
+
89
+ // The scope a session's `source` declares, or null when the registry does not
90
+ // declare it. An UNRECOGNISED source resolves to null and therefore to NO
91
+ // permissions — it never defaults to "full" (a null/absent source is a DECLARED
92
+ // row here, not an implicit default: it predates the box source existing).
93
+ function scopeFor(source) {
94
+ const key = source === undefined ? null : source;
95
+ if (key !== null && typeof key !== 'string') return null;
96
+ return _bySource.get(key) || null;
97
+ }
98
+
99
+ // Does this session's scope narrow at all? Unknown source → true (fail-closed:
100
+ // treated as narrowing to nothing, which is what scopeFor/narrowToSessionScope do).
101
+ function isScoped(source) {
102
+ const scope = scopeFor(source);
103
+ return !scope || !scope.unscoped;
104
+ }
105
+
106
+ // The deny-by-default allow-list for a source; empty for an unscoped source (it
107
+ // narrows nothing) AND for an unrecognised one (it admits nothing) — callers must
108
+ // consult scopeFor()/isScoped() to tell those apart, exactly as the resolver does.
109
+ const NO_KEYS = Object.freeze([]);
110
+ function allowedKeys(source) {
111
+ const scope = scopeFor(source);
112
+ return scope ? scope.permissions : NO_KEYS;
113
+ }
114
+
115
+ function sources() {
116
+ return SESSION_SCOPES.map((s) => s.source);
117
+ }
118
+
119
+ module.exports = {
120
+ SESSION_SCOPES,
121
+ validateAgainstCatalog,
122
+ normalizeScope,
123
+ scopeFor,
124
+ isScoped,
125
+ allowedKeys,
126
+ sources,
127
+ };
@@ -0,0 +1,38 @@
1
+ {
2
+ "_comment": [
3
+ "The SESSION-SCOPE registry (BV1.R99, ADR 0151 §3; the model behind ADR 0053).",
4
+ "A session's scope is capability attached to the TOKEN, orthogonal to the builder's roles.",
5
+ "Each row declares EITHER unscoped:true (the session narrows nothing) OR a deny-by-default",
6
+ "permissions allow-list (unlisted => denied). A scope can only ever NARROW: effective",
7
+ "authority is (the builder's role permissions) INTERSECT (this allow-list), never a union.",
8
+ "Every key here is validated against modules/governance/catalog.js at load by",
9
+ "modules/governance/session-scopes.js — unknown, system:true and principal keys are refused.",
10
+ "Read as DATA by src/bongos/auth.js (fs, never require — ADR 0083's one-way rule)."
11
+ ],
12
+ "scopes": [
13
+ {
14
+ "source": "cli",
15
+ "label": "CLI session",
16
+ "unscoped": true,
17
+ "note": "Device Flow token on a builder's own machine — inside the trust boundary, carries the builder's full role authority."
18
+ },
19
+ {
20
+ "source": "web",
21
+ "label": "web session",
22
+ "unscoped": true,
23
+ "note": "Hall browser cookie — same authority as the CLI session."
24
+ },
25
+ {
26
+ "source": null,
27
+ "label": "legacy session",
28
+ "unscoped": true,
29
+ "note": "Rows minted before migration 090 added a source column; they predate the 'box' source existing, so they were never box tokens. Declared explicitly rather than defaulted — an UNRECOGNISED source denies."
30
+ },
31
+ {
32
+ "source": "box",
33
+ "label": "box-scoped session",
34
+ "permissions": ["box.manage.own"],
35
+ "note": "Baked into cloud-init so a dev box can bootstrap (#818/SR#2, ADR 0053). The token lives ON the box — OUTSIDE the ADR 0016 trust boundary — so it reaches only its own box record, whatever its builder's rank."
36
+ }
37
+ ]
38
+ }