@bongos/core 1.19.186 → 1.19.187
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 +78 -73
- package/config/government.neutral.json +6 -6
- package/docs/adr/0172-editable-rank-roles-substrate-axis.md +1 -1
- package/docs/api/openapi.json +8 -8
- package/docs/api-reference.md +7 -7
- package/modules/builder-settings/wandering-prefs.js +13 -13
- package/modules/government/catalog.js +75 -75
- package/modules/government/config.js +43 -33
- package/modules/government/db.js +57 -57
- package/modules/government/protected-surfaces.js +7 -7
- package/modules/government/protected-surfaces.json +4 -4
- package/modules/government/ranks.js +17 -17
- package/modules/government/resolver.js +24 -24
- package/modules/government/routes/government.js +86 -86
- package/modules/government/routes/rank-sync.js +6 -6
- package/modules/government/session-scopes.js +7 -7
- package/modules/government/session-scopes.json +5 -5
- package/modules/hall-ui/public/government.css +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/government.js +3 -3
- package/modules/lifecycle/claim-eligibility.js +1 -1
- package/modules/lifecycle/db.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +138 -0
- package/src/bongos/auth.js +2 -2
- package/src/module-api.js +3 -1
- package/tests/auth_page_gate.mjs +2 -2
- package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +36 -36
- package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
- package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
- package/tests/{governance_config.mjs → government_config.mjs} +54 -54
- package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
- package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
- package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
- package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
- package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +5 -5
- package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
- package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
- package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
- package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
- package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
- package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
- package/tests/{governance_routes.mjs → government_routes.mjs} +32 -32
- package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
- package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
- package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
- package/tests/government_vocabulary.mjs +94 -0
- package/tests/hall_page_gate_map.mjs +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// The
|
|
3
|
+
// The government module's kernel PORT registration (ADR 0151, BV1.R95).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// government exposes the permission RESOLVER as the `government` kernel PORT.
|
|
6
6
|
// CORE files that must NOT import a module resolve it instead — first among them
|
|
7
7
|
// src/bongos/auth.js, whose `requirePermission()` gate (R97) needs a builder's
|
|
8
8
|
// effective permission set on every gated request. The one-way rule (ADR 0083)
|
|
9
|
-
// forbids core requiring modules/
|
|
9
|
+
// forbids core requiring modules/government/ directly; the port is how the
|
|
10
10
|
// capability crosses that line.
|
|
11
11
|
//
|
|
12
12
|
// Why a route factory for a (currently) routeless capability: `mountModuleRoutes`
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// rebuilds the GDS router, so registering here keeps the port present for the
|
|
15
15
|
// route-building tests too. Registration is hasProvider-guarded so
|
|
16
16
|
// re-instantiation never trips the seam's single-provider duplicate guard.
|
|
17
|
-
// (Mirrors modules/economy/routes/reward.js.) The R110
|
|
17
|
+
// (Mirrors modules/economy/routes/reward.js.) The R110 government CRUD
|
|
18
18
|
// endpoints live on this same router, below.
|
|
19
19
|
|
|
20
20
|
const express = require('express');
|
|
@@ -27,7 +27,7 @@ const sessionScopes = require('../session-scopes');
|
|
|
27
27
|
// The port surface. Deliberately narrow: resolution + the catalog reads a gate or
|
|
28
28
|
// the Archon tab needs. It grows with R98-R101 (principals, session-scopes,
|
|
29
29
|
// ownership scoping, the protected-surface registry) — one object port rather
|
|
30
|
-
// than a port per capability, so core resolves `
|
|
30
|
+
// than a port per capability, so core resolves `government` once.
|
|
31
31
|
//
|
|
32
32
|
// The principal half (R98) is READ-ONLY by construction: resolution keyed on an
|
|
33
33
|
// exact system_role, plus `listPrincipals` for the tab to render. There is no
|
|
@@ -47,10 +47,10 @@ const sessionScopes = require('../session-scopes');
|
|
|
47
47
|
// The session-scope half (R99) is READ-ONLY and deliberately NOT the enforcement
|
|
48
48
|
// path: the kernel gate (auth.requireBuilder) reads the same registry as static
|
|
49
49
|
// data, so a request never depends on this port being registered to stay
|
|
50
|
-
// deny-by-default. What the port adds is the composed answer —
|
|
51
|
-
// session's scope — for the
|
|
50
|
+
// deny-by-default. What the port adds is the composed answer — ranks ∩ this
|
|
51
|
+
// session's scope — for the Government tab and for any gate that wants the
|
|
52
52
|
// session's real authority rather than the builder's.
|
|
53
|
-
const
|
|
53
|
+
const governmentPort = {
|
|
54
54
|
resolveBuilderPermissions: resolver.resolveBuilderPermissions,
|
|
55
55
|
// The pure catalog-drop half, for the session-load fold (task 1002556): the
|
|
56
56
|
// kernel feeds it the raw grant keys its own session query carried, so a
|
|
@@ -58,7 +58,7 @@ const governancePort = {
|
|
|
58
58
|
// resolveBuilderPermissions given the same rows — DB-free by construction.
|
|
59
59
|
effectivePermissions: resolver.effectivePermissions,
|
|
60
60
|
builderHasPermissions: resolver.builderHasPermissions,
|
|
61
|
-
|
|
61
|
+
getRankKeysForBuilder: db.getRankKeysForBuilder,
|
|
62
62
|
isSystem: catalog.isSystem,
|
|
63
63
|
isGrantable: catalog.isGrantable,
|
|
64
64
|
permissionKeys: catalog.permissionKeys,
|
|
@@ -82,13 +82,13 @@ const governancePort = {
|
|
|
82
82
|
|
|
83
83
|
function registerSeams() {
|
|
84
84
|
if (!api.hasProvider('government')) {
|
|
85
|
-
api.registerProvider('government',
|
|
85
|
+
api.registerProvider('government', governmentPort);
|
|
86
86
|
}
|
|
87
87
|
// The session-load fold (task 1002556): the kernel's lookupSession embeds this
|
|
88
88
|
// fragment so the SAME round-trip carries the builder's raw grant keys, and
|
|
89
89
|
// requirePermission runs the pure effectivePermissions instead of a second
|
|
90
90
|
// query. Registered HERE — capability-named, module-agnostic on the kernel
|
|
91
|
-
// side (ADR 0083 §Decision #4) — because this module owns the
|
|
91
|
+
// side (ADR 0083 §Decision #4) — because this module owns the government SQL.
|
|
92
92
|
// Contract: a boot-time CONSTANT (never user input) yielding one
|
|
93
93
|
// `raw_permission_keys` column against the kernel query's alias `s`
|
|
94
94
|
// (builder_sessions). Raw keys only — the catalog-drop stays in the resolver.
|
|
@@ -103,38 +103,38 @@ function registerSeams() {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// ── the R110 CRUD surface (ADR 0151 §2 — the
|
|
106
|
+
// ── the R110 CRUD surface (ADR 0151 §2 — the Government tab's server half) ────
|
|
107
107
|
//
|
|
108
108
|
// Every route — the three reads included — gates on the `government.manage`
|
|
109
|
-
// atom (system, floor archon): this surface EDITS the
|
|
109
|
+
// atom (system, floor archon): this surface EDITS the rank/permission graph
|
|
110
110
|
// itself, the meta-escalation the catalog flags non-grantable, and even listing
|
|
111
111
|
// it exposes the full authority map. The invariants are enforced HERE, against
|
|
112
112
|
// the catalog, not in the db layer (governance_001's app-side-validation split).
|
|
113
113
|
//
|
|
114
114
|
// ADR 0172 (owner decision, 2026-08-16) split what used to be ONE wall — "a seeded
|
|
115
|
-
// rank
|
|
115
|
+
// rank is immutable" — into two, because grants and assignment are immutable
|
|
116
116
|
// for different reasons and only one of those reasons still holds:
|
|
117
117
|
//
|
|
118
|
-
// • GRANTS on the three sub-Archon
|
|
118
|
+
// • GRANTS on the three sub-Archon ranks are now EDITABLE. Their rows were
|
|
119
119
|
// always ordinary `government_rank_permissions` rows (the governance_002 seed
|
|
120
120
|
// writes them); the old refusal was policy, not structure. What a rank means is
|
|
121
121
|
// now the owner's to set from the tab, and `catalog.isGrantableTo()` answers
|
|
122
|
-
// which keys per
|
|
122
|
+
// which keys per rank. The ARCHON rank stays immutable — it holds
|
|
123
123
|
// everything, and its immutability is what makes lockout impossible.
|
|
124
|
-
// • ASSIGNMENT of every seeded rank
|
|
124
|
+
// • ASSIGNMENT of every seeded rank is still refused, unchanged: it derives
|
|
125
125
|
// from `builders.rank` via the R95b sync, so a second writer here would fork
|
|
126
126
|
// authority truth and the sync would silently undo it on the next rank write.
|
|
127
|
-
// Changing who holds a rank
|
|
127
|
+
// Changing who holds a rank is still `PATCH /builders/:id/rank`.
|
|
128
128
|
//
|
|
129
|
-
// The custom-
|
|
130
|
-
// (R94's wall — "The
|
|
131
|
-
// SUBSTRATE keys stay non-grantable to everything,
|
|
129
|
+
// The custom-rank wall is unchanged: a `system:true` key stays non-grantable there
|
|
130
|
+
// (R94's wall — "The government API (R110) rejects granting it"), and the three
|
|
131
|
+
// SUBSTRATE keys stay non-grantable to everything, ranks included, because a
|
|
132
132
|
// grant row for one delegates nothing (ADR 0043 enforces them out of band).
|
|
133
133
|
const { validateOrRespond, LIMITS, parseId } = api;
|
|
134
134
|
const RANK_KEY_RE = /^[a-z][a-z0-9-]*$/;
|
|
135
135
|
const RANK_KEY_MAX = 64;
|
|
136
136
|
|
|
137
|
-
module.exports = function
|
|
137
|
+
module.exports = function buildGovernmentRouter() {
|
|
138
138
|
registerSeams();
|
|
139
139
|
const router = express.Router();
|
|
140
140
|
|
|
@@ -150,40 +150,40 @@ module.exports = function buildGovernanceRouter() {
|
|
|
150
150
|
return rankKey;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// Load a
|
|
154
|
-
// refuse only the Archon rank
|
|
155
|
-
//
|
|
156
|
-
// `catalog.isGrantableTo(key,
|
|
157
|
-
async function
|
|
158
|
-
const
|
|
159
|
-
if (!
|
|
153
|
+
// Load a rank whose GRANTS this API may edit (ADR 0172): 404 on a missing rank,
|
|
154
|
+
// refuse only the Archon rank. Every other rank — the three sub-Archon
|
|
155
|
+
// ranks and every custom one — is grant-editable; WHICH keys is then
|
|
156
|
+
// `catalog.isGrantableTo(key, rank)`, per rank. Returns null after responding.
|
|
157
|
+
async function loadGrantEditableRank(res, rankKey) {
|
|
158
|
+
const rank = await db.getRankByKey(rankKey);
|
|
159
|
+
if (!rank) {
|
|
160
160
|
res.fail('rank_not_found', 404, { rank_key: rankKey });
|
|
161
161
|
return null;
|
|
162
162
|
}
|
|
163
|
-
if (
|
|
163
|
+
if (rank.is_system && rank.seeded_rank === ARCHON) {
|
|
164
164
|
res.fail('archon_rank_immutable', 403, { rank_key: rankKey, reason: ARCHON_REASON });
|
|
165
165
|
return null;
|
|
166
166
|
}
|
|
167
|
-
return
|
|
167
|
+
return rank;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
// Load a
|
|
170
|
+
// Load a rank this API may ASSIGN. Unchanged from R110: every seeded rank is
|
|
171
171
|
// refused, because assignment derives from builders.rank via the R95b sync.
|
|
172
|
-
async function
|
|
173
|
-
const
|
|
174
|
-
if (!
|
|
172
|
+
async function loadAssignableRank(res, rankKey) {
|
|
173
|
+
const rank = await db.getRankByKey(rankKey);
|
|
174
|
+
if (!rank) {
|
|
175
175
|
res.fail('rank_not_found', 404, { rank_key: rankKey });
|
|
176
176
|
return null;
|
|
177
177
|
}
|
|
178
|
-
if (
|
|
178
|
+
if (rank.is_system) {
|
|
179
179
|
res.fail('seeded_rank_assignment_forbidden', 403, { rank_key: rankKey, reason: ASSIGN_REASON });
|
|
180
180
|
return null;
|
|
181
181
|
}
|
|
182
|
-
return
|
|
182
|
+
return rank;
|
|
183
183
|
}
|
|
184
184
|
const ARCHON = 'archon';
|
|
185
|
-
const ARCHON_REASON = 'the Archon rank-
|
|
186
|
-
const ASSIGN_REASON = 'a seeded rank-
|
|
185
|
+
const ARCHON_REASON = 'the Archon rank-rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
|
|
186
|
+
const ASSIGN_REASON = 'a seeded rank-rank is assigned by rank (R95b sync); change the builder\'s rank instead';
|
|
187
187
|
|
|
188
188
|
// ── one-release compatibility aliases: /governance/* -> /government/* ──────
|
|
189
189
|
// ADR 0174 committed to keeping the pre-rename paths answering for exactly one
|
|
@@ -205,19 +205,19 @@ module.exports = function buildGovernanceRouter() {
|
|
|
205
205
|
router.all(`${LEGACY_PATH_PREFIX}/*rest`, (req, res, next) => {
|
|
206
206
|
const rest = req.originalUrl.slice(req.originalUrl.indexOf(LEGACY_PATH_PREFIX) + LEGACY_PATH_PREFIX.length);
|
|
207
207
|
if (!rest) return next();
|
|
208
|
-
const target = `/government${rest.replace(/^\/
|
|
208
|
+
const target = `/government${rest.replace(/^\/ranks(?=\/|$)/, '/ranks')}`;
|
|
209
209
|
console.warn(`[government] legacy path hit: ${req.method} ${req.originalUrl} -> ${target} (ADR 0174 alias; remove one release after the rename)`);
|
|
210
210
|
return res.redirect(308, target);
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
-
// rank: perm government.manage — the full
|
|
213
|
+
// rank: perm government.manage — the full rank list with each rank's grants.
|
|
214
214
|
router.get('/government/ranks', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
215
215
|
try {
|
|
216
|
-
const ranks = await db.
|
|
216
|
+
const ranks = await db.listRanksWithGrants();
|
|
217
217
|
res.json({ ranks });
|
|
218
218
|
} catch (err) {
|
|
219
219
|
console.error('[gds] GET /government/ranks', err);
|
|
220
|
-
res.fail('
|
|
220
|
+
res.fail('ranks_list_failed', { status: 500, message: 'internal error' });
|
|
221
221
|
}
|
|
222
222
|
});
|
|
223
223
|
|
|
@@ -225,11 +225,11 @@ module.exports = function buildGovernanceRouter() {
|
|
|
225
225
|
// from code (the catalog is CODE, not a table); no DB read. Principals are
|
|
226
226
|
// read-only by construction. Three flags the tab renders its locks from, and the
|
|
227
227
|
// tab must read all three rather than just `grantable`, because since ADR 0172
|
|
228
|
-
// grantability depends on WHICH
|
|
229
|
-
// • `grantable` — the CUSTOM-
|
|
230
|
-
// • `system` — an in-app Archon power: locked on a custom
|
|
231
|
-
// a sub-Archon rank
|
|
232
|
-
// • `substrate` — locked EVERYWHERE, on every
|
|
228
|
+
// grantability depends on WHICH rank is selected:
|
|
229
|
+
// • `grantable` — the CUSTOM-rank answer (system:false). Unchanged meaning.
|
|
230
|
+
// • `system` — an in-app Archon power: locked on a custom rank, togglable on
|
|
231
|
+
// a sub-Archon rank.
|
|
232
|
+
// • `substrate` — locked EVERYWHERE, on every rank, forever (ADR 0043).
|
|
233
233
|
router.get('/government/permissions', api.requireBuilder, api.requirePermission('government.manage'), (req, res) => {
|
|
234
234
|
const permissions = catalog.PERMISSIONS.map((p) => ({
|
|
235
235
|
key: p.key,
|
|
@@ -244,7 +244,7 @@ module.exports = function buildGovernanceRouter() {
|
|
|
244
244
|
res.json({ permissions, principals: catalog.principals() });
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
-
// rank: perm government.manage — every builder↔
|
|
247
|
+
// rank: perm government.manage — every builder↔rank assignment row.
|
|
248
248
|
router.get('/government/assignments', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
249
249
|
try {
|
|
250
250
|
const assignments = await db.listAssignments();
|
|
@@ -255,7 +255,7 @@ module.exports = function buildGovernanceRouter() {
|
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
// rank: perm government.manage — create a CUSTOM
|
|
258
|
+
// rank: perm government.manage — create a CUSTOM rank (is_system false, no
|
|
259
259
|
// seeded_rank — the seeded four exist only via governance_002). Grants are
|
|
260
260
|
// added one at a time via the toggle below, so creation carries none.
|
|
261
261
|
router.post('/government/ranks', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
@@ -269,7 +269,7 @@ module.exports = function buildGovernanceRouter() {
|
|
|
269
269
|
return res.fail('bad_rank_key', 400, { rank_key: rankKey, expected: 'a kebab-case slug (^[a-z][a-z0-9-]*$)' });
|
|
270
270
|
}
|
|
271
271
|
try {
|
|
272
|
-
const rank = await db.
|
|
272
|
+
const rank = await db.createRank({
|
|
273
273
|
rankKey,
|
|
274
274
|
label: req.body.label,
|
|
275
275
|
description: req.body.description ?? null,
|
|
@@ -281,38 +281,38 @@ module.exports = function buildGovernanceRouter() {
|
|
|
281
281
|
return res.fail('rank_exists', 409, { rank_key: rankKey });
|
|
282
282
|
}
|
|
283
283
|
console.error('[gds] POST /government/ranks', err);
|
|
284
|
-
res.fail('
|
|
284
|
+
res.fail('rank_create_failed', { status: 500, message: 'internal error' });
|
|
285
285
|
}
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
-
// rank: perm government.manage — grant one permission to a
|
|
289
|
-
// half). The order of refusals is the authority order:
|
|
290
|
-
// the Archon one → the key exists → the key is grantable TO THIS
|
|
288
|
+
// rank: perm government.manage — grant one permission to a rank (the toggle's ON
|
|
289
|
+
// half). The order of refusals is the authority order: rank exists → rank is not
|
|
290
|
+
// the Archon one → the key exists → the key is grantable TO THIS RANK.
|
|
291
291
|
router.put('/government/ranks/:rankKey/permissions/:permissionKey', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
292
292
|
const rankKey = rankKeyParam(req, res);
|
|
293
293
|
if (rankKey === null) return;
|
|
294
294
|
const permissionKey = req.params.permissionKey;
|
|
295
295
|
try {
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
296
|
+
const rank = await loadGrantEditableRank(res, rankKey);
|
|
297
|
+
if (rank === null) return;
|
|
298
298
|
if (!catalog.byKey(permissionKey)) {
|
|
299
299
|
return res.fail('unknown_permission', 400, { permission_key: permissionKey });
|
|
300
300
|
}
|
|
301
|
-
if (!catalog.isGrantableTo(permissionKey,
|
|
301
|
+
if (!catalog.isGrantableTo(permissionKey, rank)) {
|
|
302
302
|
return res.fail('permission_not_grantable', 400, {
|
|
303
303
|
permission_key: permissionKey,
|
|
304
304
|
rank_key: rankKey,
|
|
305
305
|
// Name the ACTUAL wall. "Not grantable" now has three distinct causes and
|
|
306
306
|
// a caller that cannot tell them apart cannot tell a policy choice
|
|
307
|
-
// (widen the
|
|
307
|
+
// (widen the rank) from a structural one (nothing will ever work).
|
|
308
308
|
reason: catalog.isSubstrate(permissionKey)
|
|
309
|
-
? 'substrate-enforced (ADR 0043) — enforced outside the app by the pre-push hook / main-audit / the rank floor, so a grant row here would confer nothing. Non-grantable to EVERY
|
|
309
|
+
? 'substrate-enforced (ADR 0043) — enforced outside the app by the pre-push hook / main-audit / the rank floor, so a grant row here would confer nothing. Non-grantable to EVERY rank.'
|
|
310
310
|
: catalog.isPrincipalKey(permissionKey)
|
|
311
|
-
? 'a machine-principal permission — held by an exact builders.system_role identity, never by a
|
|
312
|
-
: 'a system (trust-boundary) permission is non-grantable to a CUSTOM
|
|
311
|
+
? 'a machine-principal permission — held by an exact builders.system_role identity, never by a rank (ADR 0151 §3)'
|
|
312
|
+
: 'a system (trust-boundary) permission is non-grantable to a CUSTOM rank; only a sub-Archon rank-rank may carry one (ADR 0172)',
|
|
313
313
|
});
|
|
314
314
|
}
|
|
315
|
-
const granted = await db.
|
|
315
|
+
const granted = await db.grantRankPermission({ rankKey, permissionKey });
|
|
316
316
|
res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, granted });
|
|
317
317
|
} catch (err) {
|
|
318
318
|
console.error('[gds] PUT /government/ranks/:rankKey/permissions/:permissionKey', err);
|
|
@@ -329,9 +329,9 @@ module.exports = function buildGovernanceRouter() {
|
|
|
329
329
|
if (rankKey === null) return;
|
|
330
330
|
const permissionKey = req.params.permissionKey;
|
|
331
331
|
try {
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
const removed = await db.
|
|
332
|
+
const rank = await loadGrantEditableRank(res, rankKey);
|
|
333
|
+
if (rank === null) return;
|
|
334
|
+
const removed = await db.revokeRankPermission({ rankKey, permissionKey });
|
|
335
335
|
res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, removed });
|
|
336
336
|
} catch (err) {
|
|
337
337
|
console.error('[gds] DELETE /government/ranks/:rankKey/permissions/:permissionKey', err);
|
|
@@ -339,42 +339,42 @@ module.exports = function buildGovernanceRouter() {
|
|
|
339
339
|
}
|
|
340
340
|
});
|
|
341
341
|
|
|
342
|
-
// rank: perm government.manage — restore a SEEDED rank
|
|
343
|
-
// default (ADR 0172). The counterweight to making the
|
|
342
|
+
// rank: perm government.manage — restore a SEEDED rank to its catalog
|
|
343
|
+
// default (ADR 0172). The counterweight to making the ranks editable: a
|
|
344
344
|
// rank's grant set is ~60 rows, so an owner who drifts one has no practical way
|
|
345
345
|
// back by clicking, and "what does Trusted mean by default" is a question only
|
|
346
|
-
// the catalog can answer. Restores catalog.seedForRank(
|
|
346
|
+
// the catalog can answer. Restores catalog.seedForRank(rank.seeded_rank) — the
|
|
347
347
|
// same derivation governance_002 seeded from, so the answer cannot drift from
|
|
348
348
|
// the floors even after later catalog edits.
|
|
349
349
|
//
|
|
350
|
-
// Deliberately NOT offered for a CUSTOM
|
|
350
|
+
// Deliberately NOT offered for a CUSTOM rank: a custom rank has no catalog
|
|
351
351
|
// default to return to (its "default" is the empty set it was created with), so
|
|
352
352
|
// the honest verb there is revoking the grants, which the toggle already does.
|
|
353
353
|
router.post('/government/ranks/:rankKey/reset', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
354
354
|
const rankKey = rankKeyParam(req, res);
|
|
355
355
|
if (rankKey === null) return;
|
|
356
356
|
try {
|
|
357
|
-
const
|
|
358
|
-
if (
|
|
359
|
-
if (!
|
|
357
|
+
const rank = await loadGrantEditableRank(res, rankKey);
|
|
358
|
+
if (rank === null) return;
|
|
359
|
+
if (!rank.is_system || !catalog.RANK_ORDER.includes(rank.seeded_rank)) {
|
|
360
360
|
return res.fail('rank_has_no_default', 400, {
|
|
361
361
|
rank_key: rankKey,
|
|
362
|
-
reason: 'only a seeded rank-
|
|
362
|
+
reason: 'only a seeded rank-rank has a catalog default to reset to; revoke a custom rank\'s grants instead',
|
|
363
363
|
});
|
|
364
364
|
}
|
|
365
365
|
// The seed is the floor derivation, which never contains a principal key and
|
|
366
366
|
// — below archon — never a system one. Filtering by isGrantableTo anyway keeps
|
|
367
367
|
// reset from being a back door that writes what the toggle refuses.
|
|
368
|
-
const keys = catalog.seedForRank(
|
|
369
|
-
const count = await db.
|
|
370
|
-
res.json({ ok: true, rank_key: rankKey, seeded_rank:
|
|
368
|
+
const keys = catalog.seedForRank(rank.seeded_rank).filter((k) => catalog.isGrantableTo(k, rank));
|
|
369
|
+
const count = await db.resetRankGrants({ rankKey, permissionKeys: keys });
|
|
370
|
+
res.json({ ok: true, rank_key: rankKey, seeded_rank: rank.seeded_rank, permission_count: count });
|
|
371
371
|
} catch (err) {
|
|
372
372
|
console.error('[gds] POST /government/ranks/:rankKey/reset', err);
|
|
373
373
|
res.fail('reset_failed', { status: 500, message: 'internal error' });
|
|
374
374
|
}
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
-
// rank: perm government.manage — assign a CUSTOM
|
|
377
|
+
// rank: perm government.manage — assign a CUSTOM rank to a builder.
|
|
378
378
|
// assigned_by is ALWAYS the session builder, never input (the audit trail's
|
|
379
379
|
// "who did this" must be the authenticated actor).
|
|
380
380
|
router.post('/government/assignments', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
@@ -387,9 +387,9 @@ module.exports = function buildGovernanceRouter() {
|
|
|
387
387
|
return res.fail('bad_rank_key', 400, { rank_key: rankKey, expected: 'a kebab-case slug (^[a-z][a-z0-9-]*$)' });
|
|
388
388
|
}
|
|
389
389
|
try {
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
const assigned = await db.
|
|
390
|
+
const rank = await loadAssignableRank(res, rankKey);
|
|
391
|
+
if (rank === null) return;
|
|
392
|
+
const assigned = await db.assignRank({
|
|
393
393
|
builderId: req.body.builder_id,
|
|
394
394
|
rankKey,
|
|
395
395
|
assignedBy: req.builder.id,
|
|
@@ -404,7 +404,7 @@ module.exports = function buildGovernanceRouter() {
|
|
|
404
404
|
}
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
-
// rank: perm government.manage — remove a custom-
|
|
407
|
+
// rank: perm government.manage — remove a custom-rank assignment. Idempotent:
|
|
408
408
|
// `removed` says whether a row went away.
|
|
409
409
|
router.delete('/government/assignments/:builderId/:rankKey', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
410
410
|
const builderId = parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
|
|
@@ -412,9 +412,9 @@ module.exports = function buildGovernanceRouter() {
|
|
|
412
412
|
const rankKey = rankKeyParam(req, res);
|
|
413
413
|
if (rankKey === null) return;
|
|
414
414
|
try {
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
417
|
-
const removed = await db.
|
|
415
|
+
const rank = await loadAssignableRank(res, rankKey);
|
|
416
|
+
if (rank === null) return;
|
|
417
|
+
const removed = await db.unassignRank({ builderId, rankKey });
|
|
418
418
|
res.json({ ok: true, builder_id: builderId, rank_key: rankKey, removed });
|
|
419
419
|
} catch (err) {
|
|
420
420
|
console.error('[gds] DELETE /government/assignments/:builderId/:rankKey', err);
|
|
@@ -425,4 +425,4 @@ module.exports = function buildGovernanceRouter() {
|
|
|
425
425
|
return router;
|
|
426
426
|
};
|
|
427
427
|
|
|
428
|
-
module.exports.
|
|
428
|
+
module.exports.governmentPort = governmentPort;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// The
|
|
3
|
+
// The government module's rank SYNC port (ADR 0151, BV1.R95b).
|
|
4
4
|
//
|
|
5
5
|
// A second, deliberately tiny kernel port beside `government` (routes/government.js).
|
|
6
|
-
// `
|
|
6
|
+
// `government` is the READ surface every gate resolves on every request; this is the
|
|
7
7
|
// single WRITE core needs — "this builder's rank is now X, make their seeded
|
|
8
|
-
// rank
|
|
8
|
+
// rank match". They are kept apart on purpose:
|
|
9
9
|
// • the read port is on the hot path of every permission-gated request and is
|
|
10
10
|
// resolved by requirePermission(); a mutation has no business hanging off it;
|
|
11
11
|
// • this one is resolved by exactly three writers (the two _applyRankChangeTx
|
|
12
12
|
// twins + the sign-in path) and is a TRANSACTION PARTICIPANT — it takes the
|
|
13
|
-
// caller's client so the
|
|
13
|
+
// caller's client so the rank rotation commits or rolls back with the rank write.
|
|
14
14
|
// The port name is namespaced (`government.rankSync`) like the `ideas.capture` /
|
|
15
15
|
// `art.getBuilderGeminiKeyRow` / `grade.builderRollingAvgGrade` ports.
|
|
16
16
|
//
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
//
|
|
22
22
|
// The router is EMPTY and must stay empty. Rank-derived assignment is automatic and
|
|
23
23
|
// has no HTTP surface; the MANUAL assignment CRUD is R110's Archon-gated tab on the
|
|
24
|
-
//
|
|
24
|
+
// government router. An endpoint here would be a way to hand out a rank without
|
|
25
25
|
// changing the rank.
|
|
26
26
|
|
|
27
27
|
const express = require('express');
|
|
@@ -41,7 +41,7 @@ function registerSeams() {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
module.exports = function
|
|
44
|
+
module.exports = function buildGovernmentRankSyncRouter() {
|
|
45
45
|
registerSeams();
|
|
46
46
|
return express.Router();
|
|
47
47
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// modules/
|
|
3
|
+
// modules/government/session-scopes.js — the SESSION-SCOPE axis (ADR 0151 §3, BV1.R99).
|
|
4
4
|
//
|
|
5
5
|
// The FOURTH axis, and the only one attached to the TOKEN rather than to the
|
|
6
|
-
// builder.
|
|
6
|
+
// builder. Ranks say what a person may do (R95); a principal's identity says what
|
|
7
7
|
// a machine may do (R98); an ownership scope says whose row it may be done to
|
|
8
8
|
// (R100); a SESSION-SCOPE says how much of that authority this particular
|
|
9
9
|
// credential carries. A dev-box session is minted onto a machine OUTSIDE the ADR
|
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
// A SESSION-SCOPE CAN ONLY EVER NARROW. That is the whole load-bearing property,
|
|
14
14
|
// and it has two halves that fail in opposite directions:
|
|
15
15
|
// • INTERSECTION, never union — the scope may not GRANT a permission the
|
|
16
|
-
// builder's
|
|
16
|
+
// builder's ranks do not already confer, or a box token would escalate;
|
|
17
17
|
// • DENY-BY-DEFAULT — a permission the allow-list does not name is denied, or an
|
|
18
18
|
// unlisted permission leaks through.
|
|
19
19
|
// resolver.narrowToSessionScope() is the one place the two compose, and it is
|
|
20
|
-
// written as a FILTER of the
|
|
20
|
+
// written as a FILTER of the rank answer so widening is not expressible there.
|
|
21
21
|
//
|
|
22
22
|
// EXPLICIT DATA, not `if (source === 'box')`. session-scopes.json (next to this
|
|
23
23
|
// file) is the declaration; this module validates it against the permission
|
|
24
|
-
// catalog at load.
|
|
24
|
+
// catalog at load. GOVERNMENT OWNS THE POLICY, THE KERNEL STAYS THE ENFORCER —
|
|
25
25
|
// the same split R101 draws for the protected-surface registry: src/bongos/auth.js
|
|
26
26
|
// reads the same JSON as DATA (fs, never `require` — ADR 0083's one-way rule) and
|
|
27
27
|
// requireBuilder remains the gate every route composes. Nothing here enforces
|
|
@@ -64,10 +64,10 @@ function validateAgainstCatalog(scopes) {
|
|
|
64
64
|
throw new Error(`${where} allows "${key}", which the permission catalog does not define`);
|
|
65
65
|
}
|
|
66
66
|
// R98's rule, kept structural: a machine principal's capability comes from
|
|
67
|
-
// its exact system_role, never from a
|
|
67
|
+
// its exact system_role, never from a rank — so a box-scoped session
|
|
68
68
|
// carrying one must not be expressible on a scope allow-list either.
|
|
69
69
|
if (catalog.isPrincipalKey(key)) {
|
|
70
|
-
throw new Error(`${where} allows the machine-principal permission "${key}" — a session-scope narrows a builder's
|
|
70
|
+
throw new Error(`${where} allows the machine-principal permission "${key}" — a session-scope narrows a builder's RANK authority and never composes with the identity axis`);
|
|
71
71
|
}
|
|
72
72
|
// A session-scope IS the surface a credential outside the ADR 0016 trust
|
|
73
73
|
// boundary may reach. A trust-boundary permission must never appear on one,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": [
|
|
3
3
|
"The SESSION-SCOPE registry (BV1.R99, ADR 0151 §3; the model behind ADR 0053).",
|
|
4
|
-
"A session's scope is capability attached to the TOKEN, orthogonal to the builder's
|
|
4
|
+
"A session's scope is capability attached to the TOKEN, orthogonal to the builder's ranks.",
|
|
5
5
|
"Each row declares EITHER unscoped:true (the session narrows nothing) OR a deny-by-default",
|
|
6
6
|
"permissions allow-list (unlisted => denied). A scope can only ever NARROW: effective",
|
|
7
|
-
"authority is (the builder's
|
|
8
|
-
"Every key here is validated against modules/
|
|
9
|
-
"modules/
|
|
7
|
+
"authority is (the builder's rank permissions) INTERSECT (this allow-list), never a union.",
|
|
8
|
+
"Every key here is validated against modules/government/catalog.js at load by",
|
|
9
|
+
"modules/government/session-scopes.js — unknown, system:true and principal keys are refused.",
|
|
10
10
|
"Read as DATA by src/bongos/auth.js (fs, never require — ADR 0083's one-way rule)."
|
|
11
11
|
],
|
|
12
12
|
"scopes": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"source": "cli",
|
|
15
15
|
"label": "CLI session",
|
|
16
16
|
"unscoped": true,
|
|
17
|
-
"note": "Device Flow token on a builder's own machine — inside the trust boundary, carries the builder's full
|
|
17
|
+
"note": "Device Flow token on a builder's own machine — inside the trust boundary, carries the builder's full rank authority."
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"source": "web",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* government.css — Page-local styles for the Government page (ranks /
|
|
2
2
|
permissions / assignments — BV1.R111, ADR 0151). Built on the shared design
|
|
3
3
|
tokens (style.css), mirroring gate.css's card + pill idiom; the ?v= cache-bust
|
|
4
4
|
is stamped from this file's content hash at boot (src/asset-version.js). */
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
74
74
|
<script src="/builders/palette.js" defer></script>
|
|
75
75
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
76
|
-
<script src="/builders/government.js?v=2026-08-
|
|
76
|
+
<script src="/builders/government.js?v=2026-08-16-government"></script>
|
|
77
77
|
</body>
|
|
78
78
|
</html>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// web sign-in.
|
|
27
27
|
//
|
|
28
28
|
// DELIBERATELY NOT IN THE SIDEBAR YET: the nav entry + the server page gate are
|
|
29
|
-
// R113 (the next slice) — tests/hall_nav.mjs pins the
|
|
29
|
+
// R113 (the next slice) — tests/hall_nav.mjs pins the Government nav group and
|
|
30
30
|
// grows with that slice, not this one. Until then the page is reached by URL.
|
|
31
31
|
|
|
32
32
|
(() => {
|
|
@@ -402,7 +402,7 @@
|
|
|
402
402
|
|
|
403
403
|
async function init() {
|
|
404
404
|
// Confirm rank for a friendly sealed notice; the server gates the page and
|
|
405
|
-
// every /
|
|
405
|
+
// every /government endpoint on `government.manage` regardless (cosmetic
|
|
406
406
|
// defence-in-depth). government.manage is system:true, so only the archon
|
|
407
407
|
// rank can hold it — rank is an accurate client-side proxy.
|
|
408
408
|
let me;
|
|
@@ -424,7 +424,7 @@
|
|
|
424
424
|
} catch (err) {
|
|
425
425
|
if (err && err.kind === '401') return;
|
|
426
426
|
if (err && err.kind === '403') { showSealed(); return; }
|
|
427
|
-
document.getElementById('gov-grid').innerHTML = `<p class="gov-empty">Could not load
|
|
427
|
+
document.getElementById('gov-grid').innerHTML = `<p class="gov-empty">Could not load government data just now.</p>`;
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// separate, consecutive gates — see claimTask / claimTasksBatch /
|
|
22
22
|
// validateClaimBatch.
|
|
23
23
|
//
|
|
24
|
-
// Pure + DB-free so tests/
|
|
24
|
+
// Pure + DB-free so tests/government_claim_eligibility.mjs can exercise every
|
|
25
25
|
// rank × permission × requires_rank combination without Postgres.
|
|
26
26
|
|
|
27
27
|
// The two catalog keys this axis reads (modules/governance/catalog.js). Exported
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -197,7 +197,7 @@ const GENERAL_QUEUE_RANKS = new Set(LIVE_RANK_LADDER.filter((r) => r !== 'xenos'
|
|
|
197
197
|
// `task.claim.newcomer` (claim-eligibility.js) and keep this as the fallback for a
|
|
198
198
|
// builder whose role row the run-once compat backfill never created. It stays
|
|
199
199
|
// exported: tests/rank_gate.mjs pins it, and it is the oracle the parity test in
|
|
200
|
-
// tests/
|
|
200
|
+
// tests/government_claim_eligibility.mjs compares the permission predicate against.
|
|
201
201
|
|
|
202
202
|
function xenosClaimAllowed(rank, newcomerFriendly) {
|
|
203
203
|
if (GENERAL_QUEUE_RANKS.has(rank)) return true;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.187",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.187",
|
|
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.
|
|
3
|
+
"version": "1.19.187",
|
|
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",
|