@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
@@ -0,0 +1,201 @@
1
+ // tests/governance_live_proof.mjs — the C6 governance LIVE proof (BV1.R114, ADR 0151).
2
+ //
3
+ // C6's live-demonstration clause: an Archon creates a CUSTOM role granting an
4
+ // OPERATIONAL permission, assigns it to a low-trust builder, and that builder can
5
+ // perform the operational action — WHILE a `system` permission the role also "has"
6
+ // (a grant row forced past the API, straight into the DB) stays enforced by
7
+ // substrate: the deploy/protected-path attempt is still blocked.
8
+ //
9
+ // This is the goal_wall_proof.mjs pattern: the abuse MATRIX
10
+ // (tests/governance_abuse_matrix.mjs, R112) already proves every vector DB-free
11
+ // with stubs; THIS file proves the same story END-TO-END on the real stack —
12
+ // real Postgres rows, real sessions, real HTTP through the real router, the real
13
+ // resolver behind the real `governance` kernel port, no stubs, no monkeypatches.
14
+ // The captured sandbox transcript lives in docs/governance/bongos-v1-c6-live-proof.md.
15
+ //
16
+ // Real-DB integration test (the needs_rebase.mjs class): SELF-SKIPS (exit 0)
17
+ // when DATABASE_URL is unset/unreachable or the governance tables are missing,
18
+ // and is listed in run-unit-tests.js's INTEGRATION set so the DB-free lane
19
+ // never runs it.
20
+ //
21
+ // Run: DATABASE_URL=... node tests/governance_live_proof.mjs
22
+ // GDS_BASE_URL=http://localhost:3000 — optional: drive an already-running
23
+ // sandbox server (sharing that DATABASE_URL) instead of booting the router
24
+ // in-process. Same routes either way; this is how the live sandbox transcript
25
+ // was captured.
26
+
27
+ import { strict as assert } from 'node:assert';
28
+ import crypto from 'node:crypto';
29
+ import { createRequire } from 'node:module';
30
+
31
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
32
+ const require = createRequire(import.meta.url);
33
+
34
+ if (!process.env.DATABASE_URL) {
35
+ console.log('governance_live_proof.mjs SKIPPED — DATABASE_URL not set.');
36
+ process.exit(0);
37
+ }
38
+
39
+ const { Pool } = require('pg');
40
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
41
+ try {
42
+ await pool.query('SELECT 1 FROM governance_roles LIMIT 1');
43
+ } catch (e) {
44
+ console.log(`governance_live_proof.mjs SKIPPED — no migrated Postgres (${e.code || e.message}).`);
45
+ await pool.end().catch(() => {});
46
+ process.exit(0);
47
+ }
48
+
49
+ // ── fixtures (high-ID range; cleaned before AND after so reruns are safe) ────
50
+ const ARCHON = 90000701;
51
+ const THETES = 90000702;
52
+ const ROLE_KEY = 'glp-demo-triager';
53
+ const OPERATIONAL_KEY = 'idea.triage'; // system:false, floor metic — above thetes
54
+ const SYSTEM_KEY = 'deploy.push_main'; // system:true — substrate-enforced
55
+ const tok = () => 'glp' + crypto.randomBytes(24).toString('hex');
56
+ const ARCHON_TOKEN = tok();
57
+ const THETES_TOKEN = tok();
58
+
59
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
60
+
61
+ async function cleanup() {
62
+ await q(`DELETE FROM governance_roles WHERE role_key=$1`, [ROLE_KEY]); // grants+assignments cascade
63
+ await q(`DELETE FROM governance_builder_roles WHERE builder_id IN ($1,$2)`, [ARCHON, THETES]);
64
+ await q(`DELETE FROM builder_sessions WHERE builder_id IN ($1,$2)`, [ARCHON, THETES]);
65
+ await q(`DELETE FROM idea_inbox WHERE captured_by IN ($1,$2)`, [ARCHON, THETES]);
66
+ // The audit middleware records the demo's own writes; those rows FK the
67
+ // fixture builders, so they go first.
68
+ await q(`DELETE FROM audit_log WHERE builder_id IN ($1,$2)`, [ARCHON, THETES]);
69
+ await q(`DELETE FROM builders WHERE id IN ($1,$2)`, [ARCHON, THETES]);
70
+ }
71
+
72
+ async function seed() {
73
+ await q(
74
+ `INSERT INTO builders (id, github_id, github_login, display_name, rank, status)
75
+ VALUES ($1,'glp-archon','glp-archon','GLP Demo Archon','archon','active'),
76
+ ($2,'glp-thetes','glp-thetes','GLP Demo Thetes','thetes','active')`,
77
+ [ARCHON, THETES]
78
+ );
79
+ // The R95b rank-role sync runs at sign-in / rank writes; a directly-seeded
80
+ // builder mirrors its effect here (one seeded rank-role each, fail-closed
81
+ // otherwise — the archon would resolve ZERO permissions and deny itself).
82
+ await q(
83
+ `INSERT INTO governance_builder_roles (builder_id, role_key)
84
+ VALUES ($1,'archon'), ($2,'thetes')`,
85
+ [ARCHON, THETES]
86
+ );
87
+ await q(
88
+ `INSERT INTO builder_sessions (builder_id, token, source)
89
+ VALUES ($1,$2,'web'), ($3,$4,'web')`,
90
+ [ARCHON, ARCHON_TOKEN, THETES, THETES_TOKEN]
91
+ );
92
+ const [idea] = await q(
93
+ `INSERT INTO idea_inbox (title, body_md, captured_by)
94
+ VALUES ('glp live-proof fixture idea','(sandbox demo row)',$1) RETURNING id`,
95
+ [ARCHON]
96
+ );
97
+ return idea.id;
98
+ }
99
+
100
+ // ── the server: a live sandbox (GDS_BASE_URL) or the real router in-process ──
101
+ let server = null;
102
+ let base;
103
+ if (process.env.GDS_BASE_URL) {
104
+ base = process.env.GDS_BASE_URL.replace(/\/$/, '');
105
+ } else {
106
+ const express = require('express');
107
+ const { buildGdsRouter } = require('../src/bongos/routes.js');
108
+ const app = express();
109
+ app.use('/api/gds', buildGdsRouter());
110
+ server = await new Promise((resolve) => {
111
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
112
+ });
113
+ base = `http://127.0.0.1:${server.address().port}`;
114
+ }
115
+
116
+ const call = async (token, method, path, body) => {
117
+ const res = await fetch(`${base}/api/gds${path}`, {
118
+ method,
119
+ headers: {
120
+ authorization: `Bearer ${token}`,
121
+ ...(body ? { 'content-type': 'application/json' } : {}),
122
+ },
123
+ body: body ? JSON.stringify(body) : undefined,
124
+ });
125
+ const json = await res.json().catch(() => ({}));
126
+ const code = json?.error?.code || json?.error || '';
127
+ console.log(` ${method.padEnd(6)} ${path} → ${res.status}${code ? ` ${code}` : ''}`);
128
+ return { status: res.status, body: json };
129
+ };
130
+
131
+ let pass = 0;
132
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
133
+
134
+ try {
135
+ await cleanup();
136
+ const ideaId = await seed();
137
+ console.log(`live proof against ${base} (idea fixture #${ideaId})`);
138
+
139
+ // Act 1 — the gate is closed: thetes (floor for idea.triage is metic) is denied.
140
+ let r = await call(THETES_TOKEN, 'PATCH', `/inbox/${ideaId}`, { status: 'discarded' });
141
+ ok(r.status === 403 && r.body.error.code === 'permission_forbidden',
142
+ 'BEFORE: the thetes builder cannot triage (403 permission_forbidden)');
143
+
144
+ // Act 2 — an Archon creates a custom role and grants the operational permission.
145
+ r = await call(ARCHON_TOKEN, 'POST', '/governance/roles',
146
+ { role_key: ROLE_KEY, label: 'GLP demo triage role' });
147
+ ok(r.status === 201, 'the Archon creates a custom role (201)');
148
+ r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${OPERATIONAL_KEY}`);
149
+ ok(r.status === 200 && r.body.ok === true,
150
+ `the Archon grants operational '${OPERATIONAL_KEY}' to the custom role (200)`);
151
+
152
+ // Act 3 — the locked wall: a system permission is refused through the same API.
153
+ r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${SYSTEM_KEY}`);
154
+ ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
155
+ `system '${SYSTEM_KEY}' is NOT grantable, even by the Archon (400 permission_not_grantable)`);
156
+ r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/governance.manage`);
157
+ ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
158
+ 'the meta-escalation (governance.manage) is refused the same way');
159
+
160
+ // Act 4 — assign, then the same builder performs the action: no re-login, no
161
+ // restart — the per-request, uncached resolve (ADR 0016) picks the role up.
162
+ r = await call(ARCHON_TOKEN, 'POST', '/governance/assignments',
163
+ { builder_id: THETES, role_key: ROLE_KEY });
164
+ ok(r.status === 201, 'the Archon assigns the custom role to the thetes builder (201)');
165
+ r = await call(THETES_TOKEN, 'PATCH', `/inbox/${ideaId}`, { status: 'discarded' });
166
+ ok(r.status === 200, 'AFTER: the same builder, same session, can now triage (200) — the custom role works');
167
+
168
+ // Act 5 — the wall behind the wall: force the grant row no API path can create,
169
+ // so the role now "has" a system permission in the DB…
170
+ await q(
171
+ `INSERT INTO governance_role_permissions (role_key, permission_key) VALUES ($1,$2)`,
172
+ [ROLE_KEY, SYSTEM_KEY]
173
+ );
174
+ console.log(` (forced INTO the DB: grant ${ROLE_KEY} ← ${SYSTEM_KEY} — no API path can write this row)`);
175
+
176
+ // …the resolver reports it honestly (it is a defined catalog key; matrix V10
177
+ // proves no route consumes it via requirePermission, so it admits nothing
178
+ // in-app)…
179
+ const resolver = require('../modules/governance/resolver.js');
180
+ const perms = await resolver.resolveBuilderPermissions(THETES);
181
+ ok(perms.includes(OPERATIONAL_KEY), 'the resolved set includes the operational grant');
182
+ console.log(` (resolved set ${perms.includes(SYSTEM_KEY) ? 'includes' : 'omits'} the forced '${SYSTEM_KEY}' — in-app it guards no route either way)`);
183
+
184
+ // …and the SUBSTRATE stays closed: the real checker the pre-push hook, grader
185
+ // pre-pass and main-audit run — structurally grant-blind (node-builtins-only
186
+ // imports; it cannot even reach a DB) — still blocks the sub-metic builder's
187
+ // protected-path diff while the forced grant row sits in the database.
188
+ const { checkPermissionPaths } = require('../src/bongos/permission-path-check.js');
189
+ const sub = checkPermissionPaths({
190
+ changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/governance/catalog.js'],
191
+ builderRank: 'thetes',
192
+ });
193
+ ok(sub.hardFail === true && sub.findings.length === 2,
194
+ 'the substrate checker still BLOCKS the protected-path attempt (deploy/migration machinery), forced grant and all');
195
+
196
+ console.log(`\ngovernance_live_proof: ${pass} assertions passed`);
197
+ } finally {
198
+ await cleanup().catch((e) => console.error('cleanup failed', e.message));
199
+ if (server) await new Promise((resolve) => server.close(resolve));
200
+ await pool.end().catch(() => {});
201
+ }
@@ -50,30 +50,42 @@ const resolver = require('../modules/governance/resolver.js');
50
50
  const wandering = require('../modules/builder-settings/wandering-prefs.js');
51
51
  const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
52
52
 
53
- const GATES = ['requireBuilderPage', 'requireNonXenosPage', 'requireArchonPage'];
53
+ const GATES = ['requireBuilderPage', 'requireNonXenosPage', 'requireGovernancePage'];
54
54
  const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
55
55
 
56
56
  // ═══ the ORACLE — hand-written, from the rank tests R107 deleted ═════════════
57
- // requireBuilderPage admitted any session at all → every live rank
58
- // requireNonXenosPage admitted rank !== 'xenos' → thetes and up
59
- // requireArchonPage admitted rank === 'archon' → archon only
57
+ // requireBuilderPage admitted any session at all → every live rank
58
+ // requireNonXenosPage admitted rank !== 'xenos' → thetes and up
59
+ // requireGovernancePage admitted rank === 'archon' → archon only …until ADR 0157
60
+ //
61
+ // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.governance` archon→metic, so
62
+ // the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
63
+ // was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
64
+ // were first written against — and was renamed once the floor moved; the audience here,
65
+ // not the name, is the contract.
66
+ //
67
+ // This is the ONE intentional widening in this table's history. Everything R107 promised
68
+ // (identical audiences across the rank→permission migration) still holds for the other
69
+ // two gates, and the widening is expressed here as DATA so the A1 oracle keeps proving
70
+ // the gate matches its declared floor rather than merely "didn't change".
60
71
  const PRE_R107_AUDIENCE = {
61
72
  requireBuilderPage: { xenos: true, thetes: true, metic: true, archon: true },
62
73
  requireNonXenosPage: { xenos: false, thetes: true, metic: true, archon: true },
63
- requireArchonPage: { xenos: false, thetes: false, metic: false, archon: true },
74
+ requireGovernancePage: { xenos: false, thetes: false, metic: true, archon: true },
64
75
  };
65
76
  // Where a SIGNED-IN builder who fails the gate is sent. Never the sign-in flow —
66
77
  // they already have a session, so that would loop.
67
78
  const DENIED_TO = {
68
79
  requireBuilderPage: '/builders',
69
80
  requireNonXenosPage: '/builders/not-ready',
70
- requireArchonPage: '/builders',
81
+ requireGovernancePage: '/builders',
71
82
  };
72
83
  // The equivalent rank floor, for the independent cross-check below.
73
84
  const GATE_FLOOR = {
74
85
  requireBuilderPage: 'xenos',
75
86
  requireNonXenosPage: 'thetes',
76
- requireArchonPage: 'archon',
87
+ // ADR 0157: `page.view.governance` floor archon→metic.
88
+ requireGovernancePage: 'metic',
77
89
  };
78
90
  const SIGN_IN = '/api/gds/auth/web/start';
79
91
 
@@ -226,12 +238,12 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
226
238
  });
227
239
  const req = { headers: { cookie: 'gds_session=validtoken' } };
228
240
  let admitted = false;
229
- await auth.requireArchonPage(req, fakeRes(), () => { admitted = true; });
241
+ await auth.requireGovernancePage(req, fakeRes(), () => { admitted = true; });
230
242
  assert.equal(admitted, true, 'an Archon should reach the page');
231
243
  keys = []; // the role is revoked between requests
232
244
  const second = fakeRes();
233
245
  let admittedAgain = false;
234
- await auth.requireArchonPage(req, second, () => { admittedAgain = true; });
246
+ await auth.requireGovernancePage(req, second, () => { admittedAgain = true; });
235
247
  seams._reset();
236
248
  assert.equal(calls, 2, 'the resolver must be consulted on every request');
237
249
  assert.equal(admittedAgain, false, 'a revoked role must deny on the very next request');
@@ -239,8 +239,17 @@ test('B2: the route cross-reference actually covers a substantial slice of the c
239
239
  `only ${covered.length} permissions cross-reference a gated route (${floorPinned.length} by rank, ${atomPinned.length} by atom; expected >= 25)`);
240
240
  const archonCovered = covered.filter((r) => r.floor === 'archon');
241
241
  const meticCovered = covered.filter((r) => r.floor === 'metic');
242
- assert.ok(archonCovered.length >= 10, `only ${archonCovered.length} archon-floored routes cross-referenced`);
243
- assert.ok(meticCovered.length >= 10, `only ${meticCovered.length} metic-floored routes cross-referenced`);
242
+ // These minimums measure SAMPLE BREADTH — "the cross-reference exercises both sides of
243
+ // the metic/archon boundary" — not how much authority Archon ought to hold. ADR 0157
244
+ // moved 20 atoms archon→metic, so the archon population is now genuinely small by
245
+ // design (rank, identity, and the escalation keys) and the metic population is large.
246
+ // The floors moved with it: archon 10→4, metic 10→25. Lowering the archon minimum is
247
+ // NOT a weakening — the guard it replaces would now fail on a correct codebase, and a
248
+ // guard that must be deleted to ship a legitimate change stops being a guard. The
249
+ // strictness moved to where the population went: metic's minimum more than doubled, so
250
+ // a regression that quietly drained metic-floored routes still reds the build.
251
+ assert.ok(archonCovered.length >= 4, `only ${archonCovered.length} archon-floored routes cross-referenced`);
252
+ assert.ok(meticCovered.length >= 25, `only ${meticCovered.length} metic-floored routes cross-referenced`);
244
253
  });
245
254
 
246
255
  // B3 — the oracle that REPLACES B1 as routes migrate (BV1.R109). For a route gated by
@@ -13,6 +13,7 @@
13
13
  import assert from 'node:assert/strict';
14
14
  import { test } from 'node:test';
15
15
  import { createRequire } from 'node:module';
16
+ import fs from 'node:fs';
16
17
  import path from 'node:path';
17
18
  import { fileURLToPath } from 'node:url';
18
19
 
@@ -188,11 +189,76 @@ test('over the four seeded ranks, requirePermission admits exactly who requireRa
188
189
  // gate the SEED a rank-role grants and it reproduces the rank gate's verdict.
189
190
  // Scoped to catalog.RANK_ORDER — the reserved/divine ranks above archon hold no
190
191
  // seeded role, so parity is defined only over the four live ranks.
192
+ // BV1.R105 migrated every rank-gated module route (modules/*/routes/) onto the
193
+ // atoms below. Listing each one beside the requireRank(...) call it REPLACED turns
194
+ // the migration's whole claim — "the atom admits exactly who the rank gate did" —
195
+ // into an assertion instead of prose. Grouped by the gate that stood there before.
196
+ // Atoms that replaced a requireRank('archon') gate AND are still archon after
197
+ // ADR 0157 — rank/identity plus the escalation keys that make the delegation
198
+ // irreversible if moved.
199
+ const REPLACED_ARCHON = [
200
+ 'dependency.cross_tier.manage', 'gate_approval.decide',
201
+ 'task.confirm.grade_bypass', 'access_request.review',
202
+ ];
203
+ // ADR 0157 (owner decision, 2026-08-05) — "the Archon rank is responsible for rank and
204
+ // identity only". Each of these replaced a requireRank('archon') gate in R104/R105 and
205
+ // then had its catalog floor lowered archon→metic, so the audience it must now match is
206
+ // requireRank('metic','archon').
207
+ //
208
+ // These are listed EXPLICITLY rather than derived from catalog.byKey(...).floor on
209
+ // purpose: this test is an independent oracle. Deriving the expectation from the same
210
+ // constant the implementation reads would make it assert `x === x` and it would pass
211
+ // through any future floor change — including an accidental one. A hand-written list
212
+ // means a floor moving without an owner decision reds the build, which is the whole
213
+ // point of keeping parity as an assertion instead of prose.
214
+ const DELEGATED_TO_METIC_BY_ADR_0155 = [
215
+ 'autonomy.precheck', 'box.fleet.manage', 'discord.channels.manage', 'idea.ratify',
216
+ 'claim.release.any', 'criterion.unsatisfy', 'goal.reorder', 'task.newcomer_restock',
217
+ 'task.peer_votes.tally', 'override_request.decide', 'memory.read.any',
218
+ 'project.curate', 'provisioning.fleet.manage', 'security.report.adjudicate',
219
+ 'session.search',
220
+ // Minted by R105 (POST /versions had no atom); ADR 0157 then lowered it to metic.
221
+ // Its sibling `version.close` stayed archon — authoring delegates, disposition does not.
222
+ 'version.create',
223
+ // Minted BY ADR 0157, splitting the Gate page's READ off `gate_approval.decide` so a
224
+ // Metic can open /gate while approving a held PR stays Archon.
225
+ 'gate_approval.read',
226
+ ];
227
+ const REPLACED_METIC_PLUS = [
228
+ 'blocker.review', 'idea.triage', 'criterion.create', 'criterion.review',
229
+ 'criterion.satisfy', 'goal.create', 'goal.archive', 'goal.scope.widen',
230
+ 'task.create', 'task.edit', 'task.promote', 'task.demote', 'task.abandon',
231
+ 'task.vote', 'task.dependency.manage', 'task.criterion.manage',
232
+ 'security.report.file',
233
+ ];
191
234
  const cases = [
192
- ['idea.triage', ['metic', 'archon']],
193
235
  ['governance.manage', ['archon']],
194
236
  ['task.claim.any', ['thetes', 'metic', 'archon']],
195
237
  ['cost.log', ['xenos', 'thetes', 'metic', 'archon']],
238
+ // The CORE route gates migrated in BV1.R104, each paired with the requireRank
239
+ // allow-list it replaced. It is the cheapest proof that a swap did not change
240
+ // WHO can reach a route. R105's module atoms are spread in below; these stay
241
+ // listed explicitly because the REPLACED_* arrays cover modules/*/routes only,
242
+ // so dropping them would silently retire the parity proof for the core gates.
243
+ // (security.report.file / security.report.adjudicate are asserted by the R105
244
+ // arrays below and so are not repeated here.)
245
+ ['backup.manage', ['metic', 'archon']], // GET /backup/status, POST /backup/trigger
246
+ ['llm_cache.use', ['metic', 'archon']], // POST /llm-cache/{lookup,store}
247
+ ['module.enable', ['metic', 'archon']], // POST /modules/:key/enable
248
+ ['module.submit', ['metic', 'archon']], // POST /modules/:key/submit, GET /modules/submissions
249
+ // ADR 0157 lowered these three archon→metic: spending, reading the roster, and
250
+ // reading the audit log are administration, not authority over who holds authority.
251
+ ['builder.budget.set', ['metic', 'archon']], // PATCH /builders/:id/budget
252
+ ['builder.roster.read', ['metic', 'archon']], // GET /builders/roster, /grades/by-builder, /builders/:id/active-claims
253
+ ['audit_log.read', ['metic', 'archon']], // GET /audit-log
254
+ // …while these three are the identity half of ADR 0157's rank/identity mandate and
255
+ // stay archon: whose sessions are valid, who holds which rank, and who exists at all.
256
+ ['auth.session.revoke', ['archon']], // POST /auth/revoke, POST /auth/revoke-all
257
+ ['builder.rank.set', ['archon']], // PATCH /builders/:id/rank
258
+ ['builder.status.set', ['archon']], // PATCH /builders/:id/status
259
+ ...REPLACED_ARCHON.map((key) => [key, ['archon']]),
260
+ ...DELEGATED_TO_METIC_BY_ADR_0155.map((key) => [key, ['metic', 'archon']]),
261
+ ...REPLACED_METIC_PLUS.map((key) => [key, ['metic', 'archon']]),
196
262
  ];
197
263
  for (const [permission, allowedRanks] of cases) {
198
264
  for (const rank of catalog.RANK_ORDER) {
@@ -221,3 +287,61 @@ test('a principal permission (floor null) is held by NO rank-role', async () =>
221
287
  assert.equal(res._status, 403);
222
288
  }
223
289
  });
290
+
291
+ // BV1.R105 completeness — the negative half of the parity proof above.
292
+ //
293
+ // The per-route pins in route-rank-check.js are satisfied by EITHER spelling, on
294
+ // purpose, so the R104-R107 migration could land incrementally. The cost is that
295
+ // nothing there fails when a call site is simply MISSED: a forgotten requireRank
296
+ // keeps its pin green because the effective authority is unchanged. The parity
297
+ // tests above have the same blind spot — they assert the atoms that WERE replaced
298
+ // behave identically, never that none were left behind.
299
+ //
300
+ // So scan the source instead. R105 left four deliberate holdouts, each carrying a
301
+ // "Still requireRank after BV1.R105" note at its site: their permission atom was
302
+ // declared over the module's WRITES, so the read gate stayed rank-based rather than
303
+ // minting a second atom meaning "may look at this". Anything else is a miss.
304
+ //
305
+ // ADR 0157 cleared THREE of those four. It had to: lowering a write atom to metic
306
+ // while its read sibling stayed on requireRank('archon') would have left the read
307
+ // STRICTER than the write — a Metic able to confirm a vulnerability report but not
308
+ // list one, and a Gate page open to Metic whose only data endpoint 403s. So the ADR
309
+ // extended each atom's declared guards to name its read (and split out
310
+ // `gate_approval.read`, since approving must stay Archon while reading must not),
311
+ // which is exactly the "mint an atom that covers the read" step R105 deferred.
312
+ test('every modules/*/routes gate is migrated, bar the one documented holdout', () => {
313
+ const RETAINED = [
314
+ // Still requireRank after BV1.R105 — this route did not EXIST when R105 was
315
+ // written. modules/security/routes/scans.js landed on main from task 1002547
316
+ // while R105 sat at 'completed' awaiting an owner override decision, so it was
317
+ // authored against the requireRank spelling that was still current. Untouched by
318
+ // ADR 0157: its gate is already requireRank('metic','archon'), so it is not an
319
+ // Archon surface and the delegation had nothing to move. Migrating it is still
320
+ // worth doing — it means minting an atom over the write — but it is the
321
+ // continuing R107/R108 migration's job, not this ADR's.
322
+ 'security/routes/scans.js',
323
+ ].sort();
324
+
325
+ const modulesDir = path.join(ROOT, 'modules');
326
+ const found = [];
327
+ for (const key of fs.readdirSync(modulesDir)) {
328
+ const routesDir = path.join(modulesDir, key, 'routes');
329
+ if (!fs.existsSync(routesDir)) continue;
330
+ for (const file of fs.readdirSync(routesDir)) {
331
+ if (!file.endsWith('.js')) continue;
332
+ const text = fs.readFileSync(path.join(routesDir, file), 'utf8');
333
+ // Line-based so a requireRank named in prose (there are ~20 such mentions)
334
+ // is not mistaken for a live gate. A gate is always a call.
335
+ const live = text.split('\n').some((line) => {
336
+ const t = line.trim();
337
+ if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return false;
338
+ return /requireRank\s*\(/.test(line);
339
+ });
340
+ if (live) found.push(`${key}/routes/${file}`);
341
+ }
342
+ }
343
+
344
+ assert.deepEqual(found.sort(), RETAINED,
345
+ 'a modules/*/routes gate still calls requireRank — migrate it to requirePermission, ' +
346
+ 'or add it to RETAINED with a "Still requireRank after BV1.R105" note explaining why');
347
+ });