@bongos/core 1.19.1072 → 1.19.1074

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +2 -1
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +84 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
@@ -93,8 +93,7 @@ async function respondWithLedger(res, builderId, opts) {
93
93
  module.exports = function buildCreditsRouter() {
94
94
  const router = express.Router();
95
95
 
96
- // Route ORDER is deliberate, and the same rule modules/dev-box/routes/box.js
97
- // documents: the route-rank auditor classifies from a lookahead window that
96
+ // Route ORDER is deliberate: the route-rank auditor classifies from a lookahead window that
98
97
  // checks requireRank/requirePermission before requireBuilder, so the
99
98
  // PRIVILEGED route is declared FIRST. Below the own-scoped one, its gate
100
99
  // would bleed upward and mislabel that route as privileged.
@@ -47,8 +47,8 @@
47
47
  // one; the Archon tab reads `PRINCIPALS` to SHOW them and has no counterpart write.
48
48
  //
49
49
  // OWNERSHIP SCOPE — the third AXIS (BV1.R100, ADR 0151 §3). "May act on OWN X" is a
50
- // PARAMETERIZED permission, not a global grant: box.manage.own ≠ the cross-owner
51
- // box authority. Two optional fields make that axis explicit DATA (the way
50
+ // PARAMETERIZED permission, not a global grant: task.abandon.own ≠ the cross-owner
51
+ // task authority. Two optional fields make that axis explicit DATA (the way
52
52
  // `principal` made the identity axis data) rather than something inferred from a
53
53
  // key's spelling:
54
54
  // • scope — 'own' = ownership-scoped. Holding it authorizes acting ONLY on a
@@ -62,11 +62,11 @@
62
62
  // 'any' = the CROSS-OWNER counterpart over the same resource: it
63
63
  // reaches every owner's row, so no predicate applies.
64
64
  // null = not ownership-scoped (the resource has no owner axis).
65
- // • resource — WHAT is owned ('claim', 'box', 'memory', …). It is what LINKS an
65
+ // • resource — WHAT is owned ('claim', 'task', 'memory', …). It is what LINKS an
66
66
  // 'own' key to its 'any' counterpart, which a name cannot: the
67
- // cross-owner box authority is spelled `box.fleet.manage`, while
68
- // `task.claim.any` ends in `.any` and means "any task in the pool",
69
- // not "any owner's resource". Inferring the axis from the suffix
67
+ // cross-owner authority over `task.abandon.own` is spelled plain
68
+ // `task.abandon`, while `task.claim.any` ends in `.any` and means
69
+ // "any task in the pool", not "any owner's resource". Inferring the axis from the suffix
70
70
  // would get both of those wrong — hence data.
71
71
  // Asserted at load: scope ⟺ resource; a `.own`-suffixed key MUST declare scope
72
72
  // 'own' (a namespace can never outrun the data); a scoped key needs a real rank
@@ -220,7 +220,6 @@ const PERMISSIONS = [
220
220
  { key: 'llm_cache.use', system: false, floor: 'metic', guards: 'POST /llm-cache/{lookup,store}' },
221
221
  { key: 'backup.manage', system: false, floor: 'metic', guards: 'GET /backup/status, POST /backup/trigger' },
222
222
  { key: 'source.clone.local', system: false, floor: 'metic', guards: 'Metic+ local clone / collaborator invite (ADR 0035)' },
223
- { key: 'box.source.full', system: false, floor: 'metic', guards: 'GET /box/source-access → full (vs starter); boxScopeForRank' },
224
223
  // Agent authoring, split in two because the two acts differ in consequence
225
224
  // (task 1002493). Authoring writes a definition; ARMING is what turns a piece
226
225
  // of text into something that spends money and reads the repo, and it is the
@@ -252,7 +251,6 @@ const PERMISSIONS = [
252
251
  { key: 'criterion.unsatisfy', system: false, floor: 'metic', guards: 'POST /done-when/:id/unsatisfy (ADR 0157, was archon)' },
253
252
  { key: 'security.report.adjudicate', system: false, floor: 'metic', guards: 'GET /security/reports, POST /security/reports/:id/{confirm,dispute,fix,link-fix}, /security/docs CRUD (ADR 0157, was archon)' },
254
253
  { key: 'provisioning.fleet.manage', system: false, floor: 'metic', guards: 'GET /provisioning/fleet[/cost-ledger], POST .../force-teardown (ADR 0157, was archon)' },
255
- { key: 'box.fleet.manage', system: false, floor: 'metic', scope: 'any', resource: 'box', verb: 'manage', guards: 'GET /boxes[/cost-ledger], POST /boxes/:b/close, PATCH .../block (ADR 0157, was archon)' },
256
254
  { key: 'autonomy.precheck', system: false, floor: 'metic', guards: 'GET /autonomy/precheck — unsupervised-runner gate (ADR 0157, was archon); also GET /autonomy/runs, the gate\'s past decisions (task 1002669)' },
257
255
  { key: 'discord.channels.manage', system: false, floor: 'metic', guards: 'GET /discord/channels/plan, POST .../reconcile (ADR 0157, was archon)' },
258
256
  // Metic by the discord.channels.manage analogue — configuring a project-wide
@@ -334,9 +332,6 @@ const PERMISSIONS = [
334
332
  // against another builder's claim. A future `claim.manage.any` (verb `manage`)
335
333
  // would dominate `act` and belongs here; `claim.release.any` never does.
336
334
  { key: 'task.act.own', system: false, floor: 'xenos', scope: 'own', resource: 'claim', verb: 'act', counterpart: [], guards: 'act on OWN active claim (release, cost, notes) — ownership-scoped' },
337
- // The one family whose counterpart is REAL: `box.fleet.manage` is the same `manage`
338
- // verb over every owner's box, so it genuinely subsumes managing your own.
339
- { key: 'box.manage.own', system: false, floor: 'xenos', scope: 'own', resource: 'box', verb: 'manage', counterpart: ['box.fleet.manage'], guards: 'own dev-box lifecycle — ownership-scoped' },
340
335
  // Empty for the same reason as task.act.own: `memory.read.any` READS every
341
336
  // builder's memory, which is not the authority to MANAGE one. This is the pair the
342
337
  // task-1002671 verification probed (finding 12) — a read grant was admitting a
@@ -373,12 +368,12 @@ const PERMISSIONS = [
373
368
  { key: 'blocker.file', system: false, floor: 'xenos', guards: 'POST /blockers' },
374
369
  { key: 'idea.file', system: false, floor: 'xenos', guards: 'POST /inbox (own idea capture)' },
375
370
  { key: 'page.view.public', system: false, floor: 'xenos', guards: 'public read surfaces (incl. xenos)' },
376
- // ADR 0157: floor archon→metic, which is what opens /watch, /harbor and /gate to the
371
+ // ADR 0157: floor archon→metic, which is what opens /watch and /gate to the
377
372
  // working rank. The gate FUNCTION was renamed to match (`requireArchonPage` →
378
373
  // `requireGovernmentPage`) once the floor moved. Viewing is all this grants: every
379
374
  // write those pages reach is its own atom, and `government.manage` (the
380
375
  // ranks/permissions tab) stayed archon.
381
- { key: 'page.view.government', system: false, floor: 'metic', guards: 'the government/monitoring pages — /watch, /harbor, /gate, /government (requireGovernmentPage; write = government.manage, archon)' },
376
+ { key: 'page.view.government', system: false, floor: 'metic', guards: 'the government/monitoring pages — /watch, /gate, /government (requireGovernmentPage; write = government.manage, archon)' },
382
377
  ];
383
378
 
384
379
  // ── invariants asserted at module load (fail loud on an authoring typo) ─────────
@@ -394,8 +389,9 @@ const OWNERSHIP_SCOPES = Object.freeze(['own', 'any']);
394
389
  // row — a read grant admitting a write.
395
390
  //
396
391
  // The verb is DECLARED, never parsed out of the key string. A positional parse is
397
- // wrong on the live population (`box.fleet.manage` is resource `box`, verb `manage`
398
- // — the middle segment is not the verb), and the same reason `scope` is data:
392
+ // wrong on the live population (`task.abandon` is verb `act`, a word the key never
393
+ // spells; `me.wandering.set` is resource `builder_prefs`, not `me`), and the same
394
+ // reason `scope` is data:
399
395
  // "the namespace can never outrun the data".
400
396
  const OWNERSHIP_VERBS = Object.freeze(['read', 'set', 'release', 'act', 'manage']);
401
397
  // Which verbs a verb SUBSUMES, beyond itself (every verb dominates itself). Kept
@@ -549,7 +545,7 @@ for (const [resource, fam] of _byResource) {
549
545
  throw new Error(`government catalog: resource "${resource}" declares a cross-owner permission (${fam.any.join(', ')}) with no ownership-scoped counterpart`);
550
546
  }
551
547
  // Reaching EVERY owner's row must never be cheaper than reaching your own. If it
552
- // were, a rank could manage the whole fleet but not its own box — the model
548
+ // were, a rank could act on everyone's rows but not its own — the model
553
549
  // inverted, and the Government tab would render the wider grant as the safer one.
554
550
  for (const anyKey of fam.any) {
555
551
  for (const ownKey of fam.own) {
@@ -740,7 +736,7 @@ function crossOwnerKeys() {
740
736
  return PERMISSIONS.filter((p) => p.scope === 'any').map((p) => p.key);
741
737
  }
742
738
  // The DECLARED escalation path for `key` — what a route offers as the alternative
743
- // to owning the row ("your own box, or box.fleet.manage"). Empty when nothing may
739
+ // to owning the row ("a task you created, or task.abandon"). Empty when nothing may
744
740
  // escalate past it, which is the common case.
745
741
  //
746
742
  // Read straight off the key's own `counterpart`, NOT derived from its resource
@@ -0,0 +1,33 @@
1
+ -- government_019_retire_box_permissions.sql — REVOKE the three dev-box permission
2
+ -- keys from every rank that holds them (task 1003894, goal 1000120).
3
+ --
4
+ -- Goal 1000120 removed the dev box: its module and routes (task 1003892), and in
5
+ -- task 1003894 the three catalog keys that gated them — `box.source.full`,
6
+ -- `box.fleet.manage` and `box.manage.own`. The catalog is the DECLARATION; the
7
+ -- grant rows are what a builder's permission set is folded from. With the keys
8
+ -- gone from modules/government/catalog.js the resolver already drops these rows
9
+ -- (a key the catalog does not define grants nothing), so this changes no one's
10
+ -- effective authority. It keeps the grant table honest: every row in it names a
11
+ -- permission that exists.
12
+ --
13
+ -- ONE statement for EVERY rank, seeded and custom. The three keys were
14
+ -- `system: false`, so an Archon could have granted them to a custom rank, and a
15
+ -- per-rank DELETE would leave those rows behind. tests/government_seed.mjs reads
16
+ -- this shape (no `rank_key` predicate) as "revoked from every rank" when it folds
17
+ -- the grant migrations into the effective seed.
18
+ --
19
+ -- NOT governance_002 or governance_007: those granted the keys and are applied
20
+ -- history — migrations are run-once and never rewritten.
21
+ --
22
+ -- NOT idempotent by ON CONFLICT — a DELETE needs none. Re-running it deletes zero
23
+ -- rows and raises nothing.
24
+ --
25
+ -- `government_rank_permissions`, NOT the `governance_*` spelling:
26
+ -- government_001_rename_from_governance renamed the table with no shim.
27
+
28
+ BEGIN;
29
+
30
+ DELETE FROM government_rank_permissions
31
+ WHERE permission_key IN ('box.source.full', 'box.fleet.manage', 'box.manage.own');
32
+
33
+ COMMIT;
@@ -2,6 +2,7 @@
2
2
  "key": "government",
3
3
  "title": "Government (configurable ranks & permissions)",
4
4
  "description": "Discord-style configurable ranks and permissions (ADR 0151, BONGOS-V1 goal 1000030; renamed governance\u2192government by ADR 0174). Permissions are the atomic authority unit and the four ranks (xenos/thetes/metic/archon) are the positions in the government; this module owns the code-defined permission catalog (each key flagged `system` = trust-boundary, `substrate` = enforced out of band, or operational = delegable) plus the compatibility seed that reproduces the rank thresholds exactly. A core-domain module, default-on: a Cloud Bongos instance ships configurable ranks the way it ships its status page. The word `role` is deliberately absent here \u2014 it is reserved for the three crafts (Artist, Ideator, Builder). Preserves ADR 0016 (authority resolved server-side, per request, uncached).",
5
+ "version": "1.0.0",
5
6
  "coreVersion": "^1.18.0",
6
7
  "default": true,
7
8
  "maintenance": {
@@ -41,17 +41,9 @@
41
41
  { "glob": "src/bongos/routes.js", "group": "authz", "why": "the route MOUNT table (a new ungated route lands here)" },
42
42
  { "glob": "server.js", "group": "authz", "why": "the Express entrypoint — wires every router + host routing" },
43
43
  { "glob": "modules/government/", "group": "authz", "why": "the same authz core under its post-ADR-0174 name (goal 1000068, task 1003003). Listed BEFORE the folder moves, deliberately: `protected` is DERIVED from this registry, so a module key whose globs are absent here computes protected:false — which would silently drop the Archon gate on goal creation AND the pre-push block on these files for the whole window between the key landing and the folder arriving. The prospective-carve precedent is the modules/government/ entry directly above (ADR 0086 §4)." },
44
- { "glob": "modules/dev-box/boxes.js", "group": "box-access", "why": "boxScopeForRank / provision floor — decides who gets a full source clone" },
45
- { "glob": "modules/dev-box/box-access.js", "group": "box-access", "why": "decideSourceAccess (rank → starter|full source)" },
46
- { "glob": "modules/dev-box/box-credential.js", "group": "box-access", "why": "mints the box source credential" },
47
- { "glob": "scripts/gds/provision-config.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.DO_API_TOKEN. The group already protects who DECIDES box access (boxes.js, box-access.js, box-credential.js) but protected nothing that HOLDS the infrastructure credentials those decisions spend — an asymmetry idea 1000692 raised and this entry closes. The set is computed, not listed: the tripwire in tests/government_protected_surfaces.mjs recomputes every process.env credential read from the tracked tree and reds if one is unregistered." },
48
- { "glob": "scripts/gds/provision-net.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.CLOUDFLARE_API_TOKEN in checkDnsTokenScope. The filing task asserted this code lived in scripts/gds/provision.js; it had moved, and provision.js reads no credential at all. Registering the named file rather than the computed one would have recorded the gap as closed while closing nothing." },
49
- { "glob": "scripts/gds/box-infra.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.CLOUDFLARE_API_TOKEN and process.env.CLOUDFLARE_ACCOUNT_ID into the CF config the box plane provisions against." },
50
- { "glob": "scripts/gds/cf-tunnel.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: defaults its token parameter to process.env.CLOUDFLARE_API_TOKEN, so it reads the credential whenever a caller omits one. Not named by the recorded decision — the recomputation found it, which is the whole argument for computing the set instead of transcribing it." },
51
- { "glob": "scripts/gds/box.js", "group": "box-access", "why": "task 1002891. CREDENTIAL READER: reads process.env.DO_API_TOKEN — the DigitalOcean control plane for the box fleet. Also not named by the recorded decision. Registering it does NOT widen any module wall: the dev-box module key already lists this file and already derives protected:true from the modules/dev-box/ entries above, so the derived protected-module set is unchanged (verified before and after against src/bongos/module-scope-map.js)." },
52
- { "glob": "scripts/gds/box-auth-check.js", "group": "box-access", "why": "task 1004024. Split out of the box.js family and never registered; box-infra.js was already listed by name above." },
53
- { "glob": "scripts/gds/box-sync.js", "group": "box-access", "why": "task 1004024. Split out of the box.js family and never registered; box-infra.js was already listed by name above." },
54
- { "glob": "scripts/gds/do-api.js", "group": "box-access", "why": "task 1002891. The DigitalOcean API client the token is handed TO. Unlike the five above it reads no process.env value — it takes the token as a parameter and throws when a live call has none — so the drift tripwire does not demand it and its absence from that computed set is expected, not a bug. It is registered because the recorded decision named it and because it is the code that puts the credential on the wire." },
44
+ { "glob": "scripts/gds/provision-config.js", "group": "control-plane", "why": "task 1002891. CREDENTIAL READER: reads process.env.DO_API_TOKEN. Before this entry nothing that HOLDS the control plane's infrastructure credentials was protected at any rank — an asymmetry idea 1000692 raised and this entry closes. The set is computed, not listed: the tripwire in tests/government_protected_surfaces.mjs recomputes every process.env credential read from the tracked tree and reds if one is unregistered." },
45
+ { "glob": "scripts/gds/provision-net.js", "group": "control-plane", "why": "task 1002891. CREDENTIAL READER: reads process.env.CLOUDFLARE_API_TOKEN in checkDnsTokenScope. The filing task asserted this code lived in scripts/gds/provision.js; it had moved, and provision.js reads no credential at all. Registering the named file rather than the computed one would have recorded the gap as closed while closing nothing." },
46
+ { "glob": "scripts/gds/do-api.js", "group": "control-plane", "why": "task 1002891. The DigitalOcean API client the token is handed TO. Unlike the two above it reads no process.env value — it takes the token as a parameter and throws when a live call has none — so the drift tripwire does not demand it and its absence from that computed set is expected, not a bug. It is registered because the recorded decision named it and because it is the code that puts the credential on the wire." },
55
47
  { "glob": "modules/grading/grader.js", "group": "pipeline", "why": "BV1.R73: grading carved to modules/grading/ — the glob follows the file" },
56
48
  { "glob": "modules/grading/grader-config.js", "group": "pipeline", "why": "task 1004024. Carved out of grader.js: the deterministic permission-path pre-pass, the server-side score guards a model cannot talk past, the subagent spawn and the prompt. grader.js alone protected the facade and none of the gate." },
57
49
  { "glob": "modules/grading/grader-model.js", "group": "pipeline", "why": "task 1004024. Carved out of grader.js: the deterministic permission-path pre-pass, the server-side score guards a model cannot talk past, the subagent spawn and the prompt. grader.js alone protected the facade and none of the gate." },
@@ -29,7 +29,6 @@
29
29
 
30
30
  const catalog = require('./catalog');
31
31
  const db = require('./db');
32
- const sessionScopes = require('./session-scopes');
33
32
 
34
33
  // Pure core of the RANK axis, split out so it is unit-testable without a database.
35
34
  //
@@ -93,7 +92,7 @@ async function resolveBuilderPermissions(builderId) {
93
92
  // false so a mis-authored gate denies rather than admits (the same posture
94
93
  // requireRank takes on an all-unknown allow-list).
95
94
  //
96
- // It also REFUSES ownership-scoped keys (R100). "Do you hold box.manage.own" has
95
+ // It also REFUSES ownership-scoped keys (R100). "Do you hold task.abandon.own" has
97
96
  // no unconditional answer — the permission is parameterized by a resource this
98
97
  // function was never given — and returning true would be precisely the collapse
99
98
  // of `.own` into an unconditional grant the axis exists to prevent. The caller
@@ -116,7 +115,7 @@ async function builderHasPermissions(builderId, required) {
116
115
  // handler that loaded the row knows it)
117
116
  //
118
117
  // 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
118
+ // claim/memory/task row. So the caller SUPPLIES the owner and the model refuses to
120
119
  // answer without it — `ownerId` is not an option, it is the second half of the
121
120
  // question. That is what keeps `.own` from degrading into `.any`: there is no code
122
121
  // path here that turns an ownership-scoped key into an admit on its own.
@@ -156,7 +155,7 @@ async function authorizeOwned(builderId, key, context) {
156
155
 
157
156
  // The composition every own-scoped route actually wants: reach this row because it
158
157
  // 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
158
+ // family (task.abandon for task.abandon.own). One resolve, both
160
159
  // branches. A resource with no cross-owner form by design (credential) simply has
161
160
  // no second branch, so the answer stays own-only.
162
161
  async function authorizeOwnedOrAny(builderId, ownKey, context) {
@@ -167,46 +166,6 @@ async function authorizeOwnedOrAny(builderId, ownKey, context) {
167
166
  return sameOwner(context && context.ownerId, builderId) && held.includes(ownKey);
168
167
  }
169
168
 
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. Ranks 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 rank-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 ranks do
181
- // not confer — so the composition below is written as a FILTER of the RANK
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 RANK 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 rank 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 ranks ∩ 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
169
  module.exports = {
211
170
  resolveBuilderPermissions,
212
171
  builderHasPermissions,
@@ -216,6 +175,4 @@ module.exports = {
216
175
  sameOwner,
217
176
  authorizeOwned,
218
177
  authorizeOwnedOrAny,
219
- narrowToSessionScope,
220
- resolveSessionPermissions,
221
178
  };
@@ -78,8 +78,6 @@ const governmentPort = {
78
78
  sameOwner: resolver.sameOwner,
79
79
  isOwnershipScoped: catalog.isOwnershipScoped,
80
80
  crossOwnerKeysFor: catalog.crossOwnerKeysFor,
81
- resolveSessionPermissions: resolver.resolveSessionPermissions,
82
- narrowToSessionScope: resolver.narrowToSessionScope,
83
81
  sessionScopeFor: sessionScopes.scopeFor,
84
82
  listSessionScopes: () => sessionScopes.SESSION_SCOPES,
85
83
  // The Board Room's membership axis (R06 of goal 1000069): who governs, right
@@ -1,127 +1,87 @@
1
1
  'use strict';
2
2
 
3
- // modules/government/session-scopes.js — the SESSION-SCOPE axis (ADR 0151 §3, BV1.R99).
3
+ // modules/government/session-scopes.js — the SESSION-SOURCE registry (ADR 0151 §3, BV1.R99).
4
4
  //
5
- // The FOURTH axis, and the only one attached to the TOKEN rather than to the
6
- // builder. Ranks 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.
5
+ // The one government axis attached to the TOKEN rather than to the builder: which
6
+ // kinds of session may authenticate at all. Every builder_sessions row carries a
7
+ // `source`; session-scopes.json (next to this file) declares the sources this
8
+ // instance accepts, and a session whose source it does not declare is DENIED by the
9
+ // kernel on every route and page. Every declared source carries its builder's full
10
+ // rank authority.
12
11
  //
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 ranks 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 rank answer so widening is not expressible there.
12
+ // It used to be more. A row could declare a NARROWING scope — a deny-by-default
13
+ // permissions allow-list, composed with the rank answer as an intersection — for a
14
+ // token living outside the ADR 0016 trust boundary. Its only user was the
15
+ // machine-baked token (ADR 0053) that goal 1000120 retired, so the allow-list form,
16
+ // its catalog cross-check and the resolver's narrowing left with it (task 1003894).
17
+ // A row that still declares one is REFUSED at load: with the enforcement gone, it
18
+ // would read as a restriction while granting full authority.
21
19
  //
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. GOVERNMENT 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.
20
+ // GOVERNMENT OWNS THE LIST, THE KERNEL STAYS THE ENFORCER — the same split R101
21
+ // draws for the protected-surface registry: src/bongos/auth.js reads the same JSON
22
+ // as DATA (fs, never `require` — ADR 0083's one-way rule) and requireBuilder remains
23
+ // the gate every route composes. Nothing here enforces anything.
30
24
  //
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.
25
+ // FAIL-CLOSED at load: an empty registry, a duplicate or malformed source, or a row
26
+ // declaring a scope all THROW. An unreadable policy is a broken install, not an
27
+ // opted-out one.
36
28
 
37
- const catalog = require('./catalog');
38
29
  const registry = require('./session-scopes.json');
39
30
 
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.
31
+ // Normalize a declared row into the same shape the kernel loader produces:
32
+ // `source` null for the legacy sourceless row.
42
33
  function normalizeScope(row) {
43
34
  return Object.freeze({
44
35
  source: row && row.source !== undefined ? row.source : null,
45
36
  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] : []),
37
+ declaresScope: !!(row && (row.permissions !== undefined || row.unscoped !== undefined)),
48
38
  });
49
39
  }
50
40
 
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) {
41
+ // The load-time assertions. Exported so the shipped check itself is what the tests
42
+ // exercise, rather than a re-implementation of it.
43
+ function validateScopes(scopes) {
44
+ if (!Array.isArray(scopes) || scopes.length === 0) {
45
+ throw new Error('session-scope registry declares no session sources. Refusing to run with no session-source model (ADR 0151).');
46
+ }
47
+ const seen = new Set();
55
48
  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`);
49
+ const where = `session-scope registry: source "${scope.source}"`;
50
+ if (scope.source !== null && (typeof scope.source !== 'string' || !scope.source)) {
51
+ throw new Error('session-scope registry: every row needs a non-empty session source string, or null for the legacy sourceless row');
61
52
  }
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 rank — 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 RANK 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
- }
53
+ if (seen.has(scope.source)) throw new Error(`${where} is declared twice`);
54
+ seen.add(scope.source);
55
+ if (scope.declaresScope) {
56
+ throw new Error(`${where} declares a scope (permissions/unscoped). Narrowing session scopes were removed (task 1003894); a row may only declare a source that carries its builder's full authority.`);
78
57
  }
79
58
  }
80
59
  return scopes;
81
60
  }
82
61
 
83
62
  const SESSION_SCOPES = Object.freeze(
84
- validateAgainstCatalog(Object.freeze((registry.scopes || []).map(normalizeScope))),
63
+ validateScopes(Object.freeze((registry.scopes || []).map(normalizeScope))),
85
64
  );
86
65
 
87
66
  const _bySource = new Map(SESSION_SCOPES.map((s) => [s.source, s]));
88
67
 
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).
68
+ // The declared row for a session's `source`, or null when the registry does not
69
+ // declare it — which the kernel treats as DENY, never as "full" (a null/absent
70
+ // source is a DECLARED row here, not an implicit default).
93
71
  function scopeFor(source) {
94
72
  const key = source === undefined ? null : source;
95
73
  if (key !== null && typeof key !== 'string') return null;
96
74
  return _bySource.get(key) || null;
97
75
  }
98
76
 
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
77
  function sources() {
116
78
  return SESSION_SCOPES.map((s) => s.source);
117
79
  }
118
80
 
119
81
  module.exports = {
120
82
  SESSION_SCOPES,
121
- validateAgainstCatalog,
83
+ validateScopes,
122
84
  normalizeScope,
123
85
  scopeFor,
124
- isScoped,
125
- allowedKeys,
126
86
  sources,
127
87
  };
@@ -1,38 +1,28 @@
1
1
  {
2
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 ranks.",
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 rank permissions) INTERSECT (this allow-list), never a union.",
8
- "Every key here is validated against modules/government/catalog.js at load by",
9
- "modules/government/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)."
3
+ "The SESSION-SOURCE registry (BV1.R99, ADR 0151 §3): which kinds of session token may authenticate.",
4
+ "Every builder_sessions row carries a `source`. A session whose source is declared here carries its",
5
+ "builder's full rank authority; a session whose source is NOT declared is DENIED on every route and page.",
6
+ "Rows declare a source only. The narrowing form (a deny-by-default permissions allow-list) was removed",
7
+ "with its last user in task 1003894, and both loaders refuse a row that still declares one.",
8
+ "Read as DATA by src/bongos/auth.js (fs, never require — ADR 0083's one-way rule) and by",
9
+ "modules/government/session-scopes.js."
11
10
  ],
12
11
  "scopes": [
13
12
  {
14
13
  "source": "cli",
15
14
  "label": "CLI session",
16
- "unscoped": true,
17
15
  "note": "Device Flow token on a builder's own machine — inside the trust boundary, carries the builder's full rank authority."
18
16
  },
19
17
  {
20
18
  "source": "web",
21
19
  "label": "web session",
22
- "unscoped": true,
23
20
  "note": "Hall browser cookie — same authority as the CLI session."
24
21
  },
25
22
  {
26
23
  "source": null,
27
24
  "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."
25
+ "note": "Rows minted before migration 090 added a source column. Declared explicitly rather than defaulted — an UNRECOGNISED source denies."
36
26
  }
37
27
  ]
38
28
  }
@@ -2,6 +2,7 @@
2
2
  "key": "grading",
3
3
  "title": "Grading",
4
4
  "description": "The grade-a-diff adversarial worker panel (Narc/Quality/Hacker/Efficiency) + deterministic route-rank/permission-path pre-passes + the scoring rubric. A core-domain module carved out of the GDS core in BV1.R73 (ADR 0091); default-on because every instance grades its own ships. Registers the `grade` kernel PORT the lifecycle resolves at confirm/ship time.",
5
+ "version": "1.0.0",
5
6
  "coreVersion": "^1.6.0",
6
7
  "default": true,
7
8
  "maintenance": { "status": "core-maintained" },
@@ -2,6 +2,7 @@
2
2
  "key": "hall-ui",
3
3
  "title": "Builders' Hall",
4
4
  "description": "The authenticated builders' hall served at builders.<apex> (builders.example.com) — the roster, work board, standing/karma, leaderboard, achievements, primer, system maps, settings, and the Archon-only watch/gate pages, all polling /api/bongos/* and the read-only /api/bongos/public/* surface. The largest WEB-SURFACE core-domain module (~13k LOC), carved out of the GDS core in BV1.R85 (ADR 0093 §1; 02-module-map §6). default-on: a Cloud Bongos instance ships its hall like it ships its status page. Registers no port and contributes no routes — it declares a webSurface (host prefix + static dir). Unlike the generic status surface, the hall keeps its dedicated serving block in serve-internal.js (the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection) — the webSurfaces declaration is the discovery + scope-map + window.__MODULES__ record; serve-internal serves it from this module's public/ dir with that richer machinery. The me/public/leaderboard read endpoints it renders stay SHARED core in routes/{me,public,builders}.js (the status surface + the off-box mirror read them too).",
5
+ "version": "1.0.0",
5
6
  "coreVersion": "^1.11.0",
6
7
  "default": true,
7
8
  "maintenance": { "status": "core-maintained" },
@@ -133,8 +133,8 @@
133
133
  // ---- shared row pieces ---------------------------------------------------
134
134
  //
135
135
  // board-room.js's header called these per-page boilerplate "by convention
136
- // (gate.js, harbor.js and watch.js each carry their own)". That is not what
137
- // the tree says: those three write .fact-pill markup inline and carry no such
136
+ // (gate.js and watch.js each carry their own)". That is not what the tree
137
+ // says: those pages write .fact-pill markup inline and carry no such
138
138
  // helper. This exact trio is declared in precisely two files — these two — as
139
139
  // byte-identical copies, so it is a two-file duplication like the rest.
140
140