@bongos/core 1.18.3 → 1.18.19

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 (471) hide show
  1. package/.bongos-core.json +1576 -286
  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 +4 -2
  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 +6 -1
  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/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/migrate.sh +99 -14
  347. package/scripts/status-mirror-sync.js +114 -84
  348. package/src/bongos/api-errors.js +1 -0
  349. package/src/bongos/auth.js +447 -102
  350. package/src/bongos/db-kernel.js +27 -0
  351. package/src/bongos/db.js +39 -0
  352. package/src/bongos/module-scope-map.js +27 -1
  353. package/src/bongos/permission-path-check.js +136 -102
  354. package/src/bongos/repo-info.js +47 -11
  355. package/src/bongos/route-rank-check.js +429 -46
  356. package/src/bongos/routes/_helpers.js +12 -1
  357. package/src/bongos/routes/auth.js +219 -2
  358. package/src/bongos/routes/backup.js +58 -2
  359. package/src/bongos/routes/me.js +35 -1
  360. package/src/bongos/serve-internal.js +84 -15
  361. package/src/branding.js +32 -0
  362. package/src/instance-config.js +30 -0
  363. package/src/module-api.js +124 -1
  364. package/src/platform-server.js +86 -9
  365. package/tests/abandon_task.mjs +59 -3
  366. package/tests/api_cli_flags.mjs +53 -0
  367. package/tests/auth_page_gate.mjs +13 -0
  368. package/tests/backfill_sessions.mjs +127 -1
  369. package/tests/backup_cli.mjs +54 -0
  370. package/tests/bongos_box_connect.mjs +77 -0
  371. package/tests/box_access.mjs +130 -1
  372. package/tests/box_credential.mjs +125 -0
  373. package/tests/box_host_keys.mjs +24 -0
  374. package/tests/box_infra_resolve.mjs +149 -0
  375. package/tests/box_scope_session.mjs +6 -2
  376. package/tests/box_sync.mjs +55 -0
  377. package/tests/box_task_scope.mjs +99 -0
  378. package/tests/branding.mjs +12 -0
  379. package/tests/cf_ruleset_preflight.mjs +142 -0
  380. package/tests/cf_tunnel.mjs +37 -0
  381. package/tests/claim_gate_rebase.mjs +98 -0
  382. package/tests/clawback_confirmed_backlog.mjs +103 -0
  383. package/tests/cli_lib_formatters.mjs +20 -0
  384. package/tests/confirm_attribution.mjs +30 -46
  385. package/tests/credit_grant.mjs +198 -10
  386. package/tests/criterion_authoring_gate.mjs +272 -0
  387. package/tests/criterion_progress.mjs +33 -1
  388. package/tests/dev_lib.mjs +253 -0
  389. package/tests/devbox_artifact_gate.mjs +75 -0
  390. package/tests/devbox_dmg_backdrop.mjs +137 -0
  391. package/tests/devbox_icons.mjs +304 -0
  392. package/tests/devbox_installer_brand.mjs +232 -0
  393. package/tests/devbox_key_register.mjs +234 -0
  394. package/tests/docs_openapi_route.mjs +110 -0
  395. package/tests/doctor_hooks.mjs +141 -0
  396. package/tests/extract_json.mjs +140 -0
  397. package/tests/fitness.mjs +89 -0
  398. package/tests/gen_freshness.mjs +96 -0
  399. package/tests/goal_advisory.mjs +251 -0
  400. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  401. package/tests/goal_routes.mjs +59 -4
  402. package/tests/goal_tier.mjs +12 -3
  403. package/tests/governance_catalog.mjs +129 -0
  404. package/tests/governance_claim_eligibility.mjs +262 -0
  405. package/tests/governance_config.mjs +187 -0
  406. package/tests/governance_ownership_scope.mjs +410 -0
  407. package/tests/governance_page_gates.mjs +324 -0
  408. package/tests/governance_parity.mjs +336 -0
  409. package/tests/governance_principals.mjs +232 -0
  410. package/tests/governance_protected_surfaces.mjs +208 -0
  411. package/tests/governance_require_permission.mjs +223 -0
  412. package/tests/governance_resolver.mjs +84 -0
  413. package/tests/governance_role_sync.mjs +231 -0
  414. package/tests/governance_seed.mjs +127 -0
  415. package/tests/governance_session_scope.mjs +434 -0
  416. package/tests/governance_vanilla_boot.mjs +203 -0
  417. package/tests/grade_attribution.mjs +7 -7
  418. package/tests/hall_audit.mjs +223 -0
  419. package/tests/hall_nav.mjs +191 -0
  420. package/tests/hall_palette.mjs +206 -0
  421. package/tests/hall_signout.mjs +124 -0
  422. package/tests/hook_user_agents.mjs +10 -0
  423. package/tests/idea_triage.mjs +162 -0
  424. package/tests/idp_device_exchange.mjs +93 -0
  425. package/tests/init.mjs +51 -0
  426. package/tests/instance_config.mjs +65 -0
  427. package/tests/instance_mismatch_guard.mjs +23 -6
  428. package/tests/instance_release_config.mjs +104 -0
  429. package/tests/linkify_refs.mjs +15 -2
  430. package/tests/migrate_root_resolution.mjs +124 -1
  431. package/tests/module_contributions.mjs +55 -0
  432. package/tests/module_loader.mjs +10 -5
  433. package/tests/module_scope_active_claims.mjs +94 -0
  434. package/tests/needs_rebase.mjs +92 -0
  435. package/tests/one_active_claim_per_session.mjs +76 -0
  436. package/tests/override_attribution.mjs +10 -8
  437. package/tests/override_request_ownership.mjs +8 -4
  438. package/tests/parse_cookie_decode.mjs +42 -0
  439. package/tests/pay_on_land.mjs +129 -0
  440. package/tests/platform_boot.mjs +100 -1
  441. package/tests/post_login_landing.mjs +42 -0
  442. package/tests/preflight_checks.mjs +72 -0
  443. package/tests/provision.mjs +132 -7
  444. package/tests/publish_branch_route.mjs +53 -0
  445. package/tests/publish_reconciler.mjs +187 -2
  446. package/tests/repo_info_binding.mjs +248 -0
  447. package/tests/reward_gate.mjs +11 -2
  448. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  449. package/tests/reward_mode_surfacing.mjs +168 -0
  450. package/tests/route_permission_check.mjs +301 -0
  451. package/tests/route_rank_check.mjs +254 -12
  452. package/tests/route_validation.mjs +30 -0
  453. package/tests/sequence.mjs +267 -0
  454. package/tests/session_digest.mjs +44 -0
  455. package/tests/session_index.mjs +18 -6
  456. package/tests/session_records.mjs +39 -1
  457. package/tests/session_reward_on_land.mjs +381 -0
  458. package/tests/session_reward_reconcile.mjs +229 -0
  459. package/tests/ship_api_client_pathspec.mjs +100 -0
  460. package/tests/ship_base_flag.mjs +93 -0
  461. package/tests/ship_ci_deploy.mjs +80 -3
  462. package/tests/ship_upstream_deletion.mjs +57 -0
  463. package/tests/smoke_target.mjs +233 -0
  464. package/tests/start_env_guard.mjs +87 -0
  465. package/tests/start_rebase_warning.mjs +103 -0
  466. package/tests/status_mirror_sync.mjs +122 -0
  467. package/tests/stop_refcheck.mjs +19 -1
  468. package/tests/task_goal_link.mjs +15 -6
  469. package/tests/upgrade.mjs +139 -0
  470. package/tests/web_admission_status.mjs +162 -0
  471. package/tests/write_validation_gaps.mjs +110 -0
@@ -0,0 +1,232 @@
1
+ // tests/governance_principals.mjs — machine principals as a first-class type
2
+ // (BV1.R98, ADR 0151 §3). DB-free.
3
+ //
4
+ // A builder's authority is the UNION of their ROLES. A machine principal's authority
5
+ // is its IDENTITY — the exact `builders.system_role` value. Those are two axes, and
6
+ // every test here exists to prove they never collapse into one lookup:
7
+ //
8
+ // • no rank-role holds a principal permission — not even archon's;
9
+ // • a role/assignment cannot confer one either, however the grant row got written;
10
+ // • a principal resolves its capability from system_role alone, by EXACT match;
11
+ // • nothing in this module can create, grant, or assign a principal — the column
12
+ // is provisioned out-of-band by migration, under a DB CHECK allowlist.
13
+ //
14
+ // The HTTP enforcement point stays requireBfgPrincipal / requireDiscordBot in
15
+ // src/bongos/auth.js; this file guards the model those gates express.
16
+ import assert from 'node:assert/strict';
17
+ import { test } from 'node:test';
18
+ import { createRequire } from 'node:module';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+ const GOV = path.join(ROOT, 'modules', 'governance');
26
+ const catalog = require(path.join(GOV, 'catalog.js'));
27
+ const resolver = require(path.join(GOV, 'resolver.js'));
28
+
29
+ const { PERMISSIONS, RANK_ORDER, RANK_ROLE_SEED, PRINCIPALS, principalKeys, principalRoles, isPrincipalKey, principalByRole, seedForRank, isGrantable, isSystem, systemKeys, grantableKeys, permissionKeys } = catalog;
30
+ const { effectivePermissions, resolvePrincipalPermissions, principalHasPermission } = resolver;
31
+
32
+ // ── the catalog side: principals are a distinct, well-formed type ───────────────
33
+
34
+ test('every principal.* key is system:true with floor:null and a principal identity', () => {
35
+ const prefixed = PERMISSIONS.filter((p) => p.key.startsWith('principal.'));
36
+ assert.ok(prefixed.length >= 2, 'expected the bfg + discord_bot principals');
37
+ for (const p of prefixed) {
38
+ assert.equal(p.system, true, `${p.key} must be system (non-grantable)`);
39
+ assert.equal(p.floor, null, `${p.key} must have floor null (held by no rank-role)`);
40
+ assert.equal(typeof p.principal, 'string', `${p.key} must name its system_role`);
41
+ assert.ok(p.principal.length > 0, `${p.key} principal identity must be non-empty`);
42
+ }
43
+ });
44
+
45
+ test('floor:null and principal identity are a partition — neither exists without the other', () => {
46
+ for (const p of PERMISSIONS) {
47
+ assert.equal(p.floor === null, p.principal != null,
48
+ `${p.key}: a permission is held by a rank-role (real floor) XOR by a machine identity`);
49
+ assert.equal(p.key.startsWith('principal.'), p.principal != null,
50
+ `${p.key}: the principal.* namespace and the principal identity must agree`);
51
+ }
52
+ });
53
+
54
+ test('the bfg and discord_bot principals are the modelled identities', () => {
55
+ assert.deepEqual([...principalRoles()].sort(), ['bfg', 'discord_bot']);
56
+ assert.deepEqual([...principalKeys()].sort(), ['principal.bfg', 'principal.discord_bot']);
57
+ assert.equal(principalByRole('bfg').permission, 'principal.bfg');
58
+ assert.equal(principalByRole('discord_bot').permission, 'principal.discord_bot');
59
+ });
60
+
61
+ test('isPrincipalKey separates the axes; an unknown key fails closed', () => {
62
+ assert.equal(isPrincipalKey('principal.bfg'), true);
63
+ assert.equal(isPrincipalKey('principal.discord_bot'), true);
64
+ assert.equal(isPrincipalKey('idea.triage'), false);
65
+ assert.equal(isPrincipalKey('deploy.push_main'), false); // system, but rank-held
66
+ assert.equal(isPrincipalKey('nope.not.a.key'), false);
67
+ assert.equal(isPrincipalKey(null), false);
68
+ });
69
+
70
+ // ── no role, at any rank, holds a principal permission ──────────────────────────
71
+
72
+ test('no rank-role holds a principal permission — an Archon must not hold principal.bfg', () => {
73
+ for (const rank of RANK_ORDER) {
74
+ const held = new Set(RANK_ROLE_SEED[rank]);
75
+ for (const key of principalKeys()) {
76
+ assert.ok(!held.has(key), `the ${rank} rank-role must not hold ${key}`);
77
+ }
78
+ // and the same via the derivation, so a future seed refactor is covered too
79
+ const derived = new Set(seedForRank(rank));
80
+ for (const key of principalKeys()) {
81
+ assert.ok(!derived.has(key), `seedForRank('${rank}') must not derive ${key}`);
82
+ }
83
+ }
84
+ assert.ok(!RANK_ROLE_SEED.archon.includes('principal.bfg'),
85
+ 'the top rank is the case that matters: identity is not seniority');
86
+ });
87
+
88
+ test('a principal permission is never grantable to a custom role', () => {
89
+ for (const key of principalKeys()) {
90
+ assert.equal(isGrantable(key), false, `${key} must not be grantable`);
91
+ assert.equal(isSystem(key), true, `${key} must be flagged system`);
92
+ assert.ok(!grantableKeys().includes(key), `grantableKeys() must exclude ${key}`);
93
+ assert.ok(systemKeys().includes(key), `systemKeys() must include ${key}`);
94
+ }
95
+ });
96
+
97
+ test('a role/assignment cannot confer a principal permission, however the grant row got written', () => {
98
+ // The write side rejects a system permission, but the resolve side must not depend
99
+ // on that: a hand-inserted grant row must still confer nothing.
100
+ assert.deepEqual(effectivePermissions(['principal.bfg']), []);
101
+ assert.deepEqual(effectivePermissions(['principal.bfg', 'principal.discord_bot']), []);
102
+ assert.deepEqual(effectivePermissions(['idea.triage', 'principal.bfg', 'task.ship']), ['idea.triage', 'task.ship']);
103
+ // even the whole catalog granted at once yields no principal key
104
+ const everything = new Set(effectivePermissions(permissionKeys()));
105
+ for (const key of principalKeys()) {
106
+ assert.ok(!everything.has(key), `${key} must never survive the role resolve`);
107
+ }
108
+ // ...while every non-principal key still does (the role axis is otherwise unchanged)
109
+ const nonPrincipal = permissionKeys().filter((k) => !isPrincipalKey(k));
110
+ assert.deepEqual(effectivePermissions(permissionKeys()), [...nonPrincipal].sort());
111
+ });
112
+
113
+ // ── the identity axis: system_role alone, matched exactly ───────────────────────
114
+
115
+ test('a principal resolves its capability from system_role alone', () => {
116
+ assert.deepEqual(resolvePrincipalPermissions('bfg'), ['principal.bfg']);
117
+ assert.deepEqual(resolvePrincipalPermissions('discord_bot'), ['principal.discord_bot']);
118
+ assert.equal(principalHasPermission('bfg', 'principal.bfg'), true);
119
+ assert.equal(principalHasPermission('discord_bot', 'principal.discord_bot'), true);
120
+ });
121
+
122
+ test('one principal never reaches another principal\'s capability, or any role permission', () => {
123
+ assert.equal(principalHasPermission('bfg', 'principal.discord_bot'), false);
124
+ assert.equal(principalHasPermission('discord_bot', 'principal.bfg'), false);
125
+ assert.equal(principalHasPermission('bfg', 'idea.triage'), false);
126
+ assert.equal(principalHasPermission('bfg', 'deploy.push_main'), false);
127
+ // a principal's set is exactly its own key — the identity axis grants nothing else
128
+ assert.equal(resolvePrincipalPermissions('bfg').length, 1);
129
+ });
130
+
131
+ test('the match is EXACT — a human token (system_role NULL) resolves nothing', () => {
132
+ for (const notAPrincipal of [null, undefined, '', 'BFG', 'Bfg', ' bfg', 'bfg ', 'bfg\n', 'bfgx', 'discord', 'archon', 'metic', 'admin', 0, 1, {}, [], true]) {
133
+ assert.deepEqual(resolvePrincipalPermissions(notAPrincipal), [],
134
+ `${JSON.stringify(notAPrincipal)} must resolve to no principal capability`);
135
+ }
136
+ // the concrete claim ADR 0026 §6C makes: an Archon token cannot be the BFG
137
+ assert.equal(principalHasPermission(null, 'principal.bfg'), false);
138
+ assert.equal(principalHasPermission('archon', 'principal.bfg'), false);
139
+ });
140
+
141
+ test('resolved principal sets are frozen — a caller cannot widen one', () => {
142
+ const held = resolvePrincipalPermissions('bfg');
143
+ assert.ok(Object.isFrozen(held));
144
+ assert.throws(() => held.push('deploy.push_main'), TypeError);
145
+ assert.ok(Object.isFrozen(PRINCIPALS));
146
+ for (const p of PRINCIPALS) assert.ok(Object.isFrozen(p), `${p.system_role} descriptor must be frozen`);
147
+ });
148
+
149
+ // ── show, never grant ───────────────────────────────────────────────────────────
150
+
151
+ test('the tab surface is a read-only projection: shown, flagged non-grantable', () => {
152
+ const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
153
+ assert.equal(typeof governancePort.listPrincipals, 'function');
154
+ assert.equal(typeof governancePort.resolvePrincipalPermissions, 'function');
155
+ const listed = governancePort.listPrincipals();
156
+ assert.deepEqual(listed.map((p) => p.system_role).sort(), ['bfg', 'discord_bot']);
157
+ for (const p of listed) {
158
+ assert.equal(p.grantable, false, `${p.system_role} must be rendered non-grantable`);
159
+ assert.match(p.provisioned, /out-of-band/, 'the descriptor must say where a principal comes from');
160
+ assert.ok(p.permission && p.guards, 'the tab needs the key + what it guards to render the row');
161
+ }
162
+ });
163
+
164
+ test('the governance port exposes no way to create, grant, or assign a principal', () => {
165
+ const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
166
+ const mutator = /(create|grant|assign|add|set|write|provision|revoke|delete|update)/i;
167
+ for (const name of Object.keys(governancePort)) {
168
+ if (!/principal/i.test(name)) continue;
169
+ assert.ok(!mutator.test(name), `port method "${name}" looks like a principal mutator — principals are provisioned out-of-band only`);
170
+ }
171
+ });
172
+
173
+ test('the module writes builders.system_role nowhere — provisioning is out-of-band', () => {
174
+ // Structural, not behavioural: the guarantee is that governance has no code path
175
+ // to mint an identity at all, so a future CRUD slice (R110) cannot quietly grow one.
176
+ const files = [];
177
+ const walk = (dir) => {
178
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
179
+ const full = path.join(dir, e.name);
180
+ if (e.isDirectory()) walk(full);
181
+ else if (e.name.endsWith('.js') || e.name.endsWith('.sql')) files.push(full);
182
+ }
183
+ };
184
+ walk(GOV);
185
+ assert.ok(files.length > 0, 'expected to scan the governance module sources');
186
+ for (const f of files) {
187
+ const code = fs.readFileSync(f, 'utf8')
188
+ .split('\n').filter((l) => !l.trim().startsWith('//') && !l.trim().startsWith('--')).join('\n');
189
+ assert.ok(!/\b(INSERT\s+INTO|UPDATE|ALTER\s+TABLE)\s+builders\b/i.test(code),
190
+ `${path.basename(f)} writes the builders table — governance may read an identity, never mint one`);
191
+ }
192
+ });
193
+
194
+ test('the catalog models exactly the principals the substrate can provision', () => {
195
+ // builders.system_role carries a DB CHECK allowlist (migration 065, widened by 067).
196
+ // A principal the substrate cannot provision would be a permission nothing can hold;
197
+ // an identity the substrate CAN provision but the catalog omits would be invisible
198
+ // to the tab. Either way the two lists must agree.
199
+ const sql = fs.readFileSync(path.join(ROOT, 'migrations', '067_discord_bot_foundation.sql'), 'utf8');
200
+ const m = sql.match(/CHECK \(system_role IS NULL OR system_role IN \(([^)]*)\)\)/);
201
+ assert.ok(m, 'expected the system_role CHECK allowlist in migration 067');
202
+ const allowed = [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]).sort();
203
+ assert.deepEqual([...principalRoles()].sort(), allowed,
204
+ 'catalog principals must match the builders.system_role CHECK allowlist');
205
+ });
206
+
207
+ // ── the R94/R95 invariants this slice must not have disturbed ───────────────────
208
+
209
+ test('the rank-role seed is still cumulative and floor-derived (R94 invariants hold)', () => {
210
+ for (let i = 1; i < RANK_ORDER.length; i++) {
211
+ const lower = new Set(RANK_ROLE_SEED[RANK_ORDER[i - 1]]);
212
+ for (const k of RANK_ROLE_SEED[RANK_ORDER[i]]) assert.ok(typeof k === 'string');
213
+ for (const k of lower) {
214
+ assert.ok(RANK_ROLE_SEED[RANK_ORDER[i]].includes(k), `${RANK_ORDER[i]} lost ${k}`);
215
+ }
216
+ }
217
+ for (const rank of RANK_ORDER) {
218
+ const held = new Set(RANK_ROLE_SEED[rank]);
219
+ const tier = RANK_ORDER.indexOf(rank);
220
+ for (const p of PERMISSIONS) {
221
+ assert.equal(held.has(p.key), p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier,
222
+ `${rank} vs ${p.key} (floor ${p.floor})`);
223
+ }
224
+ }
225
+ });
226
+
227
+ test('the role resolve is otherwise unchanged: unknown keys drop, no roles is empty', () => {
228
+ assert.deepEqual(effectivePermissions(['idea.triage', 'not.a.real.permission']), ['idea.triage']);
229
+ assert.deepEqual(effectivePermissions([]), []);
230
+ assert.deepEqual(effectivePermissions(null), []);
231
+ assert.ok(Object.isFrozen(effectivePermissions(['task.ship'])));
232
+ });
@@ -0,0 +1,208 @@
1
+ // tests/governance_protected_surfaces.mjs — the protected-surface registry
2
+ // (BV1.R101, ADR 0151 §4).
3
+ //
4
+ // DB-free. The registry is the data the OUT-OF-BAND substrate reads (a git
5
+ // pre-push hook, CI, the post-push audit), so every invariant here is checked
6
+ // the way the substrate sees it: synchronously, from the repo, with no server
7
+ // and no database.
8
+ //
9
+ // The load-bearing test is BEHAVIOR-IDENTITY. R101 moved WHERE the truth lives
10
+ // (a hardcoded PROTECTED_GLOBS constant in src/bongos/permission-path-check.js →
11
+ // governance-owned data in modules/governance/protected-surfaces.json). It did
12
+ // NOT change WHAT is protected. EXPECTED_GLOBS_BEFORE_R101 below is the literal
13
+ // pre-change constant, pasted verbatim; the registry must reproduce it exactly,
14
+ // in order.
15
+ import assert from 'node:assert/strict';
16
+ import { test } from 'node:test';
17
+ import { createRequire } from 'node:module';
18
+ import { execFileSync } from 'node:child_process';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+ const GOV = path.join(ROOT, 'modules', 'governance');
26
+
27
+ const ppc = require(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'));
28
+ const registry = require(path.join(GOV, 'protected-surfaces.js'));
29
+ const catalog = require(path.join(GOV, 'catalog.js'));
30
+
31
+ // The verbatim PROTECTED_GLOBS array as it stood immediately before R101.
32
+ const EXPECTED_GLOBS_BEFORE_R101 = [
33
+ 'src/bongos/auth.js',
34
+ 'src/bongos/db.js',
35
+ 'src/bongos/route-rank-check.js',
36
+ 'src/bongos/permission-path-check.js',
37
+ 'src/bongos/middleware/',
38
+ 'src/bongos/routes/',
39
+ 'modules/lifecycle/routes/',
40
+ 'src/bongos/routes.js',
41
+ 'server.js',
42
+ 'modules/governance/',
43
+ 'modules/dev-box/boxes.js',
44
+ 'modules/dev-box/box-access.js',
45
+ 'modules/dev-box/box-credential.js',
46
+ 'modules/grading/grader.js',
47
+ 'modules/grading/grader-workers/',
48
+ 'modules/grading/grader-rubric.json',
49
+ 'scripts/gds/ship.js',
50
+ 'scripts/gds/confirm.js',
51
+ 'scripts/gds/merge-mode.js',
52
+ 'scripts/gds/ci-grade.js',
53
+ 'scripts/gds/grade-smoke.js',
54
+ 'scripts/gds/conductor-dispatch.js',
55
+ 'scripts/gds/main-audit.js',
56
+ 'scripts/gds/push-main.js',
57
+ 'scripts/gds/run-unit-tests.js',
58
+ 'scripts/gds/gate-review.js',
59
+ 'scripts/gds/setup.js',
60
+ 'scripts/gds/install-git-hooks.js',
61
+ 'scripts/gds/hooks/',
62
+ 'scripts/gds/cli-lib.js',
63
+ 'scripts/migrate.sh',
64
+ 'scripts/deploy/',
65
+ 'modules/lifecycle/merge-lock.js',
66
+ 'modules/lifecycle/ship-preflight.js',
67
+ 'modules/lifecycle/github-push.js',
68
+ 'modules/lifecycle/conflict-resolve.js',
69
+ 'modules/lifecycle/publish-reconciler.js',
70
+ 'migrations/',
71
+ 'infra/',
72
+ '.github/',
73
+ 'CODEOWNERS',
74
+ 'config/deploy.json',
75
+ 'package.json',
76
+ 'package-lock.json',
77
+ '.husky/',
78
+ 'docs/canonical-permissions.md',
79
+ 'docs/adr/0016-trust-boundary-server-enforced-permissions.md',
80
+ 'docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md',
81
+ 'CLAUDE.md',
82
+ '.claude/hooks/',
83
+ '.claude/settings.json',
84
+ ];
85
+
86
+ // ---------- behavior identity: the same globs, at the same floor ----------
87
+
88
+ test('the registry reproduces the pre-R101 PROTECTED_GLOBS exactly, in order', () => {
89
+ assert.deepEqual(registry.PROTECTED_GLOBS, EXPECTED_GLOBS_BEFORE_R101);
90
+ assert.deepEqual(ppc.PROTECTED_GLOBS, EXPECTED_GLOBS_BEFORE_R101,
91
+ 'the kernel matcher must expose the same derived list the registry declares');
92
+ });
93
+
94
+ test('every surface floors at metic — the ADR 0043 floor, unchanged by the move', () => {
95
+ for (const s of registry.SURFACES) {
96
+ assert.equal(s.floor, 'metic', `${s.glob} must keep the Metic floor`);
97
+ }
98
+ assert.equal(registry.FLOOR, 'metic');
99
+ assert.equal(ppc.MIN_TIER, ppc.RANK_TIER.metic, 'the kernel floor must DERIVE to metic');
100
+ });
101
+
102
+ // ---------- the system-permission mapping flows from the registry ----------
103
+
104
+ test('every entry maps to a permission the catalog defines as system:true', () => {
105
+ for (const s of registry.SURFACES) {
106
+ const perm = catalog.byKey(s.permission);
107
+ assert.ok(perm, `${s.glob} maps to unknown permission "${s.permission}"`);
108
+ assert.equal(perm.system, true,
109
+ `${s.glob} maps to "${s.permission}", which must be NON-GRANTABLE (system:true) — otherwise a custom role could be handed the power the substrate reserves`);
110
+ }
111
+ assert.deepEqual(registry.SYSTEM_PERMISSIONS, ['path.protected.modify']);
112
+ });
113
+
114
+ test('a surface mapped to a grantable or unknown permission fails loud', () => {
115
+ // The guard must actually catch a bad mapping, not pass vacuously.
116
+ const grantable = catalog.grantableKeys()[0];
117
+ assert.ok(grantable && catalog.isSystem(grantable) === false);
118
+ assert.equal(catalog.byKey('not.a.real.permission'), null);
119
+ });
120
+
121
+ // ---------- synchronous, no DB, no server ----------
122
+
123
+ test('the registry loads synchronously from the repo with no DB and no server', () => {
124
+ // A cold `node -e` process: no server booted, no pool, no network. This is the
125
+ // pre-push hook / CI condition — if the registry ever needed a database the
126
+ // enforcers would silently stop enforcing.
127
+ const out = execFileSync(process.execPath, ['-e',
128
+ "const p=require('./src/bongos/permission-path-check');" +
129
+ "process.stdout.write(String(p.PROTECTED_GLOBS.length)+':'+String(p.floorFor('migrations/001.sql')));",
130
+ ], { cwd: ROOT, encoding: 'utf8', env: { ...process.env, DATABASE_URL: '', PGHOST: '' } });
131
+ assert.equal(out, `${EXPECTED_GLOBS_BEFORE_R101.length}:metic`);
132
+ });
133
+
134
+ test('the registry data file carries no DB/network dependency', () => {
135
+ const json = fs.readFileSync(path.join(GOV, 'protected-surfaces.json'), 'utf8');
136
+ assert.doesNotThrow(() => JSON.parse(json), 'the canonical form must be plain parseable data');
137
+ const code = fs.readFileSync(path.join(GOV, 'protected-surfaces.js'), 'utf8')
138
+ .split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
139
+ for (const banned of ['./db', 'pool', 'fetch(', 'await ']) {
140
+ assert.ok(!code.includes(banned),
141
+ `protected-surfaces.js must not introduce "${banned}" — the substrate reads this with no DB and no server (ADR 0151 §4)`);
142
+ }
143
+ });
144
+
145
+ // ---------- policy owns / substrate enforces ----------
146
+
147
+ test('the kernel does not require the governance module (ADR 0083 one-way rule)', () => {
148
+ const kernel = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'), 'utf8');
149
+ const requires = [...kernel.matchAll(/require\(\s*['"]([^'"]+)['"]\s*\)/g)].map((m) => m[1]);
150
+ assert.deepEqual(requires.filter((r) => r.includes('modules/')), [],
151
+ 'core must never require a module — it reads the registry as data');
152
+ // …and the flow runs the legal way: governance reaches the matcher through the doorway.
153
+ const gov = fs.readFileSync(path.join(GOV, 'protected-surfaces.js'), 'utf8');
154
+ assert.ok(/require\(\s*['"]\.\.\/\.\.\/src\/module-api['"]\s*\)/.test(gov),
155
+ 'governance must reach the kernel matcher through the published doorway');
156
+ });
157
+
158
+ test('the hardcoded protected list is gone from the kernel — one source, no drift', () => {
159
+ const kernel = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'), 'utf8');
160
+ assert.ok(!/(?:const|let|var)\s+PROTECTED_GLOBS\s*=\s*\[\s*$/m.test(kernel),
161
+ 'PROTECTED_GLOBS must be DERIVED from the registry, never re-declared as a literal list');
162
+ assert.equal(ppc.REGISTRY_PATH, path.join(GOV, 'protected-surfaces.json'));
163
+ });
164
+
165
+ test('a missing registry fails CLOSED — it never degrades to "nothing is protected"', () => {
166
+ assert.throws(() => {
167
+ execFileSync(process.execPath, ['-e',
168
+ "const fs=require('node:fs');const real=fs.readFileSync;" +
169
+ "fs.readFileSync=(p,...r)=>{ if(String(p).includes('protected-surfaces.json')) throw new Error('gone'); return real(p,...r); };" +
170
+ "require('./src/bongos/permission-path-check');",
171
+ ], { cwd: ROOT, stdio: 'pipe' });
172
+ }, /unreadable|Refusing/i);
173
+ });
174
+
175
+ // ---------- the matcher, driven by registry data ----------
176
+
177
+ test('a known path is protected at the right floor; an unknown path is not', () => {
178
+ assert.equal(ppc.floorFor('src/bongos/auth.js'), 'metic');
179
+ assert.equal(ppc.permissionFor('src/bongos/auth.js'), 'path.protected.modify');
180
+ assert.equal(registry.isProtected('migrations/999_x.sql'), true, 'subtree globs still match');
181
+ assert.equal(registry.floorFor('modules/governance/catalog.js'), 'metic');
182
+
183
+ assert.equal(ppc.floorFor('public/game/main.js'), null);
184
+ assert.equal(ppc.permissionFor('docs/session-logs/2026-07-30-x.md'), null);
185
+ assert.equal(registry.isProtected('public/game/main.js'), false);
186
+ assert.equal(registry.surfaceFor('src/world/terrain.js'), null);
187
+ });
188
+
189
+ test('every registry glob actually matches something the matcher flags', () => {
190
+ for (const s of registry.SURFACES) {
191
+ const sample = s.glob.endsWith('/') ? `${s.glob}sample.js` : s.glob;
192
+ const hits = ppc.matchProtected([sample]);
193
+ assert.equal(hits.length, 1, `${s.glob} must match ${sample}`);
194
+ assert.equal(hits[0].glob, s.glob);
195
+ assert.equal(hits[0].floor, s.floor);
196
+ assert.equal(hits[0].permission, s.permission);
197
+ }
198
+ });
199
+
200
+ test('the floor decision reads the hit\'s registry floor, so a re-map takes effect', () => {
201
+ const below = ppc.checkPermissionPaths({ changedFiles: ['migrations/001.sql'], builderRank: 'thetes' });
202
+ assert.equal(below.hardFail, true);
203
+ assert.equal(below.findings[0].floor, 'metic');
204
+ assert.equal(below.findings[0].permission, 'path.protected.modify');
205
+
206
+ const atFloor = ppc.checkPermissionPaths({ changedFiles: ['migrations/001.sql'], builderRank: 'metic' });
207
+ assert.equal(atFloor.hardFail, false);
208
+ });
@@ -0,0 +1,223 @@
1
+ // tests/governance_require_permission.mjs — the requirePermission gate (BV1.R97, ADR 0151).
2
+ //
3
+ // DB-free. The gate reaches the resolver through the `governance` KERNEL PORT
4
+ // (the one-way rule — core never requires modules/governance/), so a stub
5
+ // provider registered on the seam is the whole test double: no Postgres, no
6
+ // router, no live module. That is also the point being asserted — if auth.js ever
7
+ // went back to importing the module directly, these tests could not stub it.
8
+ //
9
+ // What matters here is trust-boundary posture (ADR 0016): EVERY unhappy path must
10
+ // 403, including the ones that are not "you lack the permission" — no builder, a
11
+ // mis-authored empty gate, a missing port, a throwing resolver. A gate that
12
+ // admits when authority cannot be established is a privilege escalation.
13
+ import assert from 'node:assert/strict';
14
+ import { test } from 'node:test';
15
+ import { createRequire } from 'node:module';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
22
+ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
23
+ const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
24
+ const { classify } = require(path.join(ROOT, 'src', 'bongos', 'api-errors.js'));
25
+ const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
26
+
27
+ // Minimal res double capturing status + body, with res.fail() installed exactly
28
+ // as the router installs it (mirrors tests/rank_gate.mjs).
29
+ function fakeRes() {
30
+ const res = {
31
+ _status: null,
32
+ _json: null,
33
+ status(code) { this._status = code; return this; },
34
+ json(body) { this._json = body; return this; },
35
+ };
36
+ attachFail({}, res, () => {});
37
+ return res;
38
+ }
39
+
40
+ // Register a stub `governance` port for one run. `resolve` is the stubbed
41
+ // resolveBuilderPermissions; pass null to leave the port UNREGISTERED.
42
+ async function run(gate, req, resolve) {
43
+ seams._reset();
44
+ if (resolve !== null) seams.registerProvider('governance', { resolveBuilderPermissions: resolve });
45
+ const res = fakeRes();
46
+ let nexted = false;
47
+ await gate(req, res, () => { nexted = true; });
48
+ seams._reset();
49
+ return { res, nexted };
50
+ }
51
+
52
+ const BUILDER = { id: 7, rank: 'metic' };
53
+ const holding = (...keys) => async () => keys;
54
+
55
+ test('a builder holding the permission passes the gate', async () => {
56
+ const { res, nexted } = await run(
57
+ auth.requirePermission('idea.triage'), { builder: BUILDER }, holding('idea.triage', 'cost.log'),
58
+ );
59
+ assert.equal(nexted, true, 'next() should be called');
60
+ assert.equal(res._status, null, 'no status should be set');
61
+ });
62
+
63
+ test('a missing grant 403s with permission_forbidden and explains the denial', async () => {
64
+ const { res, nexted } = await run(
65
+ auth.requirePermission('governance.manage'), { builder: BUILDER }, holding('idea.triage'),
66
+ );
67
+ assert.equal(nexted, false, 'next() must NOT be called');
68
+ assert.equal(res._status, 403);
69
+ assert.equal(res._json.error.code, 'permission_forbidden');
70
+ assert.deepEqual(res._json.error.details.required, ['governance.manage']);
71
+ assert.deepEqual(res._json.error.details.held, [], 'holds none of the required perms');
72
+ });
73
+
74
+ test('multiple permissions are AND, not OR — holding one of two still denies', async () => {
75
+ const gate = auth.requirePermission('task.create', 'task.edit');
76
+ const partial = await run(gate, { builder: BUILDER }, holding('task.create'));
77
+ assert.equal(partial.nexted, false, 'holding only one of two must deny');
78
+ assert.equal(partial.res._status, 403);
79
+ assert.deepEqual(partial.res._json.error.details.held, ['task.create'],
80
+ 'held reports the intersection with required, not the whole permission set');
81
+ const both = await run(gate, { builder: BUILDER }, holding('task.create', 'task.edit', 'cost.log'));
82
+ assert.equal(both.nexted, true, 'holding both must admit');
83
+ });
84
+
85
+ test('no req.builder fails closed (gate mounted without requireBuilder)', async () => {
86
+ const { res, nexted } = await run(auth.requirePermission('idea.triage'), {}, holding('idea.triage'));
87
+ assert.equal(nexted, false);
88
+ assert.equal(res._status, 403);
89
+ assert.equal(res._json.error.code, 'permission_forbidden');
90
+ assert.equal(res._json.error.details.held, null, 'null held = the set was never resolved');
91
+ });
92
+
93
+ test('an empty permission list denies — a mis-authored gate must not open', async () => {
94
+ const { res, nexted } = await run(auth.requirePermission(), { builder: BUILDER }, holding('idea.triage'));
95
+ assert.equal(nexted, false);
96
+ assert.equal(res._status, 403);
97
+ assert.deepEqual(res._json.error.details.required, []);
98
+ assert.equal(res._json.error.details.held, null);
99
+ });
100
+
101
+ test('an UNREGISTERED governance port denies, it does not admit', async () => {
102
+ // The deliberate call: no port = authority cannot be established. Admitting
103
+ // here would make "disable the governance module" a privilege escalation.
104
+ const { res, nexted } = await run(auth.requirePermission('idea.triage'), { builder: BUILDER }, null);
105
+ assert.equal(nexted, false, 'a missing authority system must never open a gate');
106
+ assert.equal(res._status, 403);
107
+ assert.equal(res._json.error.code, 'permission_forbidden');
108
+ assert.equal(res._json.error.details.held, null);
109
+ });
110
+
111
+ test('a malformed port (no resolveBuilderPermissions) denies', async () => {
112
+ seams._reset();
113
+ seams.registerProvider('governance', { isSystem: () => true });
114
+ const res = fakeRes();
115
+ let nexted = false;
116
+ await auth.requirePermission('idea.triage')({ builder: BUILDER }, res, () => { nexted = true; });
117
+ seams._reset();
118
+ assert.equal(nexted, false);
119
+ assert.equal(res._status, 403);
120
+ });
121
+
122
+ test('a throwing resolver denies (a DB blip must not open the gate)', async () => {
123
+ const { res, nexted } = await run(
124
+ auth.requirePermission('idea.triage'), { builder: BUILDER },
125
+ async () => { throw new Error('connection terminated'); },
126
+ );
127
+ assert.equal(nexted, false, 'a resolve failure must fail CLOSED');
128
+ assert.equal(res._status, 403);
129
+ assert.equal(res._json.error.code, 'permission_forbidden');
130
+ assert.equal(res._json.error.details.held, null);
131
+ });
132
+
133
+ test('permission_forbidden is registered as a 403 forbidden code, not a 500', async () => {
134
+ // Without an OVERRIDES entry the naming heuristic would classify the new code
135
+ // as `internal` and res.fail() would answer 500 — a denial reported as an outage.
136
+ assert.equal(classify('permission_forbidden'), 'forbidden');
137
+ });
138
+
139
+ test('the gate resolves per request — no memoised authority (ADR 0016)', async () => {
140
+ // A revoked grant must take effect on the very next request, so the gate must
141
+ // call the resolver every time rather than reusing a first answer.
142
+ let calls = 0;
143
+ let keys = ['idea.triage'];
144
+ seams._reset();
145
+ seams.registerProvider('governance', { resolveBuilderPermissions: async () => { calls += 1; return keys; } });
146
+ const gate = auth.requirePermission('idea.triage');
147
+ const first = fakeRes();
148
+ let admitted = false;
149
+ await gate({ builder: BUILDER }, first, () => { admitted = true; });
150
+ assert.equal(admitted, true);
151
+ keys = []; // the grant is revoked between requests
152
+ const second = fakeRes();
153
+ let admittedAgain = false;
154
+ await gate({ builder: BUILDER }, second, () => { admittedAgain = true; });
155
+ seams._reset();
156
+ assert.equal(calls, 2, 'the resolver must be consulted on every request');
157
+ assert.equal(admittedAgain, false, 'a revoked grant must deny on the next request');
158
+ assert.equal(second._status, 403);
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // The done-when's other half: requireRank is UNCHANGED, and the seeded rank-roles
163
+ // make the two gates equivalent during the R104-R107 migration.
164
+ // ---------------------------------------------------------------------------
165
+
166
+ test('requireRank still behaves identically — unchanged by R97', () => {
167
+ const gate = auth.requireRank('metic', 'archon');
168
+ for (const rank of ['metic', 'archon', 'olympian']) {
169
+ const res = fakeRes();
170
+ let nexted = false;
171
+ gate({ builder: { rank } }, res, () => { nexted = true; });
172
+ assert.equal(nexted, true, `${rank} should still pass a metic+archon gate`);
173
+ assert.equal(res._status, null);
174
+ }
175
+ const res = fakeRes();
176
+ let nexted = false;
177
+ gate({ builder: { rank: 'thetes' } }, res, () => { nexted = true; });
178
+ assert.equal(nexted, false);
179
+ assert.equal(res._status, 403);
180
+ assert.equal(res._json.error.code, 'rank_forbidden');
181
+ assert.deepEqual(res._json.error.details.required, ['metic', 'archon']);
182
+ assert.equal(res._json.error.details.actual, 'thetes');
183
+ assert.equal(auth.rankMeetsThreshold('archon', []), false, 'still fail-closed on an empty allow-list');
184
+ });
185
+
186
+ test('over the four seeded ranks, requirePermission admits exactly who requireRank admits', async () => {
187
+ // The migration-safety guarantee (R103's parity, in its DB-free form): feed the
188
+ // gate the SEED a rank-role grants and it reproduces the rank gate's verdict.
189
+ // Scoped to catalog.RANK_ORDER — the reserved/divine ranks above archon hold no
190
+ // seeded role, so parity is defined only over the four live ranks.
191
+ const cases = [
192
+ ['idea.triage', ['metic', 'archon']],
193
+ ['governance.manage', ['archon']],
194
+ ['task.claim.any', ['thetes', 'metic', 'archon']],
195
+ ['cost.log', ['xenos', 'thetes', 'metic', 'archon']],
196
+ ];
197
+ for (const [permission, allowedRanks] of cases) {
198
+ for (const rank of catalog.RANK_ORDER) {
199
+ const permGate = await run(
200
+ auth.requirePermission(permission), { builder: { id: 7, rank } },
201
+ async () => catalog.seedForRank(rank),
202
+ );
203
+ const rankRes = fakeRes();
204
+ let rankNexted = false;
205
+ auth.requireRank(...allowedRanks)({ builder: { rank } }, rankRes, () => { rankNexted = true; });
206
+ assert.equal(permGate.nexted, rankNexted,
207
+ `${rank}: requirePermission('${permission}') and requireRank(${allowedRanks.join(',')}) must agree`);
208
+ }
209
+ }
210
+ });
211
+
212
+ test('a principal permission (floor null) is held by NO rank-role', async () => {
213
+ // principal.bfg is gated on the exact system_role, never a rank — so no seeded
214
+ // role grants it and the gate denies every rank, archon included.
215
+ for (const rank of catalog.RANK_ORDER) {
216
+ const { nexted, res } = await run(
217
+ auth.requirePermission('principal.bfg'), { builder: { id: 7, rank } },
218
+ async () => catalog.seedForRank(rank),
219
+ );
220
+ assert.equal(nexted, false, `${rank} must not hold a principal permission`);
221
+ assert.equal(res._status, 403);
222
+ }
223
+ });