@bongos/core 1.24.1 → 1.24.3
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 -43
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +6 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +140 -1
- package/docs/api-reference.md +5 -2
- package/docs/copy-inventory.md +11 -6
- package/docs/copy-registry.json +63 -8
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +35 -26
- package/modules/economy/governor-docket.js +12 -6
- package/modules/government/access-routes.js +70 -0
- package/modules/government/access.js +88 -0
- package/modules/government/asks.js +5 -2
- package/modules/government/db.js +38 -0
- package/modules/government/docket.js +12 -2
- package/modules/government/migrations/government_036_access_holders.sql +33 -0
- package/modules/government/routes/government.js +9 -0
- package/modules/government/routing.js +11 -3
- package/modules/government/waiting.js +4 -0
- package/modules/hall-ui/public/blockers-by-person.js +2 -0
- package/modules/hall-ui/public/government-access.js +108 -0
- package/modules/hall-ui/public/government.html +21 -0
- package/modules/hall-ui/public/street-inside.js +1 -1
- package/modules/hall-ui/records/blockers-people.md +14 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/government-walk.js +3 -0
- package/src/bongos/route-rank-check.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/access_holders.mjs +214 -0
- package/tests/government_board_membership.mjs +4 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/government_seed.mjs +1 -0
- package/tests/governor_docket.mjs +7 -3
- package/tests/hall_street_inside.mjs +2 -2
|
@@ -44,10 +44,12 @@ const MARKUP = /\*\*|`|^#|\[[^\]]*\]\(|https?:\/\/|<\/?[a-z]/im;
|
|
|
44
44
|
// Control characters other than tab and line breaks (line breaks have their own rule).
|
|
45
45
|
// eslint-disable-next-line no-control-regex
|
|
46
46
|
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
47
|
+
const access = require('./access');
|
|
47
48
|
const BUILDINGS = Object.freeze(['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury']);
|
|
48
49
|
// The seats an ask can wait on (gov-living.js seatResolver's grammar). An
|
|
49
50
|
// unseated building falls to the owner when it is routed (owner ruling).
|
|
50
|
-
|
|
51
|
+
// `access:<key>` (task 1005000): whoever holds that outside access (access.js).
|
|
52
|
+
const SEAT = new RegExp(`^(owner|(lead|board):(${BUILDINGS.join('|')})|committee:(${BUILDINGS.join('|')})\\.[a-z][a-z0-9_]{0,31}|${access.SEAT_PART})$`);
|
|
51
53
|
// An answer's key: what a tap sends. Its act, optional: the matter's own act it
|
|
52
54
|
// takes ('go ahead', 'record', 'force:tested_by_hand'); the deck maps it onto
|
|
53
55
|
// the desk row's real route and refuses one the row does not offer.
|
|
@@ -150,7 +152,7 @@ function checkAsk(raw) {
|
|
|
150
152
|
// ---- who it waits on: a seat the direction desk can route ----
|
|
151
153
|
const seat = str(c.waits_on).trim();
|
|
152
154
|
if (!seat) err('waits_on', 'missing', 'Say who this waits on.');
|
|
153
|
-
else if (!SEAT.test(seat)) err('waits_on', 'unknown_seat', `"${seat.slice(0, 40)}" is not a seat: say owner, lead:<building>, committee:<building>.<name> or
|
|
155
|
+
else if (!SEAT.test(seat)) err('waits_on', 'unknown_seat', `"${seat.slice(0, 40)}" is not a seat: say owner, lead:<building>, committee:<building>.<name>, board:<building> or access:<key>.`);
|
|
154
156
|
|
|
155
157
|
// ---- what happens if nobody answers ----
|
|
156
158
|
lineRule('if_nobody_answers', c.if_nobody_answers, 'Say what happens if nobody answers.');
|
|
@@ -206,6 +208,7 @@ const ASK_REQUIRED_MESSAGE = 'An AI session files a blocker with an ask: a quest
|
|
|
206
208
|
function seatWords(seat) {
|
|
207
209
|
const s = str(seat);
|
|
208
210
|
if (s === 'owner') return 'the owner';
|
|
211
|
+
if (/^access:/.test(s)) return access.accessLabel(s.slice(7));
|
|
209
212
|
const m = /^(lead|board|committee):([a-z]+)(?:\.([a-z0-9_]+))?$/.exec(s);
|
|
210
213
|
if (!m) return s;
|
|
211
214
|
const building = m[2].charAt(0).toUpperCase() + m[2].slice(1);
|
package/modules/government/db.js
CHANGED
|
@@ -888,6 +888,41 @@ async function listActiveArchons() {
|
|
|
888
888
|
return rows;
|
|
889
889
|
}
|
|
890
890
|
|
|
891
|
+
// ACCESS HOLDERS (task 1005000, government_036): who holds each outside access.
|
|
892
|
+
// Active builders only — someone who left the project is nobody to ask. A record
|
|
893
|
+
// of who to ask, never authority: no gate reads these rows (access.js).
|
|
894
|
+
async function listAccessHolders() {
|
|
895
|
+
const { rows } = await pool.query(
|
|
896
|
+
`SELECT h.access_key, h.builder_id, h.granted_at, b.github_login, b.display_name
|
|
897
|
+
FROM government_access_holders h
|
|
898
|
+
JOIN builders b ON b.id = h.builder_id
|
|
899
|
+
WHERE b.status = 'active' AND b.system_role IS NULL
|
|
900
|
+
ORDER BY h.access_key, h.granted_at, h.builder_id`,
|
|
901
|
+
);
|
|
902
|
+
return rows;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Record that a builder holds an access. Idempotent: an existing row is kept as it
|
|
906
|
+
// was (its original granted_at/by). → true when a row was added.
|
|
907
|
+
async function grantAccess(accessKey, builderId, grantedBy) {
|
|
908
|
+
const { rowCount } = await pool.query(
|
|
909
|
+
`INSERT INTO government_access_holders (access_key, builder_id, granted_by)
|
|
910
|
+
VALUES ($1, $2, $3)
|
|
911
|
+
ON CONFLICT (access_key, builder_id) DO NOTHING`,
|
|
912
|
+
[accessKey, builderId, grantedBy],
|
|
913
|
+
);
|
|
914
|
+
return rowCount > 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// → true when a row was removed.
|
|
918
|
+
async function revokeAccess(accessKey, builderId) {
|
|
919
|
+
const { rowCount } = await pool.query(
|
|
920
|
+
'DELETE FROM government_access_holders WHERE access_key = $1 AND builder_id = $2',
|
|
921
|
+
[accessKey, builderId],
|
|
922
|
+
);
|
|
923
|
+
return rowCount > 0;
|
|
924
|
+
}
|
|
925
|
+
|
|
891
926
|
// One builder of this project, or null: who a Lead appointment names. Only an
|
|
892
927
|
// active person (no machine identity) can be appointed.
|
|
893
928
|
async function getActiveBuilder(builderId) {
|
|
@@ -1306,6 +1341,9 @@ module.exports = {
|
|
|
1306
1341
|
applyLaws,
|
|
1307
1342
|
listLeads,
|
|
1308
1343
|
listActiveArchons,
|
|
1344
|
+
listAccessHolders,
|
|
1345
|
+
grantAccess,
|
|
1346
|
+
revokeAccess,
|
|
1309
1347
|
getActiveBuilder,
|
|
1310
1348
|
applyLeadAct,
|
|
1311
1349
|
listBuildouts,
|
|
@@ -86,6 +86,7 @@ const governmentConfig = require('./config');
|
|
|
86
86
|
const asksDb = require('./asks-db');
|
|
87
87
|
const routing = require('./routing');
|
|
88
88
|
const waiting = require('./waiting');
|
|
89
|
+
const access = require('./access');
|
|
89
90
|
const { ownerOf } = require('./leads');
|
|
90
91
|
|
|
91
92
|
const log = api.logger('government');
|
|
@@ -840,8 +841,17 @@ async function docketFor(builder) {
|
|
|
840
841
|
// nothing — the docket then says `routed: false` rather than guess whose it is.
|
|
841
842
|
async function loadSeats(government) {
|
|
842
843
|
try {
|
|
843
|
-
|
|
844
|
-
|
|
844
|
+
// task 1005000: who holds each outside access. Its own read fails OPEN to nobody
|
|
845
|
+
// recorded (every access seat then falls to the owner), so a missing table on an
|
|
846
|
+
// instance that has not migrated yet never stops the City routing. Started inside
|
|
847
|
+
// a .then so a db layer without the function (an older stub, the walk's store)
|
|
848
|
+
// fails open too rather than throwing before the catch is attached.
|
|
849
|
+
const accessRows = Promise.resolve().then(() => db.listAccessHolders()).catch((err) => {
|
|
850
|
+
log.warn({ err: err && err.message }, 'governor docket: access holders unreadable, access seats fall to the owner');
|
|
851
|
+
return [];
|
|
852
|
+
});
|
|
853
|
+
const [leads, archons, held] = await Promise.all([db.listLeads(), db.listActiveArchons(), accessRows]);
|
|
854
|
+
return { leads, owner: ownerOf(archons), holders: (government && government.holders) || {}, access: access.holdersByKey(held) };
|
|
845
855
|
} catch (err) {
|
|
846
856
|
log.warn({ err: err && err.message }, 'governor docket: seats unreadable, nothing routed');
|
|
847
857
|
return null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- government_036_access_holders.sql — who holds the keys OUTSIDE Bongos, and an ask
|
|
2
|
+
-- that waits on them (task 1005000, goal 1000111).
|
|
3
|
+
--
|
|
4
|
+
-- WHAT THIS ADDS
|
|
5
|
+
--
|
|
6
|
+
-- government_access_holders — one row per (outside access, builder): the GitHub
|
|
7
|
+
-- repository's admin, the server, the domain's DNS, the hosting account, package
|
|
8
|
+
-- publishing, payments. The vocabulary is modules/government/access.js `ACCESS`,
|
|
9
|
+
-- held equal to the CHECK below by tests/access_holders.mjs. Written by the owner
|
|
10
|
+
-- only (the routes gate government.manage); who granted it and when ride the row.
|
|
11
|
+
--
|
|
12
|
+
-- It is a RECORD OF WHO TO ASK, never an authority grant: no permission atom, rank
|
|
13
|
+
-- check or gate reads this table, and it stores no credential — only who holds one.
|
|
14
|
+
--
|
|
15
|
+
-- government_asks.waits_on gains the seat `access:<key>`, so an ask can wait on
|
|
16
|
+
-- whoever holds an outside access (routing.js; nobody recorded → the owner).
|
|
17
|
+
|
|
18
|
+
BEGIN;
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS government_access_holders (
|
|
21
|
+
access_key text NOT NULL
|
|
22
|
+
CHECK (access_key IN ('github-admin', 'server-ssh', 'dns', 'hosting', 'npm-publish', 'payments')),
|
|
23
|
+
builder_id bigint NOT NULL REFERENCES builders(id) ON DELETE CASCADE,
|
|
24
|
+
granted_by bigint REFERENCES builders(id) ON DELETE SET NULL,
|
|
25
|
+
granted_at timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
PRIMARY KEY (access_key, builder_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
ALTER TABLE government_asks DROP CONSTRAINT IF EXISTS government_asks_waits_on_check;
|
|
30
|
+
ALTER TABLE government_asks ADD CONSTRAINT government_asks_waits_on_check
|
|
31
|
+
CHECK (waits_on ~ '^(owner|(lead|board):(charter|rules|disputes|people|planning|automation|review|release|treasury)|committee:(charter|rules|disputes|people|planning|automation|review|release|treasury)\.[a-z][a-z0-9_]{0,31}|access:(github-admin|server-ssh|dns|hosting|npm-publish|payments))$');
|
|
32
|
+
|
|
33
|
+
COMMIT;
|
|
@@ -34,6 +34,7 @@ const counts = require('../counts');
|
|
|
34
34
|
const asks = require('../asks');
|
|
35
35
|
const asksDb = require('../asks-db');
|
|
36
36
|
const routing = require('../routing');
|
|
37
|
+
const { accessHandlers } = require('../access-routes');
|
|
37
38
|
const governmentConfig = require('../config');
|
|
38
39
|
const membershipPredicate = require('../membership-predicate');
|
|
39
40
|
const positions = require('../positions');
|
|
@@ -689,6 +690,14 @@ module.exports = function buildGovernmentRouter() {
|
|
|
689
690
|
}
|
|
690
691
|
});
|
|
691
692
|
|
|
693
|
+
// ACCESS HOLDERS (task 1005000): who holds the keys outside Bongos, so an ask can wait
|
|
694
|
+
// on access:<key>. Read on the docket's atom; recorded by the owner. A record of who to
|
|
695
|
+
// ask, never authority (access.js). Bodies: access-routes.js.
|
|
696
|
+
const accessRoutes = accessHandlers({ db, log, parseId, validateOrRespond });
|
|
697
|
+
router.get('/government/access', api.requireBuilder, api.requirePermission('board.vote.cast'), accessRoutes.read);
|
|
698
|
+
router.post('/government/access/:accessKey/holders', api.requireBuilder, api.requirePermission('government.manage'), accessRoutes.grant);
|
|
699
|
+
router.delete('/government/access/:accessKey/holders/:builderId', api.requireBuilder, api.requirePermission('government.manage'), accessRoutes.revoke);
|
|
700
|
+
|
|
692
701
|
// rank: perm board.vote.cast — the Board Room view: open items with their
|
|
693
702
|
// clock, the OPEN BALLOT, the closed history (R16 of goal 1000069). Gated on
|
|
694
703
|
// the XENOS-floored vote atom, deliberately: seeing the ballot is part of
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
// request, uncached (ADR 0016), and routes every matter with routeAll.
|
|
39
39
|
|
|
40
40
|
const { answerFor } = require('./leads');
|
|
41
|
+
const access = require('./access');
|
|
41
42
|
const asks = require('./asks');
|
|
42
43
|
|
|
43
44
|
const BUILDING_NAMES = Object.freeze({
|
|
@@ -59,6 +60,7 @@ function seatOf(matter) {
|
|
|
59
60
|
function seatLabel(seat) {
|
|
60
61
|
const s = String(seat || '');
|
|
61
62
|
if (s === 'owner') return 'the owner';
|
|
63
|
+
if (/^access:/.test(s)) return access.accessLabel(s.slice(7));
|
|
62
64
|
const m = /^(lead|board):([a-z]+)$/.exec(s);
|
|
63
65
|
if (m) return `the ${BUILDING_NAMES[m[2]] || m[2]} ${m[1] === 'lead' ? 'Lead' : 'board'}`;
|
|
64
66
|
return asks.seatWords(s);
|
|
@@ -87,7 +89,7 @@ function directoryOf(ctx) {
|
|
|
87
89
|
for (const l of Array.isArray(c.leads) ? c.leads : Object.values(c.leads || {})) {
|
|
88
90
|
if (l && !l.removed_at) put(l.builder_id, l.github_login, l.display_name);
|
|
89
91
|
}
|
|
90
|
-
for (const list of Object.values(c.holders || {})) {
|
|
92
|
+
for (const list of [...Object.values(c.holders || {}), ...Object.values(c.access || {})]) {
|
|
91
93
|
for (const p of Array.isArray(list) ? list : []) {
|
|
92
94
|
if (p && typeof p === 'object') put(p.builder_id ?? p.id, p.github_login ?? p.login, p.display_name ?? p.name);
|
|
93
95
|
}
|
|
@@ -103,7 +105,8 @@ function peopleOf(ids, dir) {
|
|
|
103
105
|
// seat the seat that answers it, after any fall
|
|
104
106
|
// builder_ids who sits there now ([] when nobody does)
|
|
105
107
|
// ctx { leads: Lead rows in office, owner: leads ownerOf(archons) shape
|
|
106
|
-
// { builders: [{ id }] }, holders: { building: [people] | null }
|
|
108
|
+
// { builders: [{ id }] }, holders: { building: [people] | null },
|
|
109
|
+
// access: { key: [holder rows] } (task 1005000) }
|
|
107
110
|
function resolveSeat(asked, ctx) {
|
|
108
111
|
const c = ctx || {};
|
|
109
112
|
const owner = c.owner || { builders: [] };
|
|
@@ -120,6 +123,11 @@ function resolveSeat(asked, ctx) {
|
|
|
120
123
|
const held = idsOf((c.holders || {})[m[1]]);
|
|
121
124
|
return held.length ? seated(asked, held) : toOwner();
|
|
122
125
|
}
|
|
126
|
+
// task 1005000: an outside access — its recorded holders; nobody recorded → the owner.
|
|
127
|
+
if ((m = /^access:([a-z-]+)$/.exec(String(asked || ''))) && access.isAccessKey(m[1])) {
|
|
128
|
+
const held = idsOf((c.access || {})[m[1]]);
|
|
129
|
+
return held.length ? seated(asked, held) : toOwner();
|
|
130
|
+
}
|
|
123
131
|
if ((m = /^committee:([a-z]+)\.[a-z0-9_]+$/.exec(String(asked || '')))) {
|
|
124
132
|
return { ...resolveSeat(`lead:${m[1]}`, c), asked };
|
|
125
133
|
}
|
|
@@ -178,7 +186,7 @@ function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
|
|
|
178
186
|
if (!m.ask) return { code: 'no_ask', status: 409, message: 'This matter has no ask to pass on yet.' };
|
|
179
187
|
if (m.source === 'board') return { code: 'vote_not_passable', status: 409, message: 'A vote is its voters\' to answer; it cannot be passed on.' };
|
|
180
188
|
const to = String(toSeat || '').trim();
|
|
181
|
-
if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name> or
|
|
189
|
+
if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name>, board:<building> or access:<key>.' };
|
|
182
190
|
const route = routeMatter(m, viewerId, ctx, now);
|
|
183
191
|
if (!route.yours) return { code: 'not_yours', status: 403, message: 'Only whoever this waits on can pass it on.' };
|
|
184
192
|
if (to === m.ask.waits_on || resolveSeat(to, ctx).seat === route.seat) return { code: 'same_seat', status: 409, message: 'It already waits on that seat.' };
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// PURE: no db. docket.js reads the blockers, their asks and the seats, then calls this.
|
|
22
22
|
|
|
23
23
|
const routing = require('./routing');
|
|
24
|
+
const access = require('./access');
|
|
24
25
|
|
|
25
26
|
// One blocker as a line under a person: what it is, where it lives, and the seat it
|
|
26
27
|
// reaches that person through, in words ("as the owner", "as the Planning Lead").
|
|
@@ -36,6 +37,9 @@ function itemOf(m, r) {
|
|
|
36
37
|
as: r.label,
|
|
37
38
|
asked: r.asked,
|
|
38
39
|
fell_to_owner: !!r.fell_to_owner,
|
|
40
|
+
// task 1005000: a blocker only an outside-access holder can clear reads as its own
|
|
41
|
+
// category ("needs repo-owner access"), not as one more of the owner's.
|
|
42
|
+
needs: /^access:/.test(String(r.asked || '')) ? access.needsWords(String(r.asked).slice(7)) : null,
|
|
39
43
|
question: m.ask && m.ask.question ? String(m.ask.question) : null,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
// "as the owner" · "as the Planning Lead" · "as the owner, since Planning has no Lead".
|
|
28
28
|
function asWords(item) {
|
|
29
|
+
// task 1005000: only whoever holds an outside access can clear it — say so first.
|
|
30
|
+
if (item.needs) return item.fell_to_owner ? item.needs + ', and nobody is recorded holding it' : item.needs;
|
|
29
31
|
var as = item.as || 'their seat';
|
|
30
32
|
if (item.fell_to_owner && /^lead:([a-z]+)$/.test(String(item.asked || ''))) {
|
|
31
33
|
var b = String(item.asked).slice(5);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// government-access.js — Outside access, on the Government page's Permissions tab
|
|
2
|
+
// (task 1005000).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Some blockers can only be cleared by whoever holds access OUTSIDE Bongos: the
|
|
5
|
+
// GitHub repository's admin, the server, DNS, hosting, package publishing, payments.
|
|
6
|
+
// The owner records who holds each here (GET/POST/DELETE /government/access), and an
|
|
7
|
+
// ask can then wait on `access:<key>` — it reaches that person, and reads on the
|
|
8
|
+
// Blockers page as "needs repo-owner access". An access nobody is recorded holding
|
|
9
|
+
// falls to the owner, like every unheld seat.
|
|
10
|
+
//
|
|
11
|
+
// A RECORD OF WHO TO ASK, NEVER A PERMISSION: holding one here unlocks nothing in
|
|
12
|
+
// Bongos, and no credential is stored — the page says so. The server gates the
|
|
13
|
+
// writes on government.manage; the Permissions tab is already sealed for everyone
|
|
14
|
+
// else (government.js sealRanksPanel), so this file draws no gate of its own.
|
|
15
|
+
//
|
|
16
|
+
// UMD so tests/access_holders.mjs can run `accessHtml` without a browser.
|
|
17
|
+
(function (root, factory) {
|
|
18
|
+
var mod = factory();
|
|
19
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = mod;
|
|
20
|
+
else root.OTBGovAccess = mod;
|
|
21
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// accessHtml(list, esc) → one row per access: its name, what it is, who holds it
|
|
25
|
+
// (each with a Remove control), or that it falls to the owner.
|
|
26
|
+
function accessHtml(list, esc) {
|
|
27
|
+
if (!Array.isArray(list) || !list.length) return '';
|
|
28
|
+
return '<ul class="ov-rows">' + list.map(function (a) {
|
|
29
|
+
var holders = Array.isArray(a.holders) ? a.holders : [];
|
|
30
|
+
var who = holders.length
|
|
31
|
+
? holders.map(function (h) {
|
|
32
|
+
return '<span class="ov-fact">' + esc(h.name || h.login || ('builder ' + h.id))
|
|
33
|
+
+ ' <button type="button" class="btn-ghost" data-act="revoke" data-key="' + esc(a.key) + '" data-id="' + esc(h.id) + '">Remove</button></span>';
|
|
34
|
+
}).join('')
|
|
35
|
+
: '<span class="ov-fact">Nobody recorded — it falls to the owner</span>';
|
|
36
|
+
return '<li class="ov-row"><div class="ov-row__main">'
|
|
37
|
+
+ '<span class="ov-row__title">' + esc(a.title || a.name) + '</span>'
|
|
38
|
+
+ '<span class="ov-row__sub">' + esc(a.what) + '</span>'
|
|
39
|
+
+ '<div class="ov-row__facts">' + who + '<span class="ov-mono">' + esc(a.seat) + '</span></div>'
|
|
40
|
+
+ '</div></li>';
|
|
41
|
+
}).join('') + '</ul>';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The Record form's picker: the same display name the list shows.
|
|
45
|
+
function optionsHtml(list, esc) {
|
|
46
|
+
return (Array.isArray(list) ? list : []).map(function (a) { return '<option value="' + esc(a.key) + '">' + esc(a.title || a.name) + '</option>'; }).join('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mount() {
|
|
50
|
+
var doc = typeof document !== 'undefined' ? document : null;
|
|
51
|
+
var box = doc && doc.getElementById('gov-access');
|
|
52
|
+
var form = doc && doc.getElementById('gov-access-form');
|
|
53
|
+
var select = doc && doc.getElementById('gov-access-key');
|
|
54
|
+
var esc = window.OTB && window.OTB.escapeHtml;
|
|
55
|
+
if (!box || !form || !select || typeof esc !== 'function') return;
|
|
56
|
+
var toast = function (m) { if (window.OTB && window.OTB.toast) window.OTB.toast(m, { duration: 3200 }); };
|
|
57
|
+
var call = function (method, path, body) {
|
|
58
|
+
return fetch('/api/bongos' + path, {
|
|
59
|
+
method: method,
|
|
60
|
+
credentials: 'same-origin',
|
|
61
|
+
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
62
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
63
|
+
}).then(function (r) {
|
|
64
|
+
return r.json().catch(function () { return {}; }).then(function (d) { return { ok: r.ok, status: r.status, data: d }; });
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
var said = function (r) { var e = r.data && r.data.error; return (e && e.message) || ('It did not save (' + r.status + ').'); };
|
|
68
|
+
|
|
69
|
+
function load() {
|
|
70
|
+
return call('GET', '/government/access').then(function (r) {
|
|
71
|
+
if (!r.ok || !r.data || !Array.isArray(r.data.access)) {
|
|
72
|
+
box.innerHTML = '<p class="ov-row__sub">Outside access could not be read just now.</p>';
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!select.options.length) {
|
|
76
|
+
select.innerHTML = optionsHtml(r.data.access, esc);
|
|
77
|
+
}
|
|
78
|
+
box.innerHTML = accessHtml(r.data.access, esc);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
box.addEventListener('click', function (ev) {
|
|
83
|
+
var btn = ev.target.closest && ev.target.closest('[data-act=revoke]');
|
|
84
|
+
if (!btn) return;
|
|
85
|
+
btn.disabled = true;
|
|
86
|
+
call('DELETE', '/government/access/' + encodeURIComponent(btn.dataset.key) + '/holders/' + encodeURIComponent(btn.dataset.id))
|
|
87
|
+
.then(function (r) { if (!r.ok) { toast(said(r)); btn.disabled = false; return null; } return load(); });
|
|
88
|
+
});
|
|
89
|
+
form.addEventListener('submit', function (ev) {
|
|
90
|
+
ev.preventDefault();
|
|
91
|
+
var id = Number(doc.getElementById('gov-access-builder').value);
|
|
92
|
+
call('POST', '/government/access/' + encodeURIComponent(select.value) + '/holders', { builder_id: id })
|
|
93
|
+
.then(function (r) {
|
|
94
|
+
if (!r.ok) { toast(said(r)); return null; }
|
|
95
|
+
form.reset();
|
|
96
|
+
return load();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
load();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
103
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount);
|
|
104
|
+
else mount();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { accessHtml: accessHtml, optionsHtml: optionsHtml };
|
|
108
|
+
}));
|
|
@@ -116,6 +116,26 @@
|
|
|
116
116
|
<div id="gov-assignments"><span class="kit-skel"></span></div>
|
|
117
117
|
</section>
|
|
118
118
|
|
|
119
|
+
<!-- task 1005000: who holds the keys OUTSIDE Bongos (GitHub admin, server, DNS,
|
|
120
|
+
hosting, publishing, payments), so an ask can wait on access:<key>.
|
|
121
|
+
Drawn by government-access.js. -->
|
|
122
|
+
<section class="scroll" aria-labelledby="gov-access-h">
|
|
123
|
+
<h2 class="scroll__h" id="gov-access-h">Outside access</h2>
|
|
124
|
+
<p class="scroll__lede">Who holds the keys outside the project, so a blocker that needs one reaches them. This is a record of who to ask: it grants nothing here and stores no password or token.</p>
|
|
125
|
+
<form class="ov-form" id="gov-access-form" autocomplete="off">
|
|
126
|
+
<div class="ov-field ov-field--sm">
|
|
127
|
+
<label class="ov-field__l" for="gov-access-builder">Builder id</label>
|
|
128
|
+
<input id="gov-access-builder" type="number" min="1" placeholder="e.g. 95" required>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="ov-field">
|
|
131
|
+
<label class="ov-field__l" for="gov-access-key">Access</label>
|
|
132
|
+
<select id="gov-access-key" required></select>
|
|
133
|
+
</div>
|
|
134
|
+
<button class="btn-accent" type="submit">Record</button>
|
|
135
|
+
</form>
|
|
136
|
+
<div id="gov-access"><span class="kit-skel"></span></div>
|
|
137
|
+
</section>
|
|
138
|
+
|
|
119
139
|
<section class="scroll" aria-labelledby="gov-principals-h">
|
|
120
140
|
<h2 class="scroll__h" id="gov-principals-h">Machine principals</h2>
|
|
121
141
|
<p class="scroll__lede">Capabilities held by a machine identity, never by a person or a rank. Shown for completeness; nothing here can be granted.</p>
|
|
@@ -143,5 +163,6 @@
|
|
|
143
163
|
BEFORE government.js, which destructures window.OTBBoardLib at IIFE entry. -->
|
|
144
164
|
<script src="/builders/board-lib.js?v=2026-09-10-boardlib1"></script>
|
|
145
165
|
<script src="/builders/government.js?v=2026-09-02-gov-rows"></script>
|
|
166
|
+
<script src="/builders/government-access.js"></script>
|
|
146
167
|
</body>
|
|
147
168
|
</html>
|
|
@@ -471,7 +471,7 @@
|
|
|
471
471
|
treasury: {
|
|
472
472
|
holds: 'The budget, and who gets paid for what.', rule: 'majority', cap: 7,
|
|
473
473
|
growth: 'Each new spending line takes a floor.', annex: 'room for an annex of spending lines',
|
|
474
|
-
floors: [['Builder rewards', '
|
|
474
|
+
floors: [['Builder rewards', 'credits earned for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting', desk: 'treasury.backup' }], ['Reserve', 'what is left this month', { reading: 'money', desk: 'treasury.budgets' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
|
|
475
475
|
},
|
|
476
476
|
});
|
|
477
477
|
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
@@ -22,3 +22,17 @@ show the seat alone — or, on /blockers, every blocker under "Requires project
|
|
|
22
22
|
fixture, because `tests/hall_preview_governor_fixtures.mjs` re-derives every route and fails on a guess.
|
|
23
23
|
|
|
24
24
|
Tests: `tests/blockers_people.mjs`.
|
|
25
|
+
|
|
26
|
+
## Outside access (task 1005000)
|
|
27
|
+
|
|
28
|
+
- **The Permissions tab gains *Outside access*** (`government-access.js`, `#gov-access`): the six keys
|
|
29
|
+
that live outside the project (repo owner, server access, domain settings, hosting account, package
|
|
30
|
+
publishing, payments), who is recorded holding each, a Remove control per holder, and a Record form.
|
|
31
|
+
An access nobody holds says "Nobody recorded — it falls to the owner". The tab is already sealed for
|
|
32
|
+
non-Archons; the server gates the writes on `government.manage`.
|
|
33
|
+
- **The lede says it grants nothing and stores no password or token** — the record is who to ask.
|
|
34
|
+
- **On the people view**, a blocker waiting on an access seat reads "needs repo-owner access" under its
|
|
35
|
+
holder (`asWords` reads the server's `needs`), and "…, and nobody is recorded holding it" when it fell
|
|
36
|
+
to the owner.
|
|
37
|
+
|
|
38
|
+
Tests: `tests/access_holders.mjs`.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.3",
|
|
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.24.
|
|
3
|
+
"version": "1.24.3",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -10801,5 +10801,21 @@
|
|
|
10801
10801
|
"id": "1004934",
|
|
10802
10802
|
"text": "We decided that Market Watch will search the web each week on the project's own AI key and under the owner's monthly cap, at about $4 to $5 per weekly check of 40 competitors."
|
|
10803
10803
|
}
|
|
10804
|
+
],
|
|
10805
|
+
"1.24.2": [
|
|
10806
|
+
{
|
|
10807
|
+
"id": "1005000",
|
|
10808
|
+
"text": "The project can now record who holds the keys that live outside it: the GitHub repository's admin, the server, the domain settings, the hosting account, package publishing and payments. The owner records them on the Government"
|
|
10809
|
+
},
|
|
10810
|
+
{
|
|
10811
|
+
"id": "1004970",
|
|
10812
|
+
"text": "The payments decision record now carries the owner's answers: credits can be tuned but not switched off, pay rules live on the settings page, and Verro is Verro Labs."
|
|
10813
|
+
}
|
|
10814
|
+
],
|
|
10815
|
+
"1.24.3": [
|
|
10816
|
+
{
|
|
10817
|
+
"id": "1005006",
|
|
10818
|
+
"text": "The Treasury screen now counts builder rewards in credits instead of dollars, and no longer says one credit is a dollar."
|
|
10819
|
+
}
|
|
10804
10820
|
]
|
|
10805
10821
|
}
|
|
@@ -388,6 +388,9 @@ function makeStore(catalog) {
|
|
|
388
388
|
}
|
|
389
389
|
return { enacted, repealed };
|
|
390
390
|
},
|
|
391
|
+
// Who holds each outside access (task 1005000): the walk records none, so every
|
|
392
|
+
// access seat falls to the owner, as it does on a project that has recorded none.
|
|
393
|
+
async listAccessHolders() { return []; },
|
|
391
394
|
// The owner seat: every active Archon, oldest id first.
|
|
392
395
|
async listActiveArchons() {
|
|
393
396
|
return [...s.builders.values()].filter((b) => isLive(b) && b.rank === 'archon')
|
|
@@ -304,6 +304,10 @@ const GOVERNMENT_PINS = {
|
|
|
304
304
|
// task 1004999: open blockers under the people they wait on, and the viewer's own.
|
|
305
305
|
'GET /government/blockers/people': 'perm:board.vote.cast',
|
|
306
306
|
'GET /government/blockers/for-me': 'perm:board.vote.cast',
|
|
307
|
+
// task 1005000: who holds the keys outside Bongos (read), recorded by the owner (write).
|
|
308
|
+
'GET /government/access': 'perm:board.vote.cast',
|
|
309
|
+
'POST /government/access/:accessKey/holders': 'perm:government.manage',
|
|
310
|
+
'DELETE /government/access/:accessKey/holders/:builderId': 'perm:government.manage',
|
|
307
311
|
// task 1004529: who decides what — a change to the decision-rules table is an
|
|
308
312
|
// amendment by another door (same atom as proposing one), and a building's
|
|
309
313
|
// matter is put to its sitting by the same atom. Only a passed amendment
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.3'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|