@bongos/core 1.18.18 → 1.19.2

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 (251) hide show
  1. package/.bongos-core.json +515 -195
  2. package/.claude/skills/builder-box/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +1 -1
  4. package/.claude/skills/goal-create/SKILL.md +1 -1
  5. package/.claude/skills/idea-triage/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +5 -5
  7. package/.claude/skills/scan-before-install/SKILL.md +165 -0
  8. package/clients/bongos-client/README.md +1 -1
  9. package/clients/bongos-client/bongos-client.global.js +63 -41
  10. package/clients/bongos-client/index.cjs +63 -41
  11. package/clients/bongos-client/index.d.ts +78 -44
  12. package/clients/bongos-client/index.mjs +63 -41
  13. package/docs/adr/0020-security-model-public-stub.md +1 -1
  14. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
  15. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  16. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
  17. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
  18. package/docs/adr/0156-user-set-display-name-wins.md +62 -0
  19. package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
  20. package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
  21. package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
  22. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
  23. package/docs/adr/README.md +8 -0
  24. package/docs/api/openapi.json +1209 -233
  25. package/docs/api-reference.md +61 -46
  26. package/docs/architecture.md +1 -1
  27. package/docs/canonical-permissions.md +61 -16
  28. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  29. package/docs/file-map.md +4 -1
  30. package/docs/onboarding/diagrams/assertions.json +3 -3
  31. package/docs/recipes/core-release-pipeline.md +179 -0
  32. package/docs/recipes/packaging-the-core.md +2 -0
  33. package/docs/recipes/parallel-goal-waves.md +1 -3
  34. package/docs/recipes/private-npm-distribution.md +2 -0
  35. package/docs/recipes/upgrading-the-core.md +2 -0
  36. package/migrations/core_197_display_name_source.sql +56 -0
  37. package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
  38. package/modules/autonomy/routes/autonomy.js +1 -1
  39. package/modules/builder-settings/db.js +7 -1
  40. package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
  41. package/modules/dev-box/routes/box.js +11 -9
  42. package/modules/discord/auth.js +3 -1
  43. package/modules/discord/routes/discord.js +8 -2
  44. package/modules/discord/ship-broadcast.js +48 -1
  45. package/modules/governance/catalog.js +70 -20
  46. package/modules/governance/db.js +102 -0
  47. package/modules/governance/migrations/governance_006_version_create.sql +38 -0
  48. package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
  49. package/modules/governance/routes/governance.js +235 -6
  50. package/modules/grading/grader-workers/index.js +18 -9
  51. package/modules/hall-ui/public/gate.js +6 -2
  52. package/modules/hall-ui/public/goals-page.js +6 -2
  53. package/modules/hall-ui/public/governance.css +100 -0
  54. package/modules/hall-ui/public/governance.html +77 -0
  55. package/modules/hall-ui/public/governance.js +353 -0
  56. package/modules/hall-ui/public/harbor.js +14 -8
  57. package/modules/hall-ui/public/instances-panel.js +3 -1
  58. package/modules/hall-ui/public/new-project-wizard.js +6 -0
  59. package/modules/hall-ui/public/people.html +51 -0
  60. package/modules/hall-ui/public/people.js +100 -0
  61. package/modules/hall-ui/public/profile.html +2 -1
  62. package/modules/hall-ui/public/profile.js +9 -3
  63. package/modules/hall-ui/public/sessions.js +4 -2
  64. package/modules/hall-ui/public/shell.js +28 -7
  65. package/modules/hall-ui/public/task-detail.js +6 -3
  66. package/modules/hall-ui/public/watch.js +17 -9
  67. package/modules/ideas/blockers.js +44 -7
  68. package/modules/ideas/routes/blockers.js +12 -7
  69. package/modules/ideas/routes/inbox.js +2 -2
  70. package/modules/lifecycle/db.js +84 -17
  71. package/modules/lifecycle/github-push.js +37 -10
  72. package/modules/lifecycle/publish-reconciler.js +128 -31
  73. package/modules/lifecycle/routes/claims.js +10 -4
  74. package/modules/lifecycle/routes/dependencies.js +2 -2
  75. package/modules/lifecycle/routes/done-when.js +4 -4
  76. package/modules/lifecycle/routes/gate-approvals.js +9 -3
  77. package/modules/lifecycle/routes/goals.js +5 -5
  78. package/modules/lifecycle/routes/tasks.js +16 -16
  79. package/modules/lifecycle/routes/versions.js +13 -2
  80. package/modules/lifecycle/ship-card.js +38 -7
  81. package/modules/memory/routes/memory.js +2 -2
  82. package/modules/onboarding/routes/access-requests.js +3 -3
  83. package/modules/platform-identity/routes/projects.js +10 -6
  84. package/modules/platform-identity/routes/scouting.js +2 -2
  85. package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
  86. package/modules/provisioning/provisioning.js +24 -6
  87. package/modules/provisioning/routes/provisioning.js +24 -6
  88. package/modules/security/artifact-scan/aggregate.js +133 -0
  89. package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
  90. package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
  91. package/modules/security/artifact-scan/detectors/credentials.js +112 -0
  92. package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
  93. package/modules/security/artifact-scan/detectors/hooks.js +116 -0
  94. package/modules/security/artifact-scan/detectors/injection.js +118 -0
  95. package/modules/security/artifact-scan/detectors/mcp.js +102 -0
  96. package/modules/security/artifact-scan/detectors/network.js +67 -0
  97. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
  98. package/modules/security/artifact-scan/detectors/permissions.js +91 -0
  99. package/modules/security/artifact-scan/detectors/shared.js +219 -0
  100. package/modules/security/artifact-scan/fence-local.js +99 -0
  101. package/modules/security/artifact-scan/fetch.js +330 -0
  102. package/modules/security/artifact-scan/floor.js +319 -0
  103. package/modules/security/artifact-scan/limits.js +66 -0
  104. package/modules/security/artifact-scan/manifest.js +203 -0
  105. package/modules/security/artifact-scan/mirror.js +158 -0
  106. package/modules/security/artifact-scan/quarantine.js +149 -0
  107. package/modules/security/artifact-scan/rebuild-spec.js +120 -0
  108. package/modules/security/artifact-scan/report.js +155 -0
  109. package/modules/security/artifact-scan/sanitize.js +317 -0
  110. package/modules/security/migrations/security_001_tool_scans.sql +145 -0
  111. package/modules/security/module.json +2 -2
  112. package/modules/security/routes/reports.js +11 -7
  113. package/modules/security/routes/scans.js +115 -0
  114. package/modules/security/scan-store.js +214 -0
  115. package/modules/security/tests/aggregate.mjs +283 -0
  116. package/modules/security/tests/artifact_scan.mjs +591 -0
  117. package/modules/security/tests/floor.mjs +549 -0
  118. package/modules/security/tests/mirror.mjs +350 -0
  119. package/modules/security/tests/sanitize_rebuild.mjs +499 -0
  120. package/modules/security/tests/scan_store.mjs +250 -0
  121. package/modules/security/tests/tool_scans_migration.mjs +116 -0
  122. package/modules/sessions/routes/sessions.js +6 -5
  123. package/package-lock.json +2 -2
  124. package/package.json +1 -1
  125. package/public-landing/create.html +13 -3
  126. package/scripts/gds/abandon.js +1 -1
  127. package/scripts/gds/audit-authorship.sh +6 -0
  128. package/scripts/gds/box.js +1 -1
  129. package/scripts/gds/claim.js +15 -0
  130. package/scripts/gds/cli-lib.js +16 -4
  131. package/scripts/gds/gating.js +28 -13
  132. package/scripts/gds/gen-api-docs.js +1 -1
  133. package/scripts/gds/goal.js +234 -0
  134. package/scripts/gds/init.js +70 -15
  135. package/scripts/gds/main-audit.js +80 -11
  136. package/scripts/gds/mirror-redact.js +6 -0
  137. package/scripts/gds/newcomer-floor.js +2 -2
  138. package/scripts/gds/provision.js +230 -94
  139. package/scripts/gds/run-unit-tests.js +9 -1
  140. package/scripts/gds/sandbox-stage.js +66 -9
  141. package/scripts/gds/search-ingest-md.js +1 -0
  142. package/scripts/gds/sequence.js +67 -8
  143. package/scripts/gds/ship.js +183 -50
  144. package/scripts/hall-preview/server.js +1 -1
  145. package/scripts/migrate.sh +99 -14
  146. package/src/bongos/auth.js +129 -27
  147. package/src/bongos/db-kernel.js +66 -5
  148. package/src/bongos/db.js +6 -7
  149. package/src/bongos/github-repos.js +19 -7
  150. package/src/bongos/goal-scope-check.js +1 -1
  151. package/src/bongos/module-scope-map.js +3 -1
  152. package/src/bongos/permission-path-check.js +1 -1
  153. package/src/bongos/route-rank-check.js +73 -30
  154. package/src/bongos/routes/audit-log.js +4 -2
  155. package/src/bongos/routes/auth.js +8 -4
  156. package/src/bongos/routes/backup.js +7 -4
  157. package/src/bongos/routes/builders.js +23 -11
  158. package/src/bongos/routes/github.js +15 -4
  159. package/src/bongos/routes/llm-cache.js +5 -2
  160. package/src/bongos/routes/modules.js +9 -5
  161. package/src/bongos/routes/security.js +21 -12
  162. package/src/bongos/serve-internal.js +22 -7
  163. package/src/module-api.js +86 -1
  164. package/src/platform-server.js +14 -0
  165. package/tests/api_client.mjs +18 -2
  166. package/tests/api_path_404.mjs +3 -0
  167. package/tests/art_stage.mjs +2 -1
  168. package/tests/atlas.mjs +32 -4
  169. package/tests/auth_admission_gate.mjs +17 -5
  170. package/tests/auth_error_brand.mjs +41 -21
  171. package/tests/auth_resolve_failure.mjs +155 -0
  172. package/tests/blocker_link.mjs +88 -0
  173. package/tests/bongos_downloads.mjs +2 -1
  174. package/tests/box_dns_repoint.mjs +2 -0
  175. package/tests/box_ship_permissions.mjs +2 -0
  176. package/tests/box_terminal_ssrf.mjs +23 -17
  177. package/tests/claim_error_surface.mjs +23 -0
  178. package/tests/claude_materialize.mjs +4 -0
  179. package/tests/cli_box_scope.mjs +40 -0
  180. package/tests/cloud_bongos_pack.mjs +34 -4
  181. package/tests/conductor.mjs +2 -1
  182. package/tests/criterion_authoring_gate.mjs +28 -5
  183. package/tests/currency_label.mjs +10 -1
  184. package/tests/deploy_instance.mjs +2 -0
  185. package/tests/devbox_downloads.mjs +2 -1
  186. package/tests/display_name.mjs +95 -6
  187. package/tests/eviction_policy.mjs +2 -1
  188. package/tests/file_map.mjs +10 -0
  189. package/tests/first_admin_bootstrap.mjs +10 -2
  190. package/tests/first_run_engine_check.mjs +2 -1
  191. package/tests/fitness.mjs +26 -6
  192. package/tests/gate_approvals.mjs +15 -2
  193. package/tests/github_push_land_proof.mjs +50 -4
  194. package/tests/github_repos_helpers.mjs +30 -5
  195. package/tests/github_token_store.mjs +23 -0
  196. package/tests/goal_abuse_matrix.mjs +13 -3
  197. package/tests/goal_cli.mjs +89 -0
  198. package/tests/governance_abuse_matrix.mjs +386 -0
  199. package/tests/governance_fold.mjs +156 -0
  200. package/tests/governance_fold_bench.mjs +50 -0
  201. package/tests/governance_live_proof.mjs +201 -0
  202. package/tests/governance_page_gates.mjs +21 -9
  203. package/tests/governance_parity.mjs +11 -2
  204. package/tests/governance_require_permission.mjs +125 -1
  205. package/tests/governance_routes.mjs +334 -0
  206. package/tests/grader.mjs +7 -3
  207. package/tests/hall_nav.mjs +14 -4
  208. package/tests/helpers.mjs +73 -0
  209. package/tests/hook_core_resolve.mjs +2 -1
  210. package/tests/host_topology_skips.mjs +97 -0
  211. package/tests/infra_exec_paths.mjs +14 -4
  212. package/tests/init.mjs +128 -0
  213. package/tests/known_failing_ratchet.mjs +89 -0
  214. package/tests/lifecycle-card.mjs +2 -1
  215. package/tests/main_audit.mjs +72 -1
  216. package/tests/memory_isolation.mjs +56 -9
  217. package/tests/migrate_root_resolution.mjs +124 -1
  218. package/tests/mirror_redact.mjs +19 -0
  219. package/tests/module_api.mjs +3 -1
  220. package/tests/module_loader.mjs +4 -0
  221. package/tests/modules.mjs +35 -7
  222. package/tests/modules_route.mjs +29 -5
  223. package/tests/onboarding_observability.mjs +49 -11
  224. package/tests/onboarding_route_signals.mjs +1 -0
  225. package/tests/one_active_claim_per_session.mjs +80 -0
  226. package/tests/pre_compact.mjs +2 -1
  227. package/tests/preview_server.mjs +2 -1
  228. package/tests/profile_route.mjs +1 -0
  229. package/tests/provision.mjs +170 -0
  230. package/tests/provisioning_onboard_mode.mjs +180 -0
  231. package/tests/publish_manifest.mjs +16 -3
  232. package/tests/publish_reconciler.mjs +181 -10
  233. package/tests/rank_tier_single_source.mjs +205 -0
  234. package/tests/recall_hints.mjs +2 -1
  235. package/tests/repo_map.mjs +4 -0
  236. package/tests/route_permission_check.mjs +14 -5
  237. package/tests/rubric_fixtures.mjs +3 -0
  238. package/tests/run_routine_two_root.mjs +2 -0
  239. package/tests/sandbox_stage.mjs +69 -7
  240. package/tests/search_ingest.mjs +7 -0
  241. package/tests/security_docs.mjs +28 -6
  242. package/tests/sequence.mjs +64 -0
  243. package/tests/session-cards.mjs +2 -1
  244. package/tests/session_start_freshness.mjs +2 -1
  245. package/tests/ship_broadcast_server.mjs +63 -1
  246. package/tests/ship_card.mjs +66 -0
  247. package/tests/ship_ci_deploy.mjs +66 -1
  248. package/tests/ship_smoke_gate.mjs +76 -0
  249. package/tests/versions_route.mjs +15 -0
  250. package/tests/workflow_exec_paths.mjs +16 -4
  251. package/tests/worktree_guard.mjs +2 -1
@@ -223,7 +223,7 @@ function checkPermissionPaths({ changedFiles, builderRank } = {}) {
223
223
  `${h.file} is a permission-sensitive surface (matches \`${h.glob}\`). `
224
224
  + `Builder rank '${builderRank}' is below ${floorLabel} and may not modify the rank / permission / `
225
225
  + `ship-grade / deploy / migration machinery (ADR 0016, SEC #863). `
226
- + `If this change is genuinely needed, an Archon must own it.`,
226
+ + `If this change is genuinely needed, a ${floorLabel}+ builder must own it.`,
227
227
  });
228
228
  }
229
229
  return {
@@ -216,10 +216,15 @@ const EXPECTED_RANKS = {
216
216
  // (dev-box fleet pins likewise live in MODULE_EXPECTED_RANKS below — BV1.R43.)
217
217
  // security engagement docs — exploit-detail report store (task 991). Pinned
218
218
  // because a silent downgrade here leaks the attacker cookbook below the
219
- // Archon line. (security-the-PROPERTY stays kernel — routes/security.js.)
220
- 'GET /security/docs': 'archon',
221
- 'GET /security/docs/:slug': 'archon',
222
- 'POST /security/docs': 'archon',
219
+ // trust line. (security-the-PROPERTY stays kernel — routes/security.js.)
220
+ // ADR 0157 lowered `security.report.adjudicate` archon→metic, so these are pinned by
221
+ // ATOM rather than by rank: the invariant worth guarding is "this route gates on the
222
+ // adjudication permission", and an atom pin states exactly that while surviving any
223
+ // future floor move without a pin edit (pinMismatch compares atom identity for
224
+ // `perm:` pins, rank only for rank pins).
225
+ 'GET /security/docs': 'perm:security.report.adjudicate',
226
+ 'GET /security/docs/:slug': 'perm:security.report.adjudicate',
227
+ 'POST /security/docs': 'perm:security.report.adjudicate',
223
228
  // The full threat model + defenses inventory (ADR 0020). Same class as the docs
224
229
  // pins above — a silent downgrade puts the defense inventory below the trust line.
225
230
  // Newly VISIBLE in BV1.R109b (declared multi-line, so previously unpinnable).
@@ -248,10 +253,13 @@ const EXPECTED_RANKS = {
248
253
  // so tightening these to `perm:` pins is a clean follow-up once R104-R107 have landed.
249
254
  const MODULE_EXPECTED_RANKS = {
250
255
  'dev-box': {
251
- 'GET /boxes': 'archon',
252
- 'GET /boxes/cost-ledger': 'archon',
253
- 'POST /boxes/:builderId/close': 'archon',
254
- 'PATCH /boxes/:builderId/block': 'archon',
256
+ // ADR 0157 lowered `box.fleet.manage` archon→metic (operating paid infra is admin
257
+ // work, not an authority grant). Re-pinned by ATOM so the guarded invariant is
258
+ // "these gate on the fleet permission", independent of its floor.
259
+ 'GET /boxes': 'perm:box.fleet.manage',
260
+ 'GET /boxes/cost-ledger': 'perm:box.fleet.manage',
261
+ 'POST /boxes/:builderId/close': 'perm:box.fleet.manage',
262
+ 'PATCH /boxes/:builderId/block': 'perm:box.fleet.manage',
255
263
  },
256
264
  // The lifecycle module's privileged routes (carved BV1.R86 — the pins follow the
257
265
  // routes from EXPECTED_RANKS). Verified only when the lifecycle module is in scope.
@@ -264,12 +272,15 @@ const MODULE_EXPECTED_RANKS = {
264
272
  'POST /tasks/:id/promote': 'metic+archon',
265
273
  'POST /tasks/:id/abandon': 'metic+archon',
266
274
  // override-request adjudication (lands credit + ship attribution).
267
- 'GET /override-requests': 'archon',
268
- 'POST /override-requests/:id/decide': 'archon',
275
+ // ADR 0157: `override_request.decide` lowered archon→metic; pinned by atom.
276
+ 'GET /override-requests': 'perm:override_request.decide',
277
+ 'POST /override-requests/:id/decide': 'perm:override_request.decide',
269
278
  // polymorphic dependency edges (ADR 0086 §5) — authoring a cross-tier
270
279
  // dependency reshapes execution order + feeds the claim gate; both pinned.
271
- 'POST /dependencies': 'archon',
272
- 'DELETE /dependencies': 'archon',
280
+ // These STAYED archon under ADR 0157: `dependency.cross_tier.manage` is system:true,
281
+ // and the catalog forbids granting a system key below archon.
282
+ 'POST /dependencies': 'perm:dependency.cross_tier.manage',
283
+ 'DELETE /dependencies': 'perm:dependency.cross_tier.manage',
273
284
  // goal tier (ADR 0086 §3) — CREATE Metic+ (Archon for a protected-module scope,
274
285
  // enforced in-handler); member admission is the protected-scope approval grant.
275
286
  'POST /goals': 'metic+archon',
@@ -279,9 +290,10 @@ const MODULE_EXPECTED_RANKS = {
279
290
  // below). The wall is in-handler (owner | manager | Archon), with a protected-
280
291
  // scope goal staying Archon-only (actor AND Metic+ target) inside the handler.
281
292
  'POST /goals/:id/members': 'any-builder',
282
- // per-version priority reorder (task 1760) — an owner decision, Archon-only;
283
- // never self-granted by a goal's own member/manager authority.
284
- 'POST /goals/reorder': 'archon',
293
+ // per-version priority reorder (task 1760) — never self-granted by a goal's own
294
+ // member/manager authority. ADR 0157 lowered `goal.reorder` archon→metic (ordering
295
+ // work is prioritisation, not an authority grant); pinned by atom.
296
+ 'POST /goals/reorder': 'perm:goal.reorder',
285
297
  // ask-to-widen (ADR 0086 §4) — widening a goal's scope_modules; never self-granted.
286
298
  'POST /goals/:id/scope': 'metic+archon',
287
299
  // goal lifecycle disposition (ADR 0086 §6) — a rank drop here would let any
@@ -342,23 +354,34 @@ const MODULE_EXPECTED_RANKS = {
342
354
  // shipped scope-truth; ADR 0154's whole safety argument is that the backward
343
355
  // ratchet did not move with the create. Previously unpinned — pinned here so a
344
356
  // silent downgrade reds the build (task 1002538).
345
- 'POST /done-when/:criterionId/unsatisfy': 'archon',
346
- // gate-surface PR approval from the hall (task 1179, ADR 0058 amendment) — the
347
- // approve route clears the gate-review hard floor; the list route exposes gate internals.
348
- 'GET /gate-approvals': 'archon',
349
- 'POST /gate-approvals/:pr/approve': 'archon',
357
+ // ADR 0157 lowered `criterion.unsatisfy` archon→metic. The backward-ratchet warning
358
+ // above still holds — un-flipping a confirmed criterion rewrites shipped scope-truth
359
+ // — but it is scope disposition, not an authority grant, so it delegates with the
360
+ // rest. Pinned by atom so the guarded invariant is the permission, not its floor.
361
+ 'POST /done-when/:criterionId/unsatisfy': 'perm:criterion.unsatisfy',
362
+ // gate-surface PR approval from the hall (task 1179, ADR 0058 amendment). ADR 0157
363
+ // SPLIT these two: the list route is `gate_approval.read` (metic — the Gate page
364
+ // opens to the working rank), while APPROVING still clears the gate-review hard
365
+ // floor and stays `gate_approval.decide` (archon, system). The split is the reason
366
+ // the page is usable at all — see the atom's note in catalog.js.
367
+ 'GET /gate-approvals': 'perm:gate_approval.read',
368
+ 'POST /gate-approvals/:pr/approve': 'perm:gate_approval.decide',
350
369
  },
351
370
  // Ratifying a criterion proposal WRITES scope-truth (it creates a done-when
352
- // criterion). Metic+ may file the proposal; only the Archon may ratify it.
371
+ // criterion). Metic+ may file the proposal; ADR 0157 also lets Metic ratify it
372
+ // (`idea.ratify` archon→metic) — the rank that triages ideas now also ratifies them.
353
373
  ideas: {
354
- 'POST /inbox/:id/ratify': 'archon',
374
+ 'POST /inbox/:id/ratify': 'perm:idea.ratify',
355
375
  },
356
376
  // Cross-builder memory READS — the counterpart to the BFG cross-builder WRITE the
357
377
  // KNOWN_RANKS note above already protects. A downgrade here breaks the memory
358
378
  // isolation invariant: any builder could read every other builder's memory.
379
+ // ADR 0157 lowered `memory.read.any` archon→metic. The isolation invariant the note
380
+ // above protects is unchanged in KIND — a cross-builder read still requires an
381
+ // explicit permission no ordinary builder holds — only its floor moved. Pinned by atom.
359
382
  memory: {
360
- 'GET /memory/builders/:builderId/files': 'archon',
361
- 'GET /memory/builders/:builderId/file': 'archon',
383
+ 'GET /memory/builders/:builderId/files': 'perm:memory.read.any',
384
+ 'GET /memory/builders/:builderId/file': 'perm:memory.read.any',
362
385
  },
363
386
  // Vulnerability-report intake + adjudication. The four adjudication routes move a
364
387
  // PAYOUT (confirm awards drachmae, dispute clears it), and the list route exposes
@@ -367,12 +390,32 @@ const MODULE_EXPECTED_RANKS = {
367
390
  // is DELIBERATELY Metic+ (the rank gate is the bounty noise-floor, V3.R63): the pin
368
391
  // catches an accidental widening to Xenos as loudly as it catches a gate removal.
369
392
  security: {
370
- 'POST /security/reports': 'metic+archon',
371
- 'GET /security/reports': 'archon',
372
- 'POST /security/reports/:id/confirm': 'archon',
373
- 'POST /security/reports/:id/dispute': 'archon',
374
- 'POST /security/reports/:id/fix': 'archon',
375
- 'POST /security/reports/:id/link-fix': 'archon',
393
+ 'POST /security/reports': 'perm:security.report.file',
394
+ // ADR 0157 lowered `security.report.adjudicate` archon→metic and extended its guards
395
+ // to name the LIST route, which BV1.R105 had left on raw requireRank for want of an
396
+ // atom that covered the read. All five now pin by atom.
397
+ 'GET /security/reports': 'perm:security.report.adjudicate',
398
+ 'POST /security/reports/:id/confirm': 'perm:security.report.adjudicate',
399
+ 'POST /security/reports/:id/dispute': 'perm:security.report.adjudicate',
400
+ 'POST /security/reports/:id/fix': 'perm:security.report.adjudicate',
401
+ 'POST /security/reports/:id/link-fix': 'perm:security.report.adjudicate',
402
+ },
403
+ // The governance tab's server half (BV1.R110, ADR 0151) — the routes that EDIT
404
+ // the role/permission graph itself. Every route, the three reads included, is
405
+ // pinned to the `governance.manage` ATOM rather than a rank: on this surface
406
+ // the atom IS the authority (it is the catalog's meta-escalation key), and a
407
+ // downgrade here is the model's worst case — the routes that write authority
408
+ // drifting below the wall that guards authority. Reads are pinned too because
409
+ // the full role/grant/assignment map is the authority model's blueprint.
410
+ governance: {
411
+ 'GET /governance/roles': 'perm:governance.manage',
412
+ 'GET /governance/permissions': 'perm:governance.manage',
413
+ 'GET /governance/assignments': 'perm:governance.manage',
414
+ 'POST /governance/roles': 'perm:governance.manage',
415
+ 'PUT /governance/roles/:roleKey/permissions/:permissionKey': 'perm:governance.manage',
416
+ 'DELETE /governance/roles/:roleKey/permissions/:permissionKey': 'perm:governance.manage',
417
+ 'POST /governance/assignments': 'perm:governance.manage',
418
+ 'DELETE /governance/assignments/:builderId/:roleKey': 'perm:governance.manage',
376
419
  },
377
420
  };
378
421
 
@@ -19,8 +19,10 @@ module.exports = function buildAuditLogRouter() {
19
19
  // Paginated, newest-first. Clamps:
20
20
  // limit → 1..500, default 100
21
21
  // offset → >= 0, default 0
22
- // rank: archon — forensics surface; only the Archon should see who did what.
23
- router.get('/audit-log', auth.requireBuilder, auth.requireRank('archon'), asyncHandler('GET /audit-log', async (req, res) => {
22
+ // rank: archon — forensics surface; only the Archon should see who did what. Gate:
23
+ // requirePermission('audit_log.read') — BV1.R104; the seeded rank-role makes the
24
+ // permission and the old rank floor identical (ADR 0151).
25
+ router.get('/audit-log', auth.requireBuilder, auth.requirePermission('audit_log.read'), asyncHandler('GET /audit-log', async (req, res) => {
24
26
  // R14 (#2001 / ADR 0119): adopt the shared helper (was a hand-rolled clamp);
25
27
  // per-endpoint cap preserved (limit 1..500 default 100, offset ≥ 0).
26
28
  const { limit, offset } = parsePagination(req, { defaultLimit: 100, maxLimit: 500 });
@@ -884,8 +884,10 @@ module.exports = function buildAuthRouter() {
884
884
  // its primary-key id. Used for surgical lockouts (e.g. a compromised CLI on
885
885
  // a known builder's laptop) without disturbing that builder's other sessions.
886
886
  // Body: { session_id: <bigint> }. Response: { ok, revoked: 0|1 }.
887
- // rank: archon — kills an arbitrary session; can lock other builders out.
888
- router.post('/auth/revoke', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
887
+ // rank: archon — kills an arbitrary session; can lock other builders out. Gate:
888
+ // requirePermission('auth.session.revoke') — BV1.R104; the seeded rank-role makes
889
+ // the permission and the old rank floor identical (ADR 0151).
890
+ router.post('/auth/revoke', auth.requireBuilder, auth.requirePermission('auth.session.revoke'), async (req, res) => {
889
891
  if (validateOrRespond(req, res, { session_id: {} })) return; // bigint as number or string; validated below
890
892
  const sessionId = req.body && req.body.session_id;
891
893
  if (sessionId === undefined || sessionId === null) {
@@ -910,8 +912,10 @@ module.exports = function buildAuthRouter() {
910
912
  // setBuilderRank when newRank moves down the ladder — C7 rank-change-near-
911
913
  // instant) and as a manual operator hammer.
912
914
  // Body: { builder_id: <bigint> }. Response: { ok, revoked: <count> }.
913
- // rank: archon — same authority as PATCH /builders/:id/rank.
914
- router.post('/auth/revoke-all', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
915
+ // rank: archon — same authority as PATCH /builders/:id/rank. Gate:
916
+ // requirePermission('auth.session.revoke') — BV1.R104; the same atom /auth/revoke
917
+ // gates on (ADR 0151).
918
+ router.post('/auth/revoke-all', auth.requireBuilder, auth.requirePermission('auth.session.revoke'), async (req, res) => {
915
919
  if (validateOrRespond(req, res, { builder_id: {} })) return; // bigint as number or string; validated below
916
920
  const builderId = req.body && req.body.builder_id;
917
921
  if (builderId === undefined || builderId === null) {
@@ -103,8 +103,10 @@ function latestDump(nowMs = Date.now()) {
103
103
  module.exports = function buildBackupRouter() {
104
104
  const router = express.Router();
105
105
 
106
- // rank: metic+archon — any working builder may check or trigger a backup.
107
- router.get('/backup/status', auth.requireBuilder, auth.requireRank('metic', 'archon'),
106
+ // rank: metic+archon — any working builder may check or trigger a backup. Gate:
107
+ // requirePermission('backup.manage') — BV1.R104; the seeded rank-role makes the
108
+ // permission and the old rank floor identical (ADR 0151).
109
+ router.get('/backup/status', auth.requireBuilder, auth.requirePermission('backup.manage'),
108
110
  asyncHandler('GET /backup/status', async (_req, res) => {
109
111
  const dump = latestDump();
110
112
  res.json({
@@ -120,8 +122,9 @@ module.exports = function buildBackupRouter() {
120
122
  });
121
123
  }));
122
124
 
123
- // rank: metic+archon
124
- router.post('/backup/trigger', auth.requireBuilder, auth.requireRank('metic', 'archon'),
125
+ // rank: metic+archon — kicks off a fresh dump; same gate as the status read.
126
+ // Gate: requirePermission('backup.manage') — BV1.R104.
127
+ router.post('/backup/trigger', auth.requireBuilder, auth.requirePermission('backup.manage'),
125
128
  asyncHandler('POST /backup/trigger', async (req, res) => {
126
129
  if (triggerRunning) {
127
130
  return res.fail('backup_already_running', { status: 409, message: 'A backup is already in progress — poll /backup/status to see it land.' });
@@ -113,7 +113,11 @@ module.exports = function buildBuildersRouter() {
113
113
  const router = express.Router();
114
114
 
115
115
  // rank: archon — sole chokepoint that mutates builders.rank; ADR 0016 §enforcement.
116
- router.patch('/builders/:id/rank', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
116
+ // Gate: requirePermission('builder.rank.set') — BV1.R104; the seeded rank-role makes
117
+ // the permission and the old rank floor identical (ADR 0151). The atom is
118
+ // `system: true`, so no CUSTOM role may ever be granted it — only the seeded archon
119
+ // role holds it, which is exactly the meta-escalation wall ADR 0016 wants.
120
+ router.patch('/builders/:id/rank', auth.requireBuilder, auth.requirePermission('builder.rank.set'), async (req, res) => {
117
121
  if (validateOrRespond(req, res, {
118
122
  rank: { required: true, type: 'string', maxLength: 32 },
119
123
  })) return;
@@ -188,8 +192,10 @@ module.exports = function buildBuildersRouter() {
188
192
  // claims, seals their session_logs, rank → xenos, blocks new claims while
189
193
  // preserving all credit/history); { status: 'active' } reactivates. Cannot
190
194
  // change your own status (no last-Archon footgun — same guard as /rank).
191
- // rank: archon — deactivation/reactivation; preserves credit history but blocks claims.
192
- router.patch('/builders/:id/status', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
195
+ // rank: archon — deactivation/reactivation; preserves credit history but blocks
196
+ // claims. Gate: requirePermission('builder.status.set') — BV1.R104; the seeded
197
+ // rank-role makes the permission and the old rank floor identical (ADR 0151).
198
+ router.patch('/builders/:id/status', auth.requireBuilder, auth.requirePermission('builder.status.set'), async (req, res) => {
193
199
  if (validateOrRespond(req, res, {
194
200
  status: { required: true, type: 'string', enum: ['active', 'inactive'] },
195
201
  reason: { type: 'string', maxLength: LIMITS.NOTES },
@@ -238,8 +244,10 @@ module.exports = function buildBuildersRouter() {
238
244
  // engages the warn/over thresholds. { monthly_budget_usd: null } clears it: the
239
245
  // builder becomes UNCAPPED (there is no default cap — ADR 0085). This is the
240
246
  // "budget per-builder configurable by Archon" done-when. 404 if no such builder.
241
- // rank: archon — changes another builder's cost ceiling.
242
- router.patch('/builders/:id/budget', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
247
+ // rank: archon — changes another builder's cost ceiling. NOT ownership-scoped:
248
+ // archon-only admin op. Gate: requirePermission('builder.budget.set') — BV1.R104;
249
+ // the seeded rank-role makes the permission and the old rank floor identical (ADR 0151).
250
+ router.patch('/builders/:id/budget', auth.requireBuilder, auth.requirePermission('builder.budget.set'), async (req, res) => {
243
251
  const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
244
252
  if (targetId === null) return;
245
253
  if (validateOrRespond(req, res, { monthly_budget_usd: {} })) return; // number ≥0, or null to clear; validated below
@@ -277,8 +285,10 @@ module.exports = function buildBuildersRouter() {
277
285
  // stale-claim-release sweeper uses but exposed as a roster knob so the
278
286
  // Archon can dial it in if needed.
279
287
  //
280
- // rank: archon — surfaces every builder's onboarding state.
281
- router.get('/builders/roster', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
288
+ // rank: archon — surfaces every builder's onboarding state. Gate:
289
+ // requirePermission('builder.roster.read') — BV1.R104; the same atom
290
+ // /grades/by-builder and /builders/:id/active-claims already gate on (ADR 0151).
291
+ router.get('/builders/roster', auth.requireBuilder, auth.requirePermission('builder.roster.read'), async (req, res) => {
282
292
  let staleHours = 24;
283
293
  if (req.query && req.query.stale_hours != null) {
284
294
  const n = Number(req.query.stale_hours);
@@ -323,8 +333,9 @@ module.exports = function buildBuildersRouter() {
323
333
  // the Ruling Hand's alone (the project-wide aggregate stays public via
324
334
  // /public/grades; this per-builder split does not).
325
335
  //
326
- // rank: archon.
327
- router.get('/grades/by-builder', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
336
+ // rank: archon — NOT ownership-scoped: archon-only cross-builder read.
337
+ // Gate: requirePermission('builder.roster.read') — BV1.R104.
338
+ router.get('/grades/by-builder', auth.requireBuilder, auth.requirePermission('builder.roster.read'), async (req, res) => {
328
339
  let days = 30;
329
340
  if (req.query && req.query.days != null) {
330
341
  const n = Number(req.query.days);
@@ -349,8 +360,9 @@ module.exports = function buildBuildersRouter() {
349
360
  // boolean "has any stale claim?"; this endpoint surfaces *which* claim
350
361
  // is stale so the Archon can target the release.
351
362
  //
352
- // rank: archon — privileged read of another builder's work surface.
353
- router.get('/builders/:id/active-claims', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
363
+ // rank: archon — privileged read of another builder's work surface. NOT
364
+ // ownership-scoped. Gate: requirePermission('builder.roster.read') — BV1.R104.
365
+ router.get('/builders/:id/active-claims', auth.requireBuilder, auth.requirePermission('builder.roster.read'), async (req, res) => {
354
366
  const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
355
367
  if (targetId === null) return;
356
368
  let staleHours = 24;
@@ -5,12 +5,13 @@
5
5
  // These act on the user's OWN GitHub identity, so they live in core auth alongside
6
6
  // the sign-in flow that captured the token — not the provisioning module (whose
7
7
  // charter holds no tokens / calls no cloud API, ADR 0111 §2). They read the
8
- // short-lived, secret-box-encrypted `public_repo` token the web callback stored
9
- // (db.js github_oauth_tokens). Two "please reconnect / fall back" signals the wizard
10
- // branches on:
8
+ // short-lived, secret-box-encrypted `repo`-scoped token the web callback stored
9
+ // (db.js github_oauth_tokens; `repo` since ADR 0155 — private repos must be
10
+ // adoptable). Three "please reconnect / fall back" signals the wizard branches on:
11
11
  // • no stored token → repo_scope_required (403) → prompt a repos sign-in
12
+ // • stale pre-0155 scope → repo_scope_required (403) → same reconnect path
12
13
  // • secret-box unconfigured → github_repos_unavailable (503) → manual repo entry
13
- // Both are behind requireBuilder + the router-wide rate-limit middleware.
14
+ // All are behind requireBuilder + the router-wide rate-limit middleware.
14
15
 
15
16
  const express = require('express');
16
17
  const auth = require('../auth');
@@ -35,6 +36,16 @@ async function resolveRepoToken(req, res) {
35
36
  res.fail('repo_scope_required', { status: 403, message: 'Grant repository access to list or create repos.' });
36
37
  return null;
37
38
  }
39
+ // A token minted BEFORE ADR 0155 carries only `public_repo`. That is enough to
40
+ // list + create PUBLIC repos, so without this check every call below still 200s —
41
+ // while silently dropping the user's PRIVATE repos from the picker, and the real
42
+ // failure surfaces minutes later as an opaque 403 on the provisioner's push (the
43
+ // task-1002561 failure mode). Re-prompt instead: the wizard already branches on
44
+ // repo_scope_required, so this reuses the well-exercised reconnect path.
45
+ if (!auth.grantedScopeCoversRepos(row.scope)) {
46
+ res.fail('repo_scope_required', { status: 403, message: 'Reconnect GitHub to include your private repositories.' });
47
+ return null;
48
+ }
38
49
  return row.token;
39
50
  }
40
51
 
@@ -59,7 +59,9 @@ module.exports = function buildLlmCacheRouter() {
59
59
  // POST /llm-cache/lookup — hit/miss + atomic hit_count bump. Best-effort:
60
60
  // any DB error inside lookup() already returns null → we report a miss.
61
61
  // rank: metic — only the same trust tier that can store may read a stored result.
62
- router.post('/llm-cache/lookup', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
62
+ // Gate: requirePermission('llm_cache.use') — BV1.R104; the seeded rank-role makes
63
+ // the permission and the old rank floor identical (ADR 0151).
64
+ router.post('/llm-cache/lookup', auth.requireBuilder, auth.requirePermission('llm_cache.use'), async (req, res) => {
63
65
  // Fields validated in depth below (HEX64 key / clamp); schema declares the
64
66
  // accepted set so strict-mode rejects a typo'd extra field.
65
67
  if (validateOrRespond(req, res, { cache_key: {}, tokens_saved_est: {} })) return;
@@ -81,7 +83,8 @@ module.exports = function buildLlmCacheRouter() {
81
83
 
82
84
  // POST /llm-cache/store — upsert a freshly-produced result.
83
85
  // rank: metic — a store seeds a result that a later run replays at $0 and trusts.
84
- router.post('/llm-cache/store', auth.requireBuilder, auth.requireRank('metic', 'archon'), bigJson, async (req, res) => {
86
+ // Gate: requirePermission('llm_cache.use') — BV1.R104.
87
+ router.post('/llm-cache/store', auth.requireBuilder, auth.requirePermission('llm_cache.use'), bigJson, async (req, res) => {
85
88
  // spec.* is re-built server-side (below) so its inner fields need no top-level
86
89
  // schema; declare the three top-level fields so strict-mode rejects extras.
87
90
  if (validateOrRespond(req, res, { spec: {}, result_stdout: {}, result_cost_usd: {} })) return;
@@ -117,8 +117,10 @@ module.exports = function buildModulesRouter() {
117
117
  });
118
118
  }));
119
119
 
120
- // rank: metic+archon — toggling a module changes running behavior for the whole instance, not just the caller.
121
- router.post('/modules/:key/enable', auth.requireBuilder, auth.requireRank('metic', 'archon'),
120
+ // rank: metic+archon — toggling a module changes running behavior for the whole
121
+ // instance, not just the caller. Gate: requirePermission('module.enable') — BV1.R104;
122
+ // the seeded rank-role makes the permission and the old rank floor identical (ADR 0151).
123
+ router.post('/modules/:key/enable', auth.requireBuilder, auth.requirePermission('module.enable'),
122
124
  asyncHandler('POST /modules/:key/enable', async (req, res) => {
123
125
  const { key } = req.params;
124
126
  if (validateOrRespond(req, res, { enabled: {} })) return; // boolean; enforced by the handler check below (preserves invalid_body)
@@ -153,8 +155,9 @@ module.exports = function buildModulesRouter() {
153
155
  // signed_by records a NEW sign-off only if none is on file; an existing
154
156
  // .upstream-signoff.json is reused as-is (not overwritten).
155
157
  //
156
- // rank: metic+archon — files into a shared review queue, not just a toggle for the caller.
157
- router.post('/modules/:key/submit', auth.requireBuilder, auth.requireRank('metic', 'archon'),
158
+ // rank: metic+archon — files into a shared review queue, not just a toggle for
159
+ // the caller. Gate: requirePermission('module.submit') — BV1.R104.
160
+ router.post('/modules/:key/submit', auth.requireBuilder, auth.requirePermission('module.submit'),
158
161
  asyncHandler('POST /modules/:key/submit', async (req, res) => {
159
162
  const { key } = req.params;
160
163
  const known = listModules();
@@ -212,7 +215,8 @@ module.exports = function buildModulesRouter() {
212
215
  // future GET .../:id can add a with-files fetch if the review UI needs it).
213
216
  //
214
217
  // rank: metic+archon — the review queue, same rank as filing into it.
215
- router.get('/modules/submissions', auth.requireBuilder, auth.requireRank('metic', 'archon'),
218
+ // Gate: requirePermission('module.submit') — BV1.R104.
219
+ router.get('/modules/submissions', auth.requireBuilder, auth.requirePermission('module.submit'),
216
220
  asyncHandler('GET /modules/submissions', async (req, res) => {
217
221
  const status = req.query.status ? String(req.query.status) : undefined;
218
222
  if (status && !['pending', 'accepted', 'rejected'].includes(status)) {
@@ -81,12 +81,15 @@ module.exports = function buildSecurityRouter() {
81
81
  // Xenos sees 403 regardless of whether the document is provisioned — we
82
82
  // don't want existence-or-not to leak below the rank line.
83
83
  //
84
- // rank: metic+archon — trust-tier access to the threat model + defenses
85
- // inventory (ADR 0020).
84
+ // rank: metic+archon — trust-tier access to the threat model + defenses inventory
85
+ // (ADR 0020). Gate: requirePermission('security.report.file') — BV1.R104; the seeded
86
+ // rank-role makes the permission and the old rank floor identical (ADR 0151).
87
+ // NB the 403 code on this route is `permission_forbidden`, not `rank_forbidden`
88
+ // (ADR 0020 §"A Xenos hitting the endpoint" updated to match).
86
89
  router.get(
87
90
  '/security/adr',
88
91
  auth.requireBuilder,
89
- auth.requireRank('metic', 'archon'),
92
+ auth.requirePermission('security.report.file'),
90
93
  async (_req, res) => {
91
94
  const result = await readFullAdr();
92
95
  if (result.ok) {
@@ -113,17 +116,21 @@ module.exports = function buildSecurityRouter() {
113
116
  // (threat-model §6.3 / ADR 0020), so a committed exploit report is an
114
117
  // attacker cookbook on every box. The SAFE conceptual docs (threat-model.md,
115
118
  // the README) stay in docs/security/; only the dated reports live here, and
116
- // ALL three routes are requireRank('archon') because they serve exploit
117
- // detail. Content is uploaded by the director via the POST route after
118
- // deploy — no exploit content ever touches the repo.
119
+ // ALL three routes gate on requirePermission('security.report.adjudicate')
120
+ // — an archon-floored atom (BV1.R104; was requireRank('archon') and admits
121
+ // exactly the same builders, ADR 0151) — because they serve exploit detail.
122
+ // Content is uploaded by the director via the POST route after deploy — no
123
+ // exploit content ever touches the repo. NB the 403 code on these three is
124
+ // now `permission_forbidden`, not `rank_forbidden`.
119
125
  // -----------------------------------------------------------------------
120
126
 
121
127
  // GET /api/gds/security/docs — list doc METADATA (no content).
122
128
  //
123
- // rank: archon — index of exploit-detail engagement reports.
129
+ // rank: archon — index of exploit-detail engagement reports. Gate:
130
+ // requirePermission('security.report.adjudicate') — BV1.R104.
124
131
  router.get('/security/docs',
125
132
  auth.requireBuilder,
126
- auth.requireRank('archon'),
133
+ auth.requirePermission('security.report.adjudicate'),
127
134
  async (_req, res) => {
128
135
  try {
129
136
  const docs = await db.listSecurityDocs();
@@ -138,10 +145,11 @@ module.exports = function buildSecurityRouter() {
138
145
 
139
146
  // GET /api/gds/security/docs/:slug — one doc WITH content, or 404.
140
147
  //
141
- // rank: archon — reads the full exploit-detail report body.
148
+ // rank: archon — reads the full exploit-detail report body. Gate:
149
+ // requirePermission('security.report.adjudicate') — BV1.R104.
142
150
  router.get('/security/docs/:slug',
143
151
  auth.requireBuilder,
144
- auth.requireRank('archon'),
152
+ auth.requirePermission('security.report.adjudicate'),
145
153
  async (req, res) => {
146
154
  const slug = String(req.params.slug || '');
147
155
  if (!SECURITY_DOC_SLUG_RE.test(slug)) {
@@ -174,10 +182,11 @@ module.exports = function buildSecurityRouter() {
174
182
  // Body: { slug, title, content_md }. content_md uses a large explicit cap
175
183
  // (full reports), NOT the small LIMITS.DESCRIPTION. Returns 201 + the row.
176
184
  //
177
- // rank: archon — writes exploit-detail report bodies.
185
+ // rank: archon — writes exploit-detail report bodies. Gate:
186
+ // requirePermission('security.report.adjudicate') — BV1.R104.
178
187
  router.post('/security/docs',
179
188
  auth.requireBuilder,
180
- auth.requireRank('archon'),
189
+ auth.requirePermission('security.report.adjudicate'),
181
190
  async (req, res) => {
182
191
  if (validateOrRespond(req, res, {
183
192
  slug: { required: true, type: 'string', maxLength: 128, minLength: 1 },
@@ -673,21 +673,36 @@ function mountInternalSurfaces(app) {
673
673
  });
674
674
 
675
675
  // The /watch monitoring page and the /harbor dev-box management page (idea 257)
676
- // and the /gate page (task 1179) are all Archon-only. Same server-side page
677
- // gate, same posture: a non-Archon never receives the shell (requireArchonPage
678
- // redirects a signed-in non-Archon to the hall, an unauthenticated visitor into
679
- // sign-in). The subdomain shim above has already rewritten the bare
680
- // /watch + /harbor + /gate onto /builders/*. (Defense in depth: every data
681
- // endpoint these pages call is independently requireRank('archon').)
676
+ // and the /gate page (task 1179) all sit behind `page.view.governance` — Metic
677
+ // and up since ADR 0157. Same server-side page gate, same posture: a builder
678
+ // without the permission never receives the shell (requireGovernancePage — named
679
+ // requireArchonPage before ADR 0157 — redirects them to the hall, and an
680
+ // unauthenticated visitor into sign-in). The subdomain shim above has already
681
+ // rewritten the bare /watch + /harbor + /gate onto /builders/*. (Defense in
682
+ // depth: every data endpoint these pages call carries its own server-side gate.)
682
683
  const WATCH_PAGE_RE = /^\/builders\/watch(?:\.html)?\/?$/;
683
684
  const HARBOR_PAGE_RE = /^\/builders\/harbor(?:\.html)?\/?$/;
684
685
  const GATE_PAGE_RE = /^\/builders\/gate(?:\.html)?\/?$/;
686
+ // The /governance roles-and-permissions tab (BV1.R113) sits one floor higher:
687
+ // the `governance.manage` atom itself (archon), the same key its data routes
688
+ // check — requireGovernanceManagePage, not the metic-floored gate above.
689
+ const GOVERNANCE_PAGE_RE = /^\/builders\/governance(?:\.html)?\/?$/;
690
+ // The /people builder directory (task 1002577): gated on `project.curate`, the
691
+ // exact permission its data route (GET /scouting) checks — same posture as the
692
+ // /governance tab above (the page can never drift wider than the API it fronts).
693
+ const PEOPLE_PAGE_RE = /^\/builders\/people(?:\.html)?\/?$/;
685
694
  app.use((req, res, next) => {
686
695
  if (isModuleSurfaceHost(req)) return next();
687
696
  if (!req.path.startsWith('/builders')) return next();
688
697
  const norm = path.posix.normalize(req.path);
689
698
  if (WATCH_PAGE_RE.test(norm) || HARBOR_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)) {
690
- return gdsAuth.requireArchonPage(req, res, next);
699
+ return gdsAuth.requireGovernancePage(req, res, next);
700
+ }
701
+ if (GOVERNANCE_PAGE_RE.test(norm)) {
702
+ return gdsAuth.requireGovernanceManagePage(req, res, next);
703
+ }
704
+ if (PEOPLE_PAGE_RE.test(norm)) {
705
+ return gdsAuth.requireProjectCuratePage(req, res, next);
691
706
  }
692
707
  next();
693
708
  });