@bongos/core 1.19.936 → 1.19.938
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 +69 -44
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +2 -0
- package/docs/api/openapi.json +91 -2
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/self-host.md +1 -1
- package/modules/government/board.js +31 -0
- package/modules/government/db.js +33 -0
- package/modules/government/migrations/government_018_passed_items_index.sql +31 -0
- package/modules/government/routes/government.js +7 -0
- package/modules/hall-ui/public/deploy.js +1 -1
- package/modules/ideas/inbox.js +11 -0
- package/modules/ideas/migrations/ideas_010_goal_ratification.sql +103 -0
- package/modules/ideas/module.json +1 -0
- package/modules/ideas/ratify.js +288 -1
- package/modules/ideas/routes/ratify-goal.js +163 -0
- package/modules/ideas/templates.js +16 -4
- package/modules/lifecycle/goal-from-idea.js +131 -0
- package/modules/lifecycle/lifecycle.js +14 -0
- package/modules/provisioning/core-upgrade.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-authorship.sh +33 -0
- package/scripts/gds/db-backup-nightly.sh +1 -1
- package/scripts/gds/instance-apex.js +11 -1
- package/scripts/gds/provision-config.js +3 -2
- package/scripts/gds/provision-core-upgrade.js +7 -7
- package/scripts/gds/provision-process.js +2 -2
- package/scripts/gds/provision-repo.js +35 -10
- package/scripts/gds/provision-units.js +3 -3
- package/scripts/gds/provision.js +6 -3
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +10 -7
- package/tests/government_seed.mjs +1 -0
- package/tests/idea_goal_ratification.mjs +553 -0
- package/tests/provision.mjs +44 -7
- package/tests/provision_account_identity.mjs +16 -0
|
@@ -14,7 +14,7 @@ const crypto = require('node:crypto');
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const path = require('node:path');
|
|
17
|
-
const { CONFIG, REPO_ROOT, coreVersionSafe, provisionerBotEmail } = require('./provision-config.js');
|
|
17
|
+
const { CONFIG, REPO_ROOT, coreVersionSafe, provisionerBotEmail, provisionerBotName } = require('./provision-config.js');
|
|
18
18
|
// The repo-identity + owner-credential leaf (task 1003081). Carved OUT of this file so
|
|
19
19
|
// upgrade.js can reach these three without pulling provision*.js into the merge
|
|
20
20
|
// resolver's executed closure; re-exported below so every existing caller is unchanged.
|
|
@@ -86,10 +86,34 @@ function seedFirstVersionCmd(inst, { privileged = false } = {}) {
|
|
|
86
86
|
// It is the ADR 0108 DESTINATION model; these generators make provision.js drive the
|
|
87
87
|
// scripts/deploy/deploy-instance.sh runbook API-driven instead of by hand.
|
|
88
88
|
const CORE_PKG_DIR = 'node_modules/@bongos/core';
|
|
89
|
-
// The
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
89
|
+
// The plane's app user: the unix account that owns the control-plane checkout, owns the
|
|
90
|
+
// backup dir, runs the backup timers, and whose GROUP every instance account joins to read
|
|
91
|
+
// the checkout. One resolver, so those four can't drift apart.
|
|
92
|
+
//
|
|
93
|
+
// Derived, never named (task 1002719). It used to be the founder's own account as a
|
|
94
|
+
// literal, and a standup on any host that account did not exist on died `invalid user` at
|
|
95
|
+
// the backup step. PROVISION_APP_USER pins it; otherwise it is the account this runner
|
|
96
|
+
// executes as, which on a plane IS the app user (the runner unit runs as it), so the live
|
|
97
|
+
// box resolves exactly what the literal said. A runner driving a different box over SSH
|
|
98
|
+
// (remoteHost) should pin it. root is refused, not resolved: the instance
|
|
99
|
+
// accounts join this account's group, and group root is not a read grant to hand a tenant.
|
|
100
|
+
// Validated because it is interpolated into shell commands and unit files.
|
|
101
|
+
const UNIX_ACCOUNT_RE = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
102
|
+
function appUser({ env = process.env, userInfo = os.userInfo } = {}) {
|
|
103
|
+
let name = String(env.PROVISION_APP_USER || '').trim();
|
|
104
|
+
if (!name) {
|
|
105
|
+
try { name = userInfo().username; } catch { name = ''; } // a uid with no passwd entry
|
|
106
|
+
}
|
|
107
|
+
if (name === 'root' || !UNIX_ACCOUNT_RE.test(name)) {
|
|
108
|
+
const why = name === 'root' ? 'is root' : 'is not a usable unix account name';
|
|
109
|
+
throw new Error(`provisioning: the app user ${JSON.stringify(name)} ${why} — set PROVISION_APP_USER to the unprivileged account that owns this checkout`);
|
|
110
|
+
}
|
|
111
|
+
return name;
|
|
112
|
+
}
|
|
113
|
+
// The app user's PRIMARY group, looked up by the shell on the box the command runs on. It
|
|
114
|
+
// need not share the account's name (a service account created into `users`), so it is
|
|
115
|
+
// never spelled as the account name.
|
|
116
|
+
function appGroupExpr() { return `$(id -gn ${appUser()})`; }
|
|
93
117
|
function standaloneRoot(inst, { base = CONFIG.standaloneBase } = {}) { return path.posix.join(base, inst.slug); }
|
|
94
118
|
// WHERE THE INSTANCE THIS CORE SERVES ACTUALLY LIVES (task 1004123).
|
|
95
119
|
//
|
|
@@ -145,7 +169,7 @@ function standaloneRegenDocsCmd() { return `node ${CORE_PKG_DIR}/scripts/gds/reg
|
|
|
145
169
|
// ── Per-instance identity: its OWN unix account + its OWN password-authed PG role ──
|
|
146
170
|
// (task 1003369, audit ref B3 of the 2026-08-29 security audit.)
|
|
147
171
|
//
|
|
148
|
-
// THE HOLE THIS CLOSES. Every instance service used to run
|
|
172
|
+
// THE HOLE THIS CLOSES. Every instance service used to run as the one shared app user, and
|
|
149
173
|
// co-tenant is the DEFAULT shape — so on a shared box every instance ran as the SAME
|
|
150
174
|
// unix user. A unix user may read /proc/<pid>/environ of its own processes, which means
|
|
151
175
|
// any instance could read every SIBLING's environment: GITHUB_APP_PRIVATE_KEY, the
|
|
@@ -223,7 +247,7 @@ function ensureInstanceUserCmd(inst, { privileged = false } = {}) {
|
|
|
223
247
|
// NOT re-open the two vectors this task closes: /proc/<pid>/environ stays readable only
|
|
224
248
|
// by the process's own uid, and the DB now demands a password no sibling holds.
|
|
225
249
|
function grantInstanceRepoReadCmd(inst, { privileged = false } = {}) {
|
|
226
|
-
return `${privileged ? 'sudo ' : ''}usermod -aG ${
|
|
250
|
+
return `${privileged ? 'sudo ' : ''}usermod -aG ${appGroupExpr()} ${instanceUser(inst)}`;
|
|
227
251
|
}
|
|
228
252
|
|
|
229
253
|
// Can this plane give a child instance its OWN unix account AT ALL? (task 1004018, ADR 0317)
|
|
@@ -758,7 +782,7 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
758
782
|
} else {
|
|
759
783
|
writeFile(giPath, INSTANCE_GITIGNORE);
|
|
760
784
|
}
|
|
761
|
-
exec(`git -C ${root} config user.name "
|
|
785
|
+
exec(`git -C ${root} config user.name "${provisionerBotName()}"`);
|
|
762
786
|
exec(`git -C ${root} config user.email "${provisionerBotEmail()}"`);
|
|
763
787
|
exec(`git -C ${root} add -A`);
|
|
764
788
|
const commitMsg = mode === 'adopt'
|
|
@@ -1012,7 +1036,7 @@ function buildCorePinRefreshCommit({ root, fromVersion, toVersion, repo, ownerTo
|
|
|
1012
1036
|
: `git -C ${root} push origin main`;
|
|
1013
1037
|
return {
|
|
1014
1038
|
config: [
|
|
1015
|
-
`git -C ${root} config user.name "
|
|
1039
|
+
`git -C ${root} config user.name "${provisionerBotName()}"`,
|
|
1016
1040
|
`git -C ${root} config user.email "${provisionerBotEmail()}"`,
|
|
1017
1041
|
],
|
|
1018
1042
|
add: `git -C ${root} add -A`,
|
|
@@ -1094,7 +1118,8 @@ async function refreshStandaloneCorePin(inst, deps) {
|
|
|
1094
1118
|
// cwd, deploy-instance.sh §4); co-tenant/dedicated run this control-plane checkout's src/.
|
|
1095
1119
|
|
|
1096
1120
|
module.exports = {
|
|
1097
|
-
|
|
1121
|
+
appGroupExpr,
|
|
1122
|
+
appUser,
|
|
1098
1123
|
accountPlaneGap,
|
|
1099
1124
|
accountPreflight,
|
|
1100
1125
|
CORE_PKG_DIR,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
const { CONFIG, REPO_ROOT } = require('./provision-config.js');
|
|
14
|
-
const {
|
|
14
|
+
const { CORE_PKG_DIR, appUser, dbName, instanceDbRole, instanceStateDir, instanceUser, parseTargetRef, standaloneRoot } = require('./provision-repo.js');
|
|
15
15
|
|
|
16
16
|
function serviceUnit(inst, { etcBase = CONFIG.etcBase } = {}) {
|
|
17
17
|
const envFile = path.posix.join(etcBase, inst.slug, 'web.env');
|
|
@@ -103,8 +103,8 @@ Requires=postgresql.service
|
|
|
103
103
|
|
|
104
104
|
[Service]
|
|
105
105
|
Type=oneshot
|
|
106
|
-
User=${
|
|
107
|
-
Group
|
|
106
|
+
User=${appUser()}
|
|
107
|
+
# No Group=: systemd then uses the app user's primary group, whatever it is named.
|
|
108
108
|
# The DB to dump + where dumps land — set on the unit (not via the instance web.env)
|
|
109
109
|
# so the dump targets the right database with no fallback to a stale default (root
|
|
110
110
|
# cause of the task-2210 breakage). db-backup-nightly.sh reads both as env.
|
package/scripts/gds/provision.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// WOULD run and mutates nothing (no DB writes, no shell, no cloud call). The
|
|
27
27
|
// LIVE leg is an explicit operator/timer `--apply`.
|
|
28
28
|
// • PRIVILEGE model (task 2066, ADR 0128). The intent runner runs as a NON-root
|
|
29
|
-
// user (
|
|
29
|
+
// user (the app user) with passwordless sudo, which cannot itself write under /etc,
|
|
30
30
|
// reload systemd/Caddy, or peer-auth to Postgres as a superuser. PROVISION_SUDO=1
|
|
31
31
|
// escalates exactly those steps (sudo install / sudo systemctl / sudo -u postgres);
|
|
32
32
|
// default OFF = bare commands (root / test / co-tenant-legacy contexts unchanged).
|
|
@@ -50,7 +50,7 @@ const cp = require('node:child_process');
|
|
|
50
50
|
const crypto = require('node:crypto');
|
|
51
51
|
|
|
52
52
|
const { CONFIG, MANIFEST_UA, softFailResult, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
|
|
53
|
-
const {
|
|
53
|
+
const { accountPreflight, appGroupExpr, appUser, alreadyScaffolded, buildCorePinRefreshCommit, coreCheckoutHasGit, dbCreateCmd, dbName, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd, ensureInstanceUserCmd, generateDbPassword, grantInstanceRepoReadCmd, instanceDbRole, instanceStateDir, instanceUser, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
54
54
|
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
55
55
|
const { caddyBlock, caddySnippetPath, checkCaddyWiring, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, ingressPlaneGap, ingressPreflight, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
56
56
|
const { resolveProcessManager } = require('./provision-process.js');
|
|
@@ -282,6 +282,9 @@ async function provisionInstance(inst, deps) {
|
|
|
282
282
|
// useradd would crash mid-provision instead of refusing.
|
|
283
283
|
const accountGap = accountPreflight({ exec: boxExec, apply, log, shape: inst.hosting_shape, privileged, probe: deps.accountPlaneGap });
|
|
284
284
|
if (accountGap) return { ok: false, error: accountGap };
|
|
285
|
+
// App-user preflight (task 1002719): steps below name the app user, and appUser() refuses
|
|
286
|
+
// root or an unusable name. Refuse HERE, before the status flip, not midway through.
|
|
287
|
+
try { appUser(); } catch (e) { return { ok: false, error: e.message }; }
|
|
285
288
|
// Caddy preflight (task 1002860, ADR 0303): a box whose base Caddyfile has no tls-check ask
|
|
286
289
|
// can never certify this domain, so refuse before touching it. Injectable like the DNS check.
|
|
287
290
|
const caddyCheck = deps.checkCaddyWiring || checkCaddyWiring;
|
|
@@ -640,7 +643,7 @@ runcmd:
|
|
|
640
643
|
- [ bash, -lc, "${dbCreateCmd(i)} || true" ]
|
|
641
644
|
- [ bash, -lc, "cd ${REPO_ROOT} && ${migrateCmd(i)}" ]
|
|
642
645
|
- [ bash, -lc, "${seedFirstVersionCmd(i)}" ]
|
|
643
|
-
${roleLines} - [ bash, -lc, "install -d -o ${
|
|
646
|
+
${roleLines} - [ bash, -lc, "install -d -o ${appUser()} -g ${appGroupExpr()} -m 0750 ${CONFIG.backupDir}" ]
|
|
644
647
|
- [ bash, -lc, "systemctl daemon-reload && systemctl enable --now ${i.slug} ${backupUnitName(i)}.timer" ]
|
|
645
648
|
`;
|
|
646
649
|
}
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.938'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -38,6 +38,9 @@ const P = require('../modules/provisioning/provisioning.js');
|
|
|
38
38
|
const G = require('../modules/provisioning/control-plane-guard.js');
|
|
39
39
|
const RP = require('../scripts/gds/provision-repo.js');
|
|
40
40
|
|
|
41
|
+
// task 1002719: the app user is derived, so pin one that is nobody's real account.
|
|
42
|
+
process.env.PROVISION_APP_USER = 'appsvc';
|
|
43
|
+
|
|
41
44
|
let passed = 0;
|
|
42
45
|
let failed = 0;
|
|
43
46
|
const t = async (name, fn) => {
|
|
@@ -433,7 +436,7 @@ await t('the control plane upgrades as the operator account, never as root', asy
|
|
|
433
436
|
const d = { ...makeDeps({ served: '1.19.762' }), privileged: true };
|
|
434
437
|
await R.coreUpgradeInstance(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos', port: 3002 }), d, intent());
|
|
435
438
|
const cmd = upgradeCall(d);
|
|
436
|
-
assert.match(cmd, /sudo -u
|
|
439
|
+
assert.match(cmd, /sudo -u appsvc /, 'runs as the operator account');
|
|
437
440
|
assert.doesNotMatch(cmd, /sudo node /, 'a bare `sudo node` is the bug — it lands on root');
|
|
438
441
|
});
|
|
439
442
|
|
|
@@ -442,7 +445,7 @@ await t('a tenant upgrades as its OWN account, which is also its Postgres login
|
|
|
442
445
|
await R.coreUpgradeInstance(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), d, intent());
|
|
443
446
|
const cmd = upgradeCall(d);
|
|
444
447
|
assert.match(cmd, /sudo -u bongos-test4 /, 'the instance account, not the operator and not root');
|
|
445
|
-
assert.doesNotMatch(cmd, /sudo -u
|
|
448
|
+
assert.doesNotMatch(cmd, /sudo -u appsvc /, 'one tenant must never be upgraded under another account');
|
|
446
449
|
});
|
|
447
450
|
|
|
448
451
|
await t('the database is NAMED, not discovered — an adopted repo has no core at its root to ask', async () => {
|
|
@@ -691,7 +694,7 @@ await t('a tenant WITHOUT one falls back to the app user, and says so', () => {
|
|
|
691
694
|
// is reported rather than silently papered over.
|
|
692
695
|
const d = acctDeps({ accountExists: false });
|
|
693
696
|
const r = R.resolveRunAs(inst({ hosting_shape: 'standalone', slug: 'test4' }), d);
|
|
694
|
-
assert.equal(r.runAs, '
|
|
697
|
+
assert.equal(r.runAs, 'appsvc');
|
|
695
698
|
assert.equal(r.fellBack, true);
|
|
696
699
|
assert.equal(r.wanted, 'bongos-test4', 'and names the account that is missing');
|
|
697
700
|
});
|
|
@@ -699,7 +702,7 @@ await t('a tenant WITHOUT one falls back to the app user, and says so', () => {
|
|
|
699
702
|
await t('the control plane is never probed — it has no instance account by design', () => {
|
|
700
703
|
const d = acctDeps({ accountExists: false });
|
|
701
704
|
const r = R.resolveRunAs(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos' }), d);
|
|
702
|
-
assert.deepEqual({ runAs: r.runAs, fellBack: r.fellBack }, { runAs: '
|
|
705
|
+
assert.deepEqual({ runAs: r.runAs, fellBack: r.fellBack }, { runAs: 'appsvc', fellBack: false });
|
|
703
706
|
assert.equal(d.ranWith.filter((c) => /^id -u /.test(c.cmd)).length, 0, 'no probe was spent');
|
|
704
707
|
});
|
|
705
708
|
|
|
@@ -707,7 +710,7 @@ await t('the dry run is invoked as the account that actually exists', async () =
|
|
|
707
710
|
const d = acctDeps({ accountExists: false, dryRunStdout: ' ✓ pre-flight: ok\n' });
|
|
708
711
|
await R.preflightUpgrade(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), '1.19.762', d);
|
|
709
712
|
const cmd = (d.ranWith.find((c) => /upgrade\.js/.test(c.cmd)) || {}).cmd || '';
|
|
710
|
-
assert.match(cmd, /sudo -u
|
|
713
|
+
assert.match(cmd, /sudo -u appsvc /, `must not sudo to an account that does not exist — got: ${cmd}`);
|
|
711
714
|
assert.doesNotMatch(cmd, /bongos-test4/);
|
|
712
715
|
});
|
|
713
716
|
|
|
@@ -715,7 +718,7 @@ await t('the MOVE probes identically, so preview and move cannot diverge on the
|
|
|
715
718
|
const d = acctDeps({ accountExists: false });
|
|
716
719
|
await R.coreUpgradeInstance(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), d, intent());
|
|
717
720
|
const cmd = (d.ranWith.find((c) => /upgrade\.js/.test(c.cmd)) || {}).cmd || '';
|
|
718
|
-
assert.match(cmd, /sudo -u
|
|
721
|
+
assert.match(cmd, /sudo -u appsvc /, 'the move follows the same probe as the preview');
|
|
719
722
|
});
|
|
720
723
|
|
|
721
724
|
await t('an INCONCLUSIVE probe keeps the isolated account rather than quietly widening it', () => {
|
|
@@ -789,7 +792,7 @@ await t('a ✖ line still wins over the raw tail', async () => {
|
|
|
789
792
|
await t('a fallback is recorded on the verdict the owner reads', async () => {
|
|
790
793
|
const d = acctDeps({ accountExists: false, dryRunStdout: ' ✓ pre-flight: ok\n' });
|
|
791
794
|
const v = await R.preflightUpgrade(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), '1.19.762', d);
|
|
792
|
-
assert.equal(v.ran_as, '
|
|
795
|
+
assert.equal(v.ran_as, 'appsvc');
|
|
793
796
|
assert.match(v.account_note, /no bongos-test4 account yet/);
|
|
794
797
|
});
|
|
795
798
|
|
|
@@ -42,6 +42,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
42
42
|
'government_007_amendments.sql', // R14 amendment proposals table (task 1003027)
|
|
43
43
|
'government_008_closed_items_index.sql', // R16 closed-history index (task 1003029)
|
|
44
44
|
'government_017_board_section_keys_v2.sql', // BV2.R06 board section_key CHECK widening (task 1004085)
|
|
45
|
+
'government_018_passed_items_index.sql', // BV2.R17 passed-item lookup index (task 1004096)
|
|
45
46
|
]);
|
|
46
47
|
|
|
47
48
|
// Permission keys RENAMED after the grant migrations that first granted them.
|