@bongos/core 1.21.92 → 1.21.93
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 +43 -33
- package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +1 -0
- package/docs/module-api-changelog.md +7 -0
- package/modules/autonomy/routes/autonomy.js +14 -5
- package/modules/government/decision-board.js +13 -3
- package/modules/government/decision-rules.js +61 -13
- package/modules/lifecycle/routes/done-when.js +2 -2
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/modules/lifecycle/routes/goals.js +4 -4
- package/modules/lifecycle/routes/project-settings.js +2 -2
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/modules/lifecycle/routes/versions.js +1 -1
- package/modules/npm-release/routes/release.js +1 -0
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/provisioning/routes/core-upgrade.js +11 -0
- package/modules/render-deploy/routes/act.js +1 -1
- package/modules/security/routes/reports.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/fitness.js +7 -0
- package/src/bongos/act-alone.js +84 -0
- package/src/bongos/routes/builders.js +6 -1
- package/src/module-api.js +7 -1
- package/tests/autonomy_fence_priority.mjs +3 -1
- package/tests/government_act_alone_buildings.mjs +430 -0
- package/tests/module_api.mjs +1 -0
- package/tests/npm_release_release.mjs +2 -1
- package/tests/ship_mode.mjs +2 -1
|
@@ -58,7 +58,7 @@ const express = require('express');
|
|
|
58
58
|
// core file a module may require, ADR 0083); approval-broadcast travels WITH this
|
|
59
59
|
// admission route (its sole consumer) and is an intra-module require.
|
|
60
60
|
const api = require('../../../src/module-api');
|
|
61
|
-
const auth = { requireBuilder: api.requireBuilder, requirePermission: api.requirePermission };
|
|
61
|
+
const auth = { requireBuilder: api.requireBuilder, requirePermission: api.requirePermission, requireActAlone: api.requireActAlone };
|
|
62
62
|
const { pool } = api;
|
|
63
63
|
const { validateOrRespond, parseId, accountExistenceReadRateLimit } = api;
|
|
64
64
|
// A recruit invite's door closes 30 days after it is sent, computed at read time
|
|
@@ -486,7 +486,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
486
486
|
// the SET value. Postgres infers text from the assignment anyway, but no real
|
|
487
487
|
// engine ever executes this statement — there is no integration test for this
|
|
488
488
|
// route and CI has no Postgres — so the type is stated rather than relied on.
|
|
489
|
-
router.patch('/access-requests/:id', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
489
|
+
router.patch('/access-requests/:id', auth.requireBuilder, auth.requirePermission('access_request.review'), auth.requireActAlone('people'), async (req, res) => {
|
|
490
490
|
const id = parseId(req, res, { code: 'bad_request_id' });
|
|
491
491
|
if (id === null) return;
|
|
492
492
|
if (validateOrRespond(req, res, {
|
|
@@ -567,7 +567,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
567
567
|
// pending request was resolved; 200 already_invited when an invite already
|
|
568
568
|
// stands; 409 already_member when they have an account.
|
|
569
569
|
// rank: archon — the same gate as resolving a request.
|
|
570
|
-
router.post('/access-requests/invite', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
570
|
+
router.post('/access-requests/invite', auth.requireBuilder, auth.requirePermission('access_request.review'), auth.requireActAlone('people'), async (req, res) => {
|
|
571
571
|
if (validateOrRespond(req, res, {
|
|
572
572
|
github_login: { required: true, type: 'string', minLength: 1, maxLength: 60 },
|
|
573
573
|
note: { type: 'string', maxLength: 500 },
|
|
@@ -62,6 +62,15 @@ function contributeCoreDesk() {
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// WHO DECIDES AT RELEASE (task 1004657; ADR 0363 D5). The pin move and the
|
|
66
|
+
// update rule are Release's own acts when the PIN door admitted the caller: while
|
|
67
|
+
// the project's decision-rules row hands Release to a sitting, one builder acting
|
|
68
|
+
// alone is refused (409 decided_by_sitting). Not a lone Release act, so never
|
|
69
|
+
// asked: a tenant project's own owner moving THEIR project (ADR 0339 — that
|
|
70
|
+
// project's decision, not this one's), and a dry run, which only reads.
|
|
71
|
+
const notThisProjectsRelease = (req) => req.coreUpgradeAuthority === 'owner';
|
|
72
|
+
const notADecision = (req) => notThisProjectsRelease(req) || (!!req.body && req.body.dry_run === true);
|
|
73
|
+
|
|
65
74
|
module.exports = function buildCoreUpgradeRouter() {
|
|
66
75
|
contributeCoreDesk();
|
|
67
76
|
const router = express.Router();
|
|
@@ -95,6 +104,7 @@ module.exports = function buildCoreUpgradeRouter() {
|
|
|
95
104
|
'/provisioning/instances/:id/core-upgrade',
|
|
96
105
|
api.requireBuilder,
|
|
97
106
|
requireOwnProjectOrPin({ pinGate: api.requirePermission('core.pin.move'), provisioning, pool: api.pool, log }),
|
|
107
|
+
api.requireActAlone('release', { unless: notADecision }),
|
|
98
108
|
makeCoreUpgradeHandler({
|
|
99
109
|
provisioning,
|
|
100
110
|
pool: api.pool,
|
|
@@ -130,6 +140,7 @@ module.exports = function buildCoreUpgradeRouter() {
|
|
|
130
140
|
'/provisioning/instances/:id/update-channel',
|
|
131
141
|
api.requireBuilder,
|
|
132
142
|
requireOwnProjectOrPin({ pinGate: api.requirePermission('core.pin.move'), provisioning, pool: api.pool, log }),
|
|
143
|
+
api.requireActAlone('release', { unless: notThisProjectsRelease }),
|
|
133
144
|
makeUpdateChannelHandler({
|
|
134
145
|
provisioning,
|
|
135
146
|
pool: api.pool,
|
|
@@ -26,7 +26,7 @@ const log = api.logger('render-deploy');
|
|
|
26
26
|
module.exports = function buildRenderDeployActRouter({ makeClient = defaultMakeClient, pool } = {}) {
|
|
27
27
|
const router = express.Router();
|
|
28
28
|
|
|
29
|
-
router.post('/render-deploy/act', api.requireBuilder, api.requirePermission('core.pin.move'), async (req, res) => {
|
|
29
|
+
router.post('/render-deploy/act', api.requireBuilder, api.requirePermission('core.pin.move'), api.requireActAlone('release'), async (req, res) => {
|
|
30
30
|
const db = pool || api.pool;
|
|
31
31
|
// Shape only, and strict: an undeclared field is refused. `confirm` and `expectLiveDeployId`
|
|
32
32
|
// are left to the checks below so each gets its own plain refusal rather than a schema one.
|
|
@@ -217,6 +217,7 @@ module.exports = function buildSecurityReportsRouter() {
|
|
|
217
217
|
'/security/reports/:id/confirm',
|
|
218
218
|
api.requireBuilder,
|
|
219
219
|
api.requirePermission('security.report.adjudicate'),
|
|
220
|
+
api.requireActAlone('disputes'),
|
|
220
221
|
async (req, res) => {
|
|
221
222
|
// drachmae_awarded is OPTIONAL and deliberately UNTYPED here: the handler
|
|
222
223
|
// accepts a number, a stringified number, '' and null alike, and the inline
|
|
@@ -256,6 +257,7 @@ module.exports = function buildSecurityReportsRouter() {
|
|
|
256
257
|
'/security/reports/:id/dispute',
|
|
257
258
|
api.requireBuilder,
|
|
258
259
|
api.requirePermission('security.report.adjudicate'),
|
|
260
|
+
api.requireActAlone('disputes'),
|
|
259
261
|
async (req, res) => {
|
|
260
262
|
const id = parseId(req, res, { code: 'bad_report_id', positiveInt: true });
|
|
261
263
|
if (id === null) return;
|
|
@@ -281,6 +283,7 @@ module.exports = function buildSecurityReportsRouter() {
|
|
|
281
283
|
'/security/reports/:id/fix',
|
|
282
284
|
api.requireBuilder,
|
|
283
285
|
api.requirePermission('security.report.adjudicate'),
|
|
286
|
+
api.requireActAlone('disputes'),
|
|
284
287
|
async (req, res) => {
|
|
285
288
|
const id = parseId(req, res, { code: 'bad_report_id', positiveInt: true });
|
|
286
289
|
if (id === null) return;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.93",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.93",
|
|
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.21.
|
|
3
|
+
"version": "1.21.93",
|
|
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
|
@@ -9382,5 +9382,11 @@
|
|
|
9382
9382
|
"id": "1004648",
|
|
9383
9383
|
"text": "Starting a project is now one page instead of six screens. After signing in, a founder sees \"Make something.\" with three parts: their project (name, kind, a sentence and four quick checks), a short summary of how it will be ru"
|
|
9384
9384
|
}
|
|
9385
|
+
],
|
|
9386
|
+
"1.21.93": [
|
|
9387
|
+
{
|
|
9388
|
+
"id": "1004657",
|
|
9389
|
+
"text": "When a project votes that a part of its government is decided by a group, that vote now holds on every page, not only the People page. Before, someone with the right permission could still switch unattended building on, sign o"
|
|
9390
|
+
}
|
|
9385
9391
|
]
|
|
9386
9392
|
}
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -609,6 +609,12 @@ const KERNEL_FILES = [
|
|
|
609
609
|
// this roster reads as a kernel->non-kernel leak. It imports only node built-ins
|
|
610
610
|
// (https, zlib), so joining costs nothing.
|
|
611
611
|
'src/bongos/package-registry.js',
|
|
612
|
+
// task 1004657 (ADR 0363 D5): requireActAlone, the gate a building's own page mounts
|
|
613
|
+
// after its permission gate — the same trust-gate category as auth.js, re-exported
|
|
614
|
+
// by the doorway, so leaving it off reads as a kernel->non-kernel leak. It imports
|
|
615
|
+
// only module-seams.js and logger.js (both on this roster) and reaches the
|
|
616
|
+
// government module through its port, never by require.
|
|
617
|
+
'src/bongos/act-alone.js',
|
|
612
618
|
];
|
|
613
619
|
|
|
614
620
|
// Kernel files exempt from the no-module-keys scan (Check 10) because naming a
|
|
@@ -641,6 +647,7 @@ const KERNEL_NAMES_MODULES_OK = new Set([
|
|
|
641
647
|
'src/bongos/auth-github.js', // ticks the 'onboarding' port on first sign-in
|
|
642
648
|
'src/bongos/auth-idp.js', // same, on the federated path
|
|
643
649
|
'src/bongos/auth-discord.js', // the Discord account-link flow itself
|
|
650
|
+
'src/bongos/act-alone.js', // resolves the 'government' port by key for checkActAlone (task 1004657)
|
|
644
651
|
'src/bongos/permission-path-check.js', // names module path subtrees to PROTECT them
|
|
645
652
|
'src/bongos/route-rank-check.js', // names module route dirs to rank-gate them
|
|
646
653
|
'src/role-responsibilities.js', // the ideator's VERBATIM statement reads "Make good ideas" — the English word, colliding with the `ideas` key. Owner text; unparaphrasable.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/act-alone.js — WHO DECIDES WHAT, on every building's own page
|
|
4
|
+
// (task 1004657, goal 1000111; ADR 0363 D5 and its Consequences).
|
|
5
|
+
//
|
|
6
|
+
// WHY. A project's decision-rules table (modules/government/decision-rules.js)
|
|
7
|
+
// can hand a building to a sitting: "Release is decided by the council, by
|
|
8
|
+
// majority". Until this file, only People's own routes asked the table before
|
|
9
|
+
// letting one builder act; every other building's page still let whoever held
|
|
10
|
+
// its permission act alone, so a passed amendment changed the board and not
|
|
11
|
+
// the building. requireActAlone(area) is the one gate those pages mount, AFTER
|
|
12
|
+
// their permission gate, so the permission stays the coarse wall and the row
|
|
13
|
+
// only narrows who may use it alone (never wider — ADR 0363 D5).
|
|
14
|
+
//
|
|
15
|
+
// THE REFUSAL is People's, exactly (modules/government/routes/government.js,
|
|
16
|
+
// peopleMayActAlone): 409 `decided_by_sitting` with { area, rule, who,
|
|
17
|
+
// what_this_means, open_with }, so the hall renders one sentence for every
|
|
18
|
+
// building and the builder is pointed at the sitting (POST
|
|
19
|
+
// /government/board/matters) instead.
|
|
20
|
+
//
|
|
21
|
+
// FAIL DIRECTIONS, mirroring checkActAlone and People:
|
|
22
|
+
// • no government module, or a port without checkActAlone → AS TODAY (the
|
|
23
|
+
// route's own permission is the whole gate). A malformed or missing row
|
|
24
|
+
// already reads as its as-today default inside checkActAlone (D2).
|
|
25
|
+
// • the table cannot be read at all (checkActAlone throws), or the answer is
|
|
26
|
+
// not one this gate knows → 503 `decision_rule_unreadable`, fail-CLOSED, as
|
|
27
|
+
// People does: an unreadable rule never waves a lone act through.
|
|
28
|
+
//
|
|
29
|
+
// A PASSED MATTER IS NOT A LONE ACT. When a sitting passes a matter, the owning
|
|
30
|
+
// module carries it out through its 'governor.matterAct' contribution, which
|
|
31
|
+
// calls the route's underlying write directly (modules/autonomy/matter-acts.js,
|
|
32
|
+
// modules/lifecycle/matter-acts.js, the Treasury act in
|
|
33
|
+
// src/bongos/routes/builders.js) and never passes through this middleware — the
|
|
34
|
+
// sitting is the authority. tests/government_act_alone_buildings.mjs pins that.
|
|
35
|
+
//
|
|
36
|
+
// `unless(req)` exempts a request that is not a decision at all (a dry run that
|
|
37
|
+
// only reads, the owner door of a tenant pin move) or a BRAKE (switching
|
|
38
|
+
// unattended building off, pausing one runner): stopping a machine never waits
|
|
39
|
+
// for a meeting. Pass a named function — the route classifier reads the line.
|
|
40
|
+
//
|
|
41
|
+
// Server-side, per request, uncached (ADR 0016). Core and modules reach the
|
|
42
|
+
// government module only through its port, never by require (ADR 0083).
|
|
43
|
+
|
|
44
|
+
const seams = require('../module-seams');
|
|
45
|
+
const log = require('./logger').logger.child({ src: 'act-alone' });
|
|
46
|
+
|
|
47
|
+
function logError(message) {
|
|
48
|
+
try {
|
|
49
|
+
log.error(message);
|
|
50
|
+
} catch (_) { /* logging must never decide a request */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireActAlone(area, { unless = null } = {}) {
|
|
54
|
+
if (typeof area !== 'string' || area.length === 0) {
|
|
55
|
+
throw new TypeError('requireActAlone(area): name the building whose row decides this route');
|
|
56
|
+
}
|
|
57
|
+
if (unless !== null && typeof unless !== 'function') {
|
|
58
|
+
throw new TypeError('requireActAlone(area, { unless }): unless must be a function of the request');
|
|
59
|
+
}
|
|
60
|
+
return async function actAloneGate(req, res, next) {
|
|
61
|
+
if (unless && unless(req) === true) return next();
|
|
62
|
+
const government = seams.resolveOptional('government');
|
|
63
|
+
if (!government || typeof government.checkActAlone !== 'function') return next();
|
|
64
|
+
let out;
|
|
65
|
+
try {
|
|
66
|
+
out = await government.checkActAlone(area, req.builder && req.builder.id);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
logError(`${area} decision rule unreadable: ${err && err.message}`);
|
|
69
|
+
return res.fail('decision_rule_unreadable', { status: 503, message: `who decides at ${area} could not be read just now` });
|
|
70
|
+
}
|
|
71
|
+
if (out && out.ok === true) return next();
|
|
72
|
+
if (!out || out.reason !== 'decided_by_sitting') {
|
|
73
|
+
// unknown_area is a wiring bug in the route, not a fact about the caller.
|
|
74
|
+
logError(`requireActAlone('${area}') got ${out && out.reason} — refusing fail-closed`);
|
|
75
|
+
return res.fail('decision_rule_unreadable', { status: 503, message: `who decides at ${area} could not be read just now` });
|
|
76
|
+
}
|
|
77
|
+
return res.fail('decided_by_sitting', 409, {
|
|
78
|
+
area: out.area, rule: out.rule, who: out.who,
|
|
79
|
+
what_this_means: out.what_this_means, open_with: out.open_with,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { requireActAlone };
|
|
@@ -26,6 +26,11 @@ const path = require('node:path');
|
|
|
26
26
|
const { execFile } = require('node:child_process');
|
|
27
27
|
const express = require('express');
|
|
28
28
|
const auth = require('../auth');
|
|
29
|
+
// WHO DECIDES AT TREASURY (task 1004657; ADR 0363 D5): the budget write refuses a
|
|
30
|
+
// lone act while Treasury's decision-rules row names a sitting. A passed Treasury
|
|
31
|
+
// matter sets the limit through setMonthlyBudget directly (the governor.matterAct
|
|
32
|
+
// below), never through the route, so the sitting is never refused by its own gate.
|
|
33
|
+
const { requireActAlone } = require('../act-alone');
|
|
29
34
|
const db = require('../db');
|
|
30
35
|
const { validateOrRespond, LIMITS, parseId } = require('./_helpers');
|
|
31
36
|
// The onboarding state machine carved into modules/onboarding/ (BV1.R81 / ADR
|
|
@@ -398,7 +403,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
398
403
|
// rank: archon — changes another builder's cost ceiling. NOT ownership-scoped:
|
|
399
404
|
// archon-only admin op. Gate: requirePermission('builder.budget.set') — BV1.R104;
|
|
400
405
|
// the seeded rank-role makes the permission and the old rank floor identical (ADR 0151).
|
|
401
|
-
router.patch('/builders/:id/budget', auth.requireBuilder, auth.requirePermission('builder.budget.set'), async (req, res) => {
|
|
406
|
+
router.patch('/builders/:id/budget', auth.requireBuilder, auth.requirePermission('builder.budget.set'), requireActAlone('treasury'), async (req, res) => {
|
|
402
407
|
const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
|
|
403
408
|
if (targetId === null) return;
|
|
404
409
|
if (validateOrRespond(req, res, { monthly_budget_usd: {} })) return; // number ≥0, or null to clear; validated below
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.93'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -133,6 +133,12 @@ module.exports = {
|
|
|
133
133
|
// the equivalent requireRank admitted, so a migrated module route is
|
|
134
134
|
// behaviour-identical (BV1.R105).
|
|
135
135
|
get requirePermission() { return require('./bongos/auth').requirePermission; },
|
|
136
|
+
// requireActAlone(area, { unless }) — WHO DECIDES WHAT on a building's own
|
|
137
|
+
// page (task 1004657; ADR 0363 D5). Mounted AFTER the route's permission
|
|
138
|
+
// gate: while the project's decision-rules row hands that building to a
|
|
139
|
+
// sitting, one builder acting alone is refused (409 decided_by_sitting,
|
|
140
|
+
// pointing at the matter route). No government module → as today.
|
|
141
|
+
get requireActAlone() { return require('./bongos/act-alone').requireActAlone; },
|
|
136
142
|
get rankMeetsThreshold() { return require('./bongos/auth').rankMeetsThreshold; },
|
|
137
143
|
get requireBfgPrincipal() { return require('./bongos/auth').requireBfgPrincipal; },
|
|
138
144
|
// allowBoxScope — DEPRECATED no-op, kept so the doorway loses no export inside
|
|
@@ -64,8 +64,10 @@ test('POST /autonomy/fence/priority sits behind the same owner-only atom as the
|
|
|
64
64
|
const layer = router.stack.find((l) => l.route && l.route.path === '/autonomy/fence/priority');
|
|
65
65
|
assert.ok(layer, 'the route is registered');
|
|
66
66
|
assert.equal(layer.route.methods.post, true);
|
|
67
|
-
assert.equal(layer.route.stack.length,
|
|
67
|
+
assert.equal(layer.route.stack.length, 4, 'requireBuilder + requirePermission + requireActAlone + handler');
|
|
68
68
|
assert.equal(layer.route.stack[1].handle.permKey, 'autonomy.fence.manage');
|
|
69
|
+
// task 1004657: Automation's decision rule is asked after the permission gate.
|
|
70
|
+
assert.equal(layer.route.stack[2].handle.name, 'actAloneGate');
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
test('the route sets, clears, and refuses a goal that is not allowlisted', async () => {
|