@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.
- package/.bongos-core.json +59 -54
- package/clients/bongos-client/bongos-client.global.js +19 -19
- package/clients/bongos-client/index.cjs +19 -19
- package/clients/bongos-client/index.d.ts +29 -29
- package/clients/bongos-client/index.mjs +19 -19
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +2 -2
- package/docs/api/openapi.json +117 -117
- package/docs/api-reference.md +10 -10
- package/docs/canonical-permissions.md +6 -6
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +19 -19
- package/modules/government/catalog.js +7 -7
- package/modules/government/db.js +8 -8
- package/modules/government/migrations/government_002_rename_permission_keys.sql +112 -0
- package/modules/government/module.json +1 -1
- package/modules/government/protected-surfaces.json +0 -1
- package/modules/government/ranks.js +13 -13
- package/modules/government/routes/{governance.js → government.js} +105 -80
- package/modules/hall-ui/public/gate.js +1 -1
- package/modules/hall-ui/public/governance.js +38 -38
- package/modules/hall-ui/public/harbor.js +2 -2
- package/modules/hall-ui/public/shell.js +3 -3
- package/modules/hall-ui/public/watch.js +1 -1
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/publish-manifest.js +0 -2
- package/scripts/gds/seed-governance-tasks.js +1 -1
- package/src/bongos/auth.js +7 -7
- package/src/bongos/module-scope-map.js +7 -22
- package/src/bongos/route-rank-check.js +12 -18
- package/src/bongos/serve-internal.js +2 -2
- package/src/module-api.js +4 -2
- package/tests/governance_abuse_matrix.mjs +32 -32
- package/tests/governance_catalog.mjs +3 -3
- package/tests/governance_fold.mjs +2 -2
- package/tests/governance_fold_bench.mjs +2 -2
- package/tests/governance_live_proof.mjs +4 -4
- package/tests/governance_ownership_scope.mjs +1 -1
- package/tests/governance_page_gates.mjs +4 -4
- package/tests/governance_principals.mjs +2 -2
- package/tests/governance_protected_surfaces.mjs +41 -16
- package/tests/governance_require_permission.mjs +5 -5
- package/tests/governance_resolver.mjs +2 -2
- package/tests/governance_role_sync.mjs +23 -23
- package/tests/governance_routes.mjs +94 -94
- package/tests/governance_seed.mjs +24 -9
- package/tests/governance_session_scope.mjs +4 -4
- package/tests/hall_page_gate_map.mjs +2 -2
package/docs/api-reference.md
CHANGED
|
@@ -214,19 +214,19 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
214
214
|
| GET | `/api/bongos/goals/inbox` | `any-builder` | — | GET /goals/inbox — the caller's private-goals inbox (task 1952 / ADR 0112): { invitations: invites addressed to me (accept/decline); join… |
|
|
215
215
|
| POST | `/api/bongos/goals/reorder` | `metic+archon` | `version_id`, `ordered_ids` | POST /goals/reorder — Archon drag-to-reorder: set the priority order of every goal WITHIN one version (task 1760). |
|
|
216
216
|
|
|
217
|
-
## `
|
|
217
|
+
## `government` (9)
|
|
218
218
|
|
|
219
219
|
| Method | Path | Rank | Body | Description |
|
|
220
220
|
|---|---|---|---|---|
|
|
221
|
-
| GET | `/api/bongos/
|
|
222
|
-
| POST | `/api/bongos/
|
|
223
|
-
| DELETE | `/api/bongos/
|
|
224
|
-
| GET | `/api/bongos/
|
|
225
|
-
| GET | `/api/bongos/
|
|
226
|
-
| POST | `/api/bongos/
|
|
227
|
-
| PUT | `/api/bongos/
|
|
228
|
-
| DELETE | `/api/bongos/
|
|
229
|
-
| POST | `/api/bongos/
|
|
221
|
+
| GET | `/api/bongos/government/assignments` | `archon` | — | government.manage — every builder↔role assignment row. |
|
|
222
|
+
| POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM role to a builder. |
|
|
223
|
+
| DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-role assignment. |
|
|
224
|
+
| GET | `/api/bongos/government/permissions` | `archon` | — | government.manage — the permission catalog + principals, straight from code (the catalog is CODE, not a table); no DB read. |
|
|
225
|
+
| GET | `/api/bongos/government/ranks` | `archon` | — | government.manage — the full role list with each role's grants. |
|
|
226
|
+
| POST | `/api/bongos/government/ranks` | `archon` | `rank_key`, `label`, `description` | government.manage — create a CUSTOM role (is_system false, no seeded_rank — the seeded four exist only via governance_002). |
|
|
227
|
+
| PUT | `/api/bongos/government/ranks/:rankKey/permissions/:permissionKey` | `archon` | — | government.manage — grant one permission to a role (the toggle's ON half). |
|
|
228
|
+
| DELETE | `/api/bongos/government/ranks/:rankKey/permissions/:permissionKey` | `archon` | — | government.manage — revoke a grant (the toggle's OFF half). |
|
|
229
|
+
| POST | `/api/bongos/government/ranks/:rankKey/reset` | `archon` | — | government.manage — restore a SEEDED rank-role to its catalog default (ADR 0172). |
|
|
230
230
|
|
|
231
231
|
## `grades` (1)
|
|
232
232
|
|
|
@@ -60,7 +60,7 @@ forming the ordered claim-gate tier **`xenos` (1) < `thetes` (2) < `metic` (3) <
|
|
|
60
60
|
RANK and IDENTITY, not day-to-day administration.** It is everything a Metic can do, plus: **rank**
|
|
61
61
|
(grant/revoke — the single writer), **identity** (offboard/(de)activate, admit an access request, revoke
|
|
62
62
|
sessions), the **four escalation keys** that would otherwise make that delegation self-reversible
|
|
63
|
-
(grader-bypass confirm, CI gate approval, protected-module goal scope, and `
|
|
63
|
+
(grader-bypass confirm, CI gate approval, protected-module goal scope, and `government.manage`),
|
|
64
64
|
**version close**, cross-tier dependency edges, and the substrate floors (push `main`, protected paths,
|
|
65
65
|
ssh — ADR 0043). Version-**level** criteria (a `done-when` with no `goal_id`, or one outside an open
|
|
66
66
|
version / outside a goal they own) also stay Archon per
|
|
@@ -140,7 +140,7 @@ or missing ranks fail closed.
|
|
|
140
140
|
(who is admitted), `POST /api/gds/auth/revoke[-all]` (whose sessions are valid).
|
|
141
141
|
- **the four escalation keys**, which look operational but are not: `POST /api/gds/tasks/:id/confirm`
|
|
142
142
|
(grader-bypass), `POST /api/gds/gate-approvals/:pr/approve` (CI deploy-gate override), protected-module
|
|
143
|
-
**goal scope** ([ADR 0086](adr/<redacted>.md)), and `
|
|
143
|
+
**goal scope** ([ADR 0086](adr/<redacted>.md)), and `government.manage` (the
|
|
144
144
|
roles/permissions tab). Delegating any one of them lets a Metic reach Archon in four steps — create a
|
|
145
145
|
protected-scope goal in the permission core, claim a task there (protected paths already floor at
|
|
146
146
|
*metic* per [ADR 0043](adr/<redacted>.md)), confirm
|
|
@@ -153,11 +153,11 @@ or missing ranks fail closed.
|
|
|
153
153
|
> **Amended by [ADR 0172](adr/<redacted>.md) (owner decision, 2026-08-16).**
|
|
154
154
|
> The paragraphs above describe the SEEDED ladder, which is still what a fresh instance boots with — but
|
|
155
155
|
> they are no longer a guarantee about a running one. The owner made the three sub-Archon **rank roles**
|
|
156
|
-
> editable from the governance tab and chose to include the seven in-app Archon powers, `
|
|
156
|
+
> editable from the governance tab and chose to include the seven in-app Archon powers, `government.manage`
|
|
157
157
|
> and `builder.rank.set` among them. So "no system permission is grantable below archon" now reads:
|
|
158
158
|
> *no system permission is grantable to a CUSTOM role, and none is seeded below archon* — an Archon may
|
|
159
159
|
> nonetheless hand one to a rank role deliberately. The four-step escalation chain described above is
|
|
160
|
-
> therefore **open by decision, not by accident**: whoever holds `
|
|
160
|
+
> therefore **open by decision, not by accident**: whoever holds `government.manage` can reach the rest.
|
|
161
161
|
> Two things that did NOT move, and that bound the blast radius: the **Archon** rank-role is immutable
|
|
162
162
|
> (nothing clickable revokes the permission needed to reach the tab), and the three **substrate** keys stay
|
|
163
163
|
> non-grantable to every role — ADR 0043's enforcers read the rank ladder and never consult this database,
|
|
@@ -192,9 +192,9 @@ or missing ranks fail closed.
|
|
|
192
192
|
|
|
193
193
|
**[ADR 0157](adr/<redacted>.md) added the whole operational block here** —
|
|
194
194
|
20 permissions that had been Archon-gated only by ADR 0016's fail-closed default, never by a decision:
|
|
195
|
-
the governance/monitoring **pages** (`/watch`, `/harbor`, `/gate` — `page.view.
|
|
195
|
+
the governance/monitoring **pages** (`/watch`, `/harbor`, `/gate` — `page.view.government`) and
|
|
196
196
|
`GET /api/gds/sessions/search` (whose own `/sessions` page had no server gate at all until
|
|
197
|
-
task 1002710 put it behind `page.view.
|
|
197
|
+
task 1002710 put it behind `page.view.government` alongside its three group-mates); the
|
|
198
198
|
**dev-box + provisioning fleets** (`GET /boxes[/cost-ledger]`,
|
|
199
199
|
`POST /boxes/:b/close`, `PATCH .../block`, `GET /provisioning/fleet[/cost-ledger]`,
|
|
200
200
|
`POST .../force-teardown`); **security adjudication** (`GET /security/reports`,
|
|
@@ -337,25 +337,25 @@ function createClient(opts = {}) {
|
|
|
337
337
|
// POST /goals/reorder — rank: metic+archon — POST /goals/reorder
|
|
338
338
|
postGoalsReorder: (args) => request("POST", "/goals/reorder", { hasBody: true }, args),
|
|
339
339
|
},
|
|
340
|
-
"
|
|
341
|
-
// GET /
|
|
342
|
-
|
|
343
|
-
// POST /
|
|
344
|
-
|
|
345
|
-
// DELETE /
|
|
346
|
-
|
|
347
|
-
// GET /
|
|
348
|
-
|
|
349
|
-
// GET /
|
|
350
|
-
|
|
351
|
-
// POST /
|
|
352
|
-
|
|
353
|
-
// DELETE /
|
|
354
|
-
|
|
355
|
-
// PUT /
|
|
356
|
-
|
|
357
|
-
// POST /
|
|
358
|
-
|
|
340
|
+
"government": {
|
|
341
|
+
// GET /government/assignments — rank: archon — GET /government/assignments
|
|
342
|
+
getGovernmentAssignments: (args) => request("GET", "/government/assignments", { hasBody: false }, args),
|
|
343
|
+
// POST /government/assignments — rank: archon — POST /government/assignments
|
|
344
|
+
postGovernmentAssignments: (args) => request("POST", "/government/assignments", { hasBody: true }, args),
|
|
345
|
+
// DELETE /government/assignments/{builderId}/{rankKey} — rank: archon — DELETE /government/assignments/:builderId/:rankKey
|
|
346
|
+
deleteGovernmentAssignmentsBuilderIdRankKey: (args) => request("DELETE", "/government/assignments/{builderId}/{rankKey}", { hasBody: false }, args),
|
|
347
|
+
// GET /government/permissions — rank: archon — GET /government/permissions
|
|
348
|
+
getGovernmentPermissions: (args) => request("GET", "/government/permissions", { hasBody: false }, args),
|
|
349
|
+
// GET /government/ranks — rank: archon — GET /government/ranks
|
|
350
|
+
getGovernmentRanks: (args) => request("GET", "/government/ranks", { hasBody: false }, args),
|
|
351
|
+
// POST /government/ranks — rank: archon — POST /government/ranks
|
|
352
|
+
postGovernmentRanks: (args) => request("POST", "/government/ranks", { hasBody: true }, args),
|
|
353
|
+
// DELETE /government/ranks/{rankKey}/permissions/{permissionKey} — rank: archon — DELETE /government/ranks/:rankKey/permissions/:permissionKey
|
|
354
|
+
deleteGovernmentRanksRankKeyPermissionsPermissionKey: (args) => request("DELETE", "/government/ranks/{rankKey}/permissions/{permissionKey}", { hasBody: false }, args),
|
|
355
|
+
// PUT /government/ranks/{rankKey}/permissions/{permissionKey} — rank: archon — PUT /government/ranks/:rankKey/permissions/:permissionKey
|
|
356
|
+
putGovernmentRanksRankKeyPermissionsPermissionKey: (args) => request("PUT", "/government/ranks/{rankKey}/permissions/{permissionKey}", { hasBody: true }, args),
|
|
357
|
+
// POST /government/ranks/{rankKey}/reset — rank: archon — POST /government/ranks/:rankKey/reset
|
|
358
|
+
postGovernmentRanksRankKeyReset: (args) => request("POST", "/government/ranks/{rankKey}/reset", { hasBody: true }, args),
|
|
359
359
|
},
|
|
360
360
|
"grades": {
|
|
361
361
|
// GET /grades/by-builder — rank: metic+archon — GET /grades/by-builder
|
|
@@ -104,7 +104,7 @@ const PERMISSIONS = [
|
|
|
104
104
|
{ key: 'task.confirm.grade_bypass', system: true, floor: 'archon', guards: 'POST /tasks/:id/confirm — ratify past a failed grade (env-relaxable, ADR 0041)' },
|
|
105
105
|
{ key: 'dependency.cross_tier.manage', system: true, floor: 'archon', guards: 'POST|DELETE /dependencies — cross-tier structural edges (ADR 0086)' },
|
|
106
106
|
{ key: 'goal.scope.manage', system: true, floor: 'archon', guards: 'cross-tier goal-scope edges + protected-scope admission (ADR 0086)' },
|
|
107
|
-
{ key: '
|
|
107
|
+
{ key: 'government.manage', system: true, floor: 'archon', guards: 'the governance tab routes (roles/perms/assignments CRUD, reads included — R110) — meta-escalation' },
|
|
108
108
|
{ key: 'gate_approval.decide', system: true, floor: 'archon', guards: 'POST /gate-approvals/:pr/approve — CI deploy-gate override' },
|
|
109
109
|
{ key: 'deploy.push_main', system: true, substrate: true, floor: 'archon', guards: 'git push origin main / the deploy — substrate-enforced (pre-push, main-audit)' },
|
|
110
110
|
{ key: 'path.protected.modify', system: true, substrate: true, floor: 'archon', guards: 'modifying a PROTECTED_GLOBS path below the Metic floor — substrate (ADR 0043)' },
|
|
@@ -123,7 +123,7 @@ const PERMISSIONS = [
|
|
|
123
123
|
//
|
|
124
124
|
// The four trust-boundary keys that ALSO stay Archon are system:true above
|
|
125
125
|
// (`task.confirm.grade_bypass`, `gate_approval.decide`, `goal.scope.manage`,
|
|
126
|
-
// `
|
|
126
|
+
// `government.manage`) — see the ADR's escalation-chain argument for why moving any
|
|
127
127
|
// one of them would make this delegation self-reversible.
|
|
128
128
|
{ key: 'builder.status.set', system: false, floor: 'archon', guards: 'PATCH /builders/:id/status — offboard / (de)activate' },
|
|
129
129
|
{ key: 'auth.session.revoke', system: false, floor: 'archon', guards: 'POST /auth/revoke, /auth/revoke-all' },
|
|
@@ -207,7 +207,7 @@ const PERMISSIONS = [
|
|
|
207
207
|
{ key: 'audit_log.read', system: false, floor: 'metic', guards: 'GET /audit-log (ADR 0157, was archon)' },
|
|
208
208
|
// ADR 0157 §4. Split from `gate_approval.decide` (system, archon) so the Gate PAGE
|
|
209
209
|
// can open to Metic while APPROVING a held PR stays Archon. Without the split, a
|
|
210
|
-
// Metic could reach /gate via `page.view.
|
|
210
|
+
// Metic could reach /gate via `page.view.government` and then 403 on its only data
|
|
211
211
|
// endpoint — a page that renders nothing but an error.
|
|
212
212
|
{ key: 'gate_approval.read', system: false, floor: 'metic', guards: 'GET /gate-approvals — the Gate page read side (approve = gate_approval.decide, archon)' },
|
|
213
213
|
|
|
@@ -236,9 +236,9 @@ const PERMISSIONS = [
|
|
|
236
236
|
// ADR 0157: floor archon→metic, which is what opens /watch, /harbor and /gate to the
|
|
237
237
|
// working rank. The gate FUNCTION was renamed to match (`requireArchonPage` →
|
|
238
238
|
// `requireGovernancePage`) once the floor moved. Viewing is all this grants: every
|
|
239
|
-
// write those pages reach is its own atom, and `
|
|
239
|
+
// write those pages reach is its own atom, and `government.manage` (the
|
|
240
240
|
// roles/permissions tab) stayed archon.
|
|
241
|
-
{ key: 'page.view.
|
|
241
|
+
{ key: 'page.view.government', system: false, floor: 'metic', guards: 'the governance/monitoring pages — /watch, /harbor, /gate (requireGovernancePage; write = government.manage, archon)' },
|
|
242
242
|
];
|
|
243
243
|
|
|
244
244
|
// ── invariants asserted at module load (fail loud on an authoring typo) ─────────
|
|
@@ -418,13 +418,13 @@ function substrateKeys() {
|
|
|
418
418
|
// success and delegate nothing. Refusing them keeps the tab honest.
|
|
419
419
|
// • the ARCHON rank-role — nothing. It is immutable and already holds everything,
|
|
420
420
|
// and that immutability is also the LOCKOUT GUARD: the only role carrying
|
|
421
|
-
// `
|
|
421
|
+
// `government.manage` by seed can never have it revoked, so no sequence of
|
|
422
422
|
// clicks in this tab can lock the owner out of the tab.
|
|
423
423
|
// • a SEEDED rank-role BELOW archon — everything else, the seven in-app Archon
|
|
424
424
|
// powers included. This is the delegation lever the owner asked for.
|
|
425
425
|
// • a CUSTOM role — operational only: the pre-existing R110 wall, deliberately NOT
|
|
426
426
|
// widened. A rank-role is the owner's own coarse ladder, moved by a rank write;
|
|
427
|
-
// a custom role is ad-hoc and assignable by anyone holding `
|
|
427
|
+
// a custom role is ad-hoc and assignable by anyone holding `government.manage`,
|
|
428
428
|
// so widening it would hand the second group the first group's lever.
|
|
429
429
|
//
|
|
430
430
|
// Fail-closed on everything unrecognised: unknown key, missing role, or a system role
|
package/modules/government/db.js
CHANGED
|
@@ -32,10 +32,10 @@ async function getPermissionKeysForBuilder(builderId) {
|
|
|
32
32
|
// R110/R111) and for explaining a 403. Not part of the resolve path.
|
|
33
33
|
async function getRoleKeysForBuilder(builderId) {
|
|
34
34
|
const { rows } = await pool.query(
|
|
35
|
-
`SELECT rank_key
|
|
35
|
+
`SELECT rank_key FROM government_builder_ranks WHERE builder_id = $1 ORDER BY rank_key`,
|
|
36
36
|
[builderId],
|
|
37
37
|
);
|
|
38
|
-
return rows.map((r) => r.
|
|
38
|
+
return rows.map((r) => r.rank_key);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// --- the rank→role WRITE half (BV1.R95b) ------------------------------------
|
|
@@ -57,7 +57,7 @@ async function getRoleKeysForBuilder(builderId) {
|
|
|
57
57
|
// runs on rank writes + sign-in, never in a hot loop.
|
|
58
58
|
async function getSeededRankRoles(q) {
|
|
59
59
|
const { rows } = await q.query(
|
|
60
|
-
`SELECT rank_key
|
|
60
|
+
`SELECT rank_key, seeded_rank, is_system
|
|
61
61
|
FROM government_ranks
|
|
62
62
|
WHERE is_system = true AND seeded_rank IS NOT NULL`,
|
|
63
63
|
);
|
|
@@ -68,7 +68,7 @@ async function getSeededRankRoles(q) {
|
|
|
68
68
|
// seeded rank-role from an Archon-assigned custom one.
|
|
69
69
|
async function getRoleAssignmentsForBuilder(q, builderId) {
|
|
70
70
|
const { rows } = await q.query(
|
|
71
|
-
`SELECT br.rank_key
|
|
71
|
+
`SELECT br.rank_key, r.is_system, r.seeded_rank
|
|
72
72
|
FROM government_builder_ranks br
|
|
73
73
|
JOIN government_ranks r ON r.rank_key = br.rank_key
|
|
74
74
|
WHERE br.builder_id = $1`,
|
|
@@ -127,7 +127,7 @@ async function syncRank(client, { builderId, rank } = {}) {
|
|
|
127
127
|
|
|
128
128
|
async function listRolesWithGrants() {
|
|
129
129
|
const { rows } = await pool.query(
|
|
130
|
-
`SELECT r.rank_key
|
|
130
|
+
`SELECT r.rank_key, r.label, r.description, r.is_system, r.seeded_rank,
|
|
131
131
|
r.created_by, r.created_at,
|
|
132
132
|
COALESCE(array_agg(rp.permission_key ORDER BY rp.permission_key)
|
|
133
133
|
FILTER (WHERE rp.permission_key IS NOT NULL), '{}') AS permissions
|
|
@@ -141,7 +141,7 @@ async function listRolesWithGrants() {
|
|
|
141
141
|
|
|
142
142
|
async function getRoleByKey(roleKey) {
|
|
143
143
|
const { rows } = await pool.query(
|
|
144
|
-
`SELECT rank_key
|
|
144
|
+
`SELECT rank_key, label, description, is_system, seeded_rank, created_by, created_at
|
|
145
145
|
FROM government_ranks
|
|
146
146
|
WHERE rank_key = $1`,
|
|
147
147
|
[roleKey],
|
|
@@ -153,7 +153,7 @@ async function createRole({ roleKey, label, description, createdBy }) {
|
|
|
153
153
|
const { rows } = await pool.query(
|
|
154
154
|
`INSERT INTO government_ranks (rank_key, label, description, is_system, seeded_rank, created_by)
|
|
155
155
|
VALUES ($1, $2, $3, false, NULL, $4)
|
|
156
|
-
RETURNING rank_key
|
|
156
|
+
RETURNING rank_key, label, description, is_system, seeded_rank, created_by, created_at`,
|
|
157
157
|
[roleKey, label, description ?? null, createdBy],
|
|
158
158
|
);
|
|
159
159
|
return rows[0];
|
|
@@ -210,7 +210,7 @@ async function resetRoleGrants({ roleKey, permissionKeys }) {
|
|
|
210
210
|
async function listAssignments() {
|
|
211
211
|
const { rows } = await pool.query(
|
|
212
212
|
`SELECT br.builder_id, b.github_login, b.display_name, b.rank,
|
|
213
|
-
br.rank_key
|
|
213
|
+
br.rank_key, r.label AS role_label, r.is_system, r.seeded_rank,
|
|
214
214
|
br.assigned_by, br.assigned_at
|
|
215
215
|
FROM government_builder_ranks br
|
|
216
216
|
JOIN builders b ON b.id = br.builder_id
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
-- government_002_rename_permission_keys.sql
|
|
2
|
+
-- Goal 1000068 R05 (task 1003006) — ADR 0174.
|
|
3
|
+
--
|
|
4
|
+
-- Renames the two permission KEYS that still carried the old vocabulary, in the
|
|
5
|
+
-- grant rows that hold them:
|
|
6
|
+
--
|
|
7
|
+
-- governance.manage -> government.manage
|
|
8
|
+
-- page.view.governance -> page.view.government
|
|
9
|
+
--
|
|
10
|
+
-- These are DATA, not schema: `government_rank_permissions.permission_key` is
|
|
11
|
+
-- free text validated app-side against the code catalog (it is deliberately not
|
|
12
|
+
-- a foreign key — the catalog is CODE, not a table; see governance_001).
|
|
13
|
+
--
|
|
14
|
+
-- ── WHY THIS IS SEPARATE FROM government_001 ────────────────────────────────
|
|
15
|
+
-- 001 renamed the tables and pairs with the code that QUERIES them. This one
|
|
16
|
+
-- pairs with `modules/government/catalog.js`, which declares the key strings,
|
|
17
|
+
-- and it must land in the SAME release as that edit. The coupling is sharper
|
|
18
|
+
-- than it looks: `resolver.js` DROPS any grant naming a key the catalog does not
|
|
19
|
+
-- define (the fail-closed rule). So if these rows were renamed while the catalog
|
|
20
|
+
-- still said `governance.manage`, every renamed grant would resolve to nothing —
|
|
21
|
+
-- and because that key is what gates the Government tab, the owner would be
|
|
22
|
+
-- locked out of the very surface used to diagnose it. The inverse is equally
|
|
23
|
+
-- true: shipping the catalog edit without this migration leaves the live rows
|
|
24
|
+
-- naming keys the catalog no longer defines, with the same result.
|
|
25
|
+
--
|
|
26
|
+
-- Both halves are in this commit, and the live instance's auto-upgrade sweep
|
|
27
|
+
-- stays paused (task 1003011) until the whole rename is released together.
|
|
28
|
+
--
|
|
29
|
+
-- ── ROW-COUNT SAFETY ────────────────────────────────────────────────────────
|
|
30
|
+
-- These are PRIMARY KEY columns (rank_key, permission_key). A rank that somehow
|
|
31
|
+
-- held BOTH the old and the new key would collide on UPDATE, so the update is
|
|
32
|
+
-- written to skip any row whose target already exists, and the assertion at the
|
|
33
|
+
-- end proves the old keys are gone and the new counts match what was there
|
|
34
|
+
-- before. Live baseline captured 2026-08-16 before the rename:
|
|
35
|
+
-- governance.manage -> 1 grant (the archon rank only)
|
|
36
|
+
-- page.view.governance -> 2 grants (metic + archon)
|
|
37
|
+
--
|
|
38
|
+
-- IDEMPOTENT: re-running finds no old keys and changes nothing.
|
|
39
|
+
--
|
|
40
|
+
-- ROLLBACK: reverse the two UPDATEs (new key -> old key). Lossless.
|
|
41
|
+
-- UPDATE government_rank_permissions SET permission_key = 'governance.manage'
|
|
42
|
+
-- WHERE permission_key = 'government.manage';
|
|
43
|
+
-- UPDATE government_rank_permissions SET permission_key = 'page.view.governance'
|
|
44
|
+
-- WHERE permission_key = 'page.view.government';
|
|
45
|
+
|
|
46
|
+
BEGIN;
|
|
47
|
+
|
|
48
|
+
-- Capture the before-counts so the assertion below can prove nothing was lost.
|
|
49
|
+
CREATE TEMP TABLE _govt_key_rename_before ON COMMIT DROP AS
|
|
50
|
+
SELECT
|
|
51
|
+
count(*) FILTER (WHERE permission_key = 'governance.manage') AS old_manage,
|
|
52
|
+
count(*) FILTER (WHERE permission_key = 'page.view.governance') AS old_page,
|
|
53
|
+
count(*) FILTER (WHERE permission_key = 'government.manage') AS new_manage,
|
|
54
|
+
count(*) FILTER (WHERE permission_key = 'page.view.government') AS new_page
|
|
55
|
+
FROM government_rank_permissions;
|
|
56
|
+
|
|
57
|
+
UPDATE government_rank_permissions g
|
|
58
|
+
SET permission_key = 'government.manage'
|
|
59
|
+
WHERE g.permission_key = 'governance.manage'
|
|
60
|
+
AND NOT EXISTS (
|
|
61
|
+
SELECT 1 FROM government_rank_permissions x
|
|
62
|
+
WHERE x.rank_key = g.rank_key AND x.permission_key = 'government.manage');
|
|
63
|
+
|
|
64
|
+
UPDATE government_rank_permissions g
|
|
65
|
+
SET permission_key = 'page.view.government'
|
|
66
|
+
WHERE g.permission_key = 'page.view.governance'
|
|
67
|
+
AND NOT EXISTS (
|
|
68
|
+
SELECT 1 FROM government_rank_permissions x
|
|
69
|
+
WHERE x.rank_key = g.rank_key AND x.permission_key = 'page.view.government');
|
|
70
|
+
|
|
71
|
+
-- Any row the collision guard skipped would be a duplicate grant; drop it so the
|
|
72
|
+
-- old key cannot survive. (Expected to affect zero rows — a rank holding both
|
|
73
|
+
-- spellings should not exist. Written so the assertion below can be absolute.)
|
|
74
|
+
DELETE FROM government_rank_permissions
|
|
75
|
+
WHERE permission_key IN ('governance.manage', 'page.view.governance');
|
|
76
|
+
|
|
77
|
+
DO $$
|
|
78
|
+
DECLARE
|
|
79
|
+
b record;
|
|
80
|
+
a_manage int;
|
|
81
|
+
a_page int;
|
|
82
|
+
leftover int;
|
|
83
|
+
BEGIN
|
|
84
|
+
SELECT * INTO b FROM _govt_key_rename_before;
|
|
85
|
+
SELECT count(*) INTO a_manage FROM government_rank_permissions WHERE permission_key = 'government.manage';
|
|
86
|
+
SELECT count(*) INTO a_page FROM government_rank_permissions WHERE permission_key = 'page.view.government';
|
|
87
|
+
SELECT count(*) INTO leftover FROM government_rank_permissions
|
|
88
|
+
WHERE permission_key IN ('governance.manage', 'page.view.governance');
|
|
89
|
+
|
|
90
|
+
IF leftover <> 0 THEN
|
|
91
|
+
RAISE EXCEPTION 'government key rename: % row(s) still carry an old key — rolling back.', leftover;
|
|
92
|
+
END IF;
|
|
93
|
+
|
|
94
|
+
-- Every grant that existed under either spelling must exist under the new one.
|
|
95
|
+
IF a_manage <> GREATEST(b.old_manage, b.new_manage) THEN
|
|
96
|
+
RAISE EXCEPTION 'government.manage grant count is % but % was expected (before: old=%, new=%) — rolling back.',
|
|
97
|
+
a_manage, GREATEST(b.old_manage, b.new_manage), b.old_manage, b.new_manage;
|
|
98
|
+
END IF;
|
|
99
|
+
IF a_page <> GREATEST(b.old_page, b.new_page) THEN
|
|
100
|
+
RAISE EXCEPTION 'page.view.government grant count is % but % was expected (before: old=%, new=%) — rolling back.',
|
|
101
|
+
a_page, GREATEST(b.old_page, b.new_page), b.old_page, b.new_page;
|
|
102
|
+
END IF;
|
|
103
|
+
|
|
104
|
+
-- The lockout guard: SOMEONE must still hold the key that opens this tab.
|
|
105
|
+
IF a_manage = 0 THEN
|
|
106
|
+
RAISE EXCEPTION 'no rank holds government.manage after the rename — that is a lockout. Rolling back.';
|
|
107
|
+
END IF;
|
|
108
|
+
|
|
109
|
+
RAISE NOTICE 'government key rename OK: government.manage=% page.view.government=%', a_manage, a_page;
|
|
110
|
+
END $$;
|
|
111
|
+
|
|
112
|
+
COMMIT;
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
{ "glob": "modules/lifecycle/routes/", "group": "authz", "why": "BV1.R86: the lifecycle routes carved out of src/bongos/routes/ — the gates follow the routes" },
|
|
35
35
|
{ "glob": "src/bongos/routes.js", "group": "authz", "why": "the route MOUNT table (a new ungated route lands here)" },
|
|
36
36
|
{ "glob": "server.js", "group": "authz", "why": "the Express entrypoint — wires every router + host routing" },
|
|
37
|
-
{ "glob": "modules/governance/", "group": "authz", "why": "the PRE-ADR-0174 path of the authz core. The folder itself moved to modules/government/ in goal 1000068 R03/R04, so nothing matches this glob in the core tree any more — it is kept for the one-release compatibility window because an in-flight branch or a vendored instance checkout may still carry the old path, and a protected path that silently stops being protected mid-rename is the failure this whole sequence exists to avoid. Dropped by R05 (task 1003006) with the rest of the compatibility surface." },
|
|
38
37
|
{ "glob": "modules/government/", "group": "authz", "why": "the same authz core under its post-ADR-0174 name (goal 1000068, task 1003003). Listed BEFORE the folder moves, deliberately: `protected` is DERIVED from this registry, so a module key whose globs are absent here computes protected:false — which would silently drop the Archon gate on goal creation AND the pre-push block on these files for the whole window between the key landing and the folder arriving. The prospective-carve precedent is the modules/governance/ entry directly above (ADR 0086 §4)." },
|
|
39
38
|
{ "glob": "modules/dev-box/boxes.js", "group": "box-access", "why": "boxScopeForRank / provision floor — decides who gets a full source clone" },
|
|
40
39
|
{ "glob": "modules/dev-box/box-access.js", "group": "box-access", "why": "decideSourceAccess (rank → starter|full source)" },
|
|
@@ -42,32 +42,32 @@ function isSeededRank(row) {
|
|
|
42
42
|
&& row.seeded_rank.length > 0;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// planRankSync({ rank,
|
|
45
|
+
// planRankSync({ rank, currentRanks, seededRanks }) → { target, add, remove, next }
|
|
46
46
|
//
|
|
47
47
|
// rank the builder's rank AFTER the write being synced.
|
|
48
|
-
//
|
|
49
|
-
//
|
|
48
|
+
// currentRanks the builder's assignments, each { rank_key, is_system, seeded_rank }.
|
|
49
|
+
// seededRanks the seeded rank-roles the DB defines, each { rank_key, seeded_rank,
|
|
50
50
|
// is_system }. Passed in rather than hardcoded so the four role keys
|
|
51
51
|
// stay DB truth — this file never re-declares the seed.
|
|
52
52
|
//
|
|
53
|
-
// target the seeded
|
|
53
|
+
// target the seeded rank_key the builder should end up with, or null when the
|
|
54
54
|
// rank has no seeded role (fail-closed divergence, above).
|
|
55
55
|
// add [] or [target] — empty when it is already held (idempotence).
|
|
56
56
|
// remove every OTHER seeded rank-role currently held. Custom roles never appear.
|
|
57
|
-
// next the resulting
|
|
58
|
-
function planRankSync({ rank,
|
|
59
|
-
const match =
|
|
60
|
-
const target = match ? match.
|
|
57
|
+
// next the resulting rank_key set, sorted — "what the role set becomes".
|
|
58
|
+
function planRankSync({ rank, currentRanks = [], seededRanks = [] } = {}) {
|
|
59
|
+
const match = seededRanks.find((r) => isSeededRank(r) && r.seeded_rank === rank);
|
|
60
|
+
const target = match ? match.rank_key : null;
|
|
61
61
|
|
|
62
|
-
const held =
|
|
62
|
+
const held = currentRanks.filter((r) => r && typeof r.rank_key === 'string');
|
|
63
63
|
const remove = held
|
|
64
|
-
.filter((r) => isSeededRank(r) && r.
|
|
65
|
-
.map((r) => r.
|
|
66
|
-
const add = target && !held.some((r) => r.
|
|
64
|
+
.filter((r) => isSeededRank(r) && r.rank_key !== target)
|
|
65
|
+
.map((r) => r.rank_key);
|
|
66
|
+
const add = target && !held.some((r) => r.rank_key === target) ? [target] : [];
|
|
67
67
|
|
|
68
68
|
const removed = new Set(remove);
|
|
69
69
|
const next = held
|
|
70
|
-
.map((r) => r.
|
|
70
|
+
.map((r) => r.rank_key)
|
|
71
71
|
.filter((key) => !removed.has(key))
|
|
72
72
|
.concat(add)
|
|
73
73
|
.sort();
|