@bongos/core 1.19.185 → 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 +90 -85
- package/clients/bongos-client/index.d.ts +2 -2
- package/config/government.neutral.json +6 -6
- package/docs/adr/0172-editable-rank-roles-substrate-axis.md +1 -1
- package/docs/api/openapi.json +12 -12
- package/docs/api-reference.md +7 -7
- package/modules/builder-settings/wandering-prefs.js +13 -13
- package/modules/government/catalog.js +79 -79
- package/modules/government/config.js +43 -33
- package/modules/government/db.js +58 -58
- 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 +88 -88
- package/modules/government/routes/rank-sync.js +8 -8
- package/modules/government/session-scopes.js +7 -7
- package/modules/government/session-scopes.json +5 -5
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/{governance.css → government.css} +11 -11
- package/modules/hall-ui/public/{governance.html → government.html} +14 -14
- package/modules/hall-ui/public/{governance.js → government.js} +108 -108
- package/modules/hall-ui/public/harbor.js +2 -2
- package/modules/hall-ui/public/shell.js +19 -15
- package/modules/hall-ui/public/watch.js +1 -1
- 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/scripts/hall-preview/server.js +1 -1
- package/src/bongos/auth.js +11 -11
- package/src/bongos/serve-internal.js +8 -8
- package/src/module-api.js +5 -1
- package/tests/auth_page_gate.mjs +2 -2
- package/tests/auth_resolve_failure.mjs +4 -4
- package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +39 -39
- 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} +14 -14
- 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} +36 -36
- 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_nav.mjs +8 -7
- package/tests/hall_page_gate_map.mjs +9 -9
package/docs/api-reference.md
CHANGED
|
@@ -218,15 +218,15 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
218
218
|
|
|
219
219
|
| Method | Path | Rank | Body | Description |
|
|
220
220
|
|---|---|---|---|---|
|
|
221
|
-
| GET | `/api/bongos/government/assignments` | `archon` | — | government.manage — every builder↔
|
|
222
|
-
| POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM
|
|
223
|
-
| DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-
|
|
221
|
+
| GET | `/api/bongos/government/assignments` | `archon` | — | government.manage — every builder↔rank assignment row. |
|
|
222
|
+
| POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM rank to a builder. |
|
|
223
|
+
| DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-rank assignment. |
|
|
224
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
|
|
226
|
-
| POST | `/api/bongos/government/ranks` | `archon` | `rank_key`, `label`, `description` | government.manage — create a CUSTOM
|
|
227
|
-
| PUT | `/api/bongos/government/ranks/:rankKey/permissions/:permissionKey` | `archon` | — | government.manage — grant one permission to a
|
|
225
|
+
| GET | `/api/bongos/government/ranks` | `archon` | — | government.manage — the full rank list with each rank's grants. |
|
|
226
|
+
| POST | `/api/bongos/government/ranks` | `archon` | `rank_key`, `label`, `description` | government.manage — create a CUSTOM rank (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 rank (the toggle's ON half). |
|
|
228
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
|
|
229
|
+
| POST | `/api/bongos/government/ranks/:rankKey/reset` | `archon` | — | government.manage — restore a SEEDED rank to its catalog default (ADR 0172). |
|
|
230
230
|
|
|
231
231
|
## `grades` (1)
|
|
232
232
|
|
|
@@ -125,27 +125,27 @@ const LEVEL_CONTRACT = Object.freeze({
|
|
|
125
125
|
// not name carries NO wandering metadata and resolves to the safest floor
|
|
126
126
|
// (Locked), the same fail-closed posture the resolver takes for a rank with no
|
|
127
127
|
// seeded role.
|
|
128
|
-
const
|
|
128
|
+
const RANK_WANDERING = Object.freeze({
|
|
129
129
|
xenos: Object.freeze({ default: 0, ceiling: 0 }),
|
|
130
130
|
thetes: Object.freeze({ default: 1, ceiling: 1 }),
|
|
131
131
|
metic: Object.freeze({ default: 2, ceiling: 3 }),
|
|
132
132
|
archon: Object.freeze({ default: 3, ceiling: 3 }),
|
|
133
133
|
});
|
|
134
|
-
const
|
|
134
|
+
const NO_RANK_WANDERING = Object.freeze({ default: 0, ceiling: 0 });
|
|
135
135
|
|
|
136
|
-
function
|
|
137
|
-
return Object.prototype.hasOwnProperty.call(
|
|
138
|
-
?
|
|
139
|
-
:
|
|
136
|
+
function rankWandering(rankKey) {
|
|
137
|
+
return Object.prototype.hasOwnProperty.call(RANK_WANDERING, rankKey)
|
|
138
|
+
? RANK_WANDERING[rankKey]
|
|
139
|
+
: NO_RANK_WANDERING;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// DERIVED projections of the metadata above, kept because scripts/gds/session-eval.js
|
|
143
143
|
// and the /me payload read them by rank. Deriving them means the two views cannot drift.
|
|
144
144
|
const RANK_DEFAULT = Object.freeze(
|
|
145
|
-
Object.fromEntries(Object.entries(
|
|
145
|
+
Object.fromEntries(Object.entries(RANK_WANDERING).map(([role, meta]) => [role, meta.default])),
|
|
146
146
|
);
|
|
147
147
|
const RANK_MAX = Object.freeze(
|
|
148
|
-
Object.fromEntries(Object.entries(
|
|
148
|
+
Object.fromEntries(Object.entries(RANK_WANDERING).map(([role, meta]) => [role, meta.ceiling])),
|
|
149
149
|
);
|
|
150
150
|
|
|
151
151
|
const TOP_LEVEL = LEVELS.length - 1; // 3
|
|
@@ -160,7 +160,7 @@ for (const key of LEVELS) {
|
|
|
160
160
|
for (let i = 0; i < LEVELS.length; i++) {
|
|
161
161
|
if (LEVEL_META[LEVELS[i]].n !== i) throw new Error(`wandering-prefs: LEVEL_META.${LEVELS[i]}.n must equal its index ${i}`);
|
|
162
162
|
}
|
|
163
|
-
for (const [role, meta] of Object.entries(
|
|
163
|
+
for (const [role, meta] of Object.entries(RANK_WANDERING)) {
|
|
164
164
|
if (!isLevelInt(meta.default)) throw new Error(`wandering-prefs: role '${role}' has a non-level default`);
|
|
165
165
|
if (!isLevelInt(meta.ceiling)) throw new Error(`wandering-prefs: role '${role}' has a non-level ceiling`);
|
|
166
166
|
if (meta.default > meta.ceiling) throw new Error(`wandering-prefs: default > ceiling for role '${role}'`);
|
|
@@ -182,11 +182,11 @@ function levelIndex(key) {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function defaultLevelForRank(rank) {
|
|
185
|
-
return
|
|
185
|
+
return rankWandering(rank).default;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function maxLevelForRank(rank) {
|
|
189
|
-
return
|
|
189
|
+
return rankWandering(rank).ceiling;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// The override a builder has stored (the raw 'wandering' key off behavior_prefs),
|
|
@@ -303,8 +303,8 @@ module.exports = {
|
|
|
303
303
|
LEVELS,
|
|
304
304
|
LEVEL_META,
|
|
305
305
|
LEVEL_CONTRACT,
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
RANK_WANDERING,
|
|
307
|
+
rankWandering,
|
|
308
308
|
RANK_DEFAULT,
|
|
309
309
|
RANK_MAX,
|
|
310
310
|
TOP_LEVEL,
|
|
@@ -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' },
|
|
@@ -104,15 +104,15 @@ 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: 'government.manage', system: true, floor: 'archon', guards: 'the
|
|
107
|
+
{ key: 'government.manage', system: true, floor: 'archon', guards: 'the Government tab routes (ranks/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)' },
|
|
111
111
|
{ key: 'infra.ssh', system: true, substrate: true, floor: 'archon', guards: 'ssh to the droplet / operator surface — substrate (credential placement)' },
|
|
112
|
-
{ key: 'principal.bfg', system: true, floor: null, principal: 'bfg', guards: '/memory/bfg/* — requireBfgPrincipal (exact system_role=bfg; NO rank
|
|
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)' },
|
|
@@ -235,10 +235,10 @@ const PERMISSIONS = [
|
|
|
235
235
|
{ key: 'page.view.public', system: false, floor: 'xenos', guards: 'public read surfaces (incl. xenos)' },
|
|
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
|
+
// `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
|
-
//
|
|
241
|
-
{ key: 'page.view.government', system: false, floor: 'metic', guards: 'the
|
|
240
|
+
// ranks/permissions tab) stayed archon.
|
|
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
|
|
|
244
244
|
// ── invariants asserted at module load (fail loud on an authoring typo) ─────────
|
|
@@ -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,
|