@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,44 +1,44 @@
|
|
|
1
|
-
// modules/
|
|
1
|
+
// modules/government/catalog.js — the permission catalog + compatibility seed.
|
|
2
2
|
//
|
|
3
3
|
// The single CODE source of truth for authority atoms (ADR 0151, BV1.R94). Every
|
|
4
4
|
// permission is a stable string key that maps to a REAL server capability (a route,
|
|
5
5
|
// a claim rule, a page) — a permission no route consumes does not exist. The
|
|
6
|
-
// resolver (R95), the requirePermission gate (R97), R93's
|
|
6
|
+
// resolver (R95), the requirePermission gate (R97), R93's rank seed, and the Archon
|
|
7
7
|
// tab (R110/R111) all read from here; the enumerated catalog it transcribes is
|
|
8
8
|
// docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md, grounded in
|
|
9
9
|
// docs/governance/authority-sweep-2026-07-24.md.
|
|
10
10
|
//
|
|
11
11
|
// Two orthogonal axes per permission:
|
|
12
|
-
// • system — true = trust-boundary. The
|
|
13
|
-
// to a CUSTOM
|
|
12
|
+
// • system — true = trust-boundary. The government API (R110) rejects adding it
|
|
13
|
+
// to a CUSTOM rank and the tab renders it LOCKED there; it is deploy/
|
|
14
14
|
// protected-path/rank-set/version-close/grade-bypass/cross-tier/
|
|
15
15
|
// principal power, enforced by the out-of-band substrate (ADR 0043)
|
|
16
|
-
// or reserved to Archon. The built-in `archon` rank
|
|
16
|
+
// or reserved to Archon. The built-in `archon` rank legitimately
|
|
17
17
|
// HOLDS the archon-floored system perms.
|
|
18
|
-
// — false = operational, freely composable into a custom
|
|
18
|
+
// — false = operational, freely composable into a custom rank.
|
|
19
19
|
// `system` is NOT the whole grantability answer any more — see the
|
|
20
|
-
// `substrate` axis below and `isGrantableTo()`, which is per-
|
|
20
|
+
// `substrate` axis below and `isGrantableTo()`, which is per-RANK.
|
|
21
21
|
// • substrate — true = this key's authority is enforced OUTSIDE the app, by the
|
|
22
22
|
// pre-push hook / main-audit / the rank floor the git and ssh layers
|
|
23
23
|
// read (ADR 0043). A grant row for one is INERT: no route consumes it
|
|
24
24
|
// through requirePermission, so granting it would be a toggle that
|
|
25
25
|
// looks like it worked and changes nothing. Therefore non-grantable to
|
|
26
|
-
// EVERY
|
|
26
|
+
// EVERY rank, seeded ranks included — the one wall ADR 0172's
|
|
27
27
|
// widening deliberately did not move, because moving it would make the
|
|
28
28
|
// tab lie rather than delegate. Implies system:true + floor 'archon'.
|
|
29
29
|
// • floor — the rank that holds this permission TODAY via the compat seed
|
|
30
30
|
// ('xenos' < 'thetes' < 'metic' < 'archon'), or null for a machine
|
|
31
|
-
// PRINCIPAL permission held by NO rank
|
|
32
|
-
// system_role, never a human/
|
|
31
|
+
// PRINCIPAL permission held by NO rank (gated on exact
|
|
32
|
+
// system_role, never a human/rank — ADR 0151 §3). floor drives the
|
|
33
33
|
// seed derivation below; it is seed DATA, not grantability (the
|
|
34
34
|
// `system` flag is the wall). Ownership-scoped + baseline own-work
|
|
35
35
|
// perms floor at 'xenos' (every authenticated builder holds them).
|
|
36
36
|
//
|
|
37
37
|
// PRINCIPALS — the second authority AXIS (BV1.R98, ADR 0151 §3). A builder's
|
|
38
|
-
// authority is the UNION of their
|
|
38
|
+
// authority is the UNION of their RANKS; a machine principal's authority is its
|
|
39
39
|
// IDENTITY. Those are different axes and the model keeps them apart: a principal
|
|
40
40
|
// permission carries `principal: '<system_role>'` and `floor: null`, so it is in
|
|
41
|
-
// NO rank
|
|
41
|
+
// NO rank (not even archon's) and no custom rank may hold it (`system:true`).
|
|
42
42
|
// The two are a PARTITION, asserted at load: floor:null ⟺ principal set. The only
|
|
43
43
|
// source of a principal permission is resolver.resolvePrincipalPermissions(), keyed
|
|
44
44
|
// on the exact `builders.system_role` value — a column with no API writer at all
|
|
@@ -77,10 +77,10 @@
|
|
|
77
77
|
// Archon included), and minting a key no route consumes to satisfy a symmetry
|
|
78
78
|
// check would violate this module's first rule.
|
|
79
79
|
//
|
|
80
|
-
// The
|
|
80
|
+
// The RANK_SEED is DERIVED from floors (cumulative nested supersets), so the
|
|
81
81
|
// "zero day-one behavior change" parity (R103) is structural, not hand-maintained:
|
|
82
|
-
//
|
|
83
|
-
// resolver's UNION over a builder's one rank
|
|
82
|
+
// rank(rank) = { p : p.floor !== null and tier(p.floor) <= tier(rank) }, and the
|
|
83
|
+
// resolver's UNION over a builder's one rank reproduces requireRank's
|
|
84
84
|
// at-or-above threshold exactly.
|
|
85
85
|
|
|
86
86
|
'use strict';
|
|
@@ -96,7 +96,7 @@ const PERMISSIONS = [
|
|
|
96
96
|
// ── system: true — trust-boundary (substrate- or Archon-enforced) ─────────────
|
|
97
97
|
//
|
|
98
98
|
// The seven WITHOUT `substrate` are in-app Archon powers: a real route reads them
|
|
99
|
-
// through requirePermission, so granting one to a sub-Archon rank
|
|
99
|
+
// through requirePermission, so granting one to a sub-Archon rank genuinely
|
|
100
100
|
// delegates it (ADR 0172, the owner's lever). The three WITH `substrate: true` are
|
|
101
101
|
// enforced out of band and can never be delegated by a row in this database.
|
|
102
102
|
{ key: 'builder.rank.set', system: true, floor: 'archon', guards: 'PATCH /builders/:id/rank — the single rank writer' },
|
|
@@ -112,7 +112,7 @@ const PERMISSIONS = [
|
|
|
112
112
|
{ key: 'principal.bfg', system: true, floor: null, principal: 'bfg', guards: '/memory/bfg/* — requireBfgPrincipal (exact system_role=bfg; NO rank holds it)' },
|
|
113
113
|
{ key: 'principal.discord_bot', system: true, floor: null, principal: 'discord_bot', guards: 'Discord bot routes — requireDiscordBot (exact system_role; dormant)' },
|
|
114
114
|
|
|
115
|
-
// ── operational — floor archon (admin ops; grantable to a custom
|
|
115
|
+
// ── operational — floor archon (admin ops; grantable to a custom rank) ────────
|
|
116
116
|
//
|
|
117
117
|
// ADR 0157 (owner decision, 2026-08-05) narrowed this block to IDENTITY: the Archon
|
|
118
118
|
// rank is responsible for rank and identity only. Nineteen keys that sat here for
|
|
@@ -166,15 +166,15 @@ const PERMISSIONS = [
|
|
|
166
166
|
// and operations: they spend, read, adjudicate, and shuffle work, but none of them
|
|
167
167
|
// decides WHO HOLDS AUTHORITY, so the trusted working rank carries them. All were
|
|
168
168
|
// floor 'archon' before this ADR; none is system:true, so all were already
|
|
169
|
-
// delegable to a custom
|
|
169
|
+
// delegable to a custom rank — this only lowers the seeded floor.
|
|
170
170
|
//
|
|
171
171
|
// Deliberately NOT in this block, though the owner's rule would otherwise reach them:
|
|
172
172
|
// • `version.close` — retained at archon; it is the DISPOSITION that finalises a
|
|
173
173
|
// version's scope-truth, and canonical-permissions has long paired it with rank
|
|
174
174
|
// as the two powers that never delegate. ADR 0157 keeps that pairing.
|
|
175
175
|
// • `dependency.cross_tier.manage` — system:true, and the catalog's own invariant
|
|
176
|
-
// (tests/
|
|
177
|
-
// it would mean clearing the system flag, which widens what any CUSTOM
|
|
176
|
+
// (tests/government_catalog.mjs) forbids granting a system key to metic. Moving
|
|
177
|
+
// it would mean clearing the system flag, which widens what any CUSTOM rank can
|
|
178
178
|
// be granted — a second-order change beyond the delegation that was asked for.
|
|
179
179
|
{ key: 'builder.budget.set', system: false, floor: 'metic', guards: 'PATCH /builders/:id/budget (ADR 0157, was archon)' },
|
|
180
180
|
{ key: 'criterion.unsatisfy', system: false, floor: 'metic', guards: 'POST /done-when/:id/unsatisfy (ADR 0157, was archon)' },
|
|
@@ -185,7 +185,7 @@ const PERMISSIONS = [
|
|
|
185
185
|
// siblings are `goal.create` and `task.create`, both operational. Close is the
|
|
186
186
|
// DISPOSITION that finalises a version's scope-truth; create opens it — the symmetry
|
|
187
187
|
// argument was considered and rejected, because system:true would permanently bar
|
|
188
|
-
// delegating version authoring to a custom
|
|
188
|
+
// delegating version authoring to a custom rank for no trust-boundary reason.
|
|
189
189
|
// ADR 0157 then lowered create's floor to metic while close stayed archon, which is
|
|
190
190
|
// the same asymmetry read from the other side: authoring delegates, disposition does not.
|
|
191
191
|
{ key: 'version.create', system: false, floor: 'metic', guards: 'POST /versions — create a version + its done-when criteria (ADR 0157, was archon)' },
|
|
@@ -237,7 +237,7 @@ const PERMISSIONS = [
|
|
|
237
237
|
// working rank. The gate FUNCTION was renamed to match (`requireArchonPage` →
|
|
238
238
|
// `requireGovernmentPage`) once the floor moved. Viewing is all this grants: every
|
|
239
239
|
// write those pages reach is its own atom, and `government.manage` (the
|
|
240
|
-
//
|
|
240
|
+
// ranks/permissions tab) stayed archon.
|
|
241
241
|
{ key: 'page.view.government', system: false, floor: 'metic', guards: 'the government/monitoring pages — /watch, /harbor, /gate (requireGovernmentPage; write = government.manage, archon)' },
|
|
242
242
|
];
|
|
243
243
|
|
|
@@ -249,70 +249,70 @@ const _byKey = new Map();
|
|
|
249
249
|
const _byPrincipal = new Map();
|
|
250
250
|
const _byResource = new Map();
|
|
251
251
|
for (const p of PERMISSIONS) {
|
|
252
|
-
if (typeof p.key !== 'string' || !p.key) throw new Error(`
|
|
253
|
-
if (typeof p.system !== 'boolean') throw new Error(`
|
|
254
|
-
if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`
|
|
255
|
-
if (_byKey.has(p.key)) throw new Error(`
|
|
252
|
+
if (typeof p.key !== 'string' || !p.key) throw new Error(`government catalog: permission missing a key`);
|
|
253
|
+
if (typeof p.system !== 'boolean') throw new Error(`government catalog: ${p.key} missing boolean 'system'`);
|
|
254
|
+
if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`government catalog: ${p.key} has invalid floor "${p.floor}"`);
|
|
255
|
+
if (_byKey.has(p.key)) throw new Error(`government catalog: duplicate key "${p.key}"`);
|
|
256
256
|
const principal = p.principal ?? null;
|
|
257
257
|
if (principal !== null && (typeof principal !== 'string' || !principal)) {
|
|
258
|
-
throw new Error(`
|
|
258
|
+
throw new Error(`government catalog: ${p.key} has a non-string principal identity`);
|
|
259
259
|
}
|
|
260
|
-
// The two axes PARTITION the catalog: a permission is held by a rank
|
|
260
|
+
// The two axes PARTITION the catalog: a permission is held by a rank (a real
|
|
261
261
|
// floor) XOR by a machine identity (floor null + a principal). Neither-nor would be
|
|
262
|
-
// a permission nothing can ever hold; both-and would let a
|
|
262
|
+
// a permission nothing can ever hold; both-and would let a rank reach an identity.
|
|
263
263
|
if ((p.floor === null) !== (principal !== null)) {
|
|
264
|
-
throw new Error(`
|
|
264
|
+
throw new Error(`government catalog: ${p.key} — floor:null and a principal identity must come together (ranks and identities are separate axes)`);
|
|
265
265
|
}
|
|
266
266
|
if (p.key.startsWith(PRINCIPAL_KEY_PREFIX) !== (principal !== null)) {
|
|
267
|
-
throw new Error(`
|
|
267
|
+
throw new Error(`government catalog: ${p.key} — a "${PRINCIPAL_KEY_PREFIX}*" key and a principal identity must come together`);
|
|
268
268
|
}
|
|
269
269
|
if (principal !== null) {
|
|
270
|
-
if (!p.system) throw new Error(`
|
|
271
|
-
if (_byPrincipal.has(principal)) throw new Error(`
|
|
270
|
+
if (!p.system) throw new Error(`government catalog: ${p.key} is a principal permission and must be system:true (never grantable to a rank)`);
|
|
271
|
+
if (_byPrincipal.has(principal)) throw new Error(`government catalog: duplicate principal identity "${principal}"`);
|
|
272
272
|
}
|
|
273
273
|
// ── the SUBSTRATE axis (ADR 0172) ─────────────────────────────────────────
|
|
274
274
|
// A substrate key is enforced by the out-of-band layer (pre-push hook, main-audit,
|
|
275
|
-
// the rank floor git/ssh read), so no
|
|
275
|
+
// the rank floor git/ssh read), so no rank row can confer it. Three shape rules,
|
|
276
276
|
// each closing a way the marker could quietly stop meaning that:
|
|
277
277
|
const substrate = p.substrate ?? false;
|
|
278
|
-
if (typeof substrate !== 'boolean') throw new Error(`
|
|
278
|
+
if (typeof substrate !== 'boolean') throw new Error(`government catalog: ${p.key} has a non-boolean 'substrate'`);
|
|
279
279
|
if (substrate) {
|
|
280
280
|
// Substrate power is trust-boundary power by definition — a substrate key that
|
|
281
|
-
// was somehow operational would be freely grantable to a custom
|
|
282
|
-
if (!p.system) throw new Error(`
|
|
281
|
+
// was somehow operational would be freely grantable to a custom rank.
|
|
282
|
+
if (!p.system) throw new Error(`government catalog: ${p.key} is substrate-enforced and must be system:true`);
|
|
283
283
|
// A substrate key belongs to no machine identity: the two are different reasons
|
|
284
284
|
// a key is unreachable, and collapsing them would hide one behind the other.
|
|
285
|
-
if (principal !== null) throw new Error(`
|
|
285
|
+
if (principal !== null) throw new Error(`government catalog: ${p.key} cannot be both substrate-enforced and a machine principal`);
|
|
286
286
|
// The out-of-band enforcers all compare against the top of the ladder. A lower
|
|
287
|
-
// floor would seed the key into a rank
|
|
288
|
-
if (p.floor !== 'archon') throw new Error(`
|
|
287
|
+
// floor would seed the key into a rank that the substrate then still denies.
|
|
288
|
+
if (p.floor !== 'archon') throw new Error(`government catalog: ${p.key} is substrate-enforced and must floor at 'archon' (the substrate compares against the top of the ladder)`);
|
|
289
289
|
}
|
|
290
290
|
// ── the OWNERSHIP axis (R100) ─────────────────────────────────────────────
|
|
291
291
|
const scope = p.scope ?? null;
|
|
292
292
|
const resource = p.resource ?? null;
|
|
293
293
|
if (scope !== null && !OWNERSHIP_SCOPES.includes(scope)) {
|
|
294
|
-
throw new Error(`
|
|
294
|
+
throw new Error(`government catalog: ${p.key} has invalid scope "${scope}" (expected 'own', 'any', or absent)`);
|
|
295
295
|
}
|
|
296
296
|
// An ownership SCOPE and the RESOURCE it is scoped over are one fact spelled in
|
|
297
297
|
// two fields; either alone is meaningless ('own' of what? / a resource nothing
|
|
298
298
|
// is scoped over). Same partition posture as floor ⟺ principal above.
|
|
299
299
|
if ((scope === null) !== (resource === null)) {
|
|
300
|
-
throw new Error(`
|
|
300
|
+
throw new Error(`government catalog: ${p.key} — scope and resource must come together (an ownership scope is always scoped over a named resource)`);
|
|
301
301
|
}
|
|
302
302
|
if (resource !== null && (typeof resource !== 'string' || !resource)) {
|
|
303
|
-
throw new Error(`
|
|
303
|
+
throw new Error(`government catalog: ${p.key} has a non-string resource family`);
|
|
304
304
|
}
|
|
305
305
|
// The namespace can never outrun the data: a key that CALLS itself own-scoped
|
|
306
306
|
// must BE declared own-scoped, or a gate reading the declared axis would treat a
|
|
307
307
|
// parameterized permission as an unconditional grant. Deliberately one-way — the
|
|
308
308
|
// converse would be false (me.wandering.set is own-scoped without the suffix).
|
|
309
309
|
if (p.key.endsWith(OWN_KEY_SUFFIX) && scope !== 'own') {
|
|
310
|
-
throw new Error(`
|
|
310
|
+
throw new Error(`government catalog: ${p.key} ends in "${OWN_KEY_SUFFIX}" but does not declare scope:'own' — an ownership-scoped key must say so in data`);
|
|
311
311
|
}
|
|
312
|
-
// An ownership scope NARROWS a
|
|
312
|
+
// An ownership scope NARROWS a rank-held permission; a machine identity has no
|
|
313
313
|
// owner axis, so the two never compose.
|
|
314
314
|
if (scope !== null && p.floor === null) {
|
|
315
|
-
throw new Error(`
|
|
315
|
+
throw new Error(`government catalog: ${p.key} is ownership-scoped but has no rank floor — the ownership axis narrows a RANK permission, never an identity one`);
|
|
316
316
|
}
|
|
317
317
|
const frozen = Object.freeze({ ...p, principal, scope, resource, substrate });
|
|
318
318
|
_byKey.set(p.key, frozen);
|
|
@@ -332,15 +332,15 @@ Object.freeze(PERMISSIONS);
|
|
|
332
332
|
const floorTier = (rank) => RANK_ORDER.indexOf(rank);
|
|
333
333
|
for (const [resource, fam] of _byResource) {
|
|
334
334
|
if (fam.any.length > 0 && fam.own.length === 0) {
|
|
335
|
-
throw new Error(`
|
|
335
|
+
throw new Error(`government catalog: resource "${resource}" declares a cross-owner permission (${fam.any.join(', ')}) with no ownership-scoped counterpart`);
|
|
336
336
|
}
|
|
337
337
|
// Reaching EVERY owner's row must never be cheaper than reaching your own. If it
|
|
338
338
|
// were, a rank could manage the whole fleet but not its own box — the model
|
|
339
|
-
// inverted, and the
|
|
339
|
+
// inverted, and the Government tab would render the wider grant as the safer one.
|
|
340
340
|
for (const anyKey of fam.any) {
|
|
341
341
|
for (const ownKey of fam.own) {
|
|
342
342
|
if (floorTier(_byKey.get(anyKey).floor) < floorTier(_byKey.get(ownKey).floor)) {
|
|
343
|
-
throw new Error(`
|
|
343
|
+
throw new Error(`government catalog: ${anyKey} (floor ${_byKey.get(anyKey).floor}) sits BELOW ${ownKey} (floor ${_byKey.get(ownKey).floor}) — cross-owner authority must never be cheaper than own-only`);
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
}
|
|
@@ -382,9 +382,9 @@ function isSystem(key) {
|
|
|
382
382
|
const p = _byKey.get(key);
|
|
383
383
|
return p ? p.system : null; // null = unknown key (caller fails closed)
|
|
384
384
|
}
|
|
385
|
-
// Grantable to a CUSTOM
|
|
386
|
-
// This is the NARROW question — the custom-
|
|
387
|
-
// still what config.js strips against. For the per-
|
|
385
|
+
// Grantable to a CUSTOM rank = exists and not system. Unknown key → false (fail-closed).
|
|
386
|
+
// This is the NARROW question — the custom-rank wall, unchanged by ADR 0172 and
|
|
387
|
+
// still what config.js strips against. For the per-rank answer use isGrantableTo().
|
|
388
388
|
function isGrantable(key) {
|
|
389
389
|
const p = _byKey.get(key);
|
|
390
390
|
return !!p && !p.system;
|
|
@@ -406,40 +406,40 @@ function substrateKeys() {
|
|
|
406
406
|
return PERMISSIONS.filter((p) => p.substrate).map((p) => p.key);
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
// ── grantability, per
|
|
409
|
+
// ── grantability, per RANK (ADR 0172) ──────────────────────────────────────────
|
|
410
410
|
//
|
|
411
|
-
// The owner's 2026-08-16 decision made the three sub-Archon RANK
|
|
412
|
-
// "is this grantable" stopped being a property of the key alone. `
|
|
411
|
+
// The owner's 2026-08-16 decision made the three sub-Archon RANK ranks editable, so
|
|
412
|
+
// "is this grantable" stopped being a property of the key alone. `rank` is a
|
|
413
413
|
// government_ranks row ({ is_system, seeded_rank }); the four answers:
|
|
414
414
|
//
|
|
415
|
-
// • NEVER, to any
|
|
415
|
+
// • NEVER, to any rank — a machine-principal key (held by an identity, not a rank)
|
|
416
416
|
// and a SUBSTRATE key. The substrate three are inert as grant rows: no route
|
|
417
417
|
// consumes them through requirePermission, so a toggle for one would report
|
|
418
418
|
// success and delegate nothing. Refusing them keeps the tab honest.
|
|
419
|
-
// • the ARCHON rank
|
|
420
|
-
// and that immutability is also the LOCKOUT GUARD: the only
|
|
419
|
+
// • the ARCHON rank — nothing. It is immutable and already holds everything,
|
|
420
|
+
// and that immutability is also the LOCKOUT GUARD: the only rank 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
|
-
// • a SEEDED rank
|
|
423
|
+
// • a SEEDED rank BELOW archon — everything else, the seven in-app Archon
|
|
424
424
|
// powers included. This is the delegation lever the owner asked for.
|
|
425
|
-
// • a CUSTOM
|
|
426
|
-
// widened. A rank
|
|
427
|
-
// a custom
|
|
425
|
+
// • a CUSTOM rank — operational only: the pre-existing R110 wall, deliberately NOT
|
|
426
|
+
// widened. A rank is the owner's own coarse ladder, moved by a rank write;
|
|
427
|
+
// a custom rank 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
|
-
// Fail-closed on everything unrecognised: unknown key, missing
|
|
430
|
+
// Fail-closed on everything unrecognised: unknown key, missing rank, or a system rank
|
|
431
431
|
// whose seeded_rank is not a real rank all answer false.
|
|
432
|
-
function isGrantableTo(key,
|
|
432
|
+
function isGrantableTo(key, rank) {
|
|
433
433
|
const p = _byKey.get(key);
|
|
434
|
-
if (!p || !
|
|
434
|
+
if (!p || !rank || typeof rank !== 'object') return false;
|
|
435
435
|
if (p.principal !== null || p.substrate) return false;
|
|
436
|
-
if (
|
|
437
|
-
return RANK_ORDER.includes(
|
|
436
|
+
if (rank.is_system) {
|
|
437
|
+
return RANK_ORDER.includes(rank.seeded_rank) && rank.seeded_rank !== 'archon';
|
|
438
438
|
}
|
|
439
439
|
return !p.system;
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
-
// ── the principal axis (identity, not
|
|
442
|
+
// ── the principal axis (identity, not ranks) ────────────────────────────────────
|
|
443
443
|
// Reads only. Unknown/blank input fails closed to null/false — the same posture the
|
|
444
444
|
// rest of the catalog takes, and the one the identity gates depend on.
|
|
445
445
|
function principals() {
|
|
@@ -451,7 +451,7 @@ function principalRoles() {
|
|
|
451
451
|
function principalKeys() {
|
|
452
452
|
return PRINCIPALS.map((p) => p.permission);
|
|
453
453
|
}
|
|
454
|
-
// Is this key held by a machine identity rather than by any
|
|
454
|
+
// Is this key held by a machine identity rather than by any rank? Unknown → false.
|
|
455
455
|
function isPrincipalKey(key) {
|
|
456
456
|
const p = _byKey.get(key);
|
|
457
457
|
return !!p && p.principal !== null;
|
|
@@ -503,17 +503,17 @@ function crossOwnerKeysFor(key) {
|
|
|
503
503
|
return fam ? fam.any : NO_KEYS;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
// The compat seed, DERIVED from floors (cumulative supersets). A rank
|
|
506
|
+
// The compat seed, DERIVED from floors (cumulative supersets). A rank holds
|
|
507
507
|
// every permission whose floor is a real rank at-or-below it; principals (floor
|
|
508
|
-
// null) are held by NO rank
|
|
509
|
-
//
|
|
508
|
+
// null) are held by NO rank. This is the "zero day-one change" guarantee —
|
|
509
|
+
// rank(rank) reproduces requireRank(rank)'s at-or-above threshold exactly (R103).
|
|
510
510
|
function seedForRank(rank) {
|
|
511
511
|
const tier = RANK_ORDER.indexOf(rank);
|
|
512
|
-
if (tier < 0) return []; // unknown/reserved rank → no
|
|
512
|
+
if (tier < 0) return []; // unknown/reserved rank → no rank → no permissions (fail-closed; ADR 0151)
|
|
513
513
|
return PERMISSIONS.filter((p) => p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier).map((p) => p.key);
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
const
|
|
516
|
+
const RANK_SEED = Object.freeze(
|
|
517
517
|
Object.fromEntries(RANK_ORDER.map((r) => [r, Object.freeze(seedForRank(r))])),
|
|
518
518
|
);
|
|
519
519
|
|
|
@@ -530,7 +530,7 @@ module.exports = {
|
|
|
530
530
|
systemKeys,
|
|
531
531
|
grantableKeys,
|
|
532
532
|
seedForRank,
|
|
533
|
-
|
|
533
|
+
RANK_SEED,
|
|
534
534
|
PRINCIPALS,
|
|
535
535
|
principals,
|
|
536
536
|
principalRoles,
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// modules/
|
|
3
|
+
// modules/government/config.js — the government config contract (ADR 0151, BV1.R96).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Rank TEMPLATES and their display labels are per-instance CONFIG; live grants and
|
|
6
6
|
// assignments are DB rows. This file resolves the config half, mirroring
|
|
7
7
|
// src/branding.js precedence exactly:
|
|
8
8
|
//
|
|
9
9
|
// env override -> instance config (config/government.json) -> neutral
|
|
10
10
|
// starter (config/government.neutral.json)
|
|
11
11
|
//
|
|
12
|
-
// A fresh instance with NO config resolves to the PLAIN neutral
|
|
12
|
+
// A fresh instance with NO config resolves to the PLAIN neutral rank set — never to
|
|
13
13
|
// a host's labels (fail-to-vanilla, the same posture branding takes). R102 proves
|
|
14
14
|
// that boot.
|
|
15
15
|
//
|
|
16
16
|
// WHAT LIVES HERE vs WHAT LIVES IN THE DB — get this wrong and config becomes a
|
|
17
17
|
// second source of authority truth, which is exactly what ADR 0016 forbids:
|
|
18
|
-
// • config — TEMPLATES/DEFAULTS: a
|
|
19
|
-
// permission list a starting CUSTOM
|
|
18
|
+
// • config — TEMPLATES/DEFAULTS: a rank's display label, its description, and the
|
|
19
|
+
// permission list a starting CUSTOM rank template proposes. Shapes only.
|
|
20
20
|
// • DB — LIVE STATE: government_ranks / government_rank_permissions /
|
|
21
21
|
// government_builder_ranks (R93). WHO HOLDS WHAT is a row, resolved server-side
|
|
22
22
|
// per request by resolver.js. Nothing in this file grants anything to anybody.
|
|
@@ -25,21 +25,21 @@
|
|
|
25
25
|
// 1. A template naming a `system: true` catalog key is STRIPPED. An instance's
|
|
26
26
|
// config file is editable by anyone who can write the checkout, so it must never
|
|
27
27
|
// be able to hand out deploy / rank-set / protected-path power. catalog
|
|
28
|
-
// .isGrantable() is that wall — the same one the
|
|
28
|
+
// .isGrantable() is that wall — the same one the government API and the Archon
|
|
29
29
|
// tab use, so this cannot drift from the catalog.
|
|
30
30
|
// 2. A template naming a key the catalog does not define is DROPPED (the catalog is
|
|
31
31
|
// the source of truth for what a permission IS — resolver.js, same rule).
|
|
32
|
-
// 3. A `permissions` list on one of the four SEEDED
|
|
32
|
+
// 3. A `permissions` list on one of the four SEEDED ranks is DROPPED wholesale.
|
|
33
33
|
// Their grants are DERIVED from catalog floors and materialised by
|
|
34
|
-
// governance_002; config may relabel a seeded
|
|
34
|
+
// governance_002; config may relabel a seeded rank, never re-grant it.
|
|
35
35
|
//
|
|
36
36
|
// Stripping rather than throwing matches resolver.js: a typo in a config file must
|
|
37
37
|
// fail CLOSED, not brick the boot.
|
|
38
38
|
//
|
|
39
39
|
// SLICE BOUNDARY: this file is the config CONTRACT, and deliberately has no production
|
|
40
|
-
// caller yet — R96 is the "what shape is a
|
|
40
|
+
// caller yet — R96 is the "what shape is a rank template, and what can config never
|
|
41
41
|
// do" decision, landed before the surfaces that read it. R110/R111 (the Archon
|
|
42
|
-
//
|
|
42
|
+
// Government tab) is the first intended consumer, and R102 (the vanilla-boot proof)
|
|
43
43
|
// asserts against the neutral starter. Wiring a speculative caller now would guess at
|
|
44
44
|
// their needs; the tests pin the contract until they land.
|
|
45
45
|
|
|
@@ -62,7 +62,7 @@ const SEEDED_RANK_ROLES = new Set(catalog.RANK_ORDER);
|
|
|
62
62
|
|
|
63
63
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
64
64
|
|
|
65
|
-
// Instance
|
|
65
|
+
// Instance ranks merge over neutral PER RANK KEY and per field, so an instance can
|
|
66
66
|
// relabel `metic` without restating the other three. The shape is exactly two levels
|
|
67
67
|
// deep, so this is a targeted merge rather than branding's generic deepMerge.
|
|
68
68
|
function mergeRoles(neutral, instance) {
|
|
@@ -80,13 +80,18 @@ function mergeRoles(neutral, instance) {
|
|
|
80
80
|
// widen authority, so there is no env knob that adds a permission. It exists for the
|
|
81
81
|
// co-tenant case branding's AUTH_IDP override exists for — several hosted instances
|
|
82
82
|
// sharing the neutral pack, differentiated only by env, with no config file of their
|
|
83
|
-
// own. Format: "<PREFIX>
|
|
84
|
-
// only RELABEL a
|
|
85
|
-
// inert, rather than quietly growing the
|
|
86
|
-
function applyLabelOverrides(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
// own. Format: "<PREFIX>_GOVERNMENT_RANK_LABELS=metic=Trusted,archon=Owner". It can
|
|
84
|
+
// only RELABEL a rank the merged config already defines — a typo names no rank and is
|
|
85
|
+
// inert, rather than quietly growing the rank set from an env string.
|
|
86
|
+
function applyLabelOverrides(ranks, env) {
|
|
87
|
+
// GOVERNMENT_RANK_LABELS is the post-ADR-0174 name. The pre-rename spelling is
|
|
88
|
+
// still read because an instance sets this in its own shell/env file, OUTSIDE
|
|
89
|
+
// this repo — dropping it would silently stop honouring a label override an
|
|
90
|
+
// operator already configured. Remove once instances have migrated.
|
|
91
|
+
const raw = api.resolveEnv('GOVERNMENT_RANK_LABELS', { env })
|
|
92
|
+
|| api.resolveEnv('GOVERNANCE_ROLE_LABELS', { env });
|
|
93
|
+
if (!raw) return ranks;
|
|
94
|
+
const out = { ...ranks };
|
|
90
95
|
for (const pair of String(raw).split(',')) {
|
|
91
96
|
const eq = pair.indexOf('=');
|
|
92
97
|
if (eq <= 0) continue;
|
|
@@ -116,27 +121,32 @@ function sanitizeRole(key, raw) {
|
|
|
116
121
|
seeded,
|
|
117
122
|
label: typeof raw.label === 'string' && raw.label ? raw.label : key,
|
|
118
123
|
description: typeof raw.description === 'string' ? raw.description : '',
|
|
119
|
-
// Rule 3: a seeded rank
|
|
124
|
+
// Rule 3: a seeded rank's grants come from catalog floors via governance_002.
|
|
120
125
|
permissions: seeded ? Object.freeze([]) : grantableOnly(raw.permissions),
|
|
121
126
|
});
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
// Pure resolver — all inputs injected, so tests drive it with zero I/O (the
|
|
125
130
|
// resolveBranding pattern).
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
function resolveGovernmentConfig({ neutral, instance, env = process.env } = {}) {
|
|
132
|
+
// `ranks` is the post-ADR-0174 key. An INSTANCE file authored before the rename
|
|
133
|
+
// may still say `roles`; accept it so an external self-hosted instance does not
|
|
134
|
+
// silently lose its templates on upgrade. The neutral file shipped in core is
|
|
135
|
+
// always `ranks`, so this fallback only ever reads host-owned config.
|
|
136
|
+
const instanceRanks = (instance || {}).ranks || (instance || {}).roles;
|
|
137
|
+
const merged = applyLabelOverrides(mergeRoles((neutral || {}).ranks, instanceRanks), env);
|
|
138
|
+
const ranks = {};
|
|
129
139
|
for (const key of Object.keys(merged).sort()) {
|
|
130
|
-
if (!isObj(merged[key])) continue; // a malformed entry is not a
|
|
131
|
-
|
|
140
|
+
if (!isObj(merged[key])) continue; // a malformed entry is not a rank, and grants nothing
|
|
141
|
+
ranks[key] = sanitizeRole(key, merged[key]);
|
|
132
142
|
}
|
|
133
|
-
// The four
|
|
143
|
+
// The four ranks ALWAYS exist (governance_002 seeds them unconditionally), so a
|
|
134
144
|
// config that omits or mangles one falls back to the canonical rank name rather than
|
|
135
|
-
// dropping the
|
|
145
|
+
// dropping the rank out of the display set.
|
|
136
146
|
for (const key of catalog.RANK_ORDER) {
|
|
137
|
-
if (!
|
|
147
|
+
if (!ranks[key]) ranks[key] = sanitizeRole(key, {});
|
|
138
148
|
}
|
|
139
|
-
return Object.freeze({
|
|
149
|
+
return Object.freeze({ ranks: Object.freeze(ranks) });
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
// READ, never require(): a bare require('../../config/X.json') of a boundary-excluded
|
|
@@ -148,7 +158,7 @@ function readJson(p) {
|
|
|
148
158
|
|
|
149
159
|
// Disk loader with the documented precedence. BOTH files are optional: the instance
|
|
150
160
|
// pack because a vanilla instance ships only the neutral starter, and the neutral
|
|
151
|
-
// starter because the four
|
|
161
|
+
// starter because the four ranks still resolve to their canonical names without
|
|
152
162
|
// it. Unlike branding — where a missing identity is fatal — no value here is required,
|
|
153
163
|
// so the loader fails to sane defaults rather than throwing.
|
|
154
164
|
//
|
|
@@ -157,8 +167,8 @@ function readJson(p) {
|
|
|
157
167
|
// consumer needs it on a hot path, resolve once at boot and pass the frozen result —
|
|
158
168
|
// do NOT add a cache here (this file sits next to resolver.js, whose no-cache rule is
|
|
159
169
|
// absolute; keeping config cache-free too means nobody has to check which is which).
|
|
160
|
-
function
|
|
161
|
-
return
|
|
170
|
+
function loadGovernmentConfig({ neutralPath = NEUTRAL_PATH, instancePath = INSTANCE_PATH, env = process.env } = {}) {
|
|
171
|
+
return resolveGovernmentConfig({
|
|
162
172
|
neutral: readJson(neutralPath) || {},
|
|
163
173
|
instance: readJson(instancePath) || {},
|
|
164
174
|
env,
|
|
@@ -166,8 +176,8 @@ function loadGovernanceConfig({ neutralPath = NEUTRAL_PATH, instancePath = INSTA
|
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
module.exports = {
|
|
169
|
-
|
|
170
|
-
|
|
179
|
+
resolveGovernmentConfig,
|
|
180
|
+
loadGovernmentConfig,
|
|
171
181
|
grantableOnly,
|
|
172
182
|
NEUTRAL_PATH,
|
|
173
183
|
INSTANCE_PATH,
|