@bongos/core 1.19.354 → 1.19.356
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 +48 -28
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0190-settings-apply-in-place-env-patch.md +59 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +5 -3
- package/modules/provisioning/migrations/provisioning_010_settings_apply.sql +24 -0
- package/modules/provisioning/provisioning.js +65 -23
- package/modules/provisioning/routes/provisioning.js +75 -7
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/projects.html +15 -2
- package/scripts/gds/provision-units.js +58 -0
- package/scripts/gds/provision.js +142 -12
- package/src/bongos/serve-internal.js +16 -2
- package/src/branding.js +11 -0
- package/src/module-api.js +5 -1
- package/tests/currency_label.mjs +12 -1
- package/tests/instance_manifest.mjs +8 -0
- package/tests/landing_page.mjs +18 -0
- package/tests/projects_hub_pre_uat.mjs +14 -0
- package/tests/provision_settings_apply.mjs +464 -0
- package/tests/provisioning_settings.mjs +8 -4
- package/tests/provisioning_settings_apply.mjs +372 -0
- package/tests/provisioning_settings_env.mjs +26 -9
- package/tests/provisioning_teardown_intent.mjs +5 -4
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.356",
|
|
6
|
+
"core_contract": "1.19.356",
|
|
7
|
+
"source_commit": "cb63704ad7571ab5d52fe949b6f265281f754679",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-25T14:41:47.258Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 330,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1509,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1861,
|
|
20
|
+
"tree_sha256": "983a13e5fa8e0ab31afc970fe2b7f6ebb7296e62bee4cf06257f51ce26658362",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "4229624f3aebb2f7335278e5c69b32d0d722616d38fc9a511d4c829a3c9d476e"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1439,10 +1439,15 @@
|
|
|
1439
1439
|
"mode": "0000644",
|
|
1440
1440
|
"sha256": "3067e87c4a2a1d63cb7b16526314b643f1940c4333cafe36627cd9bb5a293c0c"
|
|
1441
1441
|
},
|
|
1442
|
+
{
|
|
1443
|
+
"path": "docs/adr/0190-settings-apply-in-place-env-patch.md",
|
|
1444
|
+
"mode": "0000644",
|
|
1445
|
+
"sha256": "bbf7e4f1903e519952dc80515b9c590a9f62a51160394d9d6cb07b378f7748e5"
|
|
1446
|
+
},
|
|
1442
1447
|
{
|
|
1443
1448
|
"path": "docs/adr/README.md",
|
|
1444
1449
|
"mode": "0000644",
|
|
1445
|
-
"sha256": "
|
|
1450
|
+
"sha256": "1794e5de943ef65de6c9c0db0e82732b704c5261fe8bd8bef460ef5cc760c749"
|
|
1446
1451
|
},
|
|
1447
1452
|
{
|
|
1448
1453
|
"path": "docs/api-reference.md",
|
|
@@ -1452,7 +1457,7 @@
|
|
|
1452
1457
|
{
|
|
1453
1458
|
"path": "docs/api/openapi.json",
|
|
1454
1459
|
"mode": "0000644",
|
|
1455
|
-
"sha256": "
|
|
1460
|
+
"sha256": "ef200cfe7325260763ca25e1af1b506579d69c0c78bc8d9fa276ac2900486ac9"
|
|
1456
1461
|
},
|
|
1457
1462
|
{
|
|
1458
1463
|
"path": "docs/architecture.md",
|
|
@@ -4459,6 +4464,11 @@
|
|
|
4459
4464
|
"mode": "0000644",
|
|
4460
4465
|
"sha256": "de17cfb74f8684044abfb03e56698f91fa5aa813608f3a19d2ddf81d113e897e"
|
|
4461
4466
|
},
|
|
4467
|
+
{
|
|
4468
|
+
"path": "modules/provisioning/migrations/provisioning_010_settings_apply.sql",
|
|
4469
|
+
"mode": "0000644",
|
|
4470
|
+
"sha256": "416296ce60ce307628909d1847e52d024fd09fb8f882c0afe9db42331d06dc21"
|
|
4471
|
+
},
|
|
4462
4472
|
{
|
|
4463
4473
|
"path": "modules/provisioning/module.json",
|
|
4464
4474
|
"mode": "0000644",
|
|
@@ -4477,7 +4487,7 @@
|
|
|
4477
4487
|
{
|
|
4478
4488
|
"path": "modules/provisioning/provisioning.js",
|
|
4479
4489
|
"mode": "0000644",
|
|
4480
|
-
"sha256": "
|
|
4490
|
+
"sha256": "e6e3bcd6968be6ad16de989b2de301de7175c9040bb582529527163f75a05ff6"
|
|
4481
4491
|
},
|
|
4482
4492
|
{
|
|
4483
4493
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -4487,7 +4497,7 @@
|
|
|
4487
4497
|
{
|
|
4488
4498
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
4489
4499
|
"mode": "0000644",
|
|
4490
|
-
"sha256": "
|
|
4500
|
+
"sha256": "dc701815b1aa58895cedb1897bc5103f3250291b4ff4b507051ccda5310f7c5e"
|
|
4491
4501
|
},
|
|
4492
4502
|
{
|
|
4493
4503
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -4787,12 +4797,12 @@
|
|
|
4787
4797
|
{
|
|
4788
4798
|
"path": "package-lock.json",
|
|
4789
4799
|
"mode": "0000644",
|
|
4790
|
-
"sha256": "
|
|
4800
|
+
"sha256": "d5f53ae25273ca753b122eca1de770bb20d9a1ed1b9a7b0b105710403670e7ec"
|
|
4791
4801
|
},
|
|
4792
4802
|
{
|
|
4793
4803
|
"path": "package.json",
|
|
4794
4804
|
"mode": "0000644",
|
|
4795
|
-
"sha256": "
|
|
4805
|
+
"sha256": "881f7f9d99198d118f9bbcec082d6637b586dbc6acb850cc654cac192b0cab55"
|
|
4796
4806
|
},
|
|
4797
4807
|
{
|
|
4798
4808
|
"path": "public-docs/index.html",
|
|
@@ -5052,7 +5062,7 @@
|
|
|
5052
5062
|
{
|
|
5053
5063
|
"path": "public-landing/projects.html",
|
|
5054
5064
|
"mode": "0000644",
|
|
5055
|
-
"sha256": "
|
|
5065
|
+
"sha256": "50cecb3a7b9cbae9ec321fa4eb2da1437c6f226037406a9c6bc5fe9e18813801"
|
|
5056
5066
|
},
|
|
5057
5067
|
{
|
|
5058
5068
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5802,12 +5812,12 @@
|
|
|
5802
5812
|
{
|
|
5803
5813
|
"path": "scripts/gds/provision-units.js",
|
|
5804
5814
|
"mode": "0000644",
|
|
5805
|
-
"sha256": "
|
|
5815
|
+
"sha256": "5e65be1f313f387cbf97116c678b54cbf4616e0580a42afc98860982f4f14bfe"
|
|
5806
5816
|
},
|
|
5807
5817
|
{
|
|
5808
5818
|
"path": "scripts/gds/provision.js",
|
|
5809
5819
|
"mode": "0000644",
|
|
5810
|
-
"sha256": "
|
|
5820
|
+
"sha256": "64d0869afb99745ce08b311bb973d183746f9ed04c65bc112e5610740746eda3"
|
|
5811
5821
|
},
|
|
5812
5822
|
{
|
|
5813
5823
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -6617,7 +6627,7 @@
|
|
|
6617
6627
|
{
|
|
6618
6628
|
"path": "src/bongos/serve-internal.js",
|
|
6619
6629
|
"mode": "0000644",
|
|
6620
|
-
"sha256": "
|
|
6630
|
+
"sha256": "025676342224c2944fa2361965c12d9e6cf812fd17541c737a32fab7544c2709"
|
|
6621
6631
|
},
|
|
6622
6632
|
{
|
|
6623
6633
|
"path": "src/bongos/stealth-gate.js",
|
|
@@ -6632,7 +6642,7 @@
|
|
|
6632
6642
|
{
|
|
6633
6643
|
"path": "src/branding.js",
|
|
6634
6644
|
"mode": "0000644",
|
|
6635
|
-
"sha256": "
|
|
6645
|
+
"sha256": "239e2af1c637d0f72e8a61dc2ef5450ca0821477617941399253330e9e4afa25"
|
|
6636
6646
|
},
|
|
6637
6647
|
{
|
|
6638
6648
|
"path": "src/build-info.js",
|
|
@@ -6652,7 +6662,7 @@
|
|
|
6652
6662
|
{
|
|
6653
6663
|
"path": "src/module-api.js",
|
|
6654
6664
|
"mode": "0000644",
|
|
6655
|
-
"sha256": "
|
|
6665
|
+
"sha256": "0b23db16d6f47afb30b4f5ed970cd1a2d0aac607feda9ce4ebf9be09c0a3dc47"
|
|
6656
6666
|
},
|
|
6657
6667
|
{
|
|
6658
6668
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7282,7 +7292,7 @@
|
|
|
7282
7292
|
{
|
|
7283
7293
|
"path": "tests/currency_label.mjs",
|
|
7284
7294
|
"mode": "0000644",
|
|
7285
|
-
"sha256": "
|
|
7295
|
+
"sha256": "e6e6f835296328e1055b048c6610c2c69853d311252365f904c6a3de4a2c4d59"
|
|
7286
7296
|
},
|
|
7287
7297
|
{
|
|
7288
7298
|
"path": "tests/cutover_codemod.mjs",
|
|
@@ -8107,7 +8117,7 @@
|
|
|
8107
8117
|
{
|
|
8108
8118
|
"path": "tests/instance_manifest.mjs",
|
|
8109
8119
|
"mode": "0000644",
|
|
8110
|
-
"sha256": "
|
|
8120
|
+
"sha256": "52265f1f4ec43afc1f07467692c40ce608eaea74f2752ef1fe4ff1a7bb187387"
|
|
8111
8121
|
},
|
|
8112
8122
|
{
|
|
8113
8123
|
"path": "tests/instance_mismatch_guard.mjs",
|
|
@@ -8147,7 +8157,7 @@
|
|
|
8147
8157
|
{
|
|
8148
8158
|
"path": "tests/landing_page.mjs",
|
|
8149
8159
|
"mode": "0000644",
|
|
8150
|
-
"sha256": "
|
|
8160
|
+
"sha256": "7457d53dee7cce5770c17ec908c0f051e72092d2fa1cf5a9a1198fbdac64145a"
|
|
8151
8161
|
},
|
|
8152
8162
|
{
|
|
8153
8163
|
"path": "tests/lib_sh_resolution.mjs",
|
|
@@ -8542,7 +8552,7 @@
|
|
|
8542
8552
|
{
|
|
8543
8553
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
8544
8554
|
"mode": "0000644",
|
|
8545
|
-
"sha256": "
|
|
8555
|
+
"sha256": "aff31f49d05b6c0d0249ac44e6c44faa7d0d622f1a065a58d8a22a989122a3f2"
|
|
8546
8556
|
},
|
|
8547
8557
|
{
|
|
8548
8558
|
"path": "tests/promote_goal_id.mjs",
|
|
@@ -8559,6 +8569,11 @@
|
|
|
8559
8569
|
"mode": "0000644",
|
|
8560
8570
|
"sha256": "0fc6c6626b43458c2e065322bec5a2ecd87e50a44abd2838d81ff0f0b6abeae7"
|
|
8561
8571
|
},
|
|
8572
|
+
{
|
|
8573
|
+
"path": "tests/provision_settings_apply.mjs",
|
|
8574
|
+
"mode": "0000644",
|
|
8575
|
+
"sha256": "9666ed868774ebe49a76b15b9de6db17ac93af23a740c3384e25b73b4536cc58"
|
|
8576
|
+
},
|
|
8562
8577
|
{
|
|
8563
8578
|
"path": "tests/provisioning_default_domain.mjs",
|
|
8564
8579
|
"mode": "0000644",
|
|
@@ -8582,12 +8597,17 @@
|
|
|
8582
8597
|
{
|
|
8583
8598
|
"path": "tests/provisioning_settings.mjs",
|
|
8584
8599
|
"mode": "0000644",
|
|
8585
|
-
"sha256": "
|
|
8600
|
+
"sha256": "662d193ac3567e984da03e13ec4d41950cc29b7687fd195513e0291cff37c1c4"
|
|
8601
|
+
},
|
|
8602
|
+
{
|
|
8603
|
+
"path": "tests/provisioning_settings_apply.mjs",
|
|
8604
|
+
"mode": "0000644",
|
|
8605
|
+
"sha256": "916211a242794f56d4c4d65b492d46c168a89acb3985f64e40533221402caf53"
|
|
8586
8606
|
},
|
|
8587
8607
|
{
|
|
8588
8608
|
"path": "tests/provisioning_settings_env.mjs",
|
|
8589
8609
|
"mode": "0000644",
|
|
8590
|
-
"sha256": "
|
|
8610
|
+
"sha256": "08770277cad7e16507a7c6c074732711fb9aad2025531ad8ec9de12c09c2d2d6"
|
|
8591
8611
|
},
|
|
8592
8612
|
{
|
|
8593
8613
|
"path": "tests/provisioning_slug_available.mjs",
|
|
@@ -8597,7 +8617,7 @@
|
|
|
8597
8617
|
{
|
|
8598
8618
|
"path": "tests/provisioning_teardown_intent.mjs",
|
|
8599
8619
|
"mode": "0000644",
|
|
8600
|
-
"sha256": "
|
|
8620
|
+
"sha256": "cfc6e239d42be33dad3e030333d51734114b7c969d1e42079e77d3c468c5728d"
|
|
8601
8621
|
},
|
|
8602
8622
|
{
|
|
8603
8623
|
"path": "tests/provisioning_teardown_states.mjs",
|
|
@@ -153,7 +153,7 @@ export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown;
|
|
|
153
153
|
export interface PatchProjectsIdFeaturedRequest { featured: boolean }
|
|
154
154
|
export interface PatchProjectsIdFeaturedResponse { project: unknown }
|
|
155
155
|
export interface PatchProvisioningInstancesIdSettingsRequest { platform_visibility?: string; joinability?: string }
|
|
156
|
-
export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean;
|
|
156
|
+
export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean; saved: unknown; push: unknown }
|
|
157
157
|
export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; description?: string; status?: unknown }
|
|
158
158
|
export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
|
|
159
159
|
export interface PostAccessRequestsResponse { ok: boolean; request?: unknown; already_pending?: boolean }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ADR 0190 — A changed project setting reaches a running instance by an in-place env patch and a restart, never a template re-write
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-25
|
|
5
|
+
- **Task:** [task 1003140](https://cloudbongos.com/builders#/task/1003140) (goal 1000058 — the non-cloudbongos project lifecycle)
|
|
6
|
+
- **Relates to:** ADR 0111 (provisioning trust boundary), ADR 0128 (runner privilege model), ADR 0133 (GitHub App Manifest cred placement), task 1003139 (the standup leg), task 1002692 (domain-attach), task 1002691 (the open-intent slot), task 1002720 (F16/F19/F23 truthfulness)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Task 1003139 built the *standup* half of the platform→instance settings channel: a newly-provisioned instance reads `platform_visibility` / `joinability` from the `/etc/<slug>/web.env` the control-plane runner writes, through `src/branding.js` `ENV_OVERRIDES` onto `project.*`. That closed the "write-only state" gap for new instances only. A **running** instance loads its branding pack once — `branding()` is memoized and deep-frozen for the process lifetime — so a value changed on the platform afterwards could not reach it by any path. `PATCH /provisioning/instances/:id/settings` wrote the jsonb and pushed nothing.
|
|
11
|
+
|
|
12
|
+
The owner decided on 2026-08-19 that a restart is an acceptable cost of applying a change, and that the surface must **say so** rather than restart silently; and that a PATCH is a *request* until the intent settles, so no surface may claim the new state on PATCH return.
|
|
13
|
+
|
|
14
|
+
The natural implementation is the `domain-attach` shape (task 1002692) minus the DNS legs: re-write web.env, restart the service, verify healthz. That shape has a hazard that only shows on an instance the owner has actually used.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
1. **A `settings-apply` intent** (migration `provisioning_010`, `INTENT_ACTIONS` in lockstep). `PATCH /settings` writes the platform row **first**, then — for an instance in `SETTINGS_APPLY_FROM = {active}` on a shape in `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` — enqueues the intent via `enqueueSettingsApply`. The web tier still only enqueues; the runner remains the sole token-holder (ADR 0111 §2).
|
|
19
|
+
|
|
20
|
+
2. **The runner patches the file in place, and every expected failure returns `ok:false`.** `settingsApplyInstance` reads the instance's web.env as it stands (`sudo cat` under the privileged model, ADR 0128), upserts **only** the two settings lines with the pure `upsertEnvVars` (replace where present, append where absent), writes it back through the same temp-file + `sudo install -m 600` path every `/etc` write uses, and restarts the unit. The write and the restart are guarded: a refused install or a non-zero `systemctl restart` returns `{ ok:false, error }` so the drain loop re-pends the intent (retrying up to the attempt cap) and leaves the instance's status alone — it **never** throws, because the drain loop's catch flips an instance to `error`, and a healthy active project marked broken over a settings push would stop being probed, fall off the push path, and need a re-provision to recover. Dry-run *plans* the patch ("would patch `<keys>` in `<path>` in place") rather than writing a synthetic two-line body that would read as the exact overwrite this leg exists to avoid.
|
|
21
|
+
|
|
22
|
+
3. **The verify asks the instance whether it *read* the values, and records only what it earned.** After the restart the runner polls `/healthz` with the manifest probe's patience (`MANIFEST_VERIFY_TRIES × MANIFEST_VERIFY_INTERVAL_MS`, the F23 lesson — a false "down" here is not cosmetic, it arms the owner's re-kick paths), then fetches the instance's own public `GET /api/gds/instance` and compares `brand.project` against the pushed values (`settingsConsumed`): **true** → "restarted and reads the new values"; **false** → "restarted, but still reports the OLD values — its core may predate the settings channel"; **null** → "its core does not publish its settings yet, so whether it reads them is unknown". Answering `/healthz` proves the process, not that the setting took; an instance whose pinned core predates task 1003139 restarts fine and ignores the lines, and the event must not call that "applied".
|
|
23
|
+
|
|
24
|
+
4. **The state gate is `{active}` only.** A pre-active instance gets the same values at standup from the same `effectiveSettings()` row; a torn-down one has nothing to restart. The provision intent cannot stand in for the push — `PROVISION_ENQUEUE_FROM` deliberately excludes `active` (task 1002691).
|
|
25
|
+
|
|
26
|
+
5. **A dedicated droplet is refused honestly, at both ends.** It is stood up by cloud-init and the runner has no post-hoc exec path to it (the `placeManifestCreds` precedent). The route stores the value and answers `push.reachable:false` — *"This project runs on a machine the platform doesn't manage, so it can't be restarted from here — whoever runs that machine has to apply it there."* (not "its own server", which is also the hub's label for a *standalone* project, which **is** pushable); the runner, should such an intent ever arrive, returns `ok:false` with the exact lines to set — never a reported push that did not land.
|
|
27
|
+
|
|
28
|
+
6. **A save that changes nothing neither writes nor restarts.** The route compares the request against `effectiveSettings(inst)`; an unchanged save answers *"No change — this project already has these settings."* with no row write, no event and no intent, so a whole-form save or a retried request cannot bounce a live project. The one exception is the owner re-kick (the domain route's F19 shape): the same values re-sent while a retired push left its `error_note` re-queue the push.
|
|
29
|
+
|
|
30
|
+
7. **The response is a receipt, not a claim.** `{ ok, saved, push }` — `saved` is what the platform now stores (deliberately not `settings`, the field a form would render as the live value), and `push: { queued, restart, reachable, message }` is one shape on every branch: `restart` = a restart is what applies this, `reachable` = the platform can perform that restart. Sentences are state-only, with no wall-clock **or duration** promises ("briefly" is a claim about downtime; the runner's own failure branch contradicts it). An open slot held by other work (or by a push already *running*, which may have read the row before this write) answers `409 intent_conflict` with `details.recorded:true`; an identical *pending* push is idempotent because the runner reads the row's current values when it runs; an enqueue that throws still answers `200` *"Saved, but the restart could not be queued just now — send it again to apply it."* — a `500` would deny a durable write. A snagged (`error`) project is told its standup stopped partway and the change takes effect the next time it is stood up, not that it "finishes standing up". The archon override stamps `api:archon:<id>` on the event and the intent (the force-teardown convention).
|
|
31
|
+
|
|
32
|
+
8. **A pending push yields to a teardown.** `enqueueTeardown` now supersedes a *pending* `settings-apply` exactly as it supersedes a pending `provision` (task 1002691): a push into a project about to be torn down is dead work, and the single open-intent slot must not hold the delete hostage across retry ticks. Running intents are never yanked.
|
|
33
|
+
|
|
34
|
+
9. **The read-back is the instance's own manifest.** `clientBranding` now projects `project: { platformVisibility, joinability }` — what the instance *resolved* from its environment, env override included — so `GET /api/gds/instance` `brand.project` is where a manage page learns what the instance runs. Resolved, not enforced: enforcement lands with tasks 1003043 (visibility) and 1003044 (joinability). The allow-list ledger in `tests/currency_label.mjs` was widened deliberately; the policy is public by nature (a gated project's gate says "sign in"; an invite-only door says so).
|
|
35
|
+
|
|
36
|
+
10. **The secret never reaches the journal.** A remote (ADR 0130) file placement embeds the base64 body in the ssh command; `makeExec` now takes a `shown` label so the runner logs "write `<path>` (N bytes)" and never the payload — this leg is the first to feed a *credentialed* file back through that path, but the fix covers every remote web.env write. The locally staged temp file is created `0600`.
|
|
37
|
+
|
|
38
|
+
## Why the in-place patch is load-bearing
|
|
39
|
+
|
|
40
|
+
`domainAttachInstance` re-writes web.env by re-running `webEnvBody(inst, { federation: null })`. That regenerates the file **from the row** — and the row does not know the OAuth client id/secret that `placeManifestCreds` fetched from GitHub and placed later, nor the App private key (the ADR 0055 publish credential), nor the federation block. A template re-write therefore writes those lines back **blank**. On the domain-attach path this is masked by wizard order (address first, GitHub App second) — except on the owner re-kick (task 1002720 F19), which can run after the creds are placed; a settings push always happens on an instance the owner is signed in to, so the same move would break sign-in on the exact project they just touched. The fix is not to teach `webEnvBody` the secrets (the row must never hold them) but to change **only the lines this leg owns**. An unreadable or empty file is refused rather than "patched" — writing two lines over a missing `PORT`/`PGDATABASE` would take the instance down.
|
|
41
|
+
|
|
42
|
+
The domain-attach hazard itself is pre-existing and out of this task's scope; it is captured as idea 1000842 for its own task, with this leg's in-place shape as the fix.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
- Flipping a setting on a live co-tenant or standalone project restarts it within one runner tick; the owner is told so on every surface that carries the receipt, and told "no change" when nothing changed.
|
|
47
|
+
- The hub's card strip and the done panel label the intent by state — *"A settings change is queued — this card updates when it starts."* while pending, *"Applying a settings change — the project restarts to pick it up."* while running — instead of the generic "a settings-apply run is queued".
|
|
48
|
+
- An instance whose pinned core predates task 1003139 receives the env lines and the restart but does not *consume* them until its own core is upgraded; the runner's event says so ("does not publish its settings yet" / "still reports the OLD values") rather than "applied". The live test project (Spike Harbor) ran core 1.19.2 when this landed, so its manifest read-back waits on its own core pin. The live leg is witnessed only once this code is the control plane's pinned core (the runner executes the pinned package), so the 2026-08-25 session log records the baseline and the post-pin procedure, and the result lands as a learning on the task. Surfacing "not consumed" on the owner's card is follow-up work.
|
|
49
|
+
- `provision.js` grew by ~110 lines and stays well under the 1,500-line ratchet (the prerequisite shrink landed in task 1003206).
|
|
50
|
+
|
|
51
|
+
## Rejected
|
|
52
|
+
|
|
53
|
+
- **The template re-write** (`webEnvBody`) — blanks placed creds; see above.
|
|
54
|
+
- **Re-provisioning as the push** — `provisionInstance` is a no-op on an active instance by design, and a full standup is the wrong tool for two lines.
|
|
55
|
+
- **Skipping the restart** (hot-reload the pack) — the pack is memoized and deep-frozen on purpose; per-request re-reads would reopen the config-drift class of bug the freeze exists to prevent.
|
|
56
|
+
- **Claiming the new state on PATCH return** — contradicts the owner's honesty rule and `last_verified_up`'s whole discipline.
|
|
57
|
+
- **Queueing for a dedicated droplet anyway** — a push that cannot land, reported as queued, is exactly the F16 lie.
|
|
58
|
+
- **Treating `/healthz` as proof the setting took** — proves the process only; the manifest read-back is the witness.
|
|
59
|
+
- **Restarting on every save** — the owner accepted a restart as the cost of *applying a change*, not per Save click.
|
package/docs/adr/README.md
CHANGED
|
@@ -265,5 +265,6 @@ This keeps the decision history honest and traceable.
|
|
|
265
265
|
| 0187 | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](<redacted>.md) | auth / security posture |
|
|
266
266
|
| 0188 | [**The weekly patrol runs on CI, and a declared cadence must have something that fires it** ([task 1003209](https://cloudbongos.com/builders#/task/1003209), goal 1000079). Goal 1000076 shipped a weekly red-team patrol, a routine describing how to run it, a recipe, and a manifest row — and nothing ever ran it; the follow-up audit §7 named all three halves (no committed scheduler, no cadence for a scheduler to read, and a manifest green anyway because its wiring predicate was satisfied by the SKILL.md mentioning its own scanner). The symmetric fix — an in-process timer beside `peer-vote-tally` in `routine-timers.js` — is **wrong on correctness, not cost**: all three patrol checks read things only the REPO has (full git history, `.github/workflows/`, an auditable lockfile), and a deployed instance runs a VENDORED core out of `node_modules/@bongos/core` that has none of them, so the timer would have scanned nothing and reported clean — worse than unscheduled. The runner is therefore CI: a weekly Actions lane with `fetch-depth: 0` and a pinned, checksum-verified gitleaks (the missing binary the audit listed separately), invoking `run-routine.js redteam-weekly` so the SKILL.md's steps stay the single source of truth. `cadence:` becomes a real frontmatter field — declarative only, scheduler-agnostic. The load-bearing piece is `routine-schedule.js` (fitness Check 25), whose predicate is **relational, not existential**: it classifies the cron into the period it actually realizes and fails when that disagrees with the declared cadence — the one rot mode no substring or existence check can EVER see, since editing weekly→monthly leaves every "is it wired?" predicate green. The manifest row was re-pointed to match ("wired" now means the SCHEDULER names the routine). Finally the exit code says **a finding was LOST, not that a finding exists**: findings are advisory (ADR 0162), so filing three is a SUCCESS and red is reserved for findings that cannot be recorded — failing on findings would have gone red on run one and stayed there (the unrotated staging credential in `092e473` is a standing history hit), and an always-red weekly job is one nobody reads. Filing is credential-gated but OPTIONAL — with no session secret the patrol still runs and writes its findings to the job summary, which matters because this goal's C1 is about eliminating long-lived tokens, not adding one. Rejected: the in-process timer; teaching `routine-timers.js` generic cadence discovery (a scheduler built for nobody — its only candidate user cannot run there); failing whenever findings exist; requiring the secret; a deeper manifest predicate instead of a new check.](<redacted>.md) | security / enforcement durability |
|
|
267
267
|
| 0189 | [**A frozen baseline must justify itself: declared floors and the freeze-forever detector** ([task 1003210](https://cloudbongos.com/builders#/task/1003210), goal 1000079). The shrink-only ratchet makes a quality metric unable to REGRESS and, by the same design, unable to sit still noticeably: the follow-up audit measured the whole file at "every metric equal to baseline, 0 reduced" and the ratchet reported a clean PASS, because one green covers both actively-paying-down and nobody-has-looked-in-a-year. The ratchet has no concept of time; this adds it. **Age comes from git, never a stored `since:` field** — a timestamp beside each metric would be faster and hermetic and would also be the field that silently stops being updated, leaving old debt permanently fresh-looking, which is worse than no check because it produces confident green; an unavailable history reports UNKNOWN, not fresh (the `--require-roster` doctrine). A metric's age is the date of the OLDEST commit still carrying its current value, so a baseline recorded once and never revisited dates to its introduction — exactly the case being hunted. **The half that matters is the declared floor.** The naive "flag anything unmoved for N weeks" fires forever on metrics that are FINISHED (`require_cycle_count`'s four deliberate lazy requires; the ~28 duplicate windows ADR 0091 §2 forces because a module may not import core), and a check that cries wolf weekly is one everyone learns to skip — the same alert-fatigue failure ADR 0188 §2.4 avoids. So a metric may declare `floors: {name: {value, why}}` and a metric AT its floor is done, not stale; the `why` is mandatory and length-checked, because a floor exempts a metric from the clock permanently and must therefore be a written claim someone can disagree with — **silence was the problem**, and a floor without a reason is just a mute button. A floor exempts only DOWN TO the floor, so `duplicate_window_count`'s 59-vs-28 keeps its ~31 reducible windows on the clock. **Two severities, split by whose fault it is:** declaration rot (a floor naming a dead metric, a floor with no reason, a floor the metric already dropped below) is hermetic and caused by the diff at hand, so it HARD-FAILS the merge gate; staleness is nobody's diff and would punish whoever pushed next, so it warns in fitness and is FILED weekly by the patrol — the "repeatable, scheduled version" the audit asked for. Also fixed a latent eater: `--update-baselines` wrote a fixed object literal, so one refresh destroyed the accumulated `_comment` and (once floors landed) every floor, deleting a finished metric's justification from the repo while turning it into permanent weekly noise. Nothing is flagged today — the file is six days old — so the detector is verified by shrinking the window, not by waiting. Rejected: a stored `since:`; no exemptions; a floor with no required reason; hard-failing on staleness; patrol-only placement.](<redacted>.md) | quality ratchets / debt governance |
|
|
268
|
+
| 0190 | [**A changed project setting reaches a running instance by an in-place env patch and a restart — never a template re-write** ([task 1003140](https://cloudbongos.com/builders#/task/1003140), goal 1000058). Task 1003139 made a NEW instance read `platform_visibility` / `joinability` from the web.env the runner writes; a RUNNING instance loads its branding pack once, memoized and deep-frozen for the process lifetime, so a changed value could not reach it at all — the settings were write-only state on the platform. Decision: `PATCH /provisioning/instances/:id/settings` writes the platform row first, then for an ACTIVE instance on a shape the runner can reach enqueues a `settings-apply` intent (migration provisioning_010); the control-plane runner reads the instance's web.env as it stands, upserts ONLY the two settings lines (`upsertEnvVars`), and RESTARTS the service. The in-place patch is the load-bearing choice: the natural move — re-running `webEnvBody` as domain-attach does — regenerates the file from the row, and the row does not know the OAuth client secret or the App private key that `placeManifestCreds` placed later, so a template re-write silently blanks sign-in on the very instance the owner just touched. `SETTINGS_APPLY_FROM = {active}` because a pre-active instance gets the same values at standup and a torn-down one has nothing to restart (the provision intent cannot stand in — it excludes `active`, task 1002691); `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` because a dedicated droplet has no post-hoc exec path, and a push that cannot land is refused at the route AND the runner rather than reported. The response is a receipt, not a claim: it says the restart is coming and that the instance keeps its current setting until then, and the read-back lives on the instance's own public `GET /instance` manifest, whose `brand.project` now carries the resolved policy (`clientBranding` allow-list widened deliberately). Rejected: the template re-write; re-provisioning as the push; skipping the restart (impossible while the pack is memoized); claiming the new state on PATCH return.](<redacted>.md) | provisioning / settings channel |
|
|
268
269
|
|
|
269
270
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
package/docs/api/openapi.json
CHANGED
|
@@ -10204,7 +10204,7 @@
|
|
|
10204
10204
|
"provisioning"
|
|
10205
10205
|
],
|
|
10206
10206
|
"summary": "PATCH /provisioning/instances/:id/settings",
|
|
10207
|
-
"description": "PATCH /provisioning/instances/:id/settings — change a project's settings. Body: any subset of the known keys; strict validation refuses unknown keys and off-vocabulary values. rank: any authenticated builder (own resource; archon may change any — the override is audited via the event's actor).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
10207
|
+
"description": "PATCH /provisioning/instances/:id/settings — change a project's settings. Body: any subset of the known keys; strict validation refuses unknown keys and off-vocabulary values. rank: any authenticated builder (own resource; archon may change any — the override is audited via the event's actor). The platform's row is updated first; then, for an ACTIVE instance on a shape the runner can reach, a 'settings-apply' intent is enqueued (task 1003140): the control-plane runner patches the instance's web.env in place and RESTARTS it, because the branding pack is memoized for the process lifetime. A save that changes nothing neither writes nor restarts. The response is a request receipt — `saved` is what the platform now stores, and `push` { queued, restart, reachable, message } says whether a restart was queued and when the value takes effect — never a claim the instance already runs it; a client reads that back from the instance's own public GET /instance manifest (brand.project). 409 intent_conflict (details.recorded: true) when the open slot is held by other work — the change IS saved.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
10208
10208
|
"x-rank": "any-builder",
|
|
10209
10209
|
"x-source": "modules/provisioning/routes/provisioning.js",
|
|
10210
10210
|
"parameters": [
|
|
@@ -15916,11 +15916,13 @@
|
|
|
15916
15916
|
"ok": {
|
|
15917
15917
|
"type": "boolean"
|
|
15918
15918
|
},
|
|
15919
|
-
"
|
|
15919
|
+
"saved": {},
|
|
15920
|
+
"push": {}
|
|
15920
15921
|
},
|
|
15921
15922
|
"required": [
|
|
15922
15923
|
"ok",
|
|
15923
|
-
"
|
|
15924
|
+
"saved",
|
|
15925
|
+
"push"
|
|
15924
15926
|
]
|
|
15925
15927
|
},
|
|
15926
15928
|
"PatchTasksIdRequest": {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- provisioning_010_settings_apply.sql — the settings-apply intent action
|
|
2
|
+
-- (task 1003140, goal 1000058: the live-push half of the platform→instance
|
|
3
|
+
-- settings channel).
|
|
4
|
+
--
|
|
5
|
+
-- WHY. Task 1003139 built the standup leg: a NEW instance reads
|
|
6
|
+
-- platform_visibility / joinability from the web.env the runner writes. But a
|
|
7
|
+
-- RUNNING instance loads its branding pack once — memoized + deep-frozen for the
|
|
8
|
+
-- process lifetime — so a changed setting could not reach it without re-writing
|
|
9
|
+
-- /etc/<slug>/web.env and RESTARTING the service: the domain-attach shape minus
|
|
10
|
+
-- the DNS legs. PATCH /provisioning/instances/:id/settings now enqueues a
|
|
11
|
+
-- 'settings-apply' intent for an ACTIVE instance; the control-plane runner (the
|
|
12
|
+
-- sole token-holder, ADR 0111 §2) patches the settings lines in place and
|
|
13
|
+
-- restarts. The web tier still only ENQUEUES — the trust boundary is unchanged.
|
|
14
|
+
--
|
|
15
|
+
-- Widening a CHECK enum is additive (permits MORE values; no row changes) — the
|
|
16
|
+
-- provisioning_003/005 precedent. Applied only when the provisioning module is
|
|
17
|
+
-- enabled. Safe to re-run. Keep INTENT_ACTIONS in provisioning.js in lockstep.
|
|
18
|
+
|
|
19
|
+
ALTER TABLE provisioning_intents DROP CONSTRAINT IF EXISTS provisioning_intents_action_check;
|
|
20
|
+
ALTER TABLE provisioning_intents ADD CONSTRAINT provisioning_intents_action_check
|
|
21
|
+
CHECK (action IN ('provision', 'teardown', 'oauth-manifest', 'domain-attach', 'settings-apply'));
|
|
22
|
+
|
|
23
|
+
INSERT INTO schema_migrations (version) VALUES ('provisioning_010_settings_apply')
|
|
24
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -42,7 +42,11 @@ const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure']
|
|
|
42
42
|
// 'domain-attach' (task 1002692, migration provisioning_005): re-assert the
|
|
43
43
|
// domain-dependent standup legs for an already-ACTIVE instance that was just
|
|
44
44
|
// given its address via POST /provisioning/instances/:id/domain.
|
|
45
|
-
|
|
45
|
+
// 'settings-apply' (task 1003140, migration provisioning_010): push a changed
|
|
46
|
+
// per-project setting to an already-ACTIVE instance — patch its web.env settings
|
|
47
|
+
// lines in place + restart, since the branding pack is memoized for the process
|
|
48
|
+
// lifetime (task 1003139 built only the standup leg).
|
|
49
|
+
const INTENT_ACTIONS = ['provision', 'teardown', 'oauth-manifest', 'domain-attach', 'settings-apply'];
|
|
46
50
|
// Every state a teardown can be asked for — i.e. everything except the one
|
|
47
51
|
// state where there is provably nothing left to release. DERIVED from
|
|
48
52
|
// INSTANCE_STATES on purpose (task 1003042, walk finding N5): the old hand-kept
|
|
@@ -69,6 +73,17 @@ const PROVISION_ENQUEUE_FROM = new Set(['requested', 'provisioning', 'error', 't
|
|
|
69
73
|
// 'requested'/'error' only record it — the (re)provision run wires the domain
|
|
70
74
|
// itself. Mid-flight or torn-down states refuse truthfully.
|
|
71
75
|
const DOMAIN_ATTACH_FROM = new Set(['requested', 'error', 'active']);
|
|
76
|
+
// The ONE state a settings push is meaningful from (task 1003140). A pre-active
|
|
77
|
+
// instance needs no push — standup writes the same values from the same
|
|
78
|
+
// effectiveSettings() row — and a torn-down one has nothing to restart. The
|
|
79
|
+
// provision intent cannot stand in for it: PROVISION_ENQUEUE_FROM deliberately
|
|
80
|
+
// excludes 'active' (task 1002691).
|
|
81
|
+
const SETTINGS_APPLY_FROM = new Set(['active']);
|
|
82
|
+
// The hosting shapes the runner has a write path to. A 'dedicated' instance is
|
|
83
|
+
// its own droplet, stood up by cloud-init — the runner has no post-hoc exec path
|
|
84
|
+
// to it (the placeManifestCreds refusal) — so the route refuses the push HONESTLY
|
|
85
|
+
// rather than queue one that never lands.
|
|
86
|
+
const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'standalone']);
|
|
72
87
|
|
|
73
88
|
// Per-project settings (task 1003040, goal 1000058): the known keys + their
|
|
74
89
|
// vocabularies, enforced at the API layer — the jsonb `settings` column
|
|
@@ -244,31 +259,39 @@ function publicGithubApp(manifestRow) {
|
|
|
244
259
|
return { status: String(manifestRow.status), app_slug: manifestRow.app_slug || null };
|
|
245
260
|
}
|
|
246
261
|
|
|
247
|
-
// The instance's
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// at standup (task 1003139). PURE (row + prefix in, string out) and owned HERE, next
|
|
252
|
-
// to SETTINGS_VOCAB, rather than in provision.js — the vocabulary and the thing that
|
|
262
|
+
// The env variables that carry this instance's settings, as the instance reads
|
|
263
|
+
// them: { <PREFIX>_PLATFORM_VISIBILITY, <PREFIX>_JOINABILITY } (task 1003139 /
|
|
264
|
+
// 1003140). PURE (row + prefix in, object out) and owned HERE, next to
|
|
265
|
+
// SETTINGS_VOCAB, rather than in provision.js — the vocabulary and the thing that
|
|
253
266
|
// transmits it must not be able to drift. Values come from effectiveSettings, so a
|
|
254
267
|
// row with no stored settings emits today's behavior EXPLICITLY rather than nothing:
|
|
255
|
-
// an instance must never have to infer a policy from an absent variable.
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
// the process lifetime) — that is the 'settings-apply' intent, still to be built.
|
|
260
|
-
function settingsEnvLines(row, prefix) {
|
|
268
|
+
// an instance must never have to infer a policy from an absent variable. BOTH legs
|
|
269
|
+
// derive from this one function — the standup write (settingsEnvLines → webEnvBody)
|
|
270
|
+
// and the live push (the runner's settings-apply leg patches exactly these lines).
|
|
271
|
+
function settingsEnvVars(row, prefix) {
|
|
261
272
|
const s = effectiveSettings(row);
|
|
273
|
+
return Object.fromEntries(Object.keys(SETTINGS_VOCAB).map((key) => [`${prefix}_${key.toUpperCase()}`, s[key]]));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The web.env block the runner writes at STANDUP (task 1003139): a comment that
|
|
277
|
+
// tells the truth about where the values come from, then the settingsEnvVars
|
|
278
|
+
// lines. Changing one on an ALREADY-RUNNING instance is the 'settings-apply'
|
|
279
|
+
// intent (task 1003140): the runner re-writes these lines in place and restarts
|
|
280
|
+
// the service, because the branding pack is memoized for the process lifetime.
|
|
281
|
+
function settingsEnvLines(row, prefix) {
|
|
282
|
+
const lines = Object.entries(settingsEnvVars(row, prefix)).map(([k, v]) => `${k}=${v}`).join('\n');
|
|
262
283
|
return `# Per-project policy the platform decides and this instance obeys (task
|
|
263
284
|
# 1003139). Written at standup from the platform's provisioning_instances.settings;
|
|
264
|
-
# src/branding.js ENV_OVERRIDES maps these onto project.*.
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
${
|
|
268
|
-
${prefix}_JOINABILITY=${s.joinability}
|
|
285
|
+
# src/branding.js ENV_OVERRIDES maps these onto project.*. When the owner changes
|
|
286
|
+
# one, the platform's 'settings-apply' run (task 1003140) re-writes these lines in
|
|
287
|
+
# place and restarts this service — the pack is read once per process.
|
|
288
|
+
${lines}
|
|
269
289
|
`;
|
|
270
290
|
}
|
|
271
291
|
|
|
292
|
+
// The instance's EFFECTIVE settings: the defaults overlaid with the stored,
|
|
293
|
+
// on-vocabulary keys. An off-vocabulary or unknown stored value falls back to
|
|
294
|
+
// its default rather than leaking into the read surface. PURE.
|
|
272
295
|
function effectiveSettings(row) {
|
|
273
296
|
const stored = row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings)
|
|
274
297
|
? row.settings : {};
|
|
@@ -607,7 +630,9 @@ async function enqueueIntent(db, instanceId, action, requestedBy) {
|
|
|
607
630
|
// blind enqueue collided with such an intent and the API falsely said "teardown
|
|
608
631
|
// already queued" — the instance leaked). A pending provision is flipped to 'error'
|
|
609
632
|
// (the state CHECK has no 'superseded'; last_error says why) so the runner never
|
|
610
|
-
// stands it up. A
|
|
633
|
+
// stands it up. A PENDING settings push is superseded the same way (task 1003140):
|
|
634
|
+
// a push into a project about to be torn down is dead work, and letting it hold the
|
|
635
|
+
// delete hostage would repeat 1002691. A RUNNING intent is mid-flight and never yanked —
|
|
611
636
|
// the caller gets { conflict: 'open_intent' } with the blocking intent so the route
|
|
612
637
|
// can answer truthfully. Each statement is atomic and the index stays the arbiter:
|
|
613
638
|
// any interleaving with the runner or a concurrent enqueue lands on one of these
|
|
@@ -617,7 +642,7 @@ async function enqueueTeardown(db, instanceId, requestedBy) {
|
|
|
617
642
|
`UPDATE provisioning_intents
|
|
618
643
|
SET state = 'error', last_error = 'superseded: teardown requested',
|
|
619
644
|
updated_at = now(), resolved_at = now()
|
|
620
|
-
WHERE instance_id = $1 AND action
|
|
645
|
+
WHERE instance_id = $1 AND action IN ('provision', 'settings-apply') AND state = 'pending'
|
|
621
646
|
RETURNING id`,
|
|
622
647
|
[instanceId]
|
|
623
648
|
);
|
|
@@ -627,6 +652,22 @@ async function enqueueTeardown(db, instanceId, requestedBy) {
|
|
|
627
652
|
return { intent, created: false, superseded: false, conflict: 'open_intent' };
|
|
628
653
|
}
|
|
629
654
|
|
|
655
|
+
// Queue a settings push for an ACTIVE instance (task 1003140). The runner reads
|
|
656
|
+
// the row's CURRENT settings when it runs, so a change that lands while an
|
|
657
|
+
// identical intent is still PENDING simply rides that intent — idempotent, and
|
|
658
|
+
// truthfully "already queued". Anything else holding the open slot — other work,
|
|
659
|
+
// or a settings push already RUNNING (which may have read the row before this
|
|
660
|
+
// write) — is reported as { conflict: 'open_intent' } so the route can say so
|
|
661
|
+
// instead of claiming a push that will not carry the change. The state gate and
|
|
662
|
+
// the shape gate are the ROUTE's job (SETTINGS_APPLY_FROM / _SHAPES); this only
|
|
663
|
+
// arbitrates the open slot, like enqueueTeardown.
|
|
664
|
+
async function enqueueSettingsApply(db, instanceId, requestedBy) {
|
|
665
|
+
const { intent, created } = await enqueueIntent(db, instanceId, 'settings-apply', requestedBy);
|
|
666
|
+
if (created) return { intent, created: true };
|
|
667
|
+
if (intent.action === 'settings-apply' && intent.state === 'pending') return { intent, created: false };
|
|
668
|
+
return { intent, created: false, conflict: 'open_intent' };
|
|
669
|
+
}
|
|
670
|
+
|
|
630
671
|
// The instance's currently-open (pending|running) intent, if any.
|
|
631
672
|
async function getOpenIntent(db, instanceId) {
|
|
632
673
|
const { rows } = await db.query(
|
|
@@ -817,8 +858,9 @@ async function costLedger(db, { limit = 500 } = {}) {
|
|
|
817
858
|
module.exports = {
|
|
818
859
|
// constants
|
|
819
860
|
INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, PROJECT_TYPES, INTENT_ACTIONS, TEARDOWN_FROM,
|
|
820
|
-
settingsEnvLines,
|
|
821
|
-
PROVISION_ENQUEUE_FROM, DOMAIN_ATTACH_FROM,
|
|
861
|
+
settingsEnvLines, settingsEnvVars,
|
|
862
|
+
PROVISION_ENQUEUE_FROM, DOMAIN_ATTACH_FROM, SETTINGS_APPLY_FROM, SETTINGS_APPLY_SHAPES,
|
|
863
|
+
TIER_MONTHLY_USD, CERT_ELIGIBLE_STATES,
|
|
822
864
|
SETTINGS_VOCAB, DEFAULT_SETTINGS,
|
|
823
865
|
// pure
|
|
824
866
|
isValidSlug, costEstimateUsd, publicInstance, normalizeHostname,
|
|
@@ -831,7 +873,7 @@ module.exports = {
|
|
|
831
873
|
// data access — verification overlay (task 1002696)
|
|
832
874
|
listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote,
|
|
833
875
|
createInstance, setInstanceStatus, updateInstanceDomain, updateInstanceSettings,
|
|
834
|
-
enqueueIntent, enqueueTeardown, getOpenIntent, claimNextIntent, resolveIntent,
|
|
876
|
+
enqueueIntent, enqueueTeardown, enqueueSettingsApply, getOpenIntent, claimNextIntent, resolveIntent,
|
|
835
877
|
recordEvent, instanceStateForSlug, costLedger,
|
|
836
878
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
837
879
|
createOAuthManifest, getOAuthManifestByState, attachManifestCode,
|