@bongos/core 1.19.184 → 1.19.185

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 (48) hide show
  1. package/.bongos-core.json +59 -54
  2. package/clients/bongos-client/bongos-client.global.js +19 -19
  3. package/clients/bongos-client/index.cjs +19 -19
  4. package/clients/bongos-client/index.d.ts +29 -29
  5. package/clients/bongos-client/index.mjs +19 -19
  6. package/docs/adr/0157-archon-is-rank-and-identity-only.md +2 -2
  7. package/docs/api/openapi.json +117 -117
  8. package/docs/api-reference.md +10 -10
  9. package/docs/canonical-permissions.md +6 -6
  10. package/modules/dev-box/app/src/vendor/bongos-client.cjs +19 -19
  11. package/modules/government/catalog.js +7 -7
  12. package/modules/government/db.js +8 -8
  13. package/modules/government/migrations/government_002_rename_permission_keys.sql +112 -0
  14. package/modules/government/module.json +1 -1
  15. package/modules/government/protected-surfaces.json +0 -1
  16. package/modules/government/ranks.js +13 -13
  17. package/modules/government/routes/{governance.js → government.js} +105 -80
  18. package/modules/hall-ui/public/gate.js +1 -1
  19. package/modules/hall-ui/public/governance.js +38 -38
  20. package/modules/hall-ui/public/harbor.js +2 -2
  21. package/modules/hall-ui/public/shell.js +3 -3
  22. package/modules/hall-ui/public/watch.js +1 -1
  23. package/modules/lifecycle/routes/gate-approvals.js +1 -1
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/scripts/gds/publish-manifest.js +0 -2
  27. package/scripts/gds/seed-governance-tasks.js +1 -1
  28. package/src/bongos/auth.js +7 -7
  29. package/src/bongos/module-scope-map.js +7 -22
  30. package/src/bongos/route-rank-check.js +12 -18
  31. package/src/bongos/serve-internal.js +2 -2
  32. package/src/module-api.js +4 -2
  33. package/tests/governance_abuse_matrix.mjs +32 -32
  34. package/tests/governance_catalog.mjs +3 -3
  35. package/tests/governance_fold.mjs +2 -2
  36. package/tests/governance_fold_bench.mjs +2 -2
  37. package/tests/governance_live_proof.mjs +4 -4
  38. package/tests/governance_ownership_scope.mjs +1 -1
  39. package/tests/governance_page_gates.mjs +4 -4
  40. package/tests/governance_principals.mjs +2 -2
  41. package/tests/governance_protected_surfaces.mjs +41 -16
  42. package/tests/governance_require_permission.mjs +5 -5
  43. package/tests/governance_resolver.mjs +2 -2
  44. package/tests/governance_role_sync.mjs +23 -23
  45. package/tests/governance_routes.mjs +94 -94
  46. package/tests/governance_seed.mjs +24 -9
  47. package/tests/governance_session_scope.mjs +4 -4
  48. package/tests/hall_page_gate_map.mjs +2 -2
@@ -98,7 +98,7 @@
98
98
  // current (mirrors the old #hall-nav-work rule, task #833)
99
99
  // archon — rank === 'archon'
100
100
  // governance — rank is metic or archon (ADR 0157): the monitoring/ops pages.
101
- // Mirrors the server's `page.view.governance` floor, which that ADR
101
+ // Mirrors the server's `page.view.government` floor, which that ADR
102
102
  // lowered archon→metic. Cosmetic only — requireGovernancePage is the wall.
103
103
  // `view` marks an index view (hash-routed); `module` gates on window.__MODULES__.
104
104
  // task 2175: grouped by AUDIENCE of the data, so the sidebar reads in coherent
@@ -178,7 +178,7 @@
178
178
  { id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'governance' },
179
179
  // BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
180
180
  // Archon-only item the 'governance' gate comment anticipated — cosmetic
181
- // only; requireGovernanceManagePage (the governance.manage atom) is the wall.
181
+ // only; requireGovernanceManagePage (the government.manage atom) is the wall.
182
182
  { id: 'governance', label: 'Roles', icon: 'admin', href: '/governance', gate: 'archon' },
183
183
  ] },
184
184
  ];
@@ -420,7 +420,7 @@
420
420
  case 'member': return member;
421
421
  case 'work': return authed && (member || access.showWork);
422
422
  case 'archon': return authed && access.rank === 'archon';
423
- // ADR 0157: the monitoring/ops pages sit at the `page.view.governance` floor,
423
+ // ADR 0157: the monitoring/ops pages sit at the `page.view.government` floor,
424
424
  // which is metic. Kept as its own key rather than widening 'archon', so a
425
425
  // genuinely Archon-only nav item can still be added later.
426
426
  case 'governance': return authed && (access.rank === 'metic' || access.rank === 'archon');
@@ -885,7 +885,7 @@
885
885
  return;
886
886
  }
887
887
 
888
- // ADR 0157: Watch sits at the `page.view.governance` floor (metic), and every panel
888
+ // ADR 0157: Watch sits at the `page.view.government` floor (metic), and every panel
889
889
  // it reads (roster, security reports, audit log, sessions, box costs) delegated to
890
890
  // metic with it. Cosmetic — requireGovernancePage + the route gates are the wall.
891
891
  if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
@@ -68,7 +68,7 @@ module.exports = function buildGateApprovalsRouter() {
68
68
  // BV1.R105 left this on requireRank because `gate_approval.decide` was declared over
69
69
  // the /approve WRITE only and no atom named the read. ADR 0157 extends the catalog
70
70
  // with exactly that atom — `gate_approval.read`, floor metic — so the mapping is now
71
- // one the catalog makes. The split is load-bearing, not cosmetic: `page.view.governance`
71
+ // one the catalog makes. The split is load-bearing, not cosmetic: `page.view.government`
72
72
  // opens /gate to Metic, and without a Metic-readable data endpoint that page would
73
73
  // render nothing but a 403. Approving a held PR stays archon (`.decide`, system).
74
74
  router.get('/gate-approvals', auth.requireBuilder, auth.requirePermission('gate_approval.read'), async (req, res) => {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.184",
3
+ "version": "1.19.185",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.184",
9
+ "version": "1.19.185",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.184",
3
+ "version": "1.19.185",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -61,7 +61,6 @@ const PUBLISH_ALLOWLIST = [
61
61
  'modules/sessions/',
62
62
  'modules/autonomy/',
63
63
  'modules/security/',
64
- 'modules/governance/', // configurable roles & permissions (BV1 goal 1000030, ADR 0151) — the rank/permission machinery publishes AS the open-source methodology (ADR 0065 §4 / ADR 0098); it is in PROTECTED_GLOBS for write-protection, NOT publish-exclusion. default:true. Renamed → modules/government/ (ADR 0174); dropped with the folder in goal 1000068 R04.
65
64
  'modules/government/', // the same machinery under its post-ADR-0174 name (goal 1000068, task 1003003). Added in lockstep with the protected-surfaces.json entry: publish_manifest asserts every PROTECTED_GLOB is either topology-excluded or publishable, so registering the glob as protected WITHOUT this line default-denies the module from the published core — the open-source methodology would silently lose its permission machinery at the next release.
66
65
  'modules/hall-ui/',
67
66
  'modules/status-ui/',
@@ -213,7 +212,6 @@ const INSTANCE_EXCLUDE = [
213
212
  'config/devbox-app.json',
214
213
  'config/hierarchy.json',
215
214
  'config/government.json',
216
- 'config/governance.json', // pre-ADR-0174 spelling — a live instance may still carry one; it is host content and stays denied
217
215
  'CODEOWNERS', // names the owner (a protected surface, excluded as identity)
218
216
  // instance SEED migrations — this host's scope-truth SQL (planning seeds, world data).
219
217
  // ADR 0105: a consumer's core must not carry them; migrate.sh reads instance seeds from
@@ -216,7 +216,7 @@ const TASKS = [
216
216
  title: 'Governance API routes (roles/perms/assignments CRUD)',
217
217
  description: `**Why:** the tab's server surface.
218
218
 
219
- **Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`governance.manage\` (Archon), system permissions rejected as non-grantable server-side, and pinned in route-rank-check. Depends on R97 + R109.`,
219
+ **Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`government.manage\` (Archon), system permissions rejected as non-grantable server-side, and pinned in route-rank-check. Depends on R97 + R109.`,
220
220
  },
221
221
  {
222
222
  ref: 'R111', kind: 'feature', priority: 3, est_minutes: 180, manual_degree: 2, credits_reward: 55,
@@ -1437,9 +1437,9 @@ function signInWithReturn(req) {
1437
1437
  // ranks (catalog.RANK_ORDER):
1438
1438
  // requireBuilderPage was "any session" floor xenos → page.view.public
1439
1439
  // requireNonXenosPage was rank !== 'xenos' floor thetes → page.view.builder
1440
- // requireGovernancePage was rank === 'archon' floor archon → page.view.governance
1440
+ // requireGovernancePage was rank === 'archon' floor archon → page.view.government
1441
1441
  // That third gate was spelled `requireArchonPage` until ADR 0157 lowered
1442
- // page.view.governance's floor archon→metic and the name stopped being true; the
1442
+ // page.view.government's floor archon→metic and the name stopped being true; the
1443
1443
  // floor now lives in modules/government/catalog.js, not in the function's name.
1444
1444
  // The one accepted divergence is the one governance_002 already documents
1445
1445
  // (tests/governance_parity.mjs Part D): a builder on a reserved/divine rank holds
@@ -1456,8 +1456,8 @@ function signInWithReturn(req) {
1456
1456
  const PAGE_GATES = Object.freeze({
1457
1457
  requireBuilderPage: Object.freeze({ permission: 'page.view.public', deniedTo: '/builders' }),
1458
1458
  requireNonXenosPage: Object.freeze({ permission: 'page.view.builder', deniedTo: '/builders/not-ready' }),
1459
- requireGovernancePage: Object.freeze({ permission: 'page.view.governance', deniedTo: '/builders' }),
1460
- requireGovernanceManagePage: Object.freeze({ permission: 'governance.manage', deniedTo: '/builders' }),
1459
+ requireGovernancePage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
1460
+ requireGovernanceManagePage: Object.freeze({ permission: 'government.manage', deniedTo: '/builders' }),
1461
1461
  requireProjectCuratePage: Object.freeze({ permission: 'project.curate', deniedTo: '/builders' }),
1462
1462
  });
1463
1463
 
@@ -1534,7 +1534,7 @@ async function requireBuilderPage(req, res, next) {
1534
1534
  }
1535
1535
 
1536
1536
  // The /watch monitoring surface, the /harbor dev-box page and the /gate page
1537
- // (#726, idea 257, task 1179) — Metic and up, per `page.view.governance`'s catalog
1537
+ // (#726, idea 257, task 1179) — Metic and up, per `page.view.government`'s catalog
1538
1538
  // floor. Named `requireArchonPage` until ADR 0157 lowered that floor archon→metic
1539
1539
  // (older ADRs still use the old name; this line is the trail). Defense-in-depth:
1540
1540
  // every data endpoint these pages call is independently gated server-side, so no
@@ -1545,7 +1545,7 @@ async function requireGovernancePage(req, res, next) {
1545
1545
  }
1546
1546
 
1547
1547
  // The /governance roles-and-permissions tab (BV1.R113) — gated on the
1548
- // `governance.manage` ATOM itself (system, archon floor), the same key every
1548
+ // `government.manage` ATOM itself (system, archon floor), the same key every
1549
1549
  // /governance/* data route checks, so the page layer can never drift wider than
1550
1550
  // the API it fronts. Defense-in-depth as above: the data endpoints are all
1551
1551
  // independently gated; this keeps the tab shell from rendering for anyone else.
@@ -1716,7 +1716,7 @@ function requireRank(...allowed) {
1716
1716
  // time — never hardcoded prose, which goes stale when a floor moves (the
1717
1717
  // D-16/N-3 failure shape ADR 0157 left behind).
1718
1718
  //
1719
- // Usage: router.post('/roles', auth.requireBuilder, auth.requirePermission('governance.manage'), handler)
1719
+ // Usage: router.post('/roles', auth.requireBuilder, auth.requirePermission('government.manage'), handler)
1720
1720
  function requirePermission(...perms) {
1721
1721
  const required = Object.freeze([...perms]);
1722
1722
  return async function permissionGate(req, res, next) {
@@ -174,30 +174,15 @@ const MODULE_GLOBS = {
174
174
  // role templates (R96), listed here as goal territory; the module code lands in
175
175
  // R93+ (the carve precedes the feature — ADR 0086 §4). Written as exact paths,
176
176
  // not 'config/governance*.json' — the touches convention has no wildcards. ---
177
- // RENAMED governance → government (goal 1000068, task 1003003, ADR 0174).
178
- // Both keys live through the transition and BOTH carry BOTH sets of globs,
179
- // so the wall is identical whichever key a goal names and whichever name
180
- // the folder currently has on disk. The `government` key must land BEFORE
181
- // R04 moves the folder: goals carry scope_modules by KEY, so moving the
182
- // folder first would leave goal 1000068's own wall not matching the files
183
- // its remaining tasks need to touch, blocking the rename mid-goal.
184
- // `modules/government/` is registered in protected-surfaces.json in the
185
- // same change — `protected` DERIVES from that registry, so without it this
186
- // key would compute protected:false and silently drop the Archon gate.
177
+ // RENAMED governance → government (goal 1000068, ADR 0174). The
178
+ // transitional `governance` alias key was removed in R05 (task 1003006)
179
+ // together with the /governance/* route aliases — the same compatibility
180
+ // window. The three live goals that named the old key (1000030, 1000057,
181
+ // 1000068) had it removed from goals.scope_modules in the same change, so
182
+ // no stored row references a key the map no longer defines.
187
183
  government: [
188
- 'modules/government/', 'modules/governance/',
184
+ 'modules/government/',
189
185
  'config/government.json', 'config/government.neutral.json',
190
- 'config/governance.json', 'config/governance.neutral.json',
191
- ],
192
- // DEPRECATED ALIAS — kept for one release so goals and tasks already
193
- // scoped to `governance` keep resolving. Removed by goal 1000068 R05
194
- // (task 1003006), the release that drops the /governance/* route aliases;
195
- // the two removals are the same compatibility window and should land
196
- // together.
197
- governance: [
198
- 'modules/government/', 'modules/governance/',
199
- 'config/government.json', 'config/government.neutral.json',
200
- 'config/governance.json', 'config/governance.neutral.json',
201
186
  ],
202
187
  // --- the two web surfaces ---
203
188
  // BV1.R85: hall-UI graduated to modules/hall-ui/ — public-builders/ moved to
@@ -231,19 +231,18 @@ const EXPECTED_RANKS = {
231
231
  'GET /security/adr': 'metic+archon',
232
232
  };
233
233
 
234
- // The authz-core route pins, referenced by BOTH the `governance` and the
235
- // `government` module keys below while goal 1000068's rename is in flight
236
- // (ADR 0174, task 1003003). One object, two keys — never two literals.
234
+ // The authz-core route pins (ADR 0174). Paths and the gating atom were renamed
235
+ // governance→government in R05 (task 1003006); the module-key alias is gone.
237
236
  const GOVERNMENT_PINS = {
238
- 'GET /governance/roles': 'perm:governance.manage',
239
- 'GET /governance/permissions': 'perm:governance.manage',
240
- 'GET /governance/assignments': 'perm:governance.manage',
241
- 'POST /governance/roles': 'perm:governance.manage',
242
- 'PUT /governance/roles/:roleKey/permissions/:permissionKey': 'perm:governance.manage',
243
- 'DELETE /governance/roles/:roleKey/permissions/:permissionKey': 'perm:governance.manage',
244
- 'POST /governance/roles/:roleKey/reset': 'perm:governance.manage',
245
- 'POST /governance/assignments': 'perm:governance.manage',
246
- 'DELETE /governance/assignments/:builderId/:roleKey': 'perm:governance.manage',
237
+ 'GET /government/ranks': 'perm:government.manage',
238
+ 'GET /government/permissions': 'perm:government.manage',
239
+ 'GET /government/assignments': 'perm:government.manage',
240
+ 'POST /government/ranks': 'perm:government.manage',
241
+ 'PUT /government/ranks/:rankKey/permissions/:permissionKey': 'perm:government.manage',
242
+ 'DELETE /government/ranks/:rankKey/permissions/:permissionKey': 'perm:government.manage',
243
+ 'POST /government/ranks/:rankKey/reset': 'perm:government.manage',
244
+ 'POST /government/assignments': 'perm:government.manage',
245
+ 'DELETE /government/assignments/:builderId/:rankKey': 'perm:government.manage',
247
246
  };
248
247
 
249
248
  // Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
@@ -417,17 +416,12 @@ const MODULE_EXPECTED_RANKS = {
417
416
  },
418
417
  // The government tab's server half (BV1.R110, ADR 0151; renamed by ADR 0174) —
419
418
  // the routes that EDIT the rank/permission graph itself. Every route, the three
420
- // reads included, is pinned to the `governance.manage` ATOM rather than a rank:
419
+ // reads included, is pinned to the `government.manage` ATOM rather than a rank:
421
420
  // on this surface the atom IS the authority (it is the catalog's meta-escalation
422
421
  // key), and a downgrade here is the model's worst case — the routes that write
423
422
  // authority drifting below the wall that guards authority. Reads are pinned too
424
423
  // because the full rank/grant/assignment map is the authority model's blueprint.
425
424
  //
426
- // The ROUTE PATHS and the `governance.manage` key are renamed by R05 (task
427
- // 1003006), not here: R02 only makes the pins reachable under both module keys.
428
- // Renaming the paths before the routes move would pin routes that do not exist
429
- // and red the build for the whole window.
430
- governance: GOVERNMENT_PINS,
431
425
  // The same pins under the post-ADR-0174 module name (goal 1000068, task
432
426
  // 1003003). Pins are applied only when the module directory is IN SCOPE — at
433
427
  // least one route file was discovered under modules/<key>/ — so this entry is
@@ -749,7 +749,7 @@ function mountInternalSurfaces(app) {
749
749
  });
750
750
 
751
751
  // The /watch monitoring page and the /harbor dev-box management page (idea 257)
752
- // and the /gate page (task 1179) all sit behind `page.view.governance` — Metic
752
+ // and the /gate page (task 1179) all sit behind `page.view.government` — Metic
753
753
  // and up since ADR 0157. Same server-side page gate, same posture: a builder
754
754
  // without the permission never receives the shell (requireGovernancePage — named
755
755
  // requireArchonPage before ADR 0157 — redirects them to the hall, and an
@@ -767,7 +767,7 @@ function mountInternalSurfaces(app) {
767
767
  // group without a wall. Gated here so the group is uniform (task 1002710).
768
768
  const SESSIONS_PAGE_RE = /^\/builders\/sessions(?:\.html)?\/?$/;
769
769
  // The /governance roles-and-permissions tab (BV1.R113) sits one floor higher:
770
- // the `governance.manage` atom itself (archon), the same key its data routes
770
+ // the `government.manage` atom itself (archon), the same key its data routes
771
771
  // check — requireGovernanceManagePage, not the metic-floored gate above.
772
772
  const GOVERNANCE_PAGE_RE = /^\/builders\/governance(?:\.html)?\/?$/;
773
773
  // The /people builder directory (task 1002577): gated on `project.curate`, the
package/src/module-api.js CHANGED
@@ -390,7 +390,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
390
390
  // ad7a262 and never packaged, so ~104 non-merge commits sat on main
391
391
  // reachable by no pin; this is the artifact that ships them. Headline:
392
392
  // the governance permission system's second half (R110 roles/perms/
393
- // assignments CRUD behind governance.manage, R113 the Roles nav +
393
+ // assignments CRUD behind government.manage, R113 the Roles nav +
394
394
  // page gate, R114 the C6 live proof, ADR 0157 delegating the
395
395
  // operational block from Archon to Metic, and the permission resolve
396
396
  // folded into the session load); the scan-before-install security
@@ -795,8 +795,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
795
795
  // landed since 1.19.182 with no explicit bump. run 31973728179. (task 1002620)
796
796
  // 1.19.184 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
797
797
  // landed since 1.19.183 with no explicit bump. run 31974847472. (task 1002620)
798
+ // 1.19.185 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
799
+ // landed since 1.19.184 with no explicit bump. run 31976564067. (task 1002620)
798
800
  // ---------------------------------------------------------------------------
799
- const CORE_VERSION = '1.19.184'; // CI auto-patch carrier (ADR 0161); see changelog above
801
+ const CORE_VERSION = '1.19.185'; // CI auto-patch carrier (ADR 0161); see changelog above
800
802
 
801
803
  // A namespaced logger so a module's log lines are attributable + consistent.
802
804
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -16,7 +16,7 @@
16
16
  // V3 HTTP: assign a seeded rank-role to a builder (rank is the only writer)
17
17
  // V4 HTTP: mass-assignment on role create (is_system / permissions in body)
18
18
  // V5 HTTP: every write vector 403s outright for a caller without
19
- // governance.manage (the low-trust builder's own attempt)
19
+ // government.manage (the low-trust builder's own attempt)
20
20
  // V6 config: a role template proposing system keys is stripped; a seeded
21
21
  // role's template grants are dropped wholesale; env overrides labels only
22
22
  // V7 session-scope: an allow-list naming a system or principal key THROWS
@@ -109,9 +109,9 @@ test('every system permission floors at archon or is a principal (never seeded l
109
109
  const calls = [];
110
110
  const stub = {
111
111
  getRoleByKey: async (k) => (SEEDED_ROLES.includes(k)
112
- ? { role_key: k, label: k, is_system: true, seeded_rank: k }
113
- : { role_key: k, label: k, is_system: false, seeded_rank: null }),
114
- createRole: async (a) => ({ role_key: a.roleKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
112
+ ? { rank_key: k, label: k, is_system: true, seeded_rank: k }
113
+ : { rank_key: k, label: k, is_system: false, seeded_rank: null }),
114
+ createRole: async (a) => ({ rank_key: a.rankKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
115
115
  grantRolePermission: async () => true,
116
116
  revokeRolePermission: async () => true,
117
117
  resetRoleGrants: async ({ permissionKeys }) => permissionKeys.length,
@@ -127,7 +127,7 @@ for (const k of Object.keys(stub)) {
127
127
  }
128
128
 
129
129
  // The attacker holds the MOST privileged legitimate session — an Archon with
130
- // governance.manage. The matrix must hold even against the admin misusing the
130
+ // government.manage. The matrix must hold even against the admin misusing the
131
131
  // API, not just against a low-trust caller (V5 covers that one).
132
132
  const realRequireBuilder = api.requireBuilder;
133
133
  api.requireBuilder = (req, res, next) => { req.builder = { id: 42, rank: 'archon' }; next(); };
@@ -136,9 +136,9 @@ function grant(keys) {
136
136
  seams._reset();
137
137
  seams.registerProvider('government', { resolveBuilderPermissions: async () => keys });
138
138
  }
139
- grant(['governance.manage']);
139
+ grant(['government.manage']);
140
140
 
141
- const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'governance.js'));
141
+ const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
142
142
  const app = express();
143
143
  app.use(express.json());
144
144
  app.use(attachFail);
@@ -168,7 +168,7 @@ async function call(method, p, body) {
168
168
 
169
169
  test('V1: EVERY system permission is refused onto a custom role, before the db', async () => {
170
170
  for (const k of SYSTEM_KEYS) {
171
- const r = await call('PUT', `/governance/roles/custom-role/permissions/${k}`);
171
+ const r = await call('PUT', `/government/ranks/custom-role/permissions/${k}`);
172
172
  assert.equal(r.status, 400, `${k} must be refused`);
173
173
  assert.equal(r.json.error.code, 'permission_not_grantable', k);
174
174
  assert.equal(calls.filter(([c]) => c === 'grantRolePermission').length, 0,
@@ -185,15 +185,15 @@ test('V1: EVERY system permission is refused onto a custom role, before the db',
185
185
  test('V2a: the ARCHON rank-role refuses EVERY write — grant, revoke and reset alike', async () => {
186
186
  for (const k of [...SYSTEM_KEYS, 'idea.triage']) {
187
187
  for (const method of ['PUT', 'DELETE']) {
188
- const r = await call(method, `/governance/roles/archon/permissions/${k}`);
188
+ const r = await call(method, `/government/ranks/archon/permissions/${k}`);
189
189
  assert.equal(r.status, 403, `archon ← ${method} ${k} must be refused`);
190
- assert.equal(r.json.error.code, 'archon_role_immutable', `archon ← ${method} ${k}`);
190
+ assert.equal(r.json.error.code, 'archon_rank_immutable', `archon ← ${method} ${k}`);
191
191
  assert.equal(calls.filter(([c]) => c === 'grantRolePermission' || c === 'revokeRolePermission').length, 0);
192
192
  }
193
193
  }
194
- const r = await call('POST', '/governance/roles/archon/reset');
194
+ const r = await call('POST', '/government/ranks/archon/reset');
195
195
  assert.equal(r.status, 403, 'reset must not be a back door into the archon role');
196
- assert.equal(r.json.error.code, 'archon_role_immutable');
196
+ assert.equal(r.json.error.code, 'archon_rank_immutable');
197
197
  assert.equal(calls.filter(([c]) => c === 'resetRoleGrants').length, 0);
198
198
  });
199
199
 
@@ -201,7 +201,7 @@ test('V2b: EVERY editable rank-role still refuses every SUBSTRATE and PRINCIPAL
201
201
  const neverGrantable = [...SUBSTRATE_KEYS, ...catalog.principalKeys()];
202
202
  for (const role of EDITABLE_RANK_ROLES) {
203
203
  for (const k of neverGrantable) {
204
- const r = await call('PUT', `/governance/roles/${role}/permissions/${k}`);
204
+ const r = await call('PUT', `/government/ranks/${role}/permissions/${k}`);
205
205
  assert.equal(r.status, 400, `${role} ← ${k} must be refused`);
206
206
  assert.equal(r.json.error.code, 'permission_not_grantable', `${role} ← ${k}`);
207
207
  assert.equal(calls.filter(([c]) => c === 'grantRolePermission').length, 0,
@@ -213,7 +213,7 @@ test('V2b: EVERY editable rank-role still refuses every SUBSTRATE and PRINCIPAL
213
213
  test('V2c: an editable rank-role ACCEPTS the in-app owner powers (the ADR 0172 lever)', async () => {
214
214
  for (const role of EDITABLE_RANK_ROLES) {
215
215
  for (const k of [...OWNER_POWERS, 'idea.triage']) {
216
- const r = await call('PUT', `/governance/roles/${role}/permissions/${k}`);
216
+ const r = await call('PUT', `/government/ranks/${role}/permissions/${k}`);
217
217
  assert.equal(r.status, 200, `${role} ← ${k} must be ACCEPTED — this delegation is the owner's decision (ADR 0172)`);
218
218
  assert.equal(calls.filter(([c]) => c === 'grantRolePermission').length, 1, `${role} ← ${k} must reach the db`);
219
219
  }
@@ -222,7 +222,7 @@ test('V2c: an editable rank-role ACCEPTS the in-app owner powers (the ADR 0172 l
222
222
 
223
223
  test('V2d: reset restores the CATALOG seed, never anything wider', async () => {
224
224
  for (const role of EDITABLE_RANK_ROLES) {
225
- const r = await call('POST', `/governance/roles/${role}/reset`);
225
+ const r = await call('POST', `/government/ranks/${role}/reset`);
226
226
  assert.equal(r.status, 200, `${role} must be resettable`);
227
227
  const [, args] = calls.find(([c]) => c === 'resetRoleGrants');
228
228
  assert.deepEqual([...args.permissionKeys].sort(), [...catalog.seedForRank(role)].sort(),
@@ -233,24 +233,24 @@ test('V2d: reset restores the CATALOG seed, never anything wider', async () => {
233
233
  }
234
234
  // A custom role has no catalog default, so reset is refused rather than silently
235
235
  // clearing its grants (which would be a destructive verb wearing a restore's name).
236
- const r = await call('POST', '/governance/roles/custom-role/reset');
236
+ const r = await call('POST', '/government/ranks/custom-role/reset');
237
237
  assert.equal(r.status, 400);
238
- assert.equal(r.json.error.code, 'role_has_no_default');
238
+ assert.equal(r.json.error.code, 'rank_has_no_default');
239
239
  assert.equal(calls.filter(([c]) => c === 'resetRoleGrants').length, 0);
240
240
  });
241
241
 
242
242
  test('V3: EVERY seeded rank-role refuses assignment (rank is the only writer — R95b)', async () => {
243
243
  for (const role of SEEDED_ROLES) {
244
- const r = await call('POST', '/governance/assignments', { builder_id: 7, role_key: role });
244
+ const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: role });
245
245
  assert.equal(r.status, 403, `assigning seeded role ${role} must be refused`);
246
- assert.equal(r.json.error.code, 'seeded_role_assignment_forbidden', role);
246
+ assert.equal(r.json.error.code, 'seeded_rank_assignment_forbidden', role);
247
247
  assert.equal(calls.filter(([c]) => c === 'assignRole').length, 0);
248
248
  }
249
249
  });
250
250
 
251
251
  test('V4: mass-assignment on role create is refused outright (strict schema)', async () => {
252
- const r = await call('POST', '/governance/roles', {
253
- role_key: 'sneaky-role',
252
+ const r = await call('POST', '/government/ranks', {
253
+ rank_key: 'sneaky-role',
254
254
  label: 'Sneaky',
255
255
  is_system: true,
256
256
  seeded_rank: 'archon',
@@ -262,32 +262,32 @@ test('V4: mass-assignment on role create is refused outright (strict schema)', a
262
262
  });
263
263
 
264
264
  test('V4: a clean role create is never system and carries no grants', async () => {
265
- const r = await call('POST', '/governance/roles', { role_key: 'sneaky-role', label: 'Sneaky' });
265
+ const r = await call('POST', '/government/ranks', { rank_key: 'sneaky-role', label: 'Sneaky' });
266
266
  assert.equal(r.status, 201);
267
267
  assert.equal(r.json.role.is_system, false, 'a created role is never system');
268
268
  assert.equal(r.json.role.seeded_rank, null);
269
269
  assert.deepEqual(r.json.role.permissions, [], 'creation carries no grants');
270
270
  const c = calls.find(([k]) => k === 'createRole');
271
- assert.deepEqual(Object.keys(c[1]).sort(), ['createdBy', 'description', 'label', 'roleKey'],
271
+ assert.deepEqual(Object.keys(c[1]).sort(), ['createdBy', 'description', 'label', 'rankKey'],
272
272
  'nothing beyond the four whitelisted fields reaches the db');
273
273
  assert.equal(calls.filter(([k]) => k === 'grantRolePermission').length, 0);
274
274
  });
275
275
 
276
- test('V5: a caller without governance.manage cannot reach ANY write vector at all', async () => {
277
- grant(catalog.grantableKeys().filter((k) => k !== 'governance.manage'));
276
+ test('V5: a caller without government.manage cannot reach ANY write vector at all', async () => {
277
+ grant(catalog.grantableKeys().filter((k) => k !== 'government.manage'));
278
278
  for (const [method, p, body] of [
279
- ['POST', '/governance/roles', { role_key: 'x-role', label: 'X' }],
280
- ['PUT', '/governance/roles/custom-role/permissions/deploy.push_main', undefined],
281
- ['DELETE', '/governance/roles/custom-role/permissions/idea.triage', undefined],
282
- ['POST', '/governance/roles/metic/reset', undefined],
283
- ['POST', '/governance/assignments', { builder_id: 7, role_key: 'custom-role' }],
284
- ['DELETE', '/governance/assignments/7/custom-role', undefined],
279
+ ['POST', '/government/ranks', { rank_key: 'x-role', label: 'X' }],
280
+ ['PUT', '/government/ranks/custom-role/permissions/deploy.push_main', undefined],
281
+ ['DELETE', '/government/ranks/custom-role/permissions/idea.triage', undefined],
282
+ ['POST', '/government/ranks/metic/reset', undefined],
283
+ ['POST', '/government/assignments', { builder_id: 7, rank_key: 'custom-role' }],
284
+ ['DELETE', '/government/assignments/7/custom-role', undefined],
285
285
  ]) {
286
286
  const r = await call(method, p, body);
287
287
  assert.equal(r.status, 403, `${method} ${p} must deny — even holding every operational grant`);
288
288
  assert.equal(r.json.error.code, 'permission_forbidden', `${method} ${p}`);
289
289
  }
290
- grant(['governance.manage']);
290
+ grant(['government.manage']);
291
291
  });
292
292
 
293
293
  // ===========================================================================
@@ -57,7 +57,7 @@ test('substrate keys are the out-of-band three, and each implies system + archon
57
57
  assert.equal(byKey(k).floor, 'archon', `${k} must floor at archon`);
58
58
  assert.equal(byKey(k).principal, null, `${k} must not also be a principal key`);
59
59
  }
60
- assert.equal(cat.isSubstrate('governance.manage'), false, 'an in-app archon power is not substrate');
60
+ assert.equal(cat.isSubstrate('government.manage'), false, 'an in-app archon power is not substrate');
61
61
  assert.equal(cat.isSubstrate('nope.not.a.key'), false, 'unknown fails closed');
62
62
  });
63
63
 
@@ -67,7 +67,7 @@ test('isGrantableTo: a sub-Archon rank-role takes owner powers, a custom role do
67
67
  const custom = { is_system: false, seeded_rank: null };
68
68
 
69
69
  // The widening: an in-app archon power reaches a rank-role, never a custom one.
70
- for (const k of ['governance.manage', 'builder.rank.set', 'version.close']) {
70
+ for (const k of ['government.manage', 'builder.rank.set', 'version.close']) {
71
71
  assert.equal(cat.isGrantableTo(k, metic), true, `${k} → metic rank-role`);
72
72
  assert.equal(cat.isGrantableTo(k, custom), false, `${k} → custom role must stay refused`);
73
73
  }
@@ -173,7 +173,7 @@ test('an unknown/reserved rank resolves to no permissions (fail-closed; ADR 0151
173
173
  });
174
174
 
175
175
  test('byKey returns a frozen record or null', () => {
176
- const p = byKey('governance.manage');
176
+ const p = byKey('government.manage');
177
177
  assert.ok(p && p.system === true && p.floor === 'archon');
178
178
  assert.throws(() => { p.system = false; }, /Cannot assign|read only|frozen/i);
179
179
  assert.equal(byKey('nope'), null);
@@ -121,7 +121,7 @@ test('the kernel shape guard: the real fragment embeds; a malformed one degrades
121
121
  assert.equal(dbKernel.sessionPermsSelect(), '', 'a fragment with a statement separator is refused');
122
122
  // The REAL registered fragment passes the guard verbatim.
123
123
  seams._reset();
124
- const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'governance.js'));
124
+ const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
125
125
  buildGovernanceRouter();
126
126
  const real = seams.resolveOptional('session-grants-sql').selectFragment;
127
127
  assert.equal(dbKernel.sessionPermsSelect(), real, 'the shipped fragment embeds unchanged');
@@ -133,7 +133,7 @@ test('the module registers the capability-named SQL seam the kernel embeds', ()
133
133
  // §Decision #4); the governance route factory is what registers it. Pin the
134
134
  // registration and the column contract the kernel query depends on.
135
135
  seams._reset();
136
- const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'governance.js'));
136
+ const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
137
137
  buildGovernanceRouter();
138
138
  const provider = seams.resolveOptional('session-grants-sql');
139
139
  assert.ok(provider && typeof provider.selectFragment === 'string', 'fragment provider registered');
@@ -1,7 +1,7 @@
1
1
  // tests/governance_fold_bench.mjs — the task-1002556 benchmark, re-runnable.
2
2
  //
3
3
  // Measures end-to-end latency of a requireBuilder + requirePermission route
4
- // (GET /governance/roles) against a LIVE server, the way the fold was graded:
4
+ // (GET /government/ranks) against a LIVE server, the way the fold was graded:
5
5
  // point it at a stack before and after and compare. Asserts only route health
6
6
  // (every request 200) — the numbers are the deliverable, printed for the
7
7
  // operator to diff.
@@ -27,7 +27,7 @@ import { strict as assert } from 'node:assert';
27
27
  const BASE = (process.env.GDS_BASE_URL || '').replace(/\/$/, '');
28
28
  const TOKEN = process.env.BENCH_TOKEN || '';
29
29
  if (!BASE || !TOKEN) {
30
- console.log('governance_fold_bench.mjs SKIPPED — set GDS_BASE_URL and BENCH_TOKEN (a session token whose builder holds governance.manage).');
30
+ console.log('governance_fold_bench.mjs SKIPPED — set GDS_BASE_URL and BENCH_TOKEN (a session token whose builder holds government.manage).');
31
31
  process.exit(0);
32
32
  }
33
33
  const N = Number(process.env.BENCH_N) || 300;
@@ -142,7 +142,7 @@ try {
142
142
  'BEFORE: the thetes builder cannot triage (403 permission_forbidden)');
143
143
 
144
144
  // Act 2 — an Archon creates a custom role and grants the operational permission.
145
- r = await call(ARCHON_TOKEN, 'POST', '/governance/roles',
145
+ r = await call(ARCHON_TOKEN, 'POST', '/government/ranks',
146
146
  { role_key: ROLE_KEY, label: 'GLP demo triage role' });
147
147
  ok(r.status === 201, 'the Archon creates a custom role (201)');
148
148
  r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${OPERATIONAL_KEY}`);
@@ -153,13 +153,13 @@ try {
153
153
  r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${SYSTEM_KEY}`);
154
154
  ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
155
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`);
156
+ r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/government.manage`);
157
157
  ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
158
- 'the meta-escalation (governance.manage) is refused the same way');
158
+ 'the meta-escalation (government.manage) is refused the same way');
159
159
 
160
160
  // Act 4 — assign, then the same builder performs the action: no re-login, no
161
161
  // restart — the per-request, uncached resolve (ADR 0016) picks the role up.
162
- r = await call(ARCHON_TOKEN, 'POST', '/governance/assignments',
162
+ r = await call(ARCHON_TOKEN, 'POST', '/government/assignments',
163
163
  { builder_id: THETES, role_key: ROLE_KEY });
164
164
  ok(r.status === 201, 'the Archon assigns the custom role to the thetes builder (201)');
165
165
  r = await call(THETES_TOKEN, 'PATCH', `/inbox/${ideaId}`, { status: 'discarded' });
@@ -397,7 +397,7 @@ test('G2: the role resolve is untouched — own-scoped keys still resolve, princ
397
397
  });
398
398
 
399
399
  test('G3: the kernel port carries the ownership axis on the ONE governance port', () => {
400
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
400
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
401
401
  for (const name of ['authorizeOwned', 'authorizeOwnedOrAny', 'sameOwner', 'isOwnershipScoped', 'crossOwnerKeysFor']) {
402
402
  assert.equal(typeof governancePort[name], 'function', `the port must expose ${name}`);
403
403
  }
@@ -58,7 +58,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
58
58
  // requireNonXenosPage admitted rank !== 'xenos' → thetes and up
59
59
  // requireGovernancePage admitted rank === 'archon' → archon only …until ADR 0157
60
60
  //
61
- // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.governance` archon→metic, so
61
+ // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
62
62
  // the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
63
63
  // was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
64
64
  // were first written against — and was renamed once the floor moved; the audience here,
@@ -86,7 +86,7 @@ const DENIED_TO = {
86
86
  const GATE_PERMISSION = {
87
87
  requireBuilderPage: 'page.view.public',
88
88
  requireNonXenosPage: 'page.view.builder',
89
- requireGovernancePage: 'page.view.governance',
89
+ requireGovernancePage: 'page.view.government',
90
90
  };
91
91
  const deniedLoc = (gate, { unavailable = false } = {}) =>
92
92
  `${DENIED_TO[gate]}?denied=${GATE_PERMISSION[gate]}${unavailable ? '&reason=unavailable' : ''}`;
@@ -94,7 +94,7 @@ const deniedLoc = (gate, { unavailable = false } = {}) =>
94
94
  const GATE_FLOOR = {
95
95
  requireBuilderPage: 'xenos',
96
96
  requireNonXenosPage: 'thetes',
97
- // ADR 0157: `page.view.governance` floor archon→metic.
97
+ // ADR 0157: `page.view.government` floor archon→metic.
98
98
  requireGovernancePage: 'metic',
99
99
  };
100
100
  const SIGN_IN = '/api/gds/auth/web/start';
@@ -264,7 +264,7 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
264
264
  test('B5: every permission a page gate names is a real catalog key', async () => {
265
265
  // Not an oracle — a sanity check that the gates did not migrate onto a typo,
266
266
  // which would silently deny everyone (fail-closed, but a bug all the same).
267
- for (const key of ['page.view.public', 'page.view.builder', 'page.view.governance']) {
267
+ for (const key of ['page.view.public', 'page.view.builder', 'page.view.government']) {
268
268
  assert.ok(catalog.byKey(key), `${key} must exist in the catalog`);
269
269
  }
270
270
  });