@bongos/core 1.19.1053 → 1.19.1054

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 CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.1053",
6
- "core_contract": "1.19.1053",
7
- "source_commit": "0283c8d5c7a227fed034f765f9cb6945d821849b",
5
+ "core_version": "1.19.1054",
6
+ "core_contract": "1.19.1054",
7
+ "source_commit": "16cad90499c802cd308487cec2c824589b064b95",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-28T04:00:21.518Z",
9
+ "built_at": "2026-09-28T04:35:28.546Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 542,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3078,
20
- "tree_sha256": "17214f38e3942f908188baa54d5f09634b4997efa67f2e024f592a414a985980",
20
+ "tree_sha256": "b4fe4ba91ff03f9db59c7306f72e4f1898f4f5d0a168d05a228e0689c42765db",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2737,7 +2737,7 @@
2737
2737
  {
2738
2738
  "path": "docs/module-api-changelog.md",
2739
2739
  "mode": "0000644",
2740
- "sha256": "80a4955a66087c3dc785fe34342b05e70eecedaa5407c7e2339194d2b3faa40c"
2740
+ "sha256": "cd63efa0d5860b1d1e150d0472ea95cc8285e58526aae6696f05dc803ca7cfea"
2741
2741
  },
2742
2742
  {
2743
2743
  "path": "docs/modules-contract.md",
@@ -6897,7 +6897,7 @@
6897
6897
  {
6898
6898
  "path": "modules/provisioning/control-plane-guard.js",
6899
6899
  "mode": "0000644",
6900
- "sha256": "0bbe91cadfb2bec4c419aab26e728c239ff1772af2c3568203d8fba2fb99a2d6"
6900
+ "sha256": "beb559945e2b00edfab521a65e2c8b91671a35e8a67f6d3be30d39a7a7d8118c"
6901
6901
  },
6902
6902
  {
6903
6903
  "path": "modules/provisioning/core-upgrade-authz.js",
@@ -7107,7 +7107,7 @@
7107
7107
  {
7108
7108
  "path": "modules/provisioning/provisioning.js",
7109
7109
  "mode": "0000644",
7110
- "sha256": "58cbd888fa2ddedb174e566b861efb1749d06928f8152514e9b09d99701592af"
7110
+ "sha256": "1a4823ad0fde877d954faf5650942cb39ab063541433ac101cec04b7cb03d980"
7111
7111
  },
7112
7112
  {
7113
7113
  "path": "modules/provisioning/public-refusal.js",
@@ -8462,12 +8462,12 @@
8462
8462
  {
8463
8463
  "path": "package-lock.json",
8464
8464
  "mode": "0000644",
8465
- "sha256": "7eb8fc349775c38e2543de963016c970a69df83e016b4aabd500342bf95ce95f"
8465
+ "sha256": "7a222030251e4f1975d78ef3b4bf78d212aa17b80d001b6858f0dcf92bbf0d90"
8466
8466
  },
8467
8467
  {
8468
8468
  "path": "package.json",
8469
8469
  "mode": "0000644",
8470
- "sha256": "c2ef2620835031338cda672c27b5fd4a0b493e29a21775ef72121c92c680e42d"
8470
+ "sha256": "b4cf0e08cfe3dc1e5980144ff698d22ac1800712e3939a663d67845232d0b95c"
8471
8471
  },
8472
8472
  {
8473
8473
  "path": "public-docs/index.html",
@@ -8487,7 +8487,7 @@
8487
8487
  {
8488
8488
  "path": "release-notes.json",
8489
8489
  "mode": "0000644",
8490
- "sha256": "00467f680b00cb584d975bce06cd2d2388f2a84d176fae9ddd3532c47e846fa1"
8490
+ "sha256": "b4125b6b8f4b390e216f8686281a0a98da09c2a405f662beb8368ef6dee81ce3"
8491
8491
  },
8492
8492
  {
8493
8493
  "path": "scripts/bongos-mcp.js",
@@ -9477,12 +9477,12 @@
9477
9477
  {
9478
9478
  "path": "scripts/gds/provision-units.js",
9479
9479
  "mode": "0000644",
9480
- "sha256": "1f7afb93c2176d1e0be0b43904b94c9c09c8444cdd399469ef430234c6317731"
9480
+ "sha256": "b35e21dabea1b2b45242c4744a743926259e593e01fe337d464dc4545588309d"
9481
9481
  },
9482
9482
  {
9483
9483
  "path": "scripts/gds/provision.js",
9484
9484
  "mode": "0000644",
9485
- "sha256": "d6480d0690a7b2b464820acd9861ce9d09897b10ae815164444997208cf61227"
9485
+ "sha256": "bb767eea3debd87ad13ea859d85267b5b6be88c2eadf3c3ac236408c58f84c23"
9486
9486
  },
9487
9487
  {
9488
9488
  "path": "scripts/gds/publish-credential-check.js",
@@ -10562,7 +10562,7 @@
10562
10562
  {
10563
10563
  "path": "src/module-api.js",
10564
10564
  "mode": "0000644",
10565
- "sha256": "7525ff0387590801ff96e841e396e275f6f9c0cc6c659ecb77ec687512dfc474"
10565
+ "sha256": "b31a0374ebfe190599d53bfd40004328cc7bb1aa3906487c005a53718b990a21"
10566
10566
  },
10567
10567
  {
10568
10568
  "path": "src/module-loader/catalog.js",
@@ -11527,7 +11527,7 @@
11527
11527
  {
11528
11528
  "path": "tests/core_upgrade_door.mjs",
11529
11529
  "mode": "0000644",
11530
- "sha256": "9e19834caee7747ae1e07499ec3119903be404b6d244dfe0598d6c7ad3793aa3"
11530
+ "sha256": "52b55a04a99870a1501f2a422096337e3426a7624ec6e1728f291e426eb9567b"
11531
11531
  },
11532
11532
  {
11533
11533
  "path": "tests/core_upgrade_owner_door.mjs",
@@ -13952,7 +13952,7 @@
13952
13952
  {
13953
13953
  "path": "tests/provision_settings_apply.mjs",
13954
13954
  "mode": "0000644",
13955
- "sha256": "f56a140aa9a5093001e5d4de87e350704861ad91dc2ea5a8f9b993751f8bf4d1"
13955
+ "sha256": "edcaec20fe9fe6ae0c6fd542516d10ffb3412ab009f356af3ee52abca939342b"
13956
13956
  },
13957
13957
  {
13958
13958
  "path": "tests/provision_teardown_address.mjs",
@@ -14072,7 +14072,7 @@
14072
14072
  {
14073
14073
  "path": "tests/provisioning_settings_apply.mjs",
14074
14074
  "mode": "0000644",
14075
- "sha256": "50e311793e3baaf96724507330e813211edf238049b9aedfe63c1ee629a33d35"
14075
+ "sha256": "550f67b56dd92d413a1bafc70d76375df9c02d84f9d3e4dfd5789697ae0164f0"
14076
14076
  },
14077
14077
  {
14078
14078
  "path": "tests/provisioning_settings_env.mjs",
@@ -2593,5 +2593,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2593
2593
  landed since 1.19.1051 with no explicit bump. run 36372902384. (task 1002620)
2594
2594
  1.19.1053 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2595
2595
  landed since 1.19.1052 with no explicit bump. run 36375859084. (task 1002620)
2596
+ 1.19.1054 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2597
+ landed since 1.19.1053 with no explicit bump. run 36378326588. (task 1002620)
2596
2598
  ---------------------------------------------------------------------------
2597
2599
  ```
@@ -40,10 +40,23 @@ const { PublicRefusal } = require('./public-refusal');
40
40
  // the provisioning module — so its hall is the only one with an
41
41
  // Upgrade page at all. A preview that cannot run where the button
42
42
  // lives is not a preview (task 1004125).
43
+ // settings-apply
44
+ // — push a SAVED setting into the box's own web.env, then restart
45
+ // (task 1004334). It is `restart` plus an in-place edit of the
46
+ // settings lines, and nothing else: the runner leg re-reads the
47
+ // file, replaces only the vocabulary keys (SETTINGS_VOCAB plus its
48
+ // declared companions) and refuses outright if the file cannot be
49
+ // read, so it cannot blank a credential, a port or the database
50
+ // name. It deletes nothing, provisions nothing and moves no pin.
51
+ // Refusing it here protected the box from nothing and left the
52
+ // owner's saved settings on the platform's own row unable to reach
53
+ // the live site — PATCH …/settings on this row stored 'off' while
54
+ // cloudbongos.com kept reading 'strict', and the only remedy was a
55
+ // hand edit on a machine the owner cannot reach.
43
56
  //
44
57
  // Adding to this list is a deliberate edit someone has to argue for. It is pinned
45
58
  // by name in tests/core_upgrade_door.mjs so it cannot grow as a side effect.
46
- const CONTROL_PLANE_ACTIONS = Object.freeze(['core-upgrade', 'core-upgrade-preflight', 'restart']);
59
+ const CONTROL_PLANE_ACTIONS = Object.freeze(['core-upgrade', 'core-upgrade-preflight', 'restart', 'settings-apply']);
47
60
 
48
61
  /**
49
62
  * May this intent action be aimed at an instance of this hosting shape? PURE.
@@ -104,8 +104,9 @@ const MAX_RELEASED_DOMAINS = 8;
104
104
  // excludes 'active' (task 1002691).
105
105
  const SETTINGS_APPLY_FROM = new Set(['active']);
106
106
  // The hosting shapes the runner has a write path to. 'byo-host' is absent (the owner runs
107
- // it, we reach into nothing) and so is 'control-plane' (the platform's own row).
108
- const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'cloud-host']);
107
+ // it, we reach into nothing). 'control-plane' is the runner's OWN box, admitted by name in
108
+ // control-plane-guard.js (task 1004334) so a setting saved on the platform's row reaches it.
109
+ const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'cloud-host', 'control-plane']);
109
110
  // The ONE state a restart is meaningful from (task 1003238). Liveness is an
110
111
  // OVERLAY, not a seventh state — an instance the heartbeat cannot reach is still
111
112
  // 'active', and 'active' is exactly where a restart applies. Every earlier state
@@ -115,8 +116,8 @@ const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'cloud-host']);
115
116
  // SETTINGS_APPLY_FROM: each action declaring its own gate is this module's
116
117
  // convention, and the two answer different questions.
117
118
  const RESTART_FROM = new Set(['active']);
118
- // Same reachability rule as the settings push, and the same set for the same reason:
119
- // the runner can only restart what it runs.
119
+ // Same reachability rule as the settings push (the runner can only restart what it runs),
120
+ // minus 'control-plane': task 1004334 gave the self row a settings push, not a restart door.
120
121
  const RESTART_SHAPES = new Set(['co-tenant', 'cloud-host']);
121
122
 
122
123
  // Per-project settings (task 1003040, goal 1000058): the known keys + their
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1053",
3
+ "version": "1.19.1054",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1053",
9
+ "version": "1.19.1054",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1053",
3
+ "version": "1.19.1054",
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",
@@ -7619,5 +7619,11 @@
7619
7619
  "id": "1004328",
7620
7620
  "text": "Your thinking and the project map are now two separate pages that share only their drawing. Your thinking keeps just your own views, simple and your thinking, with your own tools: the list, recent, claude layer, my influence,"
7621
7621
  }
7622
+ ],
7623
+ "1.19.1054": [
7624
+ {
7625
+ "id": "1004334",
7626
+ "text": "The platform can now apply a saved setting to its own site, not just to the projects it hosts. Turning the artist gate off on Cloud Bongos will actually reach cloudbongos.com: the platform updates its own settings file and res"
7627
+ }
7622
7628
  ]
7623
7629
  }
@@ -304,9 +304,44 @@ function originEnvVarsFor(inst) {
304
304
  };
305
305
  }
306
306
 
307
- function settingsEnvVarsFor(inst) {
307
+ // `envBody` is the web.env being patched; only the control-plane row reads it (see
308
+ // selfEnvPrefix). Every other shape keeps the provisioned-instance prefix, unchanged.
309
+ function settingsEnvVarsFor(inst, envBody) {
308
310
  const { settingsEnvVars } = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning')); // eslint-disable-line global-require
309
- return settingsEnvVars(inst, provisionedEnvPrefix());
311
+ const prefix = inst && inst.hosting_shape === 'control-plane' ? selfEnvPrefix({ envBody }) : provisionedEnvPrefix();
312
+ return settingsEnvVars(inst, prefix);
313
+ }
314
+
315
+ // The env-var prefix the CONTROL PLANE's own web process resolves (task 1004334). The
316
+ // self row is not a provisioned instance: it loads the pack src/branding.js loads, from
317
+ // ITS instance root, so provisionedEnvPrefix() (the neutral pack's floor) is the wrong
318
+ // question — a value written under a prefix the site does not resolve is read by
319
+ // nothing, and the push would report success over a site that never changed.
320
+ //
321
+ // Precedence mirrors loadBranding: the instance pack is a <PREFIX>_BRANDING_FILE line in
322
+ // the web.env being patched (the file the web service actually boots with) when there is
323
+ // one, else <instanceRoot>/config/branding.json; under it sits the core's neutral pack,
324
+ // then the fallback. The first that names an envPrefix wins. A wrong answer is not silent: the
325
+ // leg's read-back (settingsConsumed) compares the live manifest after the restart and
326
+ // records "still reports the OLD values".
327
+ function selfEnvPrefix({ envBody = '', instanceRoot, readFile = (p) => fs.readFileSync(p, 'utf8') } = {}) {
328
+ const { FALLBACK_ENV_PREFIX, LEGACY_ENV_PREFIXES } = require('../../src/instance-config');
329
+ const root = instanceRoot || require('./provision-repo.js').selfInstanceRoot(); // eslint-disable-line global-require
330
+ const names = [FALLBACK_ENV_PREFIX, ...LEGACY_ENV_PREFIXES].map((p) => `${p}_BRANDING_FILE`);
331
+ const override = String(envBody || '').split(/\r?\n/)
332
+ .map((l) => /^([A-Z]+_BRANDING_FILE)=(.*)$/.exec(l.trim()))
333
+ .find((m) => m && names.includes(m[1]) && m[2].trim());
334
+ const candidates = [
335
+ override ? path.resolve(root, override[2].trim()) : path.join(root, 'config', 'branding.json'),
336
+ path.join(REPO_ROOT, ENV_PREFIX_PACK),
337
+ ];
338
+ for (const p of candidates) {
339
+ try {
340
+ const pack = JSON.parse(readFile(p));
341
+ if (pack && typeof pack.envPrefix === 'string' && pack.envPrefix) return pack.envPrefix;
342
+ } catch { /* absent or unreadable — the next candidate answers */ }
343
+ }
344
+ return FALLBACK_ENV_PREFIX;
310
345
  }
311
346
 
312
347
  // The instance's own public manifest — the settings read-back ADR 0190 names
@@ -375,6 +410,7 @@ module.exports = {
375
410
  instanceManifestCmd,
376
411
  originEnvVarsFor,
377
412
  provisionedEnvPrefix,
413
+ selfEnvPrefix,
378
414
  serviceUnit,
379
415
  serviceUnitPath,
380
416
  settingsConsumed,
@@ -875,12 +875,12 @@ async function domainAttachInstance(inst, deps) {
875
875
  // ---------------------------------------------------------------------------
876
876
  async function settingsApplyInstance(inst, deps) {
877
877
  const { db, provisioning, exec, writeFile, log, apply, privileged } = deps;
878
- // Same box routing as domainAttachInstance: a standalone runs control-plane-local.
879
- const standalone = inst.hosting_shape === 'cloud-host';
880
- const boxExec = standalone ? (deps.controlExec || exec) : exec;
881
- const boxWriteFile = standalone ? (deps.controlWriteFile || writeFile) : writeFile;
878
+ // A standalone runs control-plane-local; the control plane's own row IS this box (task 1004334).
879
+ const onControlBox = inst.hosting_shape === 'cloud-host' || inst.hosting_shape === 'control-plane';
880
+ const boxExec = onControlBox ? (deps.controlExec || exec) : exec;
881
+ const boxWriteFile = onControlBox ? (deps.controlWriteFile || writeFile) : writeFile;
882
882
  const sudoP = privileged ? 'sudo ' : '';
883
- const vars = settingsEnvVarsFor(inst);
883
+ const vars = settingsEnvVarsFor(inst); // the self row's prefix is re-read from the live file at the write
884
884
  const shown = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join(' ');
885
885
  log(`settings-apply ${inst.slug} (shape=${inst.hosting_shape}, status=${inst.status}): ${shown}`);
886
886
 
@@ -917,7 +917,7 @@ async function settingsApplyInstance(inst, deps) {
917
917
  }
918
918
  try {
919
919
  log(' [config] patch the settings lines of web.env in place (creds + origins untouched)');
920
- boxWriteFile(webEnvPath(inst), upsertEnvVars(current, vars), { mode: 0o600, sudo: true });
920
+ boxWriteFile(webEnvPath(inst), upsertEnvVars(current, settingsEnvVarsFor(inst, current)), { mode: 0o600, sudo: true });
921
921
  log(' [service] restart so the memoized branding pack re-reads them');
922
922
  pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
923
923
  } catch (e) {
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.1053'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1054'; // 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');
@@ -102,18 +102,32 @@ await t('the allow-list is exactly the acts that cannot destroy the box', () =>
102
102
  // dry run returns at pre-flight having moved no pin, run no npm and restarted
103
103
  // nothing, and the control plane is the only instance whose hall HAS an Upgrade
104
104
  // page — so refusing the preview there protected nothing and removed everything.
105
+ // Widened a second time, by 'settings-apply' (task 1004334), for the reason argued in
106
+ // the guard's own comment: it is restart plus an in-place edit of the settings lines,
107
+ // and without it a setting saved on the platform's row could never reach the live site.
105
108
  assert.deepEqual([...G.CONTROL_PLANE_ACTIONS].sort(),
106
- ['core-upgrade', 'core-upgrade-preflight', 'restart']);
109
+ ['core-upgrade', 'core-upgrade-preflight', 'restart', 'settings-apply']);
107
110
  });
108
111
 
109
112
  await t('the preview is permitted on the control plane — the one row that can use it', () => {
110
113
  assert.equal(G.intentAllowedForShape('core-upgrade-preflight', 'control-plane'), true);
111
114
  });
112
115
 
116
+ await t('the settings push is permitted on the control plane, and the guard says WHY by name (task 1004334)', () => {
117
+ assert.equal(G.intentAllowedForShape('settings-apply', 'control-plane'), true);
118
+ // The argument is part of the edit: an entry added without its reason is exactly the
119
+ // "grew as a side effect" this pin exists to stop.
120
+ const src = read('modules', 'provisioning', 'control-plane-guard.js');
121
+ const reason = src.slice(src.indexOf('// settings-apply'), src.indexOf('const CONTROL_PLANE_ACTIONS'));
122
+ assert.ok(reason.length > 0 && src.includes('// settings-apply'), 'the guard comment carries a settings-apply entry');
123
+ assert.match(reason, /task 1004334/, 'the reason names the task that argued it');
124
+ assert.match(reason, /deletes nothing/, 'the reason states the property that makes it safe');
125
+ });
126
+
113
127
  await t('and widening it did NOT let the destructive acts through', () => {
114
128
  // The whole risk of arguing one action onto an allow-list is arguing four.
115
129
  for (const a of P.INTENT_ACTIONS) {
116
- if (['core-upgrade', 'core-upgrade-preflight', 'restart'].includes(a)) continue;
130
+ if (['core-upgrade', 'core-upgrade-preflight', 'restart', 'settings-apply'].includes(a)) continue;
117
131
  assert.equal(G.intentAllowedForShape(a, 'control-plane'), false,
118
132
  `'${a}' must still be refused against the box the platform runs on`);
119
133
  }
@@ -315,6 +315,90 @@ await ta('the shape refusal: dedicated → ok:false with guidance naming the val
315
315
  assert.equal(writes.length, 0);
316
316
  });
317
317
 
318
+ console.log('\nthe control plane\'s own row — the platform pushes a setting to itself (task 1004334):');
319
+
320
+ const U = require('../scripts/gds/provision-units.js');
321
+ const { FALLBACK_ENV_PREFIX } = require('../src/instance-config.js');
322
+
323
+ t('selfEnvPrefix: the web.env BRANDING_FILE line wins, then the instance pack, then the neutral pack, then the fallback', () => {
324
+ const packs = (map) => (p) => {
325
+ const hit = Object.keys(map).find((k) => p.replace(/\\/g, '/').endsWith(k));
326
+ if (!hit) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }
327
+ return JSON.stringify(map[hit]);
328
+ };
329
+ const neutral = { 'config/branding.neutral.json': { envPrefix: 'NEUTRALX' } };
330
+ const withOwn = { ...neutral, 'config/branding.json': { envPrefix: 'OWNPACK' }, 'packs/site.json': { envPrefix: 'OVERRIDE' } };
331
+ const root = '/srv/instance';
332
+ assert.equal(U.selfEnvPrefix({ envBody: 'PORT=1\nBONGOS_BRANDING_FILE=packs/site.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'OVERRIDE');
333
+ assert.equal(U.selfEnvPrefix({ envBody: 'GDS_BRANDING_FILE=packs/site.json\r\n', instanceRoot: root, readFile: packs(withOwn) }), 'OVERRIDE', 'a legacy spelling still names the pack, as loadBranding reads it');
334
+ assert.equal(U.selfEnvPrefix({ envBody: 'PORT=1\n', instanceRoot: root, readFile: packs(withOwn) }), 'OWNPACK', 'no override → the instance root\'s config/branding.json');
335
+ assert.equal(U.selfEnvPrefix({ envBody: 'FOO_BRANDING_FILE=packs/site.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'OWNPACK', 'an unknown prefix is not the branding knob');
336
+ assert.equal(U.selfEnvPrefix({ envBody: '# BONGOS_BRANDING_FILE=packs/site.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'OWNPACK', 'a commented line is not set');
337
+ // loadBranding's own rule: an override that names a missing file loads NO instance pack,
338
+ // so the neutral one answers — config/branding.json is not consulted as a second chance.
339
+ assert.equal(U.selfEnvPrefix({ envBody: 'BONGOS_BRANDING_FILE=packs/gone.json\n', instanceRoot: root, readFile: packs(withOwn) }), 'NEUTRALX');
340
+ assert.equal(U.selfEnvPrefix({ envBody: '', instanceRoot: root, readFile: packs({ ...neutral, 'config/branding.json': { name: 'no prefix here' } }) }), 'NEUTRALX', 'a pack that names no envPrefix inherits the neutral one');
341
+ assert.equal(U.selfEnvPrefix({ envBody: '', instanceRoot: root, readFile: packs({}) }), FALLBACK_ENV_PREFIX, 'nothing readable → the fallback');
342
+ });
343
+
344
+ t('settingsEnvVarsFor: only the control-plane row reads the self prefix — every other shape is byte-identical to before', () => {
345
+ const body = 'BONGOS_BRANDING_FILE=/nowhere/at/all.json\n';
346
+ for (const shape of ['cloud-host', 'co-tenant']) {
347
+ assert.deepEqual(P.settingsEnvVarsFor({ ...activeStandalone, hosting_shape: shape }, body), P.settingsEnvVarsFor({ ...activeStandalone, hosting_shape: shape }), shape);
348
+ assert.ok(Object.keys(P.settingsEnvVarsFor({ ...activeStandalone, hosting_shape: shape }, body)).every((k) => k.startsWith(`${NEUTRAL_PREFIX}_`)), shape);
349
+ }
350
+ });
351
+
352
+ const selfRow = {
353
+ id: 41, slug: 'cloudbongos', hosting_shape: 'control-plane', status: 'active', owner_builder_id: 3,
354
+ port: 3002, domain: 'cloudbongos.com', settings: { artist_gate: 'off' },
355
+ };
356
+
357
+ await ta('--apply, the control-plane row: its OWN /etc/<slug>/web.env on the control-plane pair, under ITS pack\'s prefix, then restart <slug>; nothing else moves', async () => {
358
+ const fs = await import('node:fs');
359
+ const os = await import('node:os');
360
+ const path = await import('node:path');
361
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-pack-'));
362
+ const pack = path.join(dir, 'site.json');
363
+ fs.writeFileSync(pack, JSON.stringify({ envPrefix: 'CPSELF' }));
364
+ const selfEnv = `CPSELF_GITHUB_CLIENT_SECRET=keepme0123456789\nBONGOS_BRANDING_FILE=${pack}\nPORT=3002\nCPSELF_ARTIST_GATE=strict\nGITHUB_PUSH_TOKEN=keepme-too\n`;
365
+ const seen = { box: [], control: [] };
366
+ const mk = (bucket) => (cmd) => {
367
+ seen[bucket].push(cmd);
368
+ if (/\bcat /.test(cmd)) return { ok: true, stdout: selfEnv };
369
+ if (/api\/bongos\/instance/.test(cmd)) return { ok: true, stdout: manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'off', spendPayer: 'none' }) };
370
+ return { ok: true, stdout: '{"ok":true}' };
371
+ };
372
+ try {
373
+ const { deps, writes, events } = recorder({ apply: true });
374
+ deps.exec = mk('box'); deps.controlExec = mk('control');
375
+ const r = await P.settingsApplyInstance(selfRow, deps);
376
+ assert.equal(r.ok, true, JSON.stringify(r));
377
+ assert.equal(r.consumed, true, 'the read-back saw the live site report the new value');
378
+ assert.equal(seen.box.length, 0, 'nothing runs on the co-hosting box executor — the self row IS the control plane');
379
+ assert.ok(seen.control.some((c) => /^cat \/etc\/cloudbongos\/web\.env$/.test(c)), seen.control.join(' | '));
380
+ assert.ok(seen.control.some((c) => /^systemctl restart cloudbongos$/.test(c)), 'the service the core-upgrade door already restarts');
381
+ assert.deepEqual(writes.map((w) => [w.via, w.path]), [['control', '/etc/cloudbongos/web.env']]);
382
+ const out = writes[0].body;
383
+ assert.match(out, /^CPSELF_ARTIST_GATE=off$/m, 'replaced IN PLACE under the prefix the site resolves');
384
+ assert.doesNotMatch(out, /^BONGOS_ARTIST_GATE=/m, 'not written under the provisioned floor, which this site would never read');
385
+ assert.match(out, /^CPSELF_GITHUB_CLIENT_SECRET=keepme0123456789$/m, 'creds untouched');
386
+ assert.match(out, /^GITHUB_PUSH_TOKEN=keepme-too$/m);
387
+ assert.match(out, /^PORT=3002$/m);
388
+ assert.ok(![...seen.control].some((c) => /systemctl (stop|disable)|caddy|box-dns|rm -|upgrade\.js|npm/.test(c)), 'settings only — no teardown, DNS, Caddy or pin legs');
389
+ assert.match(events[0].detail, /reads the new values/);
390
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
391
+ });
392
+
393
+ await ta('the control-plane row keeps every refusal of the leg: an unreadable web.env writes nothing and restarts nothing', async () => {
394
+ const { deps, cmds, writes } = recorder({ apply: true, readFails: true });
395
+ const r = await P.settingsApplyInstance(selfRow, deps);
396
+ assert.equal(r.ok, false);
397
+ assert.match(r.error, /cannot read \/etc\/cloudbongos\/web\.env/);
398
+ assert.equal(writes.length, 0);
399
+ assert.ok(!cmds.some((c) => /restart/.test(c)), 'the platform is never restarted over a file it could not read');
400
+ });
401
+
318
402
  console.log('\nthe verdict — recorded only when earned:');
319
403
 
320
404
  await ta('--apply success, manifest matches: consumed=true, event says it reads the new values, F19 latch released, domain-bearing PASS records no liveness verdict', async () => {
@@ -72,19 +72,30 @@ t('SETTINGS_APPLY_FROM is exactly {active} — the provision intent cannot stand
72
72
  assert.ok(!provisioning.PROVISION_ENQUEUE_FROM.has('active'), 'the premise the gate rests on');
73
73
  });
74
74
 
75
- t('SETTINGS_APPLY_SHAPES is the reachable pair, and the set is a subset of HOSTING_SHAPES', () => {
76
- assert.deepEqual([...provisioning.SETTINGS_APPLY_SHAPES].sort(), ['cloud-host', 'co-tenant']);
75
+ t('SETTINGS_APPLY_SHAPES is the reachable three, and the set is a subset of HOSTING_SHAPES', () => {
76
+ // 'control-plane' joined in task 1004334: the runner's OWN box, admitted by name in
77
+ // control-plane-guard.js, so a setting saved on the platform's row reaches the live site.
78
+ assert.deepEqual([...provisioning.SETTINGS_APPLY_SHAPES].sort(), ['cloud-host', 'co-tenant', 'control-plane']);
77
79
  for (const s of provisioning.SETTINGS_APPLY_SHAPES) assert.ok(provisioning.HOSTING_SHAPES.includes(s), s);
78
80
  // The exclusion this used to assert was 'dedicated', which task 1004182 removed from
79
81
  // HOSTING_SHAPES entirely — so that assertion became vacuously true and stopped testing
80
- // anything. Name the shapes that DO exist and are still deliberately unreachable: nothing
81
- // of ours runs a byo-host, and the control plane is not pushed settings by this path.
82
- for (const unreachable of ['byo-host', 'control-plane']) {
82
+ // anything. Name the shape that DOES exist and is still deliberately unreachable:
83
+ // nothing of ours runs a byo-host.
84
+ for (const unreachable of ['byo-host']) {
83
85
  assert.ok(provisioning.HOSTING_SHAPES.includes(unreachable), `${unreachable} must be a real shape or this assertion is vacuous`);
84
86
  assert.ok(!provisioning.SETTINGS_APPLY_SHAPES.has(unreachable), unreachable);
85
87
  }
86
88
  });
87
89
 
90
+ t('the two gates agree: every settings-apply shape is one the self-row guard lets a settings-apply through', () => {
91
+ // A shape on SETTINGS_APPLY_SHAPES that the guard refuses would make the route say
92
+ // "a restart is coming" and then answer the owner with control_plane_protected.
93
+ const G = require('../modules/provisioning/control-plane-guard.js');
94
+ for (const s of provisioning.SETTINGS_APPLY_SHAPES) assert.equal(G.intentAllowedForShape('settings-apply', s), true, s);
95
+ // and the restart door was NOT opened on the self row as a side effect
96
+ assert.ok(!provisioning.RESTART_SHAPES.has('control-plane'), 'task 1004334 widened the settings push only');
97
+ });
98
+
88
99
  await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carries the settings-apply action', async () => {
89
100
  const calls = [];
90
101
  const db = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 1, action: params[1], state: 'pending' }] }; } };
@@ -104,6 +115,26 @@ await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carr
104
115
  assert.deepEqual(insert.params, [18, 'settings-apply', 'api:self', null]);
105
116
  });
106
117
 
118
+ await ta('enqueueSettingsApply on the CONTROL-PLANE row passes the self-row guard and INSERTs; a teardown on the same row is still refused (task 1004334)', async () => {
119
+ const selfRow = (calls) => ({ query: async (sql, params) => {
120
+ calls.push({ sql, params });
121
+ if (/SELECT hosting_shape FROM provisioning_instances/.test(sql)) return { rows: [{ hosting_shape: 'control-plane' }] };
122
+ return { rows: [{ id: 3, action: params[1], state: 'pending' }] };
123
+ } });
124
+ const calls = [];
125
+ const r = await provisioning.enqueueSettingsApply(selfRow(calls), 41, 'api:self');
126
+ assert.equal(r.created, true, 'the real guard (not a stub) admits the push onto the self row');
127
+ const insert = calls.find((c) => /INSERT INTO provisioning_intents/.test(c.sql));
128
+ assert.deepEqual(insert.params, [41, 'settings-apply', 'api:self', null]);
129
+ // Everything the guard existed to refuse is still refused through the same door.
130
+ for (const destructive of ['teardown', 'disconnect', 'provision', 'domain-attach', 'repo-private']) {
131
+ const seen = [];
132
+ await assert.rejects(provisioning.enqueueIntent(selfRow(seen), 41, destructive, 'api:self'),
133
+ (e) => e.code === 'control_plane_protected' && e.status === 409, destructive);
134
+ assert.ok(!seen.some((c) => /INSERT/.test(c.sql)), `${destructive}: nothing written`);
135
+ }
136
+ });
137
+
107
138
  await ta('enqueueSettingsApply: an identical PENDING push already open → created:false, no conflict (it carries this change)', async () => {
108
139
  const db = { query: async (sql) => {
109
140
  if (/INSERT/.test(sql)) { const e = new Error('dup'); e.code = '23505'; throw e; }
@@ -260,6 +291,20 @@ await ta('ACTIVE co-tenant queues too; the Archon override names WHICH archon on
260
291
  assert.equal(events[0].actor, 'api:archon:999');
261
292
  });
262
293
 
294
+ await ta('ACTIVE control-plane row (the platform itself): artist_gate off QUEUES a push and says a restart is coming (task 1004334)', async () => {
295
+ // The case TW01 hit: this save used to answer "runs on a machine the platform doesn't
296
+ // manage", stored 'off', and left the live site reading 'strict' with no way to apply it.
297
+ reset({ inst: { owner_builder_id: 7, slug: 'cloudbongos', status: 'active', hosting_shape: 'control-plane', domain: 'cloudbongos.com', port: 3002, settings: {} } });
298
+ const r = await patch('/provisioning/instances/41/settings', { artist_gate: 'off' });
299
+ assert.equal(r.status, 200, r.raw);
300
+ assert.equal(r.body.saved.artist_gate, 'off');
301
+ assert.deepEqual(updateCalls.map((c) => c.patch), [{ artist_gate: 'off' }]);
302
+ assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the row is written before the intent the runner reads it through');
303
+ assert.deepEqual(enqueueCalls, [{ id: 41, requestedBy: 'api:self' }]);
304
+ receiptShape(r.body.push, { queued: true, restart: true, reachable: true });
305
+ assert.doesNotMatch(r.body.push.message, /doesn.t manage/, 'no longer the unreachable-machine sentence');
306
+ });
307
+
263
308
  await ta('an identical PENDING push already open → 200, queued:false, "already queued … will carry this change"', async () => {
264
309
  reset();
265
310
  enqueueResult = { intent: { id: 5, action: 'settings-apply', state: 'pending' }, created: false };