@bongos/core 1.19.927 → 1.19.929
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 +83 -53
- package/docs/copy-inventory.md +36 -15
- package/docs/copy-registry.json +239 -28
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea-objects.js +109 -0
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +11 -0
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +286 -0
- package/modules/hall-ui/public/thinking.html +74 -0
- package/modules/hall-ui/public/thinking.js +418 -0
- package/modules/hall-ui/public/thinking.states.json +20 -0
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision-config.js +11 -0
- package/scripts/gds/provision-core-upgrade.js +90 -10
- package/scripts/gds/provision.js +3 -3
- package/src/bongos/serve-internal.js +7 -1
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +153 -0
- package/tests/hall_nav.mjs +1 -1
- package/tests/hall_page_gate_map.mjs +6 -0
- package/tests/hall_thinking_room.mjs +366 -0
- package/tests/helpers.mjs +124 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provision.mjs +41 -0
|
@@ -203,7 +203,11 @@ async function coreUpgradeInstance(inst, deps, intent) {
|
|
|
203
203
|
: '';
|
|
204
204
|
// Built by the SAME function the preview uses, so "what you were shown" and "what runs"
|
|
205
205
|
// cannot drift apart into two hand-maintained flag lists (task 1004121).
|
|
206
|
-
|
|
206
|
+
// The preview probes; the move must probe identically or the two commands diverge on
|
|
207
|
+
// the one field task 1004121 exists to keep identical. Skipped when !apply, where the
|
|
208
|
+
// command is printed and never run — a plan should not spend a subprocess per tick.
|
|
209
|
+
const { runAs } = resolveRunAs(inst, deps, { probe: apply });
|
|
210
|
+
const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, requestedBy, selfRoot, runAs });
|
|
207
211
|
|
|
208
212
|
if (!apply) {
|
|
209
213
|
// A --dry-run runner tick used to only PRINT this command. It now runs the CLI's own
|
|
@@ -269,6 +273,53 @@ function parsePreflight(out) {
|
|
|
269
273
|
return { checks, error };
|
|
270
274
|
}
|
|
271
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Which unix account should this instance's upgrade run as? Asks the box (task 1004128).
|
|
278
|
+
*
|
|
279
|
+
* The fleet is heterogeneous: instances provisioned before task 1003369 run as the
|
|
280
|
+
* shared APP_USER, instances provisioned after run as their own `bongos-<slug>`. Neither
|
|
281
|
+
* is guessable from the row, and getting it wrong is not a soft failure — `sudo` refuses
|
|
282
|
+
* an unknown user and the upgrade dies before pre-flight, which is exactly how the
|
|
283
|
+
* tenant leg has been broken.
|
|
284
|
+
*
|
|
285
|
+
* Falling back is NOT a privilege escalation: an instance with no account of its own is
|
|
286
|
+
* already running as APP_USER (its unit says so), so this runs the upgrade as precisely
|
|
287
|
+
* the account that owns the files and the database it is about to touch. It IS reported,
|
|
288
|
+
* because a missing per-instance account is a provisioning gap someone should close, and
|
|
289
|
+
* a silent fallback would hide it forever.
|
|
290
|
+
*/
|
|
291
|
+
function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
292
|
+
if (inst.hosting_shape === 'control-plane') return { runAs: APP_USER, fellBack: false };
|
|
293
|
+
// Re-checked HERE rather than left to instanceUser()'s own throw, for the reason
|
|
294
|
+
// upgradeInvocation states about itself: a call site whose safety depends on a
|
|
295
|
+
// helper's internals is one refactor away from a shell-interpolation path.
|
|
296
|
+
if (!isValidSlug(inst && inst.slug)) {
|
|
297
|
+
throw new Error(`resolveRunAs: refusing to probe an account for invalid slug ${JSON.stringify(inst && inst.slug)}`);
|
|
298
|
+
}
|
|
299
|
+
const wanted = instanceUser(inst);
|
|
300
|
+
if (!allowProbe) return { runAs: wanted, fellBack: false };
|
|
301
|
+
// THE SAME EXECUTOR THE UPGRADE ITSELF WILL USE. Every sibling here picks
|
|
302
|
+
// `standalone ? (controlExec || exec) : exec`, because under PROVISION_REMOTE_HOST
|
|
303
|
+
// (ADR 0130) `exec` is the REMOTE ssh executor while `controlExec` stays local. A
|
|
304
|
+
// probe on `exec` would then ask the co-hosting box whether an account exists while
|
|
305
|
+
// the command it is deciding for runs on the control plane — a wrong answer that
|
|
306
|
+
// looks like a right one.
|
|
307
|
+
const boxExec = inst.hosting_shape === 'standalone' ? (deps.controlExec || deps.exec) : deps.exec;
|
|
308
|
+
const r = boxExec(`id -u ${wanted}`, { allowFail: true });
|
|
309
|
+
const failed = !r || r.softFailed === true || r.ok === false;
|
|
310
|
+
if (!failed) return { runAs: wanted, fellBack: false };
|
|
311
|
+
// ONLY A DEFINITIVE "no such user" JUSTIFIES THE FALLBACK. A probe that could not
|
|
312
|
+
// answer — an exec timeout, an NSS hiccup — must not silently narrow the per-instance
|
|
313
|
+
// isolation of task 1003369 for this operation. So an inconclusive probe keeps the
|
|
314
|
+
// intended account: if it really is missing the upgrade fails loudly, and since this
|
|
315
|
+
// task a failure now carries the reason instead of "the dry run did not complete".
|
|
316
|
+
const said = `${(r && r.stdout) || ''}\n${(r && r.stderr) || ''}`;
|
|
317
|
+
if (!/no such user|unknown user/i.test(said)) {
|
|
318
|
+
return { runAs: wanted, fellBack: false, probeInconclusive: true };
|
|
319
|
+
}
|
|
320
|
+
return { runAs: APP_USER, fellBack: true, wanted };
|
|
321
|
+
}
|
|
322
|
+
|
|
272
323
|
// ONE BUILDER FOR BOTH LEGS (task 1004121).
|
|
273
324
|
//
|
|
274
325
|
// The preview and the move must be the same command or the preview is worthless — and
|
|
@@ -282,7 +333,7 @@ function parsePreflight(out) {
|
|
|
282
333
|
// HERE rather than left to instanceUser()'s own throw: that throw is correct today, but
|
|
283
334
|
// a call site whose safety depends on a helper's internals is one refactor away from a
|
|
284
335
|
// shell-injection path (and the apply leg's own guard sits at the route, not here).
|
|
285
|
-
function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', selfRoot = selfInstanceRoot } = {}) {
|
|
336
|
+
function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', selfRoot = selfInstanceRoot, runAs = null } = {}) {
|
|
286
337
|
if (!isValidSlug(inst && inst.slug)) {
|
|
287
338
|
throw new Error(`upgradeInvocation: refusing to build a shell command from an invalid slug ${JSON.stringify(inst && inst.slug)}`);
|
|
288
339
|
}
|
|
@@ -300,10 +351,25 @@ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, reque
|
|
|
300
351
|
// the project. So ask, rather than assume; in the legacy shape the answer is
|
|
301
352
|
// REPO_ROOT and the command below is byte-identical to what it always was.
|
|
302
353
|
const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : selfRoot();
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
354
|
+
// WHICH ACCOUNT, AND WHY IT IS RESOLVED RATHER THAN DERIVED (task 1004128).
|
|
355
|
+
//
|
|
356
|
+
// Task 1004119 wrote this as `control-plane ? APP_USER : instanceUser(inst)` and was
|
|
357
|
+
// half right. The control-plane leg is correct and verified live. The tenant leg
|
|
358
|
+
// assumed the per-instance account model of task 1003369 is provisioned everywhere —
|
|
359
|
+
// and on the live box NO `bongos-*` account exists, no such Postgres role exists, and
|
|
360
|
+
// every instance unit still says `User=lars`. So the fix for "Postgres has no role
|
|
361
|
+
// root" swapped one absent account for another, and every tenant upgrade died with
|
|
362
|
+
// `sudo: unknown user bongos-test4`.
|
|
363
|
+
//
|
|
364
|
+
// The account model is a real security control, not an accident — a distinct uid per
|
|
365
|
+
// instance is what stops a co-tenant reading a sibling's /proc/<pid>/environ. So the
|
|
366
|
+
// answer is NOT to route everyone back to the shared account. It is to run as the
|
|
367
|
+
// account the instance ACTUALLY runs as, which a caller establishes by probing
|
|
368
|
+
// (resolveRunAs below) instead of either side assuming. Passing it in keeps this
|
|
369
|
+
// builder pure; the default preserves the old derivation for callers that have not
|
|
370
|
+
// probed.
|
|
371
|
+
const account = runAs || (inst.hosting_shape === 'control-plane' ? APP_USER : instanceUser(inst));
|
|
372
|
+
const sudoP = privileged ? `sudo -u ${account} env PGDATABASE=${dbName(inst)} ` : '';
|
|
307
373
|
const cli = path.posix.join('scripts', 'gds', 'upgrade.js');
|
|
308
374
|
const shared = [`${sudoP}node ${cli}`, '--registry', `--to ${to}`,
|
|
309
375
|
instanceDir === REPO_ROOT ? '' : `--instance ${instanceDir}`]; // REPO_ROOT is where the CLI runs (runDir below)
|
|
@@ -315,7 +381,7 @@ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, reque
|
|
|
315
381
|
'--commit-pin',
|
|
316
382
|
requestedBy ? `--requested-by ${requestedBy}` : '',
|
|
317
383
|
requestedBy ? '--upgrade-source owner-control' : ''];
|
|
318
|
-
return { cmd: [...shared, ...tail].filter(Boolean).join(' '), runDir: REPO_ROOT, instanceDir, runAs };
|
|
384
|
+
return { cmd: [...shared, ...tail].filter(Boolean).join(' '), runDir: REPO_ROOT, instanceDir, runAs: account };
|
|
319
385
|
}
|
|
320
386
|
|
|
321
387
|
// Ask the upgrade CLI what it WOULD do, changing nothing. `allowFail` because a refusal
|
|
@@ -326,7 +392,8 @@ function preflightUpgrade(inst, to, deps) {
|
|
|
326
392
|
// A distinct local name: `deps.selfInstanceRoot || selfInstanceRoot` with a const of
|
|
327
393
|
// the same name would TDZ-shadow the import and crash every uninjected call.
|
|
328
394
|
const selfRoot = deps.selfInstanceRoot || selfInstanceRoot;
|
|
329
|
-
const {
|
|
395
|
+
const { runAs, fellBack, wanted } = resolveRunAs(inst, deps);
|
|
396
|
+
const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, dryRun: true, selfRoot, runAs });
|
|
330
397
|
const r = boxExec(cmd, { cwd: runDir, allowFail: true });
|
|
331
398
|
const out = `${(r && r.stdout) || ''}\n${(r && r.stderr) || ''}`;
|
|
332
399
|
const { checks, error } = parsePreflight(out);
|
|
@@ -334,7 +401,20 @@ function preflightUpgrade(inst, to, deps) {
|
|
|
334
401
|
// recognisable ✖ is still a refusal, and reporting it as a pass is the failure mode
|
|
335
402
|
// this whole task exists to remove.
|
|
336
403
|
const ok = !!(r && !r.softFailed && r.ok !== false) && !error;
|
|
337
|
-
|
|
404
|
+
// WHEN THERE IS NO ✖, SAY WHAT THE COMMAND SAID (task 1004128). The generic sentence
|
|
405
|
+
// is the LAST resort, not the first: the first live pre-flight reported "the dry run
|
|
406
|
+
// did not complete" while `sudo: unknown user bongos-test4` sat in the journal, and
|
|
407
|
+
// the second did the same over `✖ already on 1.19.927`. Both were in hand. The trim
|
|
408
|
+
// is because this reaches a DOM.
|
|
409
|
+
const raw = out.trim().split('\n').filter(Boolean).slice(-4).join('\n').slice(0, 600);
|
|
410
|
+
const why = error || (raw || null);
|
|
411
|
+
const note = fellBack
|
|
412
|
+
? `ran as ${runAs} — this instance has no ${wanted} account yet (it predates the per-instance account model)`
|
|
413
|
+
: null;
|
|
414
|
+
return {
|
|
415
|
+
target: to, ok, lines: checks, ran_as: runAs, account_note: note,
|
|
416
|
+
error: ok ? null : (why || 'the dry run did not complete, and said nothing this could report'),
|
|
417
|
+
};
|
|
338
418
|
}
|
|
339
419
|
|
|
340
420
|
// Run a dry run for one instance and cache its verdict — the ON-DEMAND leg, drained from
|
|
@@ -439,4 +519,4 @@ async function snapshotAllCoreVersions(deps) {
|
|
|
439
519
|
return { seen };
|
|
440
520
|
}
|
|
441
521
|
|
|
442
|
-
module.exports = { UPGRADE_SHAPES, versionCmd, parseServedVersion, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
|
522
|
+
module.exports = { UPGRADE_SHAPES, resolveRunAs, versionCmd, parseServedVersion, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
package/scripts/gds/provision.js
CHANGED
|
@@ -49,7 +49,7 @@ const path = require('node:path');
|
|
|
49
49
|
const cp = require('node:child_process');
|
|
50
50
|
const crypto = require('node:crypto');
|
|
51
51
|
|
|
52
|
-
const { CONFIG, MANIFEST_UA, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
|
|
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
53
|
const { APP_USER, accountPreflight, 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');
|
|
@@ -95,7 +95,7 @@ function makeExec({ apply, log = console.log, remoteHost = null, remoteCwd = nul
|
|
|
95
95
|
// `shown` call site is secret-bearing by declaration: log the exit status
|
|
96
96
|
// alone, never stderr (a remote shell may echo the failing line back).
|
|
97
97
|
const why = shownAs ? `exit status ${e && e.status != null ? e.status : '?'}` : (e.stderr || e.message || '').toString().trim().slice(0, 120);
|
|
98
|
-
if (allowFail) { log(` ~ ${shown} (non-fatal: ${why})`); return
|
|
98
|
+
if (allowFail) { log(` ~ ${shown} (non-fatal: ${why})`); return softFailResult(e, !!shownAs); }
|
|
99
99
|
log(` ✖ ${shown}`);
|
|
100
100
|
throw e;
|
|
101
101
|
}
|
|
@@ -113,7 +113,7 @@ function makeExec({ apply, log = console.log, remoteHost = null, remoteCwd = nul
|
|
|
113
113
|
// non-fatal line carries only the exit status — never e.message, whose
|
|
114
114
|
// "Command failed: <cmd>" would echo the command back into the journal.
|
|
115
115
|
const why = shownAs ? `exit status ${e && e.status != null ? e.status : '?'}` : (e.stderr || e.message || '').toString().trim().slice(0, 120);
|
|
116
|
-
if (allowFail) { log(` ~ ${label} (non-fatal: ${why})`); return
|
|
116
|
+
if (allowFail) { log(` ~ ${label} (non-fatal: ${why})`); return softFailResult(e, !!shownAs); }
|
|
117
117
|
log(` ✖ ${label}`);
|
|
118
118
|
throw e;
|
|
119
119
|
}
|
|
@@ -1154,8 +1154,14 @@ function mountInternalSurfaces(app) {
|
|
|
1154
1154
|
// shells — and matched no gate at all. The shell carries no data and every
|
|
1155
1155
|
// read behind it is gated server-side, so this closed an inconsistency rather
|
|
1156
1156
|
// than an exposure; the point is that one URL cannot be two tiers at once.
|
|
1157
|
+
// task 1004098 (BV2.R03): `thinking` — THE ROOM, a builder's own idea trail —
|
|
1158
|
+
// joins the tier `ideas` sits on, for the same settled reason: there is no
|
|
1159
|
+
// permission gate on having a thought. It needs no floor ABOVE sign-in either,
|
|
1160
|
+
// because the page reads the VIEWER's own trail and nobody else's — the id it
|
|
1161
|
+
// asks for comes from GET /me, never from the URL, so a rank could protect
|
|
1162
|
+
// nothing here that the session does not already.
|
|
1157
1163
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
1158
|
-
/^\/builders\/(?:work|settings|atlas|modules|goals|roadmap|copy-desk|studio|ideas|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
1164
|
+
/^\/builders\/(?:work|settings|atlas|modules|goals|roadmap|copy-desk|studio|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
1159
1165
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1160
1166
|
// page that embeds them.
|
|
1161
1167
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
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.929'; // 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');
|
|
@@ -647,5 +647,158 @@ await t('the runner refuses, and runs NOTHING, when it cannot see where the inst
|
|
|
647
647
|
assert.equal(d.ranWith.filter((c) => /upgrade\.js/.test(c.cmd)).length, 0, 'the box was not touched');
|
|
648
648
|
});
|
|
649
649
|
|
|
650
|
+
|
|
651
|
+
console.log('\nwhich account, and saying what went wrong (task 1004128):');
|
|
652
|
+
|
|
653
|
+
// A deps bag whose `id -u` probe can be made to succeed or fail, and whose upgrade
|
|
654
|
+
// invocation can be made to soft-fail with real command output.
|
|
655
|
+
function acctDeps({ accountExists = true, dryRunStdout = '', dryRunStderr = '', softFail = false } = {}) {
|
|
656
|
+
const d = makeDeps({ served: '1.19.762' });
|
|
657
|
+
d.privileged = true;
|
|
658
|
+
const ran = [];
|
|
659
|
+
d.ranWith = ran;
|
|
660
|
+
d.exec = (cmd, opts) => {
|
|
661
|
+
ran.push({ cmd, cwd: opts && opts.cwd });
|
|
662
|
+
if (/^id -u /.test(cmd)) {
|
|
663
|
+
if (accountExists) return { ok: true, stdout: '1001\n' };
|
|
664
|
+
// What `id -u` really says for an absent account — the definitive answer the
|
|
665
|
+
// fallback is allowed to act on.
|
|
666
|
+
return { ok: true, stdout: '', stderr: `id: '${cmd.split(' ').pop()}': no such user\n`, softFailed: true };
|
|
667
|
+
}
|
|
668
|
+
if (/\/version/.test(cmd)) return { ok: true, stdout: JSON.stringify({ coreVersion: '1.19.762' }) };
|
|
669
|
+
if (/upgrade\.js/.test(cmd)) {
|
|
670
|
+
return softFail
|
|
671
|
+
? { ok: true, stdout: dryRunStdout, stderr: dryRunStderr, softFailed: true }
|
|
672
|
+
: { ok: true, stdout: dryRunStdout, stderr: dryRunStderr };
|
|
673
|
+
}
|
|
674
|
+
return { ok: true, stdout: '' };
|
|
675
|
+
};
|
|
676
|
+
return d;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
await t('a tenant WITH its own account runs as that account — the security model is kept', () => {
|
|
680
|
+
// task 1003369: a distinct uid per instance is what stops a co-tenant reading a
|
|
681
|
+
// sibling's /proc/<pid>/environ. Falling back everywhere would quietly undo it.
|
|
682
|
+
const d = acctDeps({ accountExists: true });
|
|
683
|
+
const r = R.resolveRunAs(inst({ hosting_shape: 'standalone', slug: 'test4' }), d);
|
|
684
|
+
assert.deepEqual({ runAs: r.runAs, fellBack: r.fellBack }, { runAs: 'bongos-test4', fellBack: false });
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
await t('a tenant WITHOUT one falls back to the app user, and says so', () => {
|
|
688
|
+
// The live box has no bongos-* account at all, so every tenant upgrade died on
|
|
689
|
+
// `sudo: unknown user bongos-test4`. Falling back is not an escalation — the
|
|
690
|
+
// instance's own unit already says User=lars — but it IS a provisioning gap, so it
|
|
691
|
+
// is reported rather than silently papered over.
|
|
692
|
+
const d = acctDeps({ accountExists: false });
|
|
693
|
+
const r = R.resolveRunAs(inst({ hosting_shape: 'standalone', slug: 'test4' }), d);
|
|
694
|
+
assert.equal(r.runAs, 'lars');
|
|
695
|
+
assert.equal(r.fellBack, true);
|
|
696
|
+
assert.equal(r.wanted, 'bongos-test4', 'and names the account that is missing');
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
await t('the control plane is never probed — it has no instance account by design', () => {
|
|
700
|
+
const d = acctDeps({ accountExists: false });
|
|
701
|
+
const r = R.resolveRunAs(inst({ hosting_shape: 'control-plane', slug: 'cloudbongos' }), d);
|
|
702
|
+
assert.deepEqual({ runAs: r.runAs, fellBack: r.fellBack }, { runAs: 'lars', fellBack: false });
|
|
703
|
+
assert.equal(d.ranWith.filter((c) => /^id -u /.test(c.cmd)).length, 0, 'no probe was spent');
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await t('the dry run is invoked as the account that actually exists', async () => {
|
|
707
|
+
const d = acctDeps({ accountExists: false, dryRunStdout: ' ✓ pre-flight: ok\n' });
|
|
708
|
+
await R.preflightUpgrade(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), '1.19.762', d);
|
|
709
|
+
const cmd = (d.ranWith.find((c) => /upgrade\.js/.test(c.cmd)) || {}).cmd || '';
|
|
710
|
+
assert.match(cmd, /sudo -u lars /, `must not sudo to an account that does not exist — got: ${cmd}`);
|
|
711
|
+
assert.doesNotMatch(cmd, /bongos-test4/);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
await t('the MOVE probes identically, so preview and move cannot diverge on the account', async () => {
|
|
715
|
+
const d = acctDeps({ accountExists: false });
|
|
716
|
+
await R.coreUpgradeInstance(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), d, intent());
|
|
717
|
+
const cmd = (d.ranWith.find((c) => /upgrade\.js/.test(c.cmd)) || {}).cmd || '';
|
|
718
|
+
assert.match(cmd, /sudo -u lars /, 'the move follows the same probe as the preview');
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
await t('an INCONCLUSIVE probe keeps the isolated account rather than quietly widening it', () => {
|
|
722
|
+
// A probe that could not answer (exec timeout, NSS hiccup) is not evidence the
|
|
723
|
+
// account is absent. Falling back on it would narrow task 1003369's per-instance
|
|
724
|
+
// /proc isolation for that operation, silently, on a fleet where the account may
|
|
725
|
+
// well exist. Only `no such user` is evidence.
|
|
726
|
+
const d = makeDeps({ served: '1.19.762' });
|
|
727
|
+
d.exec = () => ({ ok: true, stdout: '', stderr: 'id: cannot connect to name service\n', softFailed: true });
|
|
728
|
+
const r = R.resolveRunAs(inst({ hosting_shape: 'standalone', slug: 'test4' }), d);
|
|
729
|
+
assert.equal(r.runAs, 'bongos-test4', 'the intended account is kept');
|
|
730
|
+
assert.equal(r.fellBack, false);
|
|
731
|
+
assert.equal(r.probeInconclusive, true, 'and the caller can tell it was not a clean answer');
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
await t('the probe uses the SAME executor the upgrade will use', () => {
|
|
735
|
+
// Under PROVISION_REMOTE_HOST (ADR 0130) `exec` is the remote ssh executor while
|
|
736
|
+
// `controlExec` stays local, and every sibling here picks controlExec for a
|
|
737
|
+
// standalone. A probe on the other one asks the wrong machine and gets a confident
|
|
738
|
+
// wrong answer.
|
|
739
|
+
const asked = { control: [], remote: [] };
|
|
740
|
+
const d = makeDeps({ served: '1.19.762' });
|
|
741
|
+
d.controlExec = (cmd) => { asked.control.push(cmd); return { ok: true, stdout: '1001\n' }; };
|
|
742
|
+
d.exec = (cmd) => { asked.remote.push(cmd); return { ok: true, stdout: '', stderr: "id: 'x': no such user\n", softFailed: true }; };
|
|
743
|
+
const r = R.resolveRunAs(inst({ hosting_shape: 'standalone', slug: 'test4' }), d);
|
|
744
|
+
assert.deepEqual(asked.remote, [], 'the remote executor was not asked');
|
|
745
|
+
assert.equal(asked.control.length, 1, 'the control-plane executor was');
|
|
746
|
+
assert.equal(r.runAs, 'bongos-test4', 'and its answer is the one that counts');
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
await t('a planning tick probes ONCE, not twice', async () => {
|
|
750
|
+
// The non-apply path prints the move it would make AND runs the CLI's own dry run, so
|
|
751
|
+
// one probe is real work. The second — for a command string that is only logged — was
|
|
752
|
+
// a subprocess per tick for nothing.
|
|
753
|
+
const d = acctDeps({ accountExists: false });
|
|
754
|
+
d.apply = false;
|
|
755
|
+
await R.coreUpgradeInstance(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), d, intent());
|
|
756
|
+
assert.equal(d.ranWith.filter((c) => /^id -u /.test(c.cmd)).length, 1,
|
|
757
|
+
'exactly one probe: the dry run that actually executes');
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
await t('an invalid slug is refused by THIS call site, not merely by the helper it calls', () => {
|
|
761
|
+
// Asserting /invalid slug/ alone proves nothing: instanceUser() throws its own
|
|
762
|
+
// message matching that, so the test passes with this guard deleted — verified by
|
|
763
|
+
// deleting it. The point of the finding is that safety here must not DEPEND on the
|
|
764
|
+
// helper's internals, so the assertion names the thrower.
|
|
765
|
+
const d = acctDeps({});
|
|
766
|
+
assert.throws(
|
|
767
|
+
() => R.resolveRunAs({ hosting_shape: 'standalone', slug: 'bad slug;rm -rf /' }, d),
|
|
768
|
+
/resolveRunAs: refusing to probe/,
|
|
769
|
+
'the refusal must come from this function, not from instanceUser further down',
|
|
770
|
+
);
|
|
771
|
+
assert.equal(d.ranWith.filter((c) => /^id -u /.test(c.cmd)).length, 0, 'and nothing was executed');
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await t('a refusal reports what the command SAID, not "did not complete"', async () => {
|
|
775
|
+
const d = acctDeps({ softFail: true, dryRunStderr: 'sudo: unknown user bongos-test4\n' });
|
|
776
|
+
const v = await R.preflightUpgrade(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), '1.19.762', d);
|
|
777
|
+
assert.equal(v.ok, false);
|
|
778
|
+
assert.match(v.error, /unknown user bongos-test4/,
|
|
779
|
+
`the reason was in hand and must travel — got: ${JSON.stringify(v.error)}`);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
await t('a ✖ line still wins over the raw tail', async () => {
|
|
783
|
+
const d = acctDeps({ softFail: true, dryRunStdout: 'noise\n ✖ already on 1.19.927 (pass --force to re-run)\n' });
|
|
784
|
+
const v = await R.preflightUpgrade(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), '1.19.762', d);
|
|
785
|
+
assert.match(v.error, /already on 1\.19\.927/);
|
|
786
|
+
assert.doesNotMatch(v.error, /^noise/, 'the parsed refusal is preferred to the raw tail');
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
await t('a fallback is recorded on the verdict the owner reads', async () => {
|
|
790
|
+
const d = acctDeps({ accountExists: false, dryRunStdout: ' ✓ pre-flight: ok\n' });
|
|
791
|
+
const v = await R.preflightUpgrade(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), '1.19.762', d);
|
|
792
|
+
assert.equal(v.ran_as, 'lars');
|
|
793
|
+
assert.match(v.account_note, /no bongos-test4 account yet/);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
await t('and a silent failure STILL refuses rather than inventing a pass', async () => {
|
|
797
|
+
const d = acctDeps({ softFail: true, dryRunStdout: '', dryRunStderr: '' });
|
|
798
|
+
const v = await R.preflightUpgrade(inst({ hosting_shape: 'standalone', slug: 'test4', port: 3006 }), '1.19.762', d);
|
|
799
|
+
assert.equal(v.ok, false, 'a command that said nothing is not a pass');
|
|
800
|
+
assert.match(v.error, /said nothing/);
|
|
801
|
+
});
|
|
802
|
+
|
|
650
803
|
console.log(`\ncore_upgrade_runner: ${passed} passed, ${failed} failed`);
|
|
651
804
|
if (failed) process.exit(1);
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -258,7 +258,7 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
258
258
|
// task 1003455 (goal 1000089): 'blockers' joins the same group, after Tasks —
|
|
259
259
|
// its subject is the project (what's stuck on the owner), not a person or a
|
|
260
260
|
// craft, and its one data source (GET /blockers) is public like Roadmap's.
|
|
261
|
-
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'blockers', 'board-room', 'ideas', 'copy-desk', 'studio'],
|
|
261
|
+
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'blockers', 'board-room', 'ideas', 'thinking', 'copy-desk', 'studio'],
|
|
262
262
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
263
263
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
264
264
|
// People (task 1002577): the /scouting directory graduated into the hall.
|
|
@@ -60,6 +60,12 @@ const ORACLE = {
|
|
|
60
60
|
// themselves server-side.
|
|
61
61
|
'/builders/studio': 'requireBuilderPage',
|
|
62
62
|
'/builders/ideas': 'requireBuilderPage',
|
|
63
|
+
// THE ROOM (task 1004098 / BV2.R03) — a builder's own idea trail. Sign-in and
|
|
64
|
+
// no rank floor, the tier /ideas sits on: there is no permission gate on
|
|
65
|
+
// having a thought, and the page reads the VIEWER's own trail (the id comes
|
|
66
|
+
// from GET /me, never from the URL), so a rank could protect nothing the
|
|
67
|
+
// session does not already.
|
|
68
|
+
'/builders/thinking': 'requireBuilderPage',
|
|
63
69
|
'/builders/collab': 'requireBuilderPage',
|
|
64
70
|
// task 1002710: the orientation reading rooms moved DOWN to this tier from a
|
|
65
71
|
// thetes+ gate. They are the onboarding documents, so gating them above xenos
|