@bongos/core 1.19.184 → 1.19.186
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 +68 -63
- package/clients/bongos-client/bongos-client.global.js +19 -19
- package/clients/bongos-client/index.cjs +19 -19
- package/clients/bongos-client/index.d.ts +29 -29
- package/clients/bongos-client/index.mjs +19 -19
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +2 -2
- package/docs/api/openapi.json +121 -121
- package/docs/api-reference.md +10 -10
- package/docs/canonical-permissions.md +6 -6
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +19 -19
- package/modules/government/catalog.js +9 -9
- package/modules/government/db.js +9 -9
- package/modules/government/migrations/government_002_rename_permission_keys.sql +112 -0
- package/modules/government/module.json +1 -1
- package/modules/government/protected-surfaces.json +0 -1
- package/modules/government/ranks.js +13 -13
- package/modules/government/routes/{governance.js → government.js} +109 -84
- package/modules/government/routes/rank-sync.js +2 -2
- package/modules/hall-ui/public/gate.js +3 -3
- package/modules/hall-ui/public/{governance.css → government.css} +10 -10
- package/modules/hall-ui/public/{governance.html → government.html} +14 -14
- package/modules/hall-ui/public/{governance.js → government.js} +117 -117
- package/modules/hall-ui/public/harbor.js +3 -3
- package/modules/hall-ui/public/shell.js +21 -17
- package/modules/hall-ui/public/watch.js +2 -2
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/publish-manifest.js +0 -2
- package/scripts/gds/seed-governance-tasks.js +1 -1
- package/scripts/hall-preview/server.js +1 -1
- package/src/bongos/auth.js +13 -13
- package/src/bongos/module-scope-map.js +7 -22
- package/src/bongos/route-rank-check.js +12 -18
- package/src/bongos/serve-internal.js +10 -10
- package/src/module-api.js +6 -2
- package/tests/auth_resolve_failure.mjs +4 -4
- package/tests/governance_abuse_matrix.mjs +35 -35
- package/tests/governance_catalog.mjs +3 -3
- package/tests/governance_fold.mjs +2 -2
- package/tests/governance_fold_bench.mjs +2 -2
- package/tests/governance_live_proof.mjs +4 -4
- package/tests/governance_ownership_scope.mjs +1 -1
- package/tests/governance_page_gates.mjs +12 -12
- package/tests/governance_principals.mjs +2 -2
- package/tests/governance_protected_surfaces.mjs +41 -16
- package/tests/governance_require_permission.mjs +5 -5
- package/tests/governance_resolver.mjs +2 -2
- package/tests/governance_role_sync.mjs +23 -23
- package/tests/governance_routes.mjs +96 -96
- package/tests/governance_seed.mjs +24 -9
- package/tests/governance_session_scope.mjs +4 -4
- package/tests/hall_nav.mjs +8 -7
- package/tests/hall_page_gate_map.mjs +8 -8
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
//
|
|
1
|
+
// modules/hall-ui/public/government.js — Government (/builders/government).
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
// CRUD routes. Discord-style: pick a
|
|
5
|
-
// the grid, assign custom
|
|
3
|
+
// The ranks/permissions/assignments tab (BV1.R111, ADR 0151 §2) over the R110
|
|
4
|
+
// CRUD routes. Discord-style: pick a rank on the left, toggle its permissions in
|
|
5
|
+
// the grid, assign custom ranks to builders below.
|
|
6
6
|
//
|
|
7
|
-
// Since ADR 0172 grantability depends on WHICH
|
|
7
|
+
// Since ADR 0172 grantability depends on WHICH rank is selected, so this page's
|
|
8
8
|
// central job is rendering three different locks honestly. The SERVER enforces all
|
|
9
9
|
// of them (ADR 0016 — nothing here grants anything; a disabled button is a
|
|
10
10
|
// courtesy, `isGrantableTo` on the route is the wall):
|
|
11
11
|
// • SUBSTRATE permissions (deploy / protected paths / ssh) render LOCKED for
|
|
12
|
-
// every
|
|
12
|
+
// every rank, always. They are enforced outside the app (ADR 0043), so a grant
|
|
13
13
|
// row confers nothing — showing a working toggle would be the UI lying.
|
|
14
|
-
// • OWNER POWERS (the seven in-app `system` keys) render LOCKED on a custom
|
|
15
|
-
// and TOGGLABLE on a sub-Archon rank
|
|
14
|
+
// • OWNER POWERS (the seven in-app `system` keys) render LOCKED on a custom rank
|
|
15
|
+
// and TOGGLABLE on a sub-Archon rank. That asymmetry is the whole of the
|
|
16
16
|
// owner's 2026-08-16 decision, and the section header says so out loud.
|
|
17
|
-
// • the ARCHON rank
|
|
17
|
+
// • the ARCHON rank is read-only in full: it holds everything, and its
|
|
18
18
|
// immutability is what makes it impossible to click your way into locking the
|
|
19
|
-
// owner out of this tab. The API refuses it as `
|
|
20
|
-
// ASSIGNMENT of any rank
|
|
21
|
-
// — you get a rank
|
|
19
|
+
// owner out of this tab. The API refuses it as `archon_rank_immutable`.
|
|
20
|
+
// ASSIGNMENT of any rank is still refused (`seeded_rank_assignment_forbidden`)
|
|
21
|
+
// — you get a rank by holding the rank, so that dial is the rank itself.
|
|
22
22
|
//
|
|
23
23
|
// Like The Gate, this reads /api/bongos/me for a friendly Sealed notice —
|
|
24
|
-
// DEFENCE IN DEPTH only. Every /
|
|
25
|
-
// gated server-side on the `
|
|
24
|
+
// DEFENCE IN DEPTH only. Every /government/* endpoint (reads included) is ALSO
|
|
25
|
+
// gated server-side on the `government.manage` atom (archon floor). On 401 →
|
|
26
26
|
// web sign-in.
|
|
27
27
|
//
|
|
28
28
|
// DELIBERATELY NOT IN THE SIDEBAR YET: the nav entry + the server page gate are
|
|
@@ -63,50 +63,50 @@
|
|
|
63
63
|
|
|
64
64
|
// The R110 refusal codes, translated for the person clicking.
|
|
65
65
|
const ERROR_COPY = {
|
|
66
|
-
permission_not_grantable: 'That permission can’t be granted to this
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
permission_not_grantable: 'That permission can’t be granted to this rank. (The server’s reply says which of the three walls applied.)',
|
|
67
|
+
archon_rank_immutable: 'The Owner rank is read-only — it holds every permission, which is what keeps you from locking yourself out of this page.',
|
|
68
|
+
rank_has_no_default: 'Only a ladder rank has a catalog default to reset to. Revoke a custom rank’s permissions individually instead.',
|
|
69
|
+
seeded_rank_assignment_forbidden: 'Ladder ranks are assigned by rank itself — change the builder’s rank instead.',
|
|
70
|
+
rank_exists: 'A rank with that key already exists.',
|
|
71
|
+
rank_not_found: 'That rank no longer exists — refreshing.',
|
|
72
72
|
builder_not_found: 'No builder with that id.',
|
|
73
73
|
unknown_permission: 'The permission catalog does not define that key.',
|
|
74
|
-
|
|
74
|
+
bad_rank_key: 'Rank keys are kebab-case: lowercase letters, digits, dashes.',
|
|
75
75
|
validation_failed: 'Missing or invalid fields.',
|
|
76
76
|
};
|
|
77
77
|
const friendlyError = (code) => ERROR_COPY[code] || null;
|
|
78
78
|
|
|
79
79
|
// ---- state ---------------------------------------------------------------
|
|
80
80
|
|
|
81
|
-
let
|
|
81
|
+
let ranks = []; // [{ rank_key, label, description, is_system, seeded_rank, permissions: [] }]
|
|
82
82
|
let permissions = []; // catalog rows: [{ key, system, floor, guards, grantable }]
|
|
83
83
|
let principals = []; // [{ system_role, permission, guards }]
|
|
84
|
-
let assignments = []; // [{ builder_id, github_login, display_name, rank,
|
|
85
|
-
let
|
|
84
|
+
let assignments = []; // [{ builder_id, github_login, display_name, rank, rank_key, rank_label, is_system }]
|
|
85
|
+
let selectedRankKey = null;
|
|
86
86
|
|
|
87
|
-
const
|
|
87
|
+
const selectedRank = () => ranks.find((r) => r.rank_key === selectedRankKey) || null;
|
|
88
88
|
|
|
89
|
-
// ---- render:
|
|
89
|
+
// ---- render: rank list ---------------------------------------------------
|
|
90
90
|
|
|
91
|
-
function
|
|
92
|
-
const list = document.getElementById('gov-
|
|
91
|
+
function renderRanks() {
|
|
92
|
+
const list = document.getElementById('gov-rank-list');
|
|
93
93
|
list.innerHTML = '';
|
|
94
|
-
for (const
|
|
95
|
-
// The Archon row is called out as read-only, because it is the one
|
|
94
|
+
for (const rank of ranks) {
|
|
95
|
+
// The Archon row is called out as read-only, because it is the one rank in
|
|
96
96
|
// this list a click will not change and the reader should know before trying.
|
|
97
|
-
const pill =
|
|
98
|
-
? `<span class="gov-pill gov-pill--locked">
|
|
99
|
-
:
|
|
100
|
-
? `<span class="gov-pill gov-pill--seeded">
|
|
97
|
+
const pill = isArchonRank(rank)
|
|
98
|
+
? `<span class="gov-pill gov-pill--locked">ladder · read-only</span>`
|
|
99
|
+
: rank.is_system
|
|
100
|
+
? `<span class="gov-pill gov-pill--seeded">ladder</span>`
|
|
101
101
|
: `<span class="gov-pill gov-pill--custom">custom</span>`;
|
|
102
102
|
const btn = document.createElement('button');
|
|
103
103
|
btn.type = 'button';
|
|
104
|
-
btn.className = 'gov-
|
|
105
|
-
btn.setAttribute('aria-pressed',
|
|
104
|
+
btn.className = 'gov-rank';
|
|
105
|
+
btn.setAttribute('aria-pressed', rank.rank_key === selectedRankKey ? 'true' : 'false');
|
|
106
106
|
btn.innerHTML =
|
|
107
|
-
`<span class="gov-
|
|
108
|
-
<span class="gov-
|
|
109
|
-
btn.addEventListener('click', () => {
|
|
107
|
+
`<span class="gov-rank__head"><span class="gov-rank__label">${escapeHtml(rank.label || rank.rank_key)}</span>${pill}</span>
|
|
108
|
+
<span class="gov-rank__meta"><code>${escapeHtml(rank.rank_key)}</code> · ${(rank.permissions || []).length} permission${(rank.permissions || []).length === 1 ? '' : 's'}</span>`;
|
|
109
|
+
btn.addEventListener('click', () => { selectedRankKey = rank.rank_key; renderRanks(); renderGrid(); });
|
|
110
110
|
list.appendChild(btn);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -117,34 +117,34 @@
|
|
|
117
117
|
// right control, NEVER to decide authority — the route runs the real one and
|
|
118
118
|
// refuses regardless of what this returns. Kept in the same order as the server's
|
|
119
119
|
// so a future catalog change lands in one obvious place on each side.
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
function grantableTo(p,
|
|
123
|
-
if (!
|
|
124
|
-
if (
|
|
120
|
+
const isArchonRank = (rank) => !!rank && rank.is_system && rank.seeded_rank === 'archon';
|
|
121
|
+
const isRankRank = (rank) => !!rank && rank.is_system;
|
|
122
|
+
function grantableTo(p, rank) {
|
|
123
|
+
if (!rank || p.substrate) return false;
|
|
124
|
+
if (rank.is_system) return rank.seeded_rank !== 'archon';
|
|
125
125
|
return !!p.grantable;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function permRow(p,
|
|
129
|
-
const held = !!
|
|
128
|
+
function permRow(p, rank) {
|
|
129
|
+
const held = !!rank && (rank.permissions || []).includes(p.key);
|
|
130
130
|
const row = document.createElement('div');
|
|
131
131
|
row.className = 'gov-perm';
|
|
132
132
|
|
|
133
133
|
// Why locked, in the words that tell the reader whether it is a policy they
|
|
134
134
|
// could change or a fact they cannot. Three causes, three sentences.
|
|
135
135
|
const lockReason = p.substrate
|
|
136
|
-
? 'Enforced outside the app (the pre-push hook, the deploy audit, the server itself). Granting it here would change nothing, so it is locked for every
|
|
137
|
-
:
|
|
138
|
-
? 'The Archon
|
|
139
|
-
: 'An owner-level permission. It can be handed to a rank
|
|
136
|
+
? 'Enforced outside the app (the pre-push hook, the deploy audit, the server itself). Granting it here would change nothing, so it is locked for every rank.'
|
|
137
|
+
: isArchonRank(rank)
|
|
138
|
+
? 'The Archon rank holds everything and is read-only.'
|
|
139
|
+
: 'An owner-level permission. It can be handed to a ladder rank, but never to a custom one.';
|
|
140
140
|
|
|
141
141
|
let control;
|
|
142
|
-
if (!
|
|
142
|
+
if (!rank) {
|
|
143
143
|
control = '';
|
|
144
|
-
} else if (!grantableTo(p,
|
|
144
|
+
} else if (!grantableTo(p, rank)) {
|
|
145
145
|
control = `<span class="gov-pill gov-pill--locked" title="${escapeHtml(lockReason)}">locked</span>`;
|
|
146
|
-
// The Archon
|
|
147
|
-
if (held &&
|
|
146
|
+
// The Archon rank is read-only rather than empty-handed, so say what it holds.
|
|
147
|
+
if (held && isArchonRank(rank)) control = `<span class="gov-held-note">held</span>${control}`;
|
|
148
148
|
} else {
|
|
149
149
|
control = `<button type="button" class="gov-toggle" aria-pressed="${held ? 'true' : 'false'}" data-perm="${escapeHtml(p.key)}">${held ? 'granted' : 'grant'}</button>`;
|
|
150
150
|
}
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
${control}`;
|
|
159
159
|
|
|
160
160
|
const btn = row.querySelector('button.gov-toggle');
|
|
161
|
-
if (btn) btn.addEventListener('click', () => toggleGrant(
|
|
161
|
+
if (btn) btn.addEventListener('click', () => toggleGrant(rank, p.key, !held, btn));
|
|
162
162
|
return row;
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -170,34 +170,34 @@
|
|
|
170
170
|
return el;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function gridNote(
|
|
174
|
-
if (!
|
|
175
|
-
if (
|
|
176
|
-
return 'The Archon
|
|
173
|
+
function gridNote(rank) {
|
|
174
|
+
if (!rank) return 'Pick a rank to see and edit what it may do.';
|
|
175
|
+
if (isArchonRank(rank)) {
|
|
176
|
+
return 'The Archon rank holds every permission and is read-only — that is what makes it impossible to lock yourself out of this page. To give a power away, hand it to one of the ranks below instead.';
|
|
177
177
|
}
|
|
178
|
-
if (
|
|
179
|
-
return `Everything a ${
|
|
178
|
+
if (isRankRank(rank)) {
|
|
179
|
+
return `Everything a ${rank.label || rank.rank_key} can do, and yours to change — every builder at this rank is affected immediately. “Reset to default” puts it back to the shipped ladder.`;
|
|
180
180
|
}
|
|
181
|
-
return 'Toggle the everyday permissions this
|
|
181
|
+
return 'Toggle the everyday permissions this rank carries. Owner powers can only go to a ladder rank, not to a custom one.';
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function renderGrid() {
|
|
185
185
|
const grid = document.getElementById('gov-grid');
|
|
186
|
-
const
|
|
187
|
-
document.getElementById('gov-grid-title').textContent =
|
|
188
|
-
? `Permissions — ${
|
|
189
|
-
document.getElementById('gov-grid-note').textContent = gridNote(
|
|
186
|
+
const rank = selectedRank();
|
|
187
|
+
document.getElementById('gov-grid-title').textContent = rank
|
|
188
|
+
? `Permissions — ${rank.label || rank.rank_key}` : 'Permissions';
|
|
189
|
+
document.getElementById('gov-grid-note').textContent = gridNote(rank);
|
|
190
190
|
|
|
191
|
-
// The reset escape hatch — only a rank
|
|
191
|
+
// The reset escape hatch — only a ladder rank has a catalog default, and the
|
|
192
192
|
// Archon one never drifts from it, so it shows on exactly the three editable ones.
|
|
193
193
|
const resetBox = document.getElementById('gov-reset');
|
|
194
194
|
resetBox.innerHTML = '';
|
|
195
|
-
if (
|
|
195
|
+
if (isRankRank(rank) && !isArchonRank(rank)) {
|
|
196
196
|
const btn = document.createElement('button');
|
|
197
197
|
btn.type = 'button';
|
|
198
198
|
btn.className = 'gov-btn gov-btn--ghost gov-btn--small';
|
|
199
199
|
btn.textContent = 'Reset to default';
|
|
200
|
-
btn.addEventListener('click', () =>
|
|
200
|
+
btn.addEventListener('click', () => resetRank(rank, btn));
|
|
201
201
|
resetBox.appendChild(btn);
|
|
202
202
|
}
|
|
203
203
|
|
|
@@ -206,30 +206,30 @@
|
|
|
206
206
|
// identity is not a row anyone can act on here.
|
|
207
207
|
const humanPerms = permissions.filter((p) => !principals.some((pr) => pr.permission === p.key));
|
|
208
208
|
// Three groups, split by WHY a row is or isn't editable rather than by the old
|
|
209
|
-
// single `grantable` flag — which since ADR 0172 answers only the custom-
|
|
209
|
+
// single `grantable` flag — which since ADR 0172 answers only the custom-rank case.
|
|
210
210
|
const substrate = humanPerms.filter((p) => p.substrate);
|
|
211
211
|
const ownerPowers = humanPerms.filter((p) => p.system && !p.substrate);
|
|
212
212
|
const operational = humanPerms.filter((p) => !p.system);
|
|
213
213
|
|
|
214
|
-
const ownerHead = !
|
|
214
|
+
const ownerHead = !rank || isRankRank(rank)
|
|
215
215
|
? `Owner powers (${ownerPowers.length})`
|
|
216
|
-
: `Owner powers — locked for custom
|
|
216
|
+
: `Owner powers — locked for custom ranks (${ownerPowers.length})`;
|
|
217
217
|
|
|
218
|
-
grid.appendChild(section(`Operational (${operational.length})`, operational.map((p) => permRow(p,
|
|
219
|
-
grid.appendChild(section(ownerHead, ownerPowers.map((p) => permRow(p,
|
|
220
|
-
grid.appendChild(section(`Enforced outside the app — always locked (${substrate.length})`, substrate.map((p) => permRow(p,
|
|
218
|
+
grid.appendChild(section(`Operational (${operational.length})`, operational.map((p) => permRow(p, rank))));
|
|
219
|
+
grid.appendChild(section(ownerHead, ownerPowers.map((p) => permRow(p, rank))));
|
|
220
|
+
grid.appendChild(section(`Enforced outside the app — always locked (${substrate.length})`, substrate.map((p) => permRow(p, rank))));
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
// Restore a rank
|
|
224
|
-
// action here that discards work in bulk — a drifted
|
|
225
|
-
async function
|
|
226
|
-
const label =
|
|
223
|
+
// Restore a ladder rank to the shipped ladder. Confirmed because it is the one
|
|
224
|
+
// action here that discards work in bulk — a drifted rank can be ~60 rows.
|
|
225
|
+
async function resetRank(rank, btn) {
|
|
226
|
+
const label = rank.label || rank.rank_key;
|
|
227
227
|
if (!window.confirm(`Reset ${label} to its default permissions?\n\nEvery change you've made to this rank is discarded, and it takes effect for every builder at this rank on their next request.`)) return;
|
|
228
228
|
btn.disabled = true;
|
|
229
229
|
try {
|
|
230
|
-
const out = await sendJson('POST', `/
|
|
230
|
+
const out = await sendJson('POST', `/government/ranks/${encodeURIComponent(rank.rank_key)}/reset`);
|
|
231
231
|
toast(`${label} reset to its default ${out.permission_count} permissions.`);
|
|
232
|
-
await
|
|
232
|
+
await reloadRanks();
|
|
233
233
|
} catch (err) {
|
|
234
234
|
if (err && err.kind === '401') return;
|
|
235
235
|
toast(`Could not reset: ${err.message || err}`);
|
|
@@ -237,12 +237,12 @@
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
async function toggleGrant(
|
|
240
|
+
async function toggleGrant(rank, permissionKey, grant, btn) {
|
|
241
241
|
btn.disabled = true;
|
|
242
242
|
try {
|
|
243
|
-
await sendJson(grant ? 'PUT' : 'DELETE', `/
|
|
243
|
+
await sendJson(grant ? 'PUT' : 'DELETE', `/government/ranks/${encodeURIComponent(rank.rank_key)}/permissions/${encodeURIComponent(permissionKey)}`);
|
|
244
244
|
toast(grant ? `Granted ${permissionKey}.` : `Revoked ${permissionKey}.`);
|
|
245
|
-
await
|
|
245
|
+
await reloadRanks();
|
|
246
246
|
} catch (err) {
|
|
247
247
|
if (err && err.kind === '401') return;
|
|
248
248
|
toast(`Could not ${grant ? 'grant' : 'revoke'}: ${err.message || err}`);
|
|
@@ -256,7 +256,7 @@
|
|
|
256
256
|
const box = document.getElementById('gov-assignments');
|
|
257
257
|
box.innerHTML = '';
|
|
258
258
|
if (assignments.length === 0) {
|
|
259
|
-
box.innerHTML = `<p class="gov-empty">No
|
|
259
|
+
box.innerHTML = `<p class="gov-empty">No rank assignments yet.</p>`;
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
262
262
|
const wrap = document.createElement('section');
|
|
@@ -265,11 +265,11 @@
|
|
|
265
265
|
const row = document.createElement('div');
|
|
266
266
|
row.className = 'gov-assign-row';
|
|
267
267
|
const who = `${escapeHtml(a.display_name || a.github_login || `builder ${a.builder_id}`)} <span class="muted">(#${escapeHtml(String(a.builder_id))} · ${escapeHtml(a.rank || '')})</span>`;
|
|
268
|
-
const
|
|
269
|
-
? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.
|
|
270
|
-
: `<span class="gov-pill gov-pill--custom">${escapeHtml(a.
|
|
268
|
+
const rankPill = a.is_system
|
|
269
|
+
? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.rank_label || a.rank_key)} · via rank</span>`
|
|
270
|
+
: `<span class="gov-pill gov-pill--custom">${escapeHtml(a.rank_label || a.rank_key)}</span>`;
|
|
271
271
|
const action = a.is_system ? '' : `<button type="button" class="gov-btn gov-btn--ghost gov-btn--small" data-act="unassign">Remove</button>`;
|
|
272
|
-
row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${
|
|
272
|
+
row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${rankPill}${action}`;
|
|
273
273
|
const btn = row.querySelector('[data-act=unassign]');
|
|
274
274
|
if (btn) btn.addEventListener('click', () => unassign(a, btn));
|
|
275
275
|
wrap.appendChild(row);
|
|
@@ -278,18 +278,18 @@
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
function renderAssignSelect() {
|
|
281
|
-
const sel = document.getElementById('gov-assign-
|
|
282
|
-
const custom =
|
|
281
|
+
const sel = document.getElementById('gov-assign-rank');
|
|
282
|
+
const custom = ranks.filter((r) => !r.is_system);
|
|
283
283
|
sel.innerHTML = custom.length
|
|
284
|
-
? custom.map((r) => `<option value="${escapeHtml(r.
|
|
285
|
-
: `<option value="" disabled selected>No custom
|
|
284
|
+
? custom.map((r) => `<option value="${escapeHtml(r.rank_key)}">${escapeHtml(r.label || r.rank_key)}</option>`).join('')
|
|
285
|
+
: `<option value="" disabled selected>No custom ranks yet</option>`;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
async function unassign(a, btn) {
|
|
289
289
|
btn.disabled = true;
|
|
290
290
|
try {
|
|
291
|
-
await sendJson('DELETE', `/
|
|
292
|
-
toast(`Removed ${a.
|
|
291
|
+
await sendJson('DELETE', `/government/assignments/${encodeURIComponent(a.builder_id)}/${encodeURIComponent(a.rank_key)}`);
|
|
292
|
+
toast(`Removed ${a.rank_label || a.rank_key} from builder ${a.builder_id}.`);
|
|
293
293
|
await reloadAssignments();
|
|
294
294
|
} catch (err) {
|
|
295
295
|
if (err && err.kind === '401') return;
|
|
@@ -320,26 +320,26 @@
|
|
|
320
320
|
|
|
321
321
|
// ---- loads ---------------------------------------------------------------
|
|
322
322
|
|
|
323
|
-
async function
|
|
324
|
-
const data = await fetchJson('/
|
|
325
|
-
|
|
326
|
-
if (!
|
|
327
|
-
|
|
323
|
+
async function reloadRanks() {
|
|
324
|
+
const data = await fetchJson('/government/ranks');
|
|
325
|
+
ranks = data.ranks || [];
|
|
326
|
+
if (!ranks.some((r) => r.rank_key === selectedRankKey)) selectedRankKey = ranks.length ? ranks[0].rank_key : null;
|
|
327
|
+
renderRanks();
|
|
328
328
|
renderGrid();
|
|
329
329
|
renderAssignSelect();
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
async function reloadAssignments() {
|
|
333
|
-
const data = await fetchJson('/
|
|
333
|
+
const data = await fetchJson('/government/assignments');
|
|
334
334
|
assignments = data.assignments || [];
|
|
335
335
|
renderAssignments();
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
async function load() {
|
|
339
|
-
const perms = await fetchJson('/
|
|
339
|
+
const perms = await fetchJson('/government/permissions');
|
|
340
340
|
permissions = perms.permissions || [];
|
|
341
341
|
principals = perms.principals || [];
|
|
342
|
-
await
|
|
342
|
+
await reloadRanks();
|
|
343
343
|
await reloadAssignments();
|
|
344
344
|
renderPrincipals();
|
|
345
345
|
}
|
|
@@ -352,20 +352,20 @@
|
|
|
352
352
|
const btn = e.target.querySelector('button');
|
|
353
353
|
btn.disabled = true;
|
|
354
354
|
const body = {
|
|
355
|
-
|
|
355
|
+
rank_key: document.getElementById('gov-create-key').value.trim(),
|
|
356
356
|
label: document.getElementById('gov-create-label').value.trim(),
|
|
357
357
|
};
|
|
358
358
|
const desc = document.getElementById('gov-create-desc').value.trim();
|
|
359
359
|
if (desc) body.description = desc;
|
|
360
360
|
try {
|
|
361
|
-
const r = await sendJson('POST', '/
|
|
362
|
-
toast(`Created
|
|
361
|
+
const r = await sendJson('POST', '/government/ranks', body);
|
|
362
|
+
toast(`Created rank ${r.rank ? r.rank.rank_key : body.rank_key}.`);
|
|
363
363
|
e.target.reset();
|
|
364
|
-
|
|
365
|
-
await
|
|
364
|
+
selectedRankKey = body.rank_key;
|
|
365
|
+
await reloadRanks();
|
|
366
366
|
} catch (err) {
|
|
367
367
|
if (err && err.kind === '401') return;
|
|
368
|
-
toast(`Could not create
|
|
368
|
+
toast(`Could not create rank: ${err.message || err}`);
|
|
369
369
|
} finally {
|
|
370
370
|
btn.disabled = false;
|
|
371
371
|
}
|
|
@@ -375,12 +375,12 @@
|
|
|
375
375
|
e.preventDefault();
|
|
376
376
|
const btn = e.target.querySelector('button');
|
|
377
377
|
const builderId = Number(document.getElementById('gov-assign-builder').value);
|
|
378
|
-
const
|
|
379
|
-
if (!
|
|
378
|
+
const rankKey = document.getElementById('gov-assign-rank').value;
|
|
379
|
+
if (!rankKey) { toast('Create a custom rank first.'); return; }
|
|
380
380
|
btn.disabled = true;
|
|
381
381
|
try {
|
|
382
|
-
await sendJson('POST', '/
|
|
383
|
-
toast(`Assigned ${
|
|
382
|
+
await sendJson('POST', '/government/assignments', { builder_id: builderId, rank_key: rankKey });
|
|
383
|
+
toast(`Assigned ${rankKey} to builder ${builderId}.`);
|
|
384
384
|
e.target.reset();
|
|
385
385
|
await reloadAssignments();
|
|
386
386
|
} catch (err) {
|
|
@@ -402,9 +402,9 @@
|
|
|
402
402
|
|
|
403
403
|
async function init() {
|
|
404
404
|
// Confirm rank for a friendly sealed notice; the server gates the page and
|
|
405
|
-
// every /governance endpoint on `
|
|
406
|
-
// defence-in-depth).
|
|
407
|
-
// rank
|
|
405
|
+
// every /governance endpoint on `government.manage` regardless (cosmetic
|
|
406
|
+
// defence-in-depth). government.manage is system:true, so only the archon
|
|
407
|
+
// rank can hold it — rank is an accurate client-side proxy.
|
|
408
408
|
let me;
|
|
409
409
|
try {
|
|
410
410
|
me = await fetchJson('/me');
|
|
@@ -416,7 +416,7 @@
|
|
|
416
416
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
417
417
|
if (rank !== 'archon') { showSealed(); return; }
|
|
418
418
|
|
|
419
|
-
document.getElementById('gov-sub').textContent = '
|
|
419
|
+
document.getElementById('gov-sub').textContent = 'Ranks, permissions, and who holds them. Ranks below Archon are yours to shape; what the substrate enforces stays locked.';
|
|
420
420
|
document.getElementById('gov-body').hidden = false;
|
|
421
421
|
wireForms();
|
|
422
422
|
try {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// shows a restricted notice below the Metic floor — DEFENCE IN DEPTH only. The data +
|
|
10
10
|
// mutation endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO gated
|
|
11
11
|
// server-side on `box.fleet.manage` (ADR 0016), and the page itself is gated by
|
|
12
|
-
// serve-internal.js
|
|
12
|
+
// serve-internal.js requireGovernmentPage — whose own floor is `page.view.government`,
|
|
13
13
|
// metic since ADR 0157. On a 401 it redirects into the web sign-in flow.
|
|
14
14
|
|
|
15
15
|
(() => {
|
|
@@ -326,9 +326,9 @@
|
|
|
326
326
|
me = null;
|
|
327
327
|
}
|
|
328
328
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
329
|
-
// ADR 0157: Harbor sits at the `page.view.
|
|
329
|
+
// ADR 0157: Harbor sits at the `page.view.government` floor (metic), and the fleet
|
|
330
330
|
// data it reads is `box.fleet.manage` (also metic now). Cosmetic —
|
|
331
|
-
//
|
|
331
|
+
// requireGovernmentPage + the route gates are the wall; this just picks body vs
|
|
332
332
|
// sealed notice.
|
|
333
333
|
if (rank !== 'metic' && rank !== 'archon') { showSealed(); return; }
|
|
334
334
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// top bar (drawer toggle, page title, greeting slot) into the static mounts
|
|
13
13
|
// (#app-sidebar / #app-topbar / #app-foot) every hall page carries;
|
|
14
14
|
// - rank-gate nav items COSMETICALLY (server-side page gates remain the real
|
|
15
|
-
// access control — serve-internal.js
|
|
15
|
+
// access control — serve-internal.js requireGovernmentPage etc.);
|
|
16
16
|
// - on the index (body[data-page="home"]) route the three views (#home,
|
|
17
17
|
// #standing, #leaderboard) plus the #achievements alias into Standing. The
|
|
18
18
|
// `#/task|idea|blocker/N` deep-link hashes belong to builders.js and are
|
|
@@ -97,13 +97,15 @@
|
|
|
97
97
|
// work — signed, but for newcomers only once the "ship three" stage is
|
|
98
98
|
// current (mirrors the old #hall-nav-work rule, task #833)
|
|
99
99
|
// archon — rank === 'archon'
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
100
|
+
// government — rank is metic or archon (ADR 0157): the monitoring/ops pages.
|
|
101
|
+
// NB this gate key is a RANK ALIAS meaning metic-or-archon; it is NOT the
|
|
102
|
+
// `government` MODULE key, and the two are unrelated (ADR 0174).
|
|
103
|
+
// Mirrors the server's `page.view.government` floor, which that ADR
|
|
104
|
+
// lowered archon→metic. Cosmetic only — requireGovernmentPage is the wall.
|
|
103
105
|
// `view` marks an index view (hash-routed); `module` gates on window.__MODULES__.
|
|
104
106
|
// task 2175: grouped by AUDIENCE of the data, so the sidebar reads in coherent
|
|
105
107
|
// runs instead of one long list — the project (unlabeled), You, Community,
|
|
106
|
-
// Docs,
|
|
108
|
+
// Docs, Government. A group's `gate` must match its most permissive item's, or the
|
|
107
109
|
// head renders over nothing.
|
|
108
110
|
var NAV = [
|
|
109
111
|
{ group: '', items: [
|
|
@@ -143,7 +145,7 @@
|
|
|
143
145
|
// requireProjectCuratePage is the wall — but a nav that disagrees with the
|
|
144
146
|
// gate teaches the ladder wrong. The module gate hides the tab on
|
|
145
147
|
// instances without the platform-identity hub.
|
|
146
|
-
{ id: 'people', label: 'People', icon: 'people', href: '/people', gate: '
|
|
148
|
+
{ id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'government', module: 'platform-identity' },
|
|
147
149
|
] },
|
|
148
150
|
{ group: 'Docs', gate: 'signed', items: [
|
|
149
151
|
// task 1002710: primer + diagrams were gate:'member' (rank !== xenos) to
|
|
@@ -168,18 +170,20 @@
|
|
|
168
170
|
// subdomain — external so the rewrite shim doesn't turn it into /builders/docs.
|
|
169
171
|
{ id: 'apidocs', label: 'API docs', icon: 'docs', href: '/docs', gate: 'signed', external: true },
|
|
170
172
|
] },
|
|
171
|
-
// Relabeled 'Archon' → 'Governance' in ADR 0157: these pages are no longer
|
|
173
|
+
// Relabeled 'Archon' → 'Governance' in ADR 0157, then → 'Government' by ADR 0174: these pages are no longer
|
|
172
174
|
// Archon-only, so a group header reading "Archon" over items a Metic can open
|
|
173
175
|
// would misdescribe the ladder to the builders most likely to be learning it.
|
|
174
|
-
{ group: '
|
|
175
|
-
{ id: 'watch', label: 'Watch', icon: 'admin', href: '/watch', gate: '
|
|
176
|
-
{ id: 'harbor', label: 'Harbor', icon: 'admin', href: '/harbor', gate: '
|
|
177
|
-
{ id: 'gate', label: 'Gate', icon: 'admin', href: '/gate', gate: '
|
|
178
|
-
{ id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: '
|
|
176
|
+
{ group: 'Government', gate: 'government', items: [
|
|
177
|
+
{ id: 'watch', label: 'Watch', icon: 'admin', href: '/watch', gate: 'government' },
|
|
178
|
+
{ id: 'harbor', label: 'Harbor', icon: 'admin', href: '/harbor', gate: 'government', module: 'dev-box' },
|
|
179
|
+
{ id: 'gate', label: 'Gate', icon: 'admin', href: '/gate', gate: 'government' },
|
|
180
|
+
{ id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'government' },
|
|
179
181
|
// BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
|
|
180
|
-
// Archon-only item the '
|
|
181
|
-
// only;
|
|
182
|
-
{ id: '
|
|
182
|
+
// Archon-only item the 'government' gate comment anticipated — cosmetic
|
|
183
|
+
// only; requireGovernmentManagePage (the government.manage atom) is the wall.
|
|
184
|
+
{ id: 'government', label: 'Permissions', icon: 'admin', href: '/government', gate: 'archon' } // NOT 'Ranks': /ranks is the signed-in ladder explainer and two nav items
|
|
185
|
+
// reading 'Ranks' is the confusion this rename exists to remove. This tab
|
|
186
|
+
// is where a rank's permissions are granted, so it says what it does.,
|
|
183
187
|
] },
|
|
184
188
|
];
|
|
185
189
|
|
|
@@ -420,10 +424,10 @@
|
|
|
420
424
|
case 'member': return member;
|
|
421
425
|
case 'work': return authed && (member || access.showWork);
|
|
422
426
|
case 'archon': return authed && access.rank === 'archon';
|
|
423
|
-
// ADR 0157: the monitoring/ops pages sit at the `page.view.
|
|
427
|
+
// ADR 0157: the monitoring/ops pages sit at the `page.view.government` floor,
|
|
424
428
|
// which is metic. Kept as its own key rather than widening 'archon', so a
|
|
425
429
|
// genuinely Archon-only nav item can still be added later.
|
|
426
|
-
case '
|
|
430
|
+
case 'government': return authed && (access.rank === 'metic' || access.rank === 'archon');
|
|
427
431
|
default: return false;
|
|
428
432
|
}
|
|
429
433
|
}
|
|
@@ -885,9 +885,9 @@
|
|
|
885
885
|
return;
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
-
// ADR 0157: Watch sits at the `page.view.
|
|
888
|
+
// ADR 0157: Watch sits at the `page.view.government` floor (metic), and every panel
|
|
889
889
|
// it reads (roster, security reports, audit log, sessions, box costs) delegated to
|
|
890
|
-
// metic with it. Cosmetic —
|
|
890
|
+
// metic with it. Cosmetic — requireGovernmentPage + the route gates are the wall.
|
|
891
891
|
if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
|
|
892
892
|
showSealed('This page is available from the Metic rank up. Your current rank does not grant access.');
|
|
893
893
|
return;
|
|
@@ -68,7 +68,7 @@ module.exports = function buildGateApprovalsRouter() {
|
|
|
68
68
|
// BV1.R105 left this on requireRank because `gate_approval.decide` was declared over
|
|
69
69
|
// the /approve WRITE only and no atom named the read. ADR 0157 extends the catalog
|
|
70
70
|
// with exactly that atom — `gate_approval.read`, floor metic — so the mapping is now
|
|
71
|
-
// one the catalog makes. The split is load-bearing, not cosmetic: `page.view.
|
|
71
|
+
// one the catalog makes. The split is load-bearing, not cosmetic: `page.view.government`
|
|
72
72
|
// opens /gate to Metic, and without a Metic-readable data endpoint that page would
|
|
73
73
|
// render nothing but a 403. Approving a held PR stays archon (`.decide`, system).
|
|
74
74
|
router.get('/gate-approvals', auth.requireBuilder, auth.requirePermission('gate_approval.read'), async (req, res) => {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.186",
|
|
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.186",
|
|
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.186",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -61,7 +61,6 @@ const PUBLISH_ALLOWLIST = [
|
|
|
61
61
|
'modules/sessions/',
|
|
62
62
|
'modules/autonomy/',
|
|
63
63
|
'modules/security/',
|
|
64
|
-
'modules/governance/', // configurable roles & permissions (BV1 goal 1000030, ADR 0151) — the rank/permission machinery publishes AS the open-source methodology (ADR 0065 §4 / ADR 0098); it is in PROTECTED_GLOBS for write-protection, NOT publish-exclusion. default:true. Renamed → modules/government/ (ADR 0174); dropped with the folder in goal 1000068 R04.
|
|
65
64
|
'modules/government/', // the same machinery under its post-ADR-0174 name (goal 1000068, task 1003003). Added in lockstep with the protected-surfaces.json entry: publish_manifest asserts every PROTECTED_GLOB is either topology-excluded or publishable, so registering the glob as protected WITHOUT this line default-denies the module from the published core — the open-source methodology would silently lose its permission machinery at the next release.
|
|
66
65
|
'modules/hall-ui/',
|
|
67
66
|
'modules/status-ui/',
|
|
@@ -213,7 +212,6 @@ const INSTANCE_EXCLUDE = [
|
|
|
213
212
|
'config/devbox-app.json',
|
|
214
213
|
'config/hierarchy.json',
|
|
215
214
|
'config/government.json',
|
|
216
|
-
'config/governance.json', // pre-ADR-0174 spelling — a live instance may still carry one; it is host content and stays denied
|
|
217
215
|
'CODEOWNERS', // names the owner (a protected surface, excluded as identity)
|
|
218
216
|
// instance SEED migrations — this host's scope-truth SQL (planning seeds, world data).
|
|
219
217
|
// ADR 0105: a consumer's core must not carry them; migrate.sh reads instance seeds from
|
|
@@ -216,7 +216,7 @@ const TASKS = [
|
|
|
216
216
|
title: 'Governance API routes (roles/perms/assignments CRUD)',
|
|
217
217
|
description: `**Why:** the tab's server surface.
|
|
218
218
|
|
|
219
|
-
**Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`
|
|
219
|
+
**Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`government.manage\` (Archon), system permissions rejected as non-grantable server-side, and pinned in route-rank-check. Depends on R97 + R109.`,
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
ref: 'R111', kind: 'feature', priority: 3, est_minutes: 180, manual_degree: 2, credits_reward: 55,
|