@bongos/core 1.19.612 → 1.19.614
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 +22 -17
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md +150 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision-net.js +30 -0
- package/scripts/gds/provision-units.js +25 -12
- package/scripts/gds/provision.js +27 -1
- package/src/module-api.js +1 -1
- package/tests/provision.mjs +55 -5
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.614",
|
|
6
|
+
"core_contract": "1.19.614",
|
|
7
|
+
"source_commit": "abed8329cedd284ae48ca75f74b991eed0bb00aa",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-09T05:18:42.201Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 459,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
14
|
"functional_verbatim": 2080,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2563,
|
|
20
|
+
"tree_sha256": "6eaf4625f3f277d948abd11748dcf8e2f7f24062b460a58dd7ea3c7033156dea",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1052,7 +1052,7 @@
|
|
|
1052
1052
|
{
|
|
1053
1053
|
"path": "docs/adr/0111-instance-hosting-provisioning-module.md",
|
|
1054
1054
|
"mode": "0000644",
|
|
1055
|
-
"sha256": "
|
|
1055
|
+
"sha256": "240c3633b9093644c55d7141eac69748eafa294bb393dab274cad508aeee248e"
|
|
1056
1056
|
},
|
|
1057
1057
|
{
|
|
1058
1058
|
"path": "docs/adr/0112-private-goals-invitations-join-requests.md",
|
|
@@ -1844,10 +1844,15 @@
|
|
|
1844
1844
|
"mode": "0000644",
|
|
1845
1845
|
"sha256": "c9cd485b1ffce55fea15fd5b544a01e5753309e291c88bd223d33e6c11c4ebec"
|
|
1846
1846
|
},
|
|
1847
|
+
{
|
|
1848
|
+
"path": "docs/adr/0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md",
|
|
1849
|
+
"mode": "0000644",
|
|
1850
|
+
"sha256": "ac348289412efce1b3d67d74fccb5ff81347d196715ff53fd77e79e22228fad3"
|
|
1851
|
+
},
|
|
1847
1852
|
{
|
|
1848
1853
|
"path": "docs/adr/README.md",
|
|
1849
1854
|
"mode": "0000644",
|
|
1850
|
-
"sha256": "
|
|
1855
|
+
"sha256": "6cdaf9ffd4b14ae27231158fc040d59f084b86a93e6ecc43fb16bdea54e32fb6"
|
|
1851
1856
|
},
|
|
1852
1857
|
{
|
|
1853
1858
|
"path": "docs/api-reference.md",
|
|
@@ -2732,7 +2737,7 @@
|
|
|
2732
2737
|
{
|
|
2733
2738
|
"path": "docs/module-api-changelog.md",
|
|
2734
2739
|
"mode": "0000644",
|
|
2735
|
-
"sha256": "
|
|
2740
|
+
"sha256": "df3d7a5da1b66f12598a82a180eb3b3373938ef7a95e17977229adf999b02195"
|
|
2736
2741
|
},
|
|
2737
2742
|
{
|
|
2738
2743
|
"path": "docs/modules-contract.md",
|
|
@@ -7632,12 +7637,12 @@
|
|
|
7632
7637
|
{
|
|
7633
7638
|
"path": "package-lock.json",
|
|
7634
7639
|
"mode": "0000644",
|
|
7635
|
-
"sha256": "
|
|
7640
|
+
"sha256": "abc3e3e3456a393e7a2113807d07adcc257b8e8af6369e6c05fc0bdf32340d49"
|
|
7636
7641
|
},
|
|
7637
7642
|
{
|
|
7638
7643
|
"path": "package.json",
|
|
7639
7644
|
"mode": "0000644",
|
|
7640
|
-
"sha256": "
|
|
7645
|
+
"sha256": "7301d49faecbce2fb275d06852d671f82f97df62ff7c94a0451e719a2457e257"
|
|
7641
7646
|
},
|
|
7642
7647
|
{
|
|
7643
7648
|
"path": "public-docs/index.html",
|
|
@@ -8412,7 +8417,7 @@
|
|
|
8412
8417
|
{
|
|
8413
8418
|
"path": "scripts/gds/provision-net.js",
|
|
8414
8419
|
"mode": "0000644",
|
|
8415
|
-
"sha256": "
|
|
8420
|
+
"sha256": "502b1aeb571d463aea02ef4db3e848d23fdd8c865e7bfc37f66679c09cc203a9"
|
|
8416
8421
|
},
|
|
8417
8422
|
{
|
|
8418
8423
|
"path": "scripts/gds/provision-repo.js",
|
|
@@ -8422,12 +8427,12 @@
|
|
|
8422
8427
|
{
|
|
8423
8428
|
"path": "scripts/gds/provision-units.js",
|
|
8424
8429
|
"mode": "0000644",
|
|
8425
|
-
"sha256": "
|
|
8430
|
+
"sha256": "2b2c2a74514de43e9015e38db56da1daff040488bf6b10ffa4bfa10042995dea"
|
|
8426
8431
|
},
|
|
8427
8432
|
{
|
|
8428
8433
|
"path": "scripts/gds/provision.js",
|
|
8429
8434
|
"mode": "0000644",
|
|
8430
|
-
"sha256": "
|
|
8435
|
+
"sha256": "82fb65fea5dd9a33770690c902680cb428eb74b1c2abdaeab7f89f444ae1c11f"
|
|
8431
8436
|
},
|
|
8432
8437
|
{
|
|
8433
8438
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -9362,7 +9367,7 @@
|
|
|
9362
9367
|
{
|
|
9363
9368
|
"path": "src/module-api.js",
|
|
9364
9369
|
"mode": "0000644",
|
|
9365
|
-
"sha256": "
|
|
9370
|
+
"sha256": "05f2b36b4f1ebb20ba4a425d9669d0ac29326557c67d3deb61a63e3e5a06a261"
|
|
9366
9371
|
},
|
|
9367
9372
|
{
|
|
9368
9373
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11812,7 +11817,7 @@
|
|
|
11812
11817
|
{
|
|
11813
11818
|
"path": "tests/provision.mjs",
|
|
11814
11819
|
"mode": "0000644",
|
|
11815
|
-
"sha256": "
|
|
11820
|
+
"sha256": "d70cc9bd673dce403787814849abbeef9ed0ea01f9e38a2d3288886879490830"
|
|
11816
11821
|
},
|
|
11817
11822
|
{
|
|
11818
11823
|
"path": "tests/provision_restart.mjs",
|
|
@@ -124,6 +124,7 @@ Inherit the budget DNA. **Default to the cheapest shape** — a co-tenant proces
|
|
|
124
124
|
- **`bongos init` gains an infra arm** — its infra answers flow into a provisioning request instead of only filing manual kickoff tasks.
|
|
125
125
|
- **Deferred / risks:** the **managed fleet control plane** is genuinely greenfield and sits above the locked per-repo GDS model — it needs its own spec once Mercury lands. **Billing/metering** is greenfield (no capability today). **Domain registration** is deferred. On-demand TLS must be hostname-allow-listed to what was actually provisioned. `bongos upgrade` is still a stub (R86).
|
|
126
126
|
- **Licensing unchanged** — managed is a service on the same AGPL artifact ([ADR 0065](<redacted>.md) §2); no open-core, no relicensing.
|
|
127
|
+
- **A provisioned instance loads its OWN branding pack** ([task 1003740](https://cloudbongos.com/builders#/task/1003740)). Provisioning originally pinned every instance's pack to the shared, committed neutral starter, because the co-tenant model of the day ran ONE checkout for all instances and differentiated them purely by env. Under [ADR 0108](<redacted>.md) that is no longer true — each instance has its own checkout, its own `config/branding.json` written at init, and runs `node_modules/@bongos/core`. The pin outlived its premise, and because `src/branding.js`'s env overrides carry domains, `auth.idp` and landing but **never `identity.productName`**, every hosted instance served the platform's name as its own: `GET /api/gds/instance` on hermeslines-marketing, mercury and demo all answered "Cloud Bongos" while their own packs on disk said otherwise. `bongos login <instance>` therefore greeted a newcomer with the wrong project at the moment they were deciding whether to trust it, and nothing looked — a healthy instance passes `/healthz` while lying about who it is. Provisioning now writes no branding-file pin at all, so `src/branding.js`'s default (`<instanceRoot>/config/branding.json`, falling back to neutral when absent) applies; standup gained an identity probe beside the healthz one, which **warns** rather than fails, since an owner may legitimately name a project after the platform.
|
|
127
128
|
- **Follow-up tasks seeded** under BONGOS-V1 / goal 26 (idempotent seeder `scripts/gds/seed-provisioning-tasks.js`, `source_ref = <redacted>*`):
|
|
128
129
|
1. **Scaffold the `provisioning` module** — manifest + `provisioning_*` tables + the web-tier intent/state routes (own + Archon), no token handling. *(buildable now)*
|
|
129
130
|
2. **Extract the shared DO + Cloudflare control-plane clients** so `box.js` and `provision.js` reuse them without a module→module import (dev-box byte-identical). *(buildable now; enables 3)*
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# 0266 — The Board Room is its own surface, reachable by whoever may vote
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-08
|
|
5
|
+
- **Tasks:** [#1003734](https://cloudbongos.com/builders#/task/1003734) (this record). Executed by [#1003735](https://cloudbongos.com/builders#/task/1003735) (the carve), [#1003736](https://cloudbongos.com/builders#/task/1003736) (the deep links), [#1003737](https://cloudbongos.com/builders#/task/1003737) (the waiting-vote count).
|
|
6
|
+
- **Goal:** [#1000111](https://cloudbongos.com/builders#/goal/1000111) — Working area 7, Government; criterion `wa7-government`.
|
|
7
|
+
- **Decided by:** the owner (Masterqua / `IAMMASTERQUA`, Archon, member of area 7), 2026-09-08.
|
|
8
|
+
- **Builds on:** [ADR 0175](<redacted>.md) (the Board Room, and §6 — who governs is configuration), [ADR 0157](<redacted>.md) (the `page.view.government` floor at metic), [ADR 0151](<redacted>.md) (permissions as the atom), [ADR 0016](<redacted>.md) (server-enforced, uncached, per request).
|
|
9
|
+
- **Related:** [ADR 0174](<redacted>.md) (government, not governance).
|
|
10
|
+
|
|
11
|
+
## 1. Context — the room with no door
|
|
12
|
+
|
|
13
|
+
The Board Room is the hall's only upward-pointing gate (ADR 0175): the surface
|
|
14
|
+
on which a Full Idea that cleared the completeness bar is ratified, and the only
|
|
15
|
+
surface on which the constitution itself can be amended. It ships today as a
|
|
16
|
+
**hash tab** (`#board-room`) inside the `/government` page, beside Permissions
|
|
17
|
+
and Constitution.
|
|
18
|
+
|
|
19
|
+
Two facts about that placement do not survive inspection.
|
|
20
|
+
|
|
21
|
+
**The nav never followed the page.** When R16 turned `/government` into three
|
|
22
|
+
rooms it correctly widened the *page* gate from `government.manage` (archon) to
|
|
23
|
+
`page.view.government` (metic+). The comment it left in
|
|
24
|
+
[`serve-internal.js`](../../src/bongos/serve-internal.js) states the principle
|
|
25
|
+
plainly — *"The shell follows the widest legitimate audience — the audience
|
|
26
|
+
changed when the page did."* But `modules/hall-ui/public/shell.js` still
|
|
27
|
+
declares that nav item as `label: 'Permissions'`, `gate: 'archon'`. So the page
|
|
28
|
+
widened and the door did not: a Metic who sits on the board has **no nav link to
|
|
29
|
+
the Board Room at all**, and the only word an Archon sees in the nav is
|
|
30
|
+
"Permissions", which does not suggest a voting room is behind it. Every other
|
|
31
|
+
government room — Watch, Harbor, Gate, Sessions — carries its own nav item. The
|
|
32
|
+
Board Room is the one room reachable only by knowing a URL fragment.
|
|
33
|
+
|
|
34
|
+
**The surface is narrower than the franchise.** `board.vote.cast` floors at
|
|
35
|
+
**xenos**, and that is deliberate: `modules/government/board.js:149` records the
|
|
36
|
+
floor is low *so that widening the board works*. The page shell requires
|
|
37
|
+
metic+. Today `membership: rank:archon` and every Archon clears both, so the
|
|
38
|
+
contradiction is latent. The moment a constitution widens membership below
|
|
39
|
+
Metic — the exact act ADR 0175 §6 exists to make a configuration change — those
|
|
40
|
+
members may cast a vote through the API and **cannot load the page they would
|
|
41
|
+
cast it on**.
|
|
42
|
+
|
|
43
|
+
The second is the load-bearing one. ADR 0175 §6 put membership, pass rule and
|
|
44
|
+
window in `config/government.json`, resolved per request and uncached, so that
|
|
45
|
+
"flipping the switch later changes numbers, not a code path that has never run."
|
|
46
|
+
A page gate keyed to a different atom than the vote reintroduces exactly the
|
|
47
|
+
code-path change that decision removed.
|
|
48
|
+
|
|
49
|
+
## 2. Decision
|
|
50
|
+
|
|
51
|
+
### 1. The Board Room becomes its own page at `/board-room`
|
|
52
|
+
|
|
53
|
+
Carved out of `/government`, which keeps Permissions and Constitution. It joins
|
|
54
|
+
Watch, Harbor, Gate and Sessions as a first-class room in the Government nav
|
|
55
|
+
group, with its own nav item.
|
|
56
|
+
|
|
57
|
+
The alternative — leaving the tab in place and adding a nav item that deep-links
|
|
58
|
+
to `/government#board-room` — was considered and rejected. It is cheaper and it
|
|
59
|
+
preserves the existing hash links for free, but it leaves the voting room inside
|
|
60
|
+
a page whose gate answers a different question, which is defect (2) above left
|
|
61
|
+
standing. The room that constrains the owner should not be a tab inside the
|
|
62
|
+
owner's admin page.
|
|
63
|
+
|
|
64
|
+
### 2. Its reach follows `board.vote.cast`, not `page.view.government`
|
|
65
|
+
|
|
66
|
+
The page gate resolves the **same atom the vote route checks**. The surface
|
|
67
|
+
reach and the right to vote become one thing, and widening the board stays a
|
|
68
|
+
configuration change.
|
|
69
|
+
|
|
70
|
+
`board.vote.cast`'s floor is only the coarse gate. **Membership is still checked
|
|
71
|
+
in-handler, per item, against that item's own snapshotted constitution** — the
|
|
72
|
+
floor decides who may receive the shell, never who may vote on what. A builder
|
|
73
|
+
who clears the floor but sits on no board sees the room and no ballot, which is
|
|
74
|
+
the correct and honest state: ADR 0175 §9 made the ballot open on purpose,
|
|
75
|
+
because "a board whose votes are invisible cannot constrain anyone."
|
|
76
|
+
|
|
77
|
+
### 3. The old deep links are migrated client-side, because a fragment never reaches the server
|
|
78
|
+
|
|
79
|
+
`#board-room` is a URL fragment. It is **not sent to the server**, so no
|
|
80
|
+
server-side route or redirect can ever see it. The compatibility path is
|
|
81
|
+
therefore a client-side redirect in `government.js`: on load, a `#board-room`
|
|
82
|
+
hash sends the browser to `/board-room`, preserving `?item=N`.
|
|
83
|
+
|
|
84
|
+
This is recorded as a decision rather than left as an implementation note
|
|
85
|
+
because it is the thing a reader will otherwise get wrong — adding
|
|
86
|
+
`GOVERNMENT_BOARD_RE` to `serve-internal.js` looks like the obvious fix and
|
|
87
|
+
silently never fires.
|
|
88
|
+
|
|
89
|
+
Three writers emit the old form and all three move (task
|
|
90
|
+
[#1003736](https://cloudbongos.com/builders#/task/1003736)): the Discord
|
|
91
|
+
window-open announcement (`modules/discord/board-broadcast.js`), the
|
|
92
|
+
`board_votes` need's action href (`modules/builder-settings/builder-needs.js`),
|
|
93
|
+
and the docs. The redirect stays for at least one release regardless, because
|
|
94
|
+
announcements already sitting in Discord channel history carry the old link
|
|
95
|
+
forever and ADR 0175 §8 makes that mention the "vote within X" notice.
|
|
96
|
+
|
|
97
|
+
### 4. A waiting-vote count belongs on the nav item, and the hall has no mechanism for one
|
|
98
|
+
|
|
99
|
+
The owner's second ask was to see that items are waiting **without going
|
|
100
|
+
inside**. The data already exists and needs nothing new server-side:
|
|
101
|
+
`boardVotesNeed` (`builder-needs.js`) carries a live count and the oldest
|
|
102
|
+
waiting item id, pre-filtered to what the vote route would actually accept, and
|
|
103
|
+
`GET /me` already serves it.
|
|
104
|
+
|
|
105
|
+
What does not exist is any badge, bell, dot or count mechanism in
|
|
106
|
+
`shell.js` — the nav renders label and icon and nothing else. Task
|
|
107
|
+
[#1003737](https://cloudbongos.com/builders#/task/1003737) adds that once,
|
|
108
|
+
generically, so a second consumer can adopt it without a second pattern. Zero is
|
|
109
|
+
silent, and the badge is visible to exactly whoever the nav item is: a count is
|
|
110
|
+
board activity, and it must not leak to someone who cannot see the room.
|
|
111
|
+
|
|
112
|
+
A **sound** is deliberately out of scope. The instance ships `alert.wav` and
|
|
113
|
+
`chime.wav`, so it would be easy and it was not asked for.
|
|
114
|
+
|
|
115
|
+
## 3. Consequences
|
|
116
|
+
|
|
117
|
+
**The `/government` tab strip loses a room, and that is the point.** Two rooms
|
|
118
|
+
remain, both administrative, both correctly behind the archon-shaped item they
|
|
119
|
+
already sit behind. The existing Permissions nav item keeps `gate: 'archon'` —
|
|
120
|
+
it *is* the archon admin surface, so that gate was never wrong for it. It was
|
|
121
|
+
wrong as the only door to three rooms.
|
|
122
|
+
|
|
123
|
+
**Widening the board becomes a real config change.** After this, an instance may
|
|
124
|
+
set `membership: rank:metic` — or wider — and the members it names can actually
|
|
125
|
+
reach the room. Before it, that edit produced members who could vote only by
|
|
126
|
+
`curl`. This is the property ADR 0175 §6 claimed and did not have.
|
|
127
|
+
|
|
128
|
+
**One more page joins the statically-parsed gate map.** `tests/hall_page_gate_map.mjs`
|
|
129
|
+
rebuilds the URL→gate map by parsing `serve-internal.js` as text, so the new
|
|
130
|
+
`const BOARD_ROOM_PAGE_RE = /…/;` must keep the bare form with nothing between
|
|
131
|
+
the `=` and the literal — a comment in that gap drops the regex out of the map
|
|
132
|
+
and the page silently tests as ungated. The file already records this rule at
|
|
133
|
+
`PROJECT_SETTINGS_PAGE_RE`; this ADR notes it because the failure is invisible.
|
|
134
|
+
|
|
135
|
+
**A defect found while scoping this, filed separately.** The hall renders needs
|
|
136
|
+
as a *single slot* (`needs.items.find(n => n.state !== 'satisfied')`), and
|
|
137
|
+
`computeNeeds` sorts only by state with a stable sort — so inside the
|
|
138
|
+
`action_needed` bucket the registry array order decides, and `boardVotesNeed` is
|
|
139
|
+
fourth, behind `artKeyNeed`. Any builder with an unresolved image-generation key
|
|
140
|
+
therefore never sees the board notice at all. It was live for the owner of this
|
|
141
|
+
decision at the moment they asked for the bell. Filed as
|
|
142
|
+
[#1003738](https://cloudbongos.com/builders#/task/1003738) rather than folded in
|
|
143
|
+
here: the badge and the banner are two surfaces, and fixing the ordering is a
|
|
144
|
+
change to what every need in the hall does.
|
|
145
|
+
|
|
146
|
+
**Rejected: a bell in the global header rather than on the item.** A header bell
|
|
147
|
+
is a second inbox — it needs its own read/unread state, its own dismissal
|
|
148
|
+
semantics, and a home for notifications that are not board votes. The ask was to
|
|
149
|
+
see the count on the section, and the nav item is where the reader already looks
|
|
150
|
+
for the room.
|
package/docs/adr/README.md
CHANGED
|
@@ -357,3 +357,4 @@ This keeps the decision history honest and traceable.
|
|
|
357
357
|
| 0263 | [**How a version closes: auto, early, roll-forward, and the maintenance exemption** ([task 1003593](https://cloudbongos.com/builders#/task/1003593) · goal 1000086 — *Strict versioning*). [ADR 0250](<redacted>.md) D5 decided a version *can* close; this is the design pass that makes its five build tasks mechanical, so they do not each invent their own refusal codes and payload shapes and meet only in production. **The asymmetry that shrinks the feature:** auto-close fires when the last non-maintenance goal achieves — so by construction every non-maintenance goal is already closed and the set needing a disposition is EMPTY. All the machinery (the disposition map, the refusal that returns the goals, successor lineage) belongs to EARLY close alone; R16 is small and R12 carries the weight. **Auto-close hangs off BOTH achievement writers** (`db-goals.achieveGoalIfComplete` and the `done-when` cascade — the deliberate duplicate D2 already governs, a require cycle being the reason they are not one helper), on the caller `exec` so it sees the ship's uncommitted flip, and **may never fail a ship**: wrapped and swallowed like `closeCompletedWorkForShip`, idempotent under `WHERE status='building'`, so a failure leaves a version un-closed (self-healing on the next reconciler sweep) and never half-closed. **Early close** is the R10 archive two-step verbatim in spirit — refuse with `version_holds_open_goals` **returning the goals**, then proceed with a per-goal `roll_forward`/`abandon` map and a required `reason`; six named codes, because the callers are agents. **Two briefs are corrected by measurement:** the lineage column is `succeeded_by_goal_id` (migration 160, on the OLD row pointing forward) — not the `succeeds_goal_id` both 0250 and R17's brief name, so R17 needs NO migration; and R18's `is_maintenance` flag is load-bearing rather than cosmetic, because answering the close count with a `title NOT LIKE` would let a hand-titled goal silently exempt itself from the gate that decides when a VERSION closes. Promotion runs inside the close txn and BEFORE roll-forward (`ensureMaintenanceGoal` returns null for a non-`building` version, so a carry-over run first silently carries nothing); zero planning versions is not an error; R19's one migration should carry the `building` AND `planning` partial unique indexes so the promotion race resolves like a 23505 rather than a prayer. Rejected: auto-creating the successor version inside a close (the "fake hotfix version" escape hatch 0250 built the override counter to prevent), writing `limitations/<version>-shipped.md` from the route (file I/O in a ship transaction — it stays R23's CLI work), and a version reopen.](<redacted>.md) | version lifecycle / scope closure |
|
|
358
358
|
| 0264 | [**The ten working areas: one goal per area, held until 5,000 builders** ([task 1003696](https://cloudbongos.com/builders#/task/1003696) · goal 1000086 — *Strict versioning*, criterion `sv-cutover-proven-live`). The owner and the full team ran a manual goal review on 2026-09-07 and cut **38 open goals down to 10**, one per working area, each with a named owner and a described 5,000-builder end state: project creation (Rini), account/community (Rini), human project management (Scott), core distribution (Lars/Will), module distribution & economy (Will), the four-role experience incl. agents (Masterqua/Rini), government (Will/Masterqua), the credit economy (Scott), platform analytics (Lars), security (everyone bar Nils and Scott). **Why the rules were not enough on their own:** [ADR 0250](<redacted>.md) D1 closes the goal set of a BUILDING version — which freezes 38 goals exactly as effectively as it would freeze 10. Enforcement cannot shrink a scope that is already too large, so the rules and the cut are two halves of one move; the horizon is a SCALE (5,000 builders), not a date, because that is the question that decides when this set stops being the right set. **The finding that reorders the goal:** none of the cut can be executed today. R10 ([task 1003597](https://cloudbongos.com/builders#/task/1003597)) made `POST /goals/:id/archive` refuse a goal holding unfinished tasks and demand a disposition — correct, and exactly D3 — but the vector that SUPPLIES a disposition is R14 ([task 1003601](https://cloudbongos.com/builders#/task/1003601)), unshipped. Every one of the 12 deletes and every fold goes through that door, so **R14 is the keystone of the whole cut**, not one feature among thirteen, and is built before the rest of the version-close chain rather than in rank order. It looks like a bug and is not: the refusal is the design, landed one task ahead of its own remedy. "Delete" means `archived`, never a row removal (`goals.status` is open|achieved|archived, migration 160; reversible via `/reopen`) — preserving the rows keeps shipped-task attribution and credit history a delete would strand. Three open goals were NOT covered by the review and are surfaced as open questions rather than guessed: 1000003 (the catch-all — already R25's target), 1000065 (assumed to be area 2's community half), and 1000088 (repo cleanup — maps to no area, needs an owner decision). Deliberately does not decide: the areas' priority order, criteria for the six goals R29 must create (inventing criteria for someone else's area is how a goal acquires scope its owner never agreed to), or any task rehoming (R25/R26).](<redacted>.md) | scope / goal set / owner decision |
|
|
359
359
|
| 0265 | [**Work with no goal survives a version close, and the order the ten-area cut actually needs** ([task 1003713](https://cloudbongos.com/builders#/task/1003713) · goal 1000086 — *Strict versioning*, criteria `sv-every-task-has-a-goal` + `sv-cutover-proven-live`). `closeVersion`'s post-apply invariant counts GOALS, and every task write inside the close is scoped `WHERE goal_id = $1` — so a task belonging to no goal is reached by no disposition, counted by no re-count, and ships stranded on a version that is over. Not hypothetical: six such tasks already sat on shipped GDS-V3/GDS-V4, and 47 rows on BONGOS-V1 carried a null `goal_id` (R11 required the column going forward but never backfilled, because every catch-all lookup matches the exact title `<VERSION> — general` and BONGOS-V1's is titled 'Work with no goal yet'). Decision: a second invariant in the same transaction refuses a close leaving ANY non-terminal task on the version, failing `CLOSE_LEAVES_STRANDED_TASKS` with the ids; exempting only the maintenance goal (its carry-forward runs in the later `onClosed` hook) — through the SAME `maintenanceGoalExemptSql` the goal count uses, never a second copy — and `__smoke__` fixtures. Consequence recorded here too: because `PATCH /tasks/:id {goal_id}` refuses a cross-version move (409 `goal_version_mismatch`) and `roll_forward` CREATES a successor rather than merging into one, [ADR 0264](<redacted>.md)'s cut must run home-then-close-then-**fold on the planning version**, where task and destination finally share a version. Rejected: a `fold_into` disposition (puts scope decisions inside a shipping transaction), creating the six new areas on the building version via R07 admissions (spends ten overrides to route around the rule they price), and a backfill instead of a gate (fixes today's rows, not the forty-eighth).](<redacted>.md) | lifecycle / version boundary |
|
|
360
|
+
| 0266 | [**The Board Room is its own surface, reachable by whoever may vote** ([task 1003734](https://cloudbongos.com/builders#/task/1003734) · goal 1000111 — *Working area 7, Government*, criterion `wa7-government`; owner decision 2026-09-08). The hall's only **upward-pointing** gate ([ADR 0175](<redacted>.md)) shipped as a hash tab inside `/government`, and two things about that did not survive inspection. **The nav never followed the page:** R16 correctly widened the PAGE gate from `government.manage` (archon) to `page.view.government` (metic+) when the page became three rooms — its own comment says *"the shell follows the widest legitimate audience"* — but `shell.js` still declares that item as label Permissions with gate archon, so a Metic who SITS on the board has no nav link at all and the only word in the nav is Permissions. Watch, Harbor, Gate and Sessions each own a nav item; the voting room was reachable only by knowing a URL fragment. **The surface was narrower than the franchise:** `board.vote.cast` floors at XENOS deliberately (`board.js:149` — the floor is low *so that widening the board works*) while the shell demands metic+, so the moment a constitution widens membership below Metic — the exact act [ADR 0175](<redacted>.md) §6 exists to make a CONFIG change — those members may vote by API and cannot load the page they would vote on, reintroducing the code-path change §6 removed. Decision: **its own page at `/board-room`, gated on the same atom the vote route checks.** The floor stays coarse — membership is still checked in-handler per item against that item's own snapshotted constitution — so a builder who clears the floor but sits on no board sees the room and no ballot, which is honest (§9 made the ballot open on purpose). **The migration is client-side because a fragment never reaches the server:** `#board-room` cannot be caught by any server route, so `government.js` redirects on load, preserving `?item=N` — recorded as a decision because adding a regex to `serve-internal.js` looks like the obvious fix and silently never fires. Three writers move (Discord `board-broadcast.js`, the `board_votes` need href, docs), and the redirect stays a release regardless because announcements already in channel history carry the old link forever. A **waiting-vote count** goes on the nav item: the data already exists (`boardVotesNeed` carries a live count pre-filtered to what the vote route would accept, served on `GET /me`), but `shell.js` has NO badge/bell/dot mechanism at all, so one is built once, generically, zero-is-silent, visible to exactly whoever the item is. Found while scoping and filed separately ([task 1003738](https://cloudbongos.com/builders#/task/1003738)): the hall renders needs as a SINGLE slot and `computeNeeds` sorts only by state with a stable sort, so registry order decides inside the `action_needed` bucket and `boardVotesNeed` sits fourth behind `artKeyNeed` — any builder with an unresolved image key never sees the board notice, which was live for this decision's own owner at the moment they asked for the bell. Rejected: a nav item merely deep-linking to the existing tab (cheaper, keeps the hash links free, but leaves the voting room inside a page whose gate answers a different question); a bell in the global header (a second inbox, needing its own read/unread and dismissal semantics); and a SOUND (`alert.wav`/`chime.wav` already ship, so it would be easy — it was not asked for).](<redacted>.md) | government / the board / hall nav |
|
|
@@ -1673,5 +1673,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1673
1673
|
landed since 1.19.610 with no explicit bump. run 34305857169. (task 1002620)
|
|
1674
1674
|
1.19.612 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1675
1675
|
landed since 1.19.611 with no explicit bump. run 34310708322. (task 1002620)
|
|
1676
|
+
1.19.613 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1677
|
+
landed since 1.19.612 with no explicit bump. run 34311691842. (task 1002620)
|
|
1678
|
+
1.19.614 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1679
|
+
landed since 1.19.613 with no explicit bump. run 34314327601. (task 1002620)
|
|
1676
1680
|
---------------------------------------------------------------------------
|
|
1677
1681
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.614",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.614",
|
|
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.
|
|
3
|
+
"version": "1.19.614",
|
|
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",
|
|
@@ -244,6 +244,34 @@ function healthzCmd(inst) {
|
|
|
244
244
|
return `curl -sS --max-time 5 http://127.0.0.1:${inst.port}/healthz`;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// The instance manifest, read over loopback — the identity companion to healthzCmd (task 1003740).
|
|
248
|
+
function identityCmd(inst) {
|
|
249
|
+
return `curl -sS --max-time 5 http://127.0.0.1:${inst.port}/api/gds/instance`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Does a freshly provisioned instance report ITSELF, or the platform it is hosted on?
|
|
253
|
+
//
|
|
254
|
+
// WHY THIS EXISTS. Provisioning used to pin every instance's branding pack to the shared neutral
|
|
255
|
+
// starter (see provision-units.js), a leftover from the co-tenant model that shared one checkout. Under
|
|
256
|
+
// ADR 0108 each instance has its own, and src/branding.js's ENV_OVERRIDES carry domains but never
|
|
257
|
+
// identity.productName — so every hosted instance served "Cloud Bongos" as its own name for
|
|
258
|
+
// months, greeting newcomers with the wrong project at sign-in. Nothing looked.
|
|
259
|
+
//
|
|
260
|
+
// PURE so it is testable without a box: takes the parsed manifest and returns a verdict.
|
|
261
|
+
// Deliberately a WARNING, not a failure — an owner may legitimately name a project after the
|
|
262
|
+
// platform, and provisioning must not refuse a working instance over a name.
|
|
263
|
+
function identityVerdict({ manifest, slug, neutralName }) {
|
|
264
|
+
const name = manifest && (manifest.name || (manifest.brand && manifest.brand.identity && manifest.brand.identity.productName));
|
|
265
|
+
if (!name) return { ok: null, reason: 'no name in manifest' };
|
|
266
|
+
// The bug's exact shape: the instance answers with the neutral pack's name while its slug says
|
|
267
|
+
// it is somebody else. A project genuinely called that keeps its name and only trips when the
|
|
268
|
+
// slug matches too — in which case it is not the bug.
|
|
269
|
+
if (neutralName && name === neutralName && slug && slug !== neutralName) {
|
|
270
|
+
return { ok: false, name, reason: `serves the platform default name "${name}" instead of its own` };
|
|
271
|
+
}
|
|
272
|
+
return { ok: true, name };
|
|
273
|
+
}
|
|
274
|
+
|
|
247
275
|
// ---------------------------------------------------------------------------
|
|
248
276
|
// The injectable executor. dry-run → log-only; --apply → child_process.
|
|
249
277
|
// ---------------------------------------------------------------------------
|
|
@@ -260,6 +288,8 @@ module.exports = {
|
|
|
260
288
|
federateInstance,
|
|
261
289
|
federationHubOrigin,
|
|
262
290
|
healthzCmd,
|
|
291
|
+
identityCmd,
|
|
292
|
+
identityVerdict,
|
|
263
293
|
shouldFederate,
|
|
264
294
|
zoneForDomain,
|
|
265
295
|
};
|
|
@@ -121,16 +121,30 @@ WantedBy=timers.target
|
|
|
121
121
|
`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// The
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
124
|
+
// The pack the provisioner reads to learn an instance's env-var PREFIX floor.
|
|
125
|
+
//
|
|
126
|
+
// It is NOT the pack the instance loads, and that distinction is the whole of task 1003740.
|
|
127
|
+
// The original co-tenant model shared ONE checkout across every instance, differentiated only by
|
|
128
|
+
// env, so provisioning pinned the pack to `config/branding.neutral.json` and let identity
|
|
129
|
+
// ride ENV OVERRIDES. That model is gone: under ADR 0108 each co-tenant has its own checkout
|
|
130
|
+
// (`WorkingDirectory=/srv/<base>/<slug>`, running `node_modules/@bongos/core`) and its own
|
|
131
|
+
// `config/branding.json` written at init.
|
|
132
|
+
//
|
|
133
|
+
// The pin outlived it, and src/branding.js's ENV_OVERRIDES cover domains, auth.idp and landing —
|
|
134
|
+
// never `identity.productName`. So every hosted instance kept serving the NEUTRAL pack's name:
|
|
135
|
+
// GET /api/gds/instance on hermeslines-marketing, mercury and demo all answered "Cloud Bongos"
|
|
136
|
+
// while each instance's own pack on disk said otherwise, and `bongos login <instance>` greeted a
|
|
137
|
+
// newcomer with the platform's name at the moment they were deciding whether to trust it.
|
|
138
|
+
//
|
|
139
|
+
// The instance now loads its own pack by src/branding.js's DEFAULT (INSTANCE_PATH =
|
|
140
|
+
// <instanceRoot>/config/branding.json), which falls back to neutral when absent — so no
|
|
141
|
+
// branding-file env line is written at all. Reading the neutral pack HERE is still correct: it is
|
|
142
|
+
// the prefix floor an instance inherits unless its own pack overrides envPrefix, and every live
|
|
143
|
+
// co-tenant pack pins `envPrefix: "CLOUDBONGOS"` explicitly.
|
|
144
|
+
const ENV_PREFIX_PACK = 'config/branding.neutral.json';
|
|
131
145
|
|
|
132
146
|
// The env-var prefix a provisioned instance actually RESOLVES — i.e. the envPrefix
|
|
133
|
-
// of the pack it loads (
|
|
147
|
+
// of the pack it loads (ENV_PREFIX_PACK). src/instance-config.resolveEnv
|
|
134
148
|
// reads <envPrefix>_<SUFFIX>, and src/branding.resolveBranding applies
|
|
135
149
|
// <envPrefix>_{PUBLIC,BUILDERS,…}_ORIGIN overrides — so a web.env var only takes
|
|
136
150
|
// effect under THIS prefix. Read from the pack (fail-soft to the vanilla default)
|
|
@@ -139,7 +153,7 @@ const PROVISIONED_BRANDING_FILE = 'config/branding.neutral.json';
|
|
|
139
153
|
// the OAuth creds were silently unreadable — task 1972.)
|
|
140
154
|
function provisionedEnvPrefix() {
|
|
141
155
|
try {
|
|
142
|
-
const pack = JSON.parse(fs.readFileSync(path.join(REPO_ROOT,
|
|
156
|
+
const pack = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, ENV_PREFIX_PACK), 'utf8'));
|
|
143
157
|
return pack.envPrefix || 'CLOUDBONGOS';
|
|
144
158
|
} catch { return 'CLOUDBONGOS'; }
|
|
145
159
|
}
|
|
@@ -228,8 +242,7 @@ GITHUB_APP_PRIVATE_KEY=${String(appPem).replace(/\r?\n/g, '\\n')}
|
|
|
228
242
|
${credComment}
|
|
229
243
|
${prefix}_GITHUB_CLIENT_ID=${clientId || ''}
|
|
230
244
|
${prefix}_GITHUB_CLIENT_SECRET=${clientSecret || ''}
|
|
231
|
-
${firstAdminLine}
|
|
232
|
-
${originLines}${appLines}PORT=${inst.port}
|
|
245
|
+
${firstAdminLine}${originLines}${appLines}PORT=${inst.port}
|
|
233
246
|
PGDATABASE=${dbName(inst)}
|
|
234
247
|
${fedLines}${settingsEnvLines(inst, prefix)}`;
|
|
235
248
|
}
|
|
@@ -318,7 +331,7 @@ function upsertEnvVars(body, vars) {
|
|
|
318
331
|
// env the hub already runs under). Null → not hub-configured → shouldFederate is false.
|
|
319
332
|
|
|
320
333
|
module.exports = {
|
|
321
|
-
|
|
334
|
+
ENV_PREFIX_PACK,
|
|
322
335
|
backupScriptPath,
|
|
323
336
|
backupService,
|
|
324
337
|
backupServicePath,
|
package/scripts/gds/provision.js
CHANGED
|
@@ -52,7 +52,7 @@ const crypto = require('node:crypto');
|
|
|
52
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');
|
|
53
53
|
const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, dbCreateCmd, dbName, 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
|
-
const { caddyBlock, caddySnippetPath, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
55
|
+
const { caddyBlock, caddySnippetPath, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
56
56
|
|
|
57
57
|
function makeExec({ apply, log = console.log, remoteHost = null, remoteCwd = null, sshOpts = '' }) {
|
|
58
58
|
// `shown` (task 1003140): what to LOG in place of the command text — for a command
|
|
@@ -503,6 +503,32 @@ async function provisionInstance(inst, deps) {
|
|
|
503
503
|
}
|
|
504
504
|
} else log(' (verified post-boot)');
|
|
505
505
|
|
|
506
|
+
// ── step 7b: identity ─────────────────────────────────────────────────────
|
|
507
|
+
// Does it answer as ITSELF? A live instance that serves the platform's name instead of its own
|
|
508
|
+
// is invisible to healthz — it is perfectly healthy and telling every newcomer the wrong project
|
|
509
|
+
// (task 1003740, which went unnoticed across every hosted instance for months). A WARNING, never
|
|
510
|
+
// a failure: an owner may legitimately name a project after the platform, and a name must not
|
|
511
|
+
// fail a working standup.
|
|
512
|
+
if (local && apply && healthy !== false) {
|
|
513
|
+
const res = boxExec(identityCmd(inst), { allowFail: true });
|
|
514
|
+
const raw = res && !res.softFailed ? (res.stdout || '') : '';
|
|
515
|
+
let manifest = null;
|
|
516
|
+
try { manifest = raw ? JSON.parse(raw) : null; } catch (_) { manifest = null; }
|
|
517
|
+
if (manifest) {
|
|
518
|
+
let neutralName = null;
|
|
519
|
+
try {
|
|
520
|
+
neutralName = (JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'config/branding.neutral.json'), 'utf8'))
|
|
521
|
+
.identity || {}).productName || null;
|
|
522
|
+
} catch (_) { /* no neutral pack readable → the check simply cannot fire */ }
|
|
523
|
+
const verdict = identityVerdict({ manifest, slug: inst.slug, neutralName });
|
|
524
|
+
if (verdict.ok === false) {
|
|
525
|
+
log(` ⚠ ${inst.slug} ${verdict.reason} — check its config/branding.json and that no env var pins its branding pack`);
|
|
526
|
+
} else if (verdict.ok) {
|
|
527
|
+
log(` [identity] answers as "${verdict.name}"`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
506
532
|
// Mark active. error_note: null — a stale note from an earlier failed run
|
|
507
533
|
// must not ride into the active state (F15's second half; the co-tenant +
|
|
508
534
|
// standalone paths never cleared it). The runner hands its own observation
|
package/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.614'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/provision.mjs
CHANGED
|
@@ -19,10 +19,11 @@ import { join } from 'node:path';
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
const P = require('../scripts/gds/provision.js');
|
|
21
21
|
|
|
22
|
-
// The env-var prefix a provisioned instance resolves = the envPrefix
|
|
23
|
-
// webEnvBody
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// The env-var prefix a provisioned instance resolves = the envPrefix an instance INHERITS from
|
|
23
|
+
// the neutral pack (task 1003740: webEnvBody no longer pins a branding pack anywhere — the
|
|
24
|
+
// instance loads its own config/branding.json by default — but the prefix floor is still the
|
|
25
|
+
// neutral pack's, and every live co-tenant pack pins the same value explicitly). Read it here so
|
|
26
|
+
// these assertions stay in lockstep with the code (both derive it), not a pinned literal.
|
|
26
27
|
const NEUTRAL_PREFIX =
|
|
27
28
|
JSON.parse(readFileSync(new URL('../config/branding.neutral.json', import.meta.url), 'utf8')).envPrefix || 'CLOUDBONGOS';
|
|
28
29
|
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -205,7 +206,6 @@ t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + bra
|
|
|
205
206
|
const b = P.webEnvBody(coTenant); // coTenant.domain === null
|
|
206
207
|
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=`));
|
|
207
208
|
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_SECRET=`));
|
|
208
|
-
assert.match(b, /GDS_BRANDING_FILE=config\/branding\.neutral\.json/);
|
|
209
209
|
assert.match(b, /PORT=3003/);
|
|
210
210
|
assert.match(b, /PGDATABASE=mercury/);
|
|
211
211
|
// The prior slug prefix was a latent bug: it never matched the neutral pack's
|
|
@@ -215,6 +215,56 @@ t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + bra
|
|
|
215
215
|
assert.doesNotMatch(b, /_BUILDERS_ORIGIN=/);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
t('webEnvBody: an instance is never pinned to the neutral pack, so it serves its OWN name (task 1003740)', () => {
|
|
219
|
+
// THE BUG THIS PINS. Provisioning used to pin every instance to config/branding.neutral.json
|
|
220
|
+
// into every instance's web.env — a leftover from the shared-checkout co-tenant model. Under
|
|
221
|
+
// ADR 0108 each instance has its own checkout and its own config/branding.json, and
|
|
222
|
+
// src/branding.js's ENV_OVERRIDES cover domains/auth.idp/landing but NEVER identity.productName.
|
|
223
|
+
// So the pin made every hosted instance answer GET /api/gds/instance with the PLATFORM's name:
|
|
224
|
+
// hermeslines-marketing, mercury and demo all said "Cloud Bongos" while their own packs on disk
|
|
225
|
+
// said otherwise, and `bongos login <instance>` greeted newcomers with the wrong project.
|
|
226
|
+
const b = P.webEnvBody({ ...coTenant, domain: 'mercury.example.com' });
|
|
227
|
+
// Matched on the distinctive half of the env var's name: the full name carries a legacy literal
|
|
228
|
+
// the goal-1000073 rename ratchet counts, and this is strictly stronger anyway — it also catches
|
|
229
|
+
// a pin written under any other prefix.
|
|
230
|
+
assert.doesNotMatch(b, /BRANDING_FILE/,
|
|
231
|
+
'provisioning must not pin the branding pack — the instance loads its own config/branding.json');
|
|
232
|
+
|
|
233
|
+
// The pin is gone, but the env PREFIX it used to keep in lockstep must still be the one
|
|
234
|
+
// ENV_OVERRIDES reads, or the origin overrides silently stop firing (the task-1972 failure).
|
|
235
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_BUILDERS_ORIGIN=`));
|
|
236
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=`));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
t('identityVerdict: a live instance serving the PLATFORM name is caught, not shrugged at (task 1003740)', () => {
|
|
240
|
+
const N = require('../scripts/gds/provision-net.js');
|
|
241
|
+
const neutralName = 'Cloud Bongos';
|
|
242
|
+
|
|
243
|
+
// The bug's exact live shape — healthz was green the whole time this was true.
|
|
244
|
+
const bad = N.identityVerdict({ manifest: { name: 'Cloud Bongos' }, slug: 'hermeslines-marketing', neutralName });
|
|
245
|
+
assert.equal(bad.ok, false);
|
|
246
|
+
assert.match(bad.reason, /platform default name/);
|
|
247
|
+
|
|
248
|
+
// Fixed: the instance answers as itself.
|
|
249
|
+
assert.equal(N.identityVerdict({ manifest: { name: 'Hermeslines Marketing' }, slug: 'hermeslines-marketing', neutralName }).ok, true);
|
|
250
|
+
|
|
251
|
+
// It reads brand.identity.productName too — the manifest carries the name in both places.
|
|
252
|
+
assert.equal(N.identityVerdict({ manifest: { brand: { identity: { productName: 'mercury' } } }, slug: 'mercury', neutralName }).ok, true);
|
|
253
|
+
|
|
254
|
+
// NOT a false positive: a project genuinely named after the platform keeps its name. The check
|
|
255
|
+
// must never fail a working standup over a legitimate name.
|
|
256
|
+
assert.equal(N.identityVerdict({ manifest: { name: 'Cloud Bongos' }, slug: 'Cloud Bongos', neutralName }).ok, true);
|
|
257
|
+
|
|
258
|
+
// Unknowable → null, never a false accusation.
|
|
259
|
+
assert.equal(N.identityVerdict({ manifest: {}, slug: 'x', neutralName }).ok, null);
|
|
260
|
+
assert.equal(N.identityVerdict({ manifest: { name: 'x' }, slug: 'x', neutralName: null }).ok, true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
t('identityCmd: probes the manifest over loopback, like healthzCmd', () => {
|
|
264
|
+
const N = require('../scripts/gds/provision-net.js');
|
|
265
|
+
assert.match(N.identityCmd({ port: 3011 }), /127\.0\.0\.1:3011\/api\/gds\/instance/);
|
|
266
|
+
});
|
|
267
|
+
|
|
218
268
|
t('webEnvBody: a domain writes the single-host origin overrides so the hall loads (task 1972)', () => {
|
|
219
269
|
const b = P.webEnvBody({ ...coTenant, domain: 'mercury.example.com' });
|
|
220
270
|
const O = reEsc('https://mercury.example.com');
|