@bongos/core 1.19.657 → 1.19.659
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 +38 -28
- package/.claude/skills/ask-for-help/SKILL.md +74 -0
- package/docs/adr/0182-project-visibility-two-axes.md +9 -1
- package/docs/file-map.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/fitness-checks-vocabulary.js +75 -0
- package/scripts/gds/fitness.js +3 -3
- package/src/bongos/platform-visibility-gate.js +7 -4
- package/src/bongos/{stealth-gate.js → prelaunch-gate.js} +71 -12
- package/src/bongos/serve-internal.js +4 -4
- package/src/module-api.js +1 -1
- package/tests/{stealth_gate.mjs → prelaunch_gate.mjs} +139 -10
- package/tests/skill_ask_for_help.mjs +72 -0
package/.bongos-core.json
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
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.659",
|
|
6
|
+
"core_contract": "1.19.659",
|
|
7
|
+
"source_commit": "feb248ee00547956060970a0a2b26b7923f1ef47",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-11T01:18:53.775Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 473,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2113,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2610,
|
|
20
|
+
"tree_sha256": "d533f43caabc05ab3636390cbbe8f63a140b0c0d84321a438dd2f243041f2301",
|
|
21
21
|
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
24
|
+
"mode": "0000644",
|
|
25
|
+
"sha256": "53dd3df0bc38a8d75de44dfb54f68df4a4f7c288f4f4bf74c84cfc3ae7b23d04"
|
|
26
|
+
},
|
|
22
27
|
{
|
|
23
28
|
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
24
29
|
"mode": "0000644",
|
|
@@ -1407,7 +1412,7 @@
|
|
|
1407
1412
|
{
|
|
1408
1413
|
"path": "docs/adr/0182-project-visibility-two-axes.md",
|
|
1409
1414
|
"mode": "0000644",
|
|
1410
|
-
"sha256": "
|
|
1415
|
+
"sha256": "5c81ca2fe6d92058039999c0d5e38b67cfdaa795ba227e341b2339bbf20815d5"
|
|
1411
1416
|
},
|
|
1412
1417
|
{
|
|
1413
1418
|
"path": "docs/adr/0183-criteria-close-themselves.md",
|
|
@@ -2772,7 +2777,7 @@
|
|
|
2772
2777
|
{
|
|
2773
2778
|
"path": "docs/file-map.md",
|
|
2774
2779
|
"mode": "0000644",
|
|
2775
|
-
"sha256": "
|
|
2780
|
+
"sha256": "fb772f5b797986dcfc60120d5bcb3c1097436d0434160028fe0f48f9f450e84e"
|
|
2776
2781
|
},
|
|
2777
2782
|
{
|
|
2778
2783
|
"path": "docs/handoff-template.md",
|
|
@@ -2782,7 +2787,7 @@
|
|
|
2782
2787
|
{
|
|
2783
2788
|
"path": "docs/module-api-changelog.md",
|
|
2784
2789
|
"mode": "0000644",
|
|
2785
|
-
"sha256": "
|
|
2790
|
+
"sha256": "b92613f789d9a907511c2b1490773a86e1c8c2793f90560bfbb8ede7c3b24652"
|
|
2786
2791
|
},
|
|
2787
2792
|
{
|
|
2788
2793
|
"path": "docs/modules-contract.md",
|
|
@@ -7747,12 +7752,12 @@
|
|
|
7747
7752
|
{
|
|
7748
7753
|
"path": "package-lock.json",
|
|
7749
7754
|
"mode": "0000644",
|
|
7750
|
-
"sha256": "
|
|
7755
|
+
"sha256": "d8f97007999a707d1f1b5cce57a4933594b9abe96061396ac0cf09bb5a22fa5d"
|
|
7751
7756
|
},
|
|
7752
7757
|
{
|
|
7753
7758
|
"path": "package.json",
|
|
7754
7759
|
"mode": "0000644",
|
|
7755
|
-
"sha256": "
|
|
7760
|
+
"sha256": "6498ec4a5891c341fa302772fd0ca40de911494cfee16009186a43208a27c18f"
|
|
7756
7761
|
},
|
|
7757
7762
|
{
|
|
7758
7763
|
"path": "public-docs/index.html",
|
|
@@ -8192,7 +8197,7 @@
|
|
|
8192
8197
|
{
|
|
8193
8198
|
"path": "scripts/gds/fitness-checks-vocabulary.js",
|
|
8194
8199
|
"mode": "0000644",
|
|
8195
|
-
"sha256": "
|
|
8200
|
+
"sha256": "ff21b52ecfe355ce19d7bf576880dfe1b561ffe33664a9a6ec06ccc704425f1d"
|
|
8196
8201
|
},
|
|
8197
8202
|
{
|
|
8198
8203
|
"path": "scripts/gds/fitness-checks-write-validation.js",
|
|
@@ -8212,7 +8217,7 @@
|
|
|
8212
8217
|
{
|
|
8213
8218
|
"path": "scripts/gds/fitness.js",
|
|
8214
8219
|
"mode": "0000644",
|
|
8215
|
-
"sha256": "
|
|
8220
|
+
"sha256": "d6120a5427f9053529639cf15a59bad0afae9602d5a1e8301017f3321493038b"
|
|
8216
8221
|
},
|
|
8217
8222
|
{
|
|
8218
8223
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -9342,13 +9347,18 @@
|
|
|
9342
9347
|
{
|
|
9343
9348
|
"path": "src/bongos/platform-visibility-gate.js",
|
|
9344
9349
|
"mode": "0000644",
|
|
9345
|
-
"sha256": "
|
|
9350
|
+
"sha256": "eea15ebe44e9450f2d62040c239828008e8953ea5ee465085d5f0e8e63fe6233"
|
|
9346
9351
|
},
|
|
9347
9352
|
{
|
|
9348
9353
|
"path": "src/bongos/pool.js",
|
|
9349
9354
|
"mode": "0000644",
|
|
9350
9355
|
"sha256": "c9edcf9cb1f027ca122388beabae488d2b771b9e7aee2cd12fbdb48ea6f0ded8"
|
|
9351
9356
|
},
|
|
9357
|
+
{
|
|
9358
|
+
"path": "src/bongos/prelaunch-gate.js",
|
|
9359
|
+
"mode": "0000644",
|
|
9360
|
+
"sha256": "16da458f537d9eaf55675fab8aab71f537882c1630baa02d2795f32f6a52454e"
|
|
9361
|
+
},
|
|
9352
9362
|
{
|
|
9353
9363
|
"path": "src/bongos/project-door.js",
|
|
9354
9364
|
"mode": "0000644",
|
|
@@ -9472,12 +9482,7 @@
|
|
|
9472
9482
|
{
|
|
9473
9483
|
"path": "src/bongos/serve-internal.js",
|
|
9474
9484
|
"mode": "0000644",
|
|
9475
|
-
"sha256": "
|
|
9476
|
-
},
|
|
9477
|
-
{
|
|
9478
|
-
"path": "src/bongos/stealth-gate.js",
|
|
9479
|
-
"mode": "0000644",
|
|
9480
|
-
"sha256": "3f105bd9c578d320a5b88cf4cebb69c63c16f7b7f81dbeb554a78d00461ca97c"
|
|
9485
|
+
"sha256": "b388de4de8993892d26a1a456eda30deb87a95cf4f64e262cb6541e04495c43d"
|
|
9481
9486
|
},
|
|
9482
9487
|
{
|
|
9483
9488
|
"path": "src/bongos/uptime-poller.js",
|
|
@@ -9507,7 +9512,7 @@
|
|
|
9507
9512
|
{
|
|
9508
9513
|
"path": "src/module-api.js",
|
|
9509
9514
|
"mode": "0000644",
|
|
9510
|
-
"sha256": "
|
|
9515
|
+
"sha256": "b44e546023213b0bedaf19d6685ea25dd9d3da8f61b33296a7d311bb4f459390"
|
|
9511
9516
|
},
|
|
9512
9517
|
{
|
|
9513
9518
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11874,6 +11879,11 @@
|
|
|
11874
11879
|
"mode": "0000644",
|
|
11875
11880
|
"sha256": "9f16091a8feeaef9e4350104dbc9671fab6f2b53f99fd82f798c77800e780b61"
|
|
11876
11881
|
},
|
|
11882
|
+
{
|
|
11883
|
+
"path": "tests/prelaunch_gate.mjs",
|
|
11884
|
+
"mode": "0000644",
|
|
11885
|
+
"sha256": "53bdf7b9e0f09b4abab2c83ed77d48bd4ecd29f2b82afecc78dd694d5b36bc9f"
|
|
11886
|
+
},
|
|
11877
11887
|
{
|
|
11878
11888
|
"path": "tests/preview_server.mjs",
|
|
11879
11889
|
"mode": "0000644",
|
|
@@ -12629,6 +12639,11 @@
|
|
|
12629
12639
|
"mode": "0000644",
|
|
12630
12640
|
"sha256": "7e4f4270188771f1fc1fa7d68e9730b4562fb772ee9ecbd04b65fd1738ee9795"
|
|
12631
12641
|
},
|
|
12642
|
+
{
|
|
12643
|
+
"path": "tests/skill_ask_for_help.mjs",
|
|
12644
|
+
"mode": "0000644",
|
|
12645
|
+
"sha256": "02424dd3b9f084d30802d0fde441ad7d6c20edcfbc8172683dc5d94d7d19725b"
|
|
12646
|
+
},
|
|
12632
12647
|
{
|
|
12633
12648
|
"path": "tests/skill_docs_unavailable_regrade.mjs",
|
|
12634
12649
|
"mode": "0000644",
|
|
@@ -12734,11 +12749,6 @@
|
|
|
12734
12749
|
"mode": "0000644",
|
|
12735
12750
|
"sha256": "aa1d79bb3f900097a911c8c6b3055debe4bb20bfe2c3d6c8c64cfd8d7688824e"
|
|
12736
12751
|
},
|
|
12737
|
-
{
|
|
12738
|
-
"path": "tests/stealth_gate.mjs",
|
|
12739
|
-
"mode": "0000644",
|
|
12740
|
-
"sha256": "bb264253e2a9c233734cf9273928db203f8f441c4e7a53f5f9ae5ea0477e20e3"
|
|
12741
|
-
},
|
|
12742
12752
|
{
|
|
12743
12753
|
"path": "tests/stop_refcheck.mjs",
|
|
12744
12754
|
"mode": "0000644",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ask-for-help
|
|
3
|
+
description: >-
|
|
4
|
+
Ask a named builder or a craft for help on a task, as a filed collab help request. Triggers: "ask <builder> to help with task N", "get <name> on this", "who can help with N", "I'm stuck on task N", "/ask-for-help".
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Route skill (authoritative).** The core action is `POST /api/gds/help-requests`. Once the addressee and the context are settled, file it and report the id — do not narrate the API.
|
|
8
|
+
|
|
9
|
+
You are asking a *person* for help on behalf of the current builder. Being asked assigns them nothing: the collab surfaces recommend, they never transfer a claim (ADR 0187 §3). Declining costs them nothing.
|
|
10
|
+
|
|
11
|
+
## Recognise the ask
|
|
12
|
+
|
|
13
|
+
"Ask <builder> to help with task N", "can someone artist-side look at this", "I'm stuck on N" — all of these are help requests. **File one before reaching for any other surface.** The failure this skill exists to stop: a session hears "ask X to help" and files a BLOCKER instead, which is the owner's review queue and is not read at `/#/collab` by anyone. Both were needed once (2026-09-10, task 1002068) and only the blocker got written.
|
|
14
|
+
|
|
15
|
+
## Pick the right surface
|
|
16
|
+
|
|
17
|
+
| The ask | Surface | Route |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| "Ask X to help with N" / "I'm stuck" | **help request** — this skill | `POST /help-requests` |
|
|
20
|
+
| "X should pick this up" / "pass N to X" | task recommendation — "you might want to claim this" | `POST /task-recommendations` (`task_id`, `recommended_to`, `reason`) |
|
|
21
|
+
| Work stuck on the owner's decision, credential, or a hand on the box | blocker — see `/blocker-review` | `POST /blockers` |
|
|
22
|
+
|
|
23
|
+
They are **complementary, not alternatives.** An owner-only task (a credential rotation, a purchase) deserves a blocker *and* a help request addressed to the owner: the blocker parks the task and holds the detail, the help request is what actually reaches them.
|
|
24
|
+
|
|
25
|
+
## How to use
|
|
26
|
+
|
|
27
|
+
1. **Resolve the addressee to an id — never guess one.**
|
|
28
|
+
```bash
|
|
29
|
+
node scripts/gds/api.js GET /api/gds/builders/directory
|
|
30
|
+
```
|
|
31
|
+
Match the `github_login` or `display_name` the user named. A name with no match is a question for the user, not a guess — a wrong `needs_builder_id` sends the ask to a stranger.
|
|
32
|
+
|
|
33
|
+
Addressing a **craft** instead of a person is the alternative: `needs_craft` is one of `artist`, `builder`, `ideator`, `ui`. It reaches only builders who declared that discipline in their profile, so a craft nobody has declared reaches nobody.
|
|
34
|
+
|
|
35
|
+
**Exactly one addressee.** Both fields is a 400 `two_addressees`; neither is `no_addressee`. The refusal body carries the valid `crafts`.
|
|
36
|
+
|
|
37
|
+
2. **Attach at most one context.** `task_id` **or** `goal_id`, never both (400 `two_contexts`). Neither is allowed — a general "who knows about X" has no context row.
|
|
38
|
+
|
|
39
|
+
3. **Write `what_is_stuck` so it is answerable without this session** (≤ 4000 chars). The recipient has none of your context. Cover:
|
|
40
|
+
- what is stuck, in plain words;
|
|
41
|
+
- why it needs *this* person (rank, access, a call only they can make) — being asked without a reason reads as work being dumped;
|
|
42
|
+
- the concrete steps, so the ask is actionable rather than a summons;
|
|
43
|
+
- how to verify it worked;
|
|
44
|
+
- what has already been tried or ruled out.
|
|
45
|
+
|
|
46
|
+
Never put a secret in it — a password, token, or key. If the work involves one, describe the rotation and let them handle the value on the box.
|
|
47
|
+
|
|
48
|
+
4. **File it.** Write the body to a file first — `api.js` silently drops a positional JSON body:
|
|
49
|
+
```bash
|
|
50
|
+
node scripts/gds/api.js POST /api/gds/help-requests --body-file <path>.json
|
|
51
|
+
```
|
|
52
|
+
```json
|
|
53
|
+
{ "needs_builder_id": 3, "task_id": 1002068, "what_is_stuck": "..." }
|
|
54
|
+
```
|
|
55
|
+
A 201 returns the row. `no_such_builder` on `needs_builder_id` means step 1 was skipped.
|
|
56
|
+
|
|
57
|
+
5. **Report it honestly, including the limit.** Give the request id and say where it lands: the recipient sees it at `/#/collab`, which reads `GET /help-requests/for-me`. That is a **pull, not a push** — help requests have no builder-needs notification (task recommendations do). If it is urgent, say so and tell the user the ask waits until that person opens Collab.
|
|
58
|
+
|
|
59
|
+
6. **Settle it when it resolves** — only the asker can, and an unsettled pile is noise:
|
|
60
|
+
```bash
|
|
61
|
+
node scripts/gds/api.js PATCH /api/gds/help-requests/<id> --body-file <path>.json
|
|
62
|
+
```
|
|
63
|
+
`{"status": "answered"}` when someone helped, `{"status": "withdrawn"}` when it is no longer needed. Check yours with `GET /api/gds/help-requests/mine`.
|
|
64
|
+
|
|
65
|
+
## Constraints
|
|
66
|
+
|
|
67
|
+
- **Never claim the task on the recipient's behalf**, and never say they are "assigned" or "on it". They still claim it themselves.
|
|
68
|
+
- **One request per ask.** Re-asking the same person about the same task makes a second open row, not a nudge.
|
|
69
|
+
- **Asking is not privileged** — any builder may file one, and gating it would exclude exactly the newcomers most likely to be stuck.
|
|
70
|
+
|
|
71
|
+
## Files this skill touches
|
|
72
|
+
|
|
73
|
+
- Calls: `GET /api/gds/builders/directory`, `POST /api/gds/help-requests`, `GET /api/gds/help-requests/{for-me,mine}`, `PATCH /api/gds/help-requests/:id`
|
|
74
|
+
- Reads: `modules/lifecycle/routes/help-requests.js` (the four routes), `modules/lifecycle/help-requests.js` (the validation rules), ADR 0187 §3 (recommend, never transfer)
|
|
@@ -150,7 +150,15 @@ Stealth's distinction from private is **at the door, not in the queue**: a refus
|
|
|
150
150
|
|
|
151
151
|
**Decision:** the *product* meaning (the owner-facing state, whose orb metaphor is locked by the criterion) keeps the word. The infra gate is the one that should be renamed — to `prelaunch` — but that touches a protected path, an env var, and a live instance's configuration, so it is **not** decided here by an ADR about visibility. It is filed as its own task with the owner's call attached.
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
**RESOLVED — the rename was taken** (owner, 2026-09-09 backlog review of goal [#1000111](https://cloudbongos.com/builders#/goal/1000111); executed by task [#1003152](https://cloudbongos.com/builders#/task/1003152)). This paragraph is kept rather than rewritten, because the open question is part of the record: what follows is what the rename actually did, not what it promised.
|
|
154
|
+
|
|
155
|
+
- `src/bongos/stealth-gate.js` → **`src/bongos/prelaunch-gate.js`**; `stealthGate()` → `prelaunchGate()`. The ~16 files carrying the PRODUCT sense are untouched, exactly as this decision says they must be.
|
|
156
|
+
- **`PRELAUNCH_PASSWORD` is primary; `STEALTH_PASSWORD` is honoured as a deprecated alias**, with a one-time startup warning when only the old name is set. The env is read in exactly one place, so honouring both is a single expression — and it had to be: `STEALTH_PASSWORD` is set on live today, and a rename that did not read it would be an instant, total outage of the instance. Dropping the alias is a separate, later task, and must not run until the live instance has `PRELAUNCH_PASSWORD` set.
|
|
157
|
+
- The submit path is **`/__prelaunch`**, with `/__stealth` still accepted and still exempt, so a form already open when the rename deployed still works.
|
|
158
|
+
- **Three identifiers deliberately did NOT move** — the cookie `cb_stealth`, the HMAC label `stealth-v1`, and the legacy path above. The first two are keyed into live cookies: <redacted> either forces every holder to re-enter the password and buys nothing, because neither string is ever read by a human. One is wire format, the other an HMAC message; neither is vocabulary.
|
|
159
|
+
- A fitness check in the ADR 0174 family (**check 19d**, `checkPrelaunchVocabulary`) now keeps the word from drifting back into the infra surface, scoped so the product surface's legitimate `stealth` never trips it. Per ADR 0174's own lesson it carries mutation tests: plant a violation, prove the build reds.
|
|
160
|
+
|
|
161
|
+
The last sentence of this decision is therefore now spent — `stealth` unqualified means the project state, and the infra gate says `prelaunch` in its identifiers as well as in prose.
|
|
154
162
|
|
|
155
163
|
## Consequences
|
|
156
164
|
|
package/docs/file-map.md
CHANGED
|
@@ -383,6 +383,7 @@ tests/
|
|
|
383
383
|
<!-- BEGIN GENERATED FILE-MAP .claude/skills/ (scripts/gds/gen-file-map.js — do not hand-edit; notes live in docs/file-map.notes.json) -->
|
|
384
384
|
```
|
|
385
385
|
.claude/skills/
|
|
386
|
+
├── ask-for-help/SKILL.md ← /ask-for-help — turn 'ask <builder> to help with task N' into a filed collab help request (POST /help-requests): resolve the name to a builder id, one addressee (person XOR craft) and one context (task XOR goal), a what_is_stuck answerable without this session, then settle it; routes the near-neighbours (recommendation vs blocker) and states that the addressed read is a pull, not a push (task 1003826)
|
|
386
387
|
├── backlog-review/SKILL.md ← /backlog-review — daily walk of status=backlog, the pre-workable state a human must say go on: splits rows waiting on a PERSON (promote / kill / water) from rows waiting on a live dep TRIGGER (counted, never walked, migration 163) and surfaces rows stranded behind an abandoned dep; runs scripts/gds/backlog-review.js (task 1003746); Metic+
|
|
387
388
|
├── blocker-review/SKILL.md ← /blocker-review — daily review of open Bongos blockers (resolve / escalate / note); Metic+
|
|
388
389
|
├── blocker-solve/SKILL.md ← /blocker-solve N — drive ONE blocker to done: do the doable parts, hand back owner-only steps, verify, auto-resolve (auto-promotes waiters); Metic+
|
|
@@ -1763,5 +1763,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1763
1763
|
landed since 1.19.655 with no explicit bump. run 34546461767. (task 1002620)
|
|
1764
1764
|
1.19.657 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1765
1765
|
landed since 1.19.656 with no explicit bump. run 34546894776. (task 1002620)
|
|
1766
|
+
1.19.658 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1767
|
+
landed since 1.19.657 with no explicit bump. run 34547406494. (task 1002620)
|
|
1768
|
+
1.19.659 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1769
|
+
landed since 1.19.658 with no explicit bump. run 34550130772. (task 1002620)
|
|
1766
1770
|
---------------------------------------------------------------------------
|
|
1767
1771
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.659",
|
|
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.659",
|
|
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.659",
|
|
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",
|
|
@@ -298,11 +298,85 @@ function checkGovernmentRenameIdentity() {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
|
|
301
|
+
// ── Check 19d — "stealth" is the PRODUCT's word, not the gate's (task 1003152) ─
|
|
302
|
+
//
|
|
303
|
+
// The third vocabulary wall, and the same defect as ADR 0174's "role": one word,
|
|
304
|
+
// two live meanings, close to OPPOSITE. `platform_identity_projects.visibility =
|
|
305
|
+
// 'stealth'` is a project that IS on the public orbs map — anonymous, rendered as
|
|
306
|
+
// a black hole, refusing applications, but published and visible; only its
|
|
307
|
+
// identity is withheld. The pre-launch gate is the reverse: the whole instance is
|
|
308
|
+
// UNREADABLE without a password. ADR 0182 locks the product sense to the orb
|
|
309
|
+
// metaphor via goal 1000046's own criterion, so the PRODUCT keeps the word and
|
|
310
|
+
// the INFRA half renamed (owner decision, 2026-09-09).
|
|
311
|
+
//
|
|
312
|
+
// SCOPED TO THE INFRA SURFACE, for the same reason check 19 is scoped to the
|
|
313
|
+
// government surface: ~16 files carry the product sense legitimately, and a
|
|
314
|
+
// repo-wide ban would have to allowlist every one of them — an allowlist longer
|
|
315
|
+
// than the rule. The honest unit is a SURFACE.
|
|
316
|
+
//
|
|
317
|
+
// NO ALLOWLIST. Two exemptions, both properties of a line rather than a file:
|
|
318
|
+
// a line naming one of the four documented COMPATIBILITY identifiers (the live
|
|
319
|
+
// cookie, the HMAC label, the legacy submit path, the deprecated env var — see
|
|
320
|
+
// prelaunch-gate.js's header for why each stayed), and a line CITING the rename
|
|
321
|
+
// or the other meaning. Everything else is drift.
|
|
322
|
+
// tests/prelaunch_gate.mjs is deliberately NOT here, the same way check 19's
|
|
323
|
+
// mutation test sits outside the government surface: that file's job includes
|
|
324
|
+
// PLANTING the banned word to prove the rule can fail, and a wall that forbids
|
|
325
|
+
// its own negative test can only be satisfied by deleting the evidence.
|
|
326
|
+
const PRELAUNCH_SURFACE = [
|
|
327
|
+
'src/bongos/prelaunch-gate.js',
|
|
328
|
+
'src/bongos/platform-visibility-gate.js',
|
|
329
|
+
];
|
|
330
|
+
// serve-internal.js is 1,500+ lines and mounts every surface on the instance;
|
|
331
|
+
// scanning it whole would drag in prose about the product's visibility. The
|
|
332
|
+
// lines that name the GATE are the only ones that can regress here.
|
|
333
|
+
const PRELAUNCH_LINE_SCOPED = [
|
|
334
|
+
{ file: 'src/bongos/serve-internal.js', mustMention: /prelaunch|PRE-LAUNCH/i },
|
|
335
|
+
];
|
|
336
|
+
const PRELAUNCH_COMPAT = /cb_stealth|stealth-v1|__stealth|STEALTH_PASSWORD/;
|
|
337
|
+
const PRELAUNCH_CITATION = /task 1003152|ADR 0182|stealth-gate\.js|platform_identity_projects/;
|
|
338
|
+
|
|
339
|
+
function checkPrelaunchVocabulary() {
|
|
340
|
+
const violations = [];
|
|
341
|
+
let scanned = 0;
|
|
342
|
+
const scanFile = (rel, lineFilter = null) => {
|
|
343
|
+
const abs = path.join(CORE_ROOT, rel);
|
|
344
|
+
let src;
|
|
345
|
+
try { src = fs.readFileSync(abs, 'utf8'); } catch { return; }
|
|
346
|
+
scanned += 1;
|
|
347
|
+
src.split('\n').forEach((line, i) => {
|
|
348
|
+
if (lineFilter && !lineFilter.test(line)) return;
|
|
349
|
+
if (!/stealth/i.test(line)) return;
|
|
350
|
+
if (PRELAUNCH_COMPAT.test(line)) return;
|
|
351
|
+
if (PRELAUNCH_CITATION.test(line)) return;
|
|
352
|
+
violations.push(`${rel}:${i + 1}: "stealth" — on the pre-launch surface the word is PRELAUNCH. `
|
|
353
|
+
+ '"stealth" belongs to the PRODUCT\'s visibility axis (ADR 0182), which means almost the opposite.'
|
|
354
|
+
+ ` → ${line.trim().slice(0, 90)}`);
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
for (const rel of PRELAUNCH_SURFACE) scanFile(rel);
|
|
358
|
+
for (const { file, mustMention } of PRELAUNCH_LINE_SCOPED) scanFile(file, mustMention);
|
|
359
|
+
return {
|
|
360
|
+
name: 'the pre-launch gate is PRELAUNCH; "stealth" is the product\'s word (task 1003152)',
|
|
361
|
+
ok: violations.length === 0,
|
|
362
|
+
hardFail: violations.length > 0,
|
|
363
|
+
violations,
|
|
364
|
+
warnings: [],
|
|
365
|
+
note: `${scanned} pre-launch-surface file(s) scanned (${PRELAUNCH_SURFACE.length} whole plus `
|
|
366
|
+
+ `${PRELAUNCH_LINE_SCOPED.length} line-scoped to the lines naming the gate); no allowlist — `
|
|
367
|
+
+ 'a documented compatibility identifier or a line CITING the rename are the only exemptions.',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
301
371
|
module.exports = {
|
|
302
372
|
GOAL_VOCAB_CITATION,
|
|
303
373
|
GOAL_VOCAB_LINE_SCOPED,
|
|
304
374
|
GOAL_VOCAB_SURFACE,
|
|
305
375
|
LIFECYCLE_DB_FILES,
|
|
376
|
+
PRELAUNCH_CITATION,
|
|
377
|
+
PRELAUNCH_COMPAT,
|
|
378
|
+
PRELAUNCH_LINE_SCOPED,
|
|
379
|
+
PRELAUNCH_SURFACE,
|
|
306
380
|
RENAME_IDENTITY_RE,
|
|
307
381
|
VOCAB_ALLOW,
|
|
308
382
|
VOCAB_BANNED,
|
|
@@ -312,6 +386,7 @@ module.exports = {
|
|
|
312
386
|
VOCAB_SURFACE,
|
|
313
387
|
checkGoalMembershipVocabulary,
|
|
314
388
|
checkGovernmentRenameIdentity,
|
|
389
|
+
checkPrelaunchVocabulary,
|
|
315
390
|
checkVocabularyWall,
|
|
316
391
|
vocabFilesToScan,
|
|
317
392
|
};
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -36,7 +36,7 @@ const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-con
|
|
|
36
36
|
const { CORE_ROOT, INSTANCE_ROOT, MODULE_ROOTS, REPO_ROOT, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
|
|
37
37
|
const { IDENTITY_SCAN_ALLOWED, IDENTITY_SCAN_EXCLUDE, IDENTITY_SCAN_EXTRA_FILES, IDENTITY_SCAN_EXTS, IDENTITY_SCAN_ROOTS, IDENTITY_SCAN_SKIP_DIRS, PROMPT_IDENTITY_LITERALS, checkDesignContract, checkKernelPromptsNoHardcodedIdentity, checkNeutralConfigsNoHostIdentity, collectIdentityFiles, scanPromptIdentity, stripCommentsForIdentity } = require('./fitness-checks-identity.js');
|
|
38
38
|
const { WRITE_VALIDATION_BASELINE, WRITE_VALIDATION_EXEMPT, checkWriteRoutesValidated, scanUnvalidatedWriteRoutes } = require('./fitness-checks-write-validation.js');
|
|
39
|
-
const { GOAL_VOCAB_CITATION, GOAL_VOCAB_LINE_SCOPED, GOAL_VOCAB_SURFACE, LIFECYCLE_DB_FILES, RENAME_IDENTITY_RE, VOCAB_ALLOW, VOCAB_BANNED, VOCAB_CITATION, VOCAB_EXTS, VOCAB_HISTORICAL_REF, VOCAB_SURFACE, checkGoalMembershipVocabulary, checkGovernmentRenameIdentity, checkVocabularyWall, vocabFilesToScan } = require('./fitness-checks-vocabulary.js');
|
|
39
|
+
const { GOAL_VOCAB_CITATION, GOAL_VOCAB_LINE_SCOPED, GOAL_VOCAB_SURFACE, LIFECYCLE_DB_FILES, PRELAUNCH_SURFACE, RENAME_IDENTITY_RE, VOCAB_ALLOW, VOCAB_BANNED, VOCAB_CITATION, VOCAB_EXTS, VOCAB_HISTORICAL_REF, VOCAB_SURFACE, checkGoalMembershipVocabulary, checkGovernmentRenameIdentity, checkPrelaunchVocabulary, checkVocabularyWall, vocabFilesToScan } = require('./fitness-checks-vocabulary.js');
|
|
40
40
|
// ============================================================================
|
|
41
41
|
// Check 1 — core ↔ host/game import boundary (ADR 0062 §1 cut-line)
|
|
42
42
|
// ============================================================================
|
|
@@ -1374,7 +1374,7 @@ const CHECKS = [
|
|
|
1374
1374
|
checkErrorEnvelope, // Check 17 — BV1 R03 / task 1990: no route hand-rolls a flat error (ADR 0116)
|
|
1375
1375
|
checkVocabularyWall, // Check 19 — ADR 0174 / goal 1000068 R07: "role" is a craft, never a rank
|
|
1376
1376
|
checkGoalMembershipVocabulary, // Check 19b — ADR 0177 / task 1003013: the FOURTH "role" (goal membership)
|
|
1377
|
-
checkGovernmentRenameIdentity, //
|
|
1377
|
+
checkGovernmentRenameIdentity, checkPrelaunchVocabulary, // 19c task 1003120 (`X`→`X` rename arrow) + 19d task 1003152 ("stealth" is the product's word) — sharing a line: this file is AT the 1,500 budget (Check 23/24 precedent below; task 1003825 buys it back)
|
|
1378
1378
|
checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
|
|
1379
1379
|
require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
|
|
1380
1380
|
require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
|
|
@@ -1436,7 +1436,7 @@ if (require.main === module) main();
|
|
|
1436
1436
|
|
|
1437
1437
|
module.exports = {
|
|
1438
1438
|
checkVocabularyWall, VOCAB_ALLOW, VOCAB_CITATION, VOCAB_EXTS, vocabFilesToScan,
|
|
1439
|
-
checkGoalMembershipVocabulary, GOAL_VOCAB_SURFACE, checkGovernmentRenameIdentity, RENAME_IDENTITY_RE, // 19c
|
|
1439
|
+
checkGoalMembershipVocabulary, GOAL_VOCAB_SURFACE, checkGovernmentRenameIdentity, RENAME_IDENTITY_RE, checkPrelaunchVocabulary, PRELAUNCH_SURFACE, // 19c task 1003120, 19d task 1003152
|
|
1440
1440
|
runAll,
|
|
1441
1441
|
resolvesToHostGame,
|
|
1442
1442
|
checkCoreHostBoundary,
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
// - What a member is shown must not be cached for a stranger: a passed
|
|
25
25
|
// response leaves marked private, whatever the route behind it said.
|
|
26
26
|
//
|
|
27
|
-
// It is NOT the pre-launch gate (
|
|
28
|
-
// password for an unannounced instance, inert unless
|
|
27
|
+
// It is NOT the pre-launch gate (prelaunch-gate.js): that one is a shared
|
|
28
|
+
// password for an unannounced instance, inert unless PRELAUNCH_PASSWORD is set,
|
|
29
29
|
// and it mounts first. This one is a per-project policy with accounts behind it.
|
|
30
30
|
// Both can be on at once; each refuses on its own terms.
|
|
31
31
|
|
|
@@ -67,7 +67,9 @@ const API = `(?:${ALL_API_PREFIXES.map(escRe).join('|')})`;
|
|
|
67
67
|
// /downloads/* the installer script and the Dev Box updater — a
|
|
68
68
|
// cookie-less `curl … | sh`, and release redirects carry
|
|
69
69
|
// no project data.
|
|
70
|
-
// /
|
|
70
|
+
// /__prelaunch the pre-launch gate's own submit, when both gates are on.
|
|
71
|
+
// /__stealth is its pre-rename alias (task 1003152) and is
|
|
72
|
+
// exempt for as long as the gate still accepts it.
|
|
71
73
|
const EXEMPT = [
|
|
72
74
|
new RegExp(`^${API}/auth(/|$)`),
|
|
73
75
|
new RegExp(`^${API}/instance$`),
|
|
@@ -76,6 +78,7 @@ const EXEMPT = [
|
|
|
76
78
|
/^\/healthz$/,
|
|
77
79
|
/^\/version$/,
|
|
78
80
|
/^\/metrics$/,
|
|
81
|
+
/^\/__prelaunch$/,
|
|
79
82
|
/^\/__stealth$/,
|
|
80
83
|
/^\/downloads\//,
|
|
81
84
|
];
|
|
@@ -127,7 +130,7 @@ function signInHref(req) {
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
// One leading slash, no '//' (protocol-relative), no backslash, no control
|
|
130
|
-
// bytes — the
|
|
133
|
+
// bytes — the pre-launch gate's rule; anything else returns to '/'.
|
|
131
134
|
function relativeReturn(raw) {
|
|
132
135
|
if (typeof raw !== 'string' || !raw.length || raw.length > 512) return null;
|
|
133
136
|
if (!raw.startsWith('/') || raw.startsWith('//') || raw.includes('\\')) return null;
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
// src/bongos/
|
|
1
|
+
// src/bongos/prelaunch-gate.js — the instance PRE-LAUNCH gate (task 1002775).
|
|
2
|
+
//
|
|
3
|
+
// ── THE NAME (task 1003152, owner decision 2026-09-09; ADR 0182 Decision 7) ──
|
|
4
|
+
//
|
|
5
|
+
// This was `stealth-gate.js` and it collided head-on with the OTHER live meaning
|
|
6
|
+
// of the word. `platform_identity_projects.visibility = 'stealth'` is a project
|
|
7
|
+
// that IS on the public orbs map — rendered as a black hole, anonymous, refusing
|
|
8
|
+
// applications, but published and visible; only its identity is withheld. This
|
|
9
|
+
// gate is close to the opposite: the whole instance is UNREADABLE without the
|
|
10
|
+
// password. Same class of defect as ADR 0174's "role", which is why the rename
|
|
11
|
+
// lives in the government goal.
|
|
12
|
+
//
|
|
13
|
+
// The PRODUCT meaning keeps the word — ADR 0182 locks it to the orb metaphor via
|
|
14
|
+
// goal 1000046's own criterion, and the ~16 files carrying that sense are
|
|
15
|
+
// deliberately untouched. Renaming those would have been the actual mistake.
|
|
16
|
+
//
|
|
17
|
+
// THREE IDENTIFIERS DID NOT MOVE, and each one is a deliberate choice, not a
|
|
18
|
+
// missed occurrence (the vocabulary check below knows them by name):
|
|
19
|
+
//
|
|
20
|
+
// • the cookie `cb_stealth` and the HMAC label `stealth-v1` — both are keyed
|
|
21
|
+
// INTO live cookies. Renaming either invalidates every outstanding cookie on
|
|
22
|
+
// a running instance and forces every holder to re-enter the password, to
|
|
23
|
+
// buy nothing: neither string is ever read by a human. The cookie name is
|
|
24
|
+
// wire format and the label is an HMAC message, not vocabulary.
|
|
25
|
+
// • `/__stealth` — the submit path. `/__prelaunch` is what the form posts to
|
|
26
|
+
// now; the old path is still accepted and still exempt, so a form already
|
|
27
|
+
// open in somebody's tab when this deploys still works instead of 401ing
|
|
28
|
+
// them back to the start.
|
|
2
29
|
//
|
|
3
30
|
// An instance that is built but not announced needs a door that is shut without
|
|
4
31
|
// being broken. The proxy can do that with basic_auth, and task 1002774 did
|
|
@@ -11,8 +38,11 @@
|
|
|
11
38
|
// So the gate lives in the app, where it can set a cookie the browser will send
|
|
12
39
|
// to every subdomain:
|
|
13
40
|
//
|
|
14
|
-
// - It is INERT unless
|
|
15
|
-
// a vanilla instance has no gate and needs no config to not have
|
|
41
|
+
// - It is INERT unless PRELAUNCH_PASSWORD is set. That is the portable
|
|
42
|
+
// default: a vanilla instance has no gate and needs no config to not have
|
|
43
|
+
// one. STEALTH_PASSWORD is honoured as a deprecated alias — it is SET ON
|
|
44
|
+
// LIVE today, and a rename that did not read the old name would be an
|
|
45
|
+
// instant outage.
|
|
16
46
|
// - The cookie is a SESSION cookie — no Max-Age, no Expires — because the
|
|
17
47
|
// owner explicitly did not want it to outlive the browser. That is also the
|
|
18
48
|
// safer default: nothing is written to disk to be found later.
|
|
@@ -35,8 +65,15 @@
|
|
|
35
65
|
const crypto = require('node:crypto');
|
|
36
66
|
const { branding } = require('../branding');
|
|
37
67
|
|
|
68
|
+
// Wire format, deliberately unrenamed — see the header. Both are keyed into
|
|
69
|
+
// cookies that are live right now.
|
|
38
70
|
const COOKIE = 'cb_stealth';
|
|
39
|
-
const
|
|
71
|
+
const HMAC_LABEL = 'stealth-v1';
|
|
72
|
+
|
|
73
|
+
const SUBMIT_PATH = '/__prelaunch';
|
|
74
|
+
// Accepted for as long as a form served before the rename might still be open.
|
|
75
|
+
const LEGACY_SUBMIT_PATH = '/__stealth';
|
|
76
|
+
const SUBMIT_PATHS = [SUBMIT_PATH, LEGACY_SUBMIT_PATH];
|
|
40
77
|
|
|
41
78
|
// Paths that must answer even to a stranger, each for its own reason:
|
|
42
79
|
// /api/* the GitHub OAuth round trip has to complete, and a gated callback
|
|
@@ -45,19 +82,37 @@ const SUBMIT_PATH = '/__stealth';
|
|
|
45
82
|
// /metrics Prometheus, already access-gated on its own terms (loopback or
|
|
46
83
|
// bearer token) — double-gating it would break the scrape
|
|
47
84
|
// /version the deploy pin check, a public commit sha
|
|
48
|
-
const EXEMPT = [/^\/api\//, /^\/healthz$/, /^\/metrics$/, /^\/version$/, /^\/__stealth$/];
|
|
85
|
+
const EXEMPT = [/^\/api\//, /^\/healthz$/, /^\/metrics$/, /^\/version$/, /^\/__prelaunch$/, /^\/__stealth$/];
|
|
49
86
|
|
|
87
|
+
// PRELAUNCH_PASSWORD is primary; STEALTH_PASSWORD is the deprecated alias. This
|
|
88
|
+
// is the ONE read site for either, which is what makes honouring both a single
|
|
89
|
+
// expression rather than a migration — and why the rename could be taken at all
|
|
90
|
+
// without an outage window on a live instance already running the old name.
|
|
50
91
|
function password() {
|
|
51
|
-
const p = process.env.STEALTH_PASSWORD;
|
|
92
|
+
const p = process.env.PRELAUNCH_PASSWORD || process.env.STEALTH_PASSWORD;
|
|
52
93
|
return typeof p === 'string' && p.length ? p : null;
|
|
53
94
|
}
|
|
54
95
|
|
|
96
|
+
// Named once at mount, never per request: a warning on every gated request would
|
|
97
|
+
// be a log flood on exactly the instance that is hardest to look at. Dropping the
|
|
98
|
+
// alias is a separate, later task — this only makes the deprecation audible.
|
|
99
|
+
function warnIfLegacyEnv(log = console.warn) {
|
|
100
|
+
const hasNew = typeof process.env.PRELAUNCH_PASSWORD === 'string' && process.env.PRELAUNCH_PASSWORD.length;
|
|
101
|
+
const hasOld = typeof process.env.STEALTH_PASSWORD === 'string' && process.env.STEALTH_PASSWORD.length;
|
|
102
|
+
if (hasNew || !hasOld) return false;
|
|
103
|
+
log('[prelaunch-gate] STEALTH_PASSWORD is DEPRECATED and still the only password set. '
|
|
104
|
+
+ 'Set PRELAUNCH_PASSWORD to the same value (task 1003152); the old name is read for now '
|
|
105
|
+
+ 'and will be dropped in a later task.');
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
function enabled() { return password() !== null; }
|
|
56
110
|
|
|
57
111
|
// The token a valid cookie must carry. Keyed by the password, so the password
|
|
58
112
|
// itself never leaves the server and a rotation invalidates every cookie.
|
|
113
|
+
// HMAC_LABEL is deliberately still 'stealth-v1' — see the header.
|
|
59
114
|
function expectedToken() {
|
|
60
|
-
return crypto.createHmac('sha256', password()).update(
|
|
115
|
+
return crypto.createHmac('sha256', password()).update(HMAC_LABEL).digest('hex');
|
|
61
116
|
}
|
|
62
117
|
|
|
63
118
|
function tokenMatches(raw) {
|
|
@@ -118,7 +173,7 @@ function relativeReturn(raw) {
|
|
|
118
173
|
if (typeof raw !== 'string' || !raw.length || raw.length > 512) return null;
|
|
119
174
|
if (!raw.startsWith('/') || raw.startsWith('//') || raw.includes('\\')) return null;
|
|
120
175
|
if (/[\x00-\x1f\x7f]/.test(raw)) return null;
|
|
121
|
-
if (raw === '/' || raw.startsWith(
|
|
176
|
+
if (raw === '/' || SUBMIT_PATHS.some((p) => raw.startsWith(p))) return null;
|
|
122
177
|
return raw;
|
|
123
178
|
}
|
|
124
179
|
|
|
@@ -178,11 +233,12 @@ function productName() {
|
|
|
178
233
|
|
|
179
234
|
// Mounted FIRST, ahead of every other surface — a route registered earlier would
|
|
180
235
|
// answer before the gate ever ran.
|
|
181
|
-
function
|
|
182
|
-
|
|
236
|
+
function prelaunchGate() {
|
|
237
|
+
warnIfLegacyEnv();
|
|
238
|
+
return function prelaunch(req, res, next) {
|
|
183
239
|
if (!enabled()) return next();
|
|
184
240
|
|
|
185
|
-
if (req.method === 'POST' && req.path
|
|
241
|
+
if (req.method === 'POST' && SUBMIT_PATHS.includes(req.path)) {
|
|
186
242
|
// The form is urlencoded; body parsers mount later (and only on some
|
|
187
243
|
// routers), so read it here rather than depending on middleware order.
|
|
188
244
|
let body = '';
|
|
@@ -226,4 +282,7 @@ function stealthGate() {
|
|
|
226
282
|
};
|
|
227
283
|
}
|
|
228
284
|
|
|
229
|
-
module.exports = {
|
|
285
|
+
module.exports = {
|
|
286
|
+
prelaunchGate, enabled, isExempt, tokenMatches, expectedToken, warnIfLegacyEnv,
|
|
287
|
+
COOKIE, HMAC_LABEL, SUBMIT_PATH, LEGACY_SUBMIT_PATH, SUBMIT_PATHS,
|
|
288
|
+
};
|
|
@@ -47,7 +47,7 @@ const { buildInfo } = require('../build-info');
|
|
|
47
47
|
const { buildDevboxDownloads } = require('../devbox-downloads');
|
|
48
48
|
const { buildBongosDownloads } = require('../bongos-downloads');
|
|
49
49
|
const { clientBranding, branding } = require('../branding');
|
|
50
|
-
const {
|
|
50
|
+
const { prelaunchGate } = require('./prelaunch-gate');
|
|
51
51
|
const { platformVisibilityGate } = require('./platform-visibility-gate');
|
|
52
52
|
const { clientModules, isModuleEnabled } = require('../modules');
|
|
53
53
|
const { hallWidgetScripts, moduleWebSurfaces } = require('../module-loader/loader');
|
|
@@ -494,15 +494,15 @@ function mountInternalSurfaces(app) {
|
|
|
494
494
|
// game routes too) — it records on res 'finish' regardless of which handler
|
|
495
495
|
// answers. The /metrics scrape endpoint is access-gated (loopback Prometheus
|
|
496
496
|
// or Bearer METRICS_TOKEN; see metrics.authorizeScrape).
|
|
497
|
-
// THE
|
|
497
|
+
// THE PRE-LAUNCH GATE (task 1002775) mounts before anything it is meant to
|
|
498
498
|
// cover — a surface registered earlier would answer first and the gate would
|
|
499
|
-
// never run. Inert unless
|
|
499
|
+
// never run. Inert unless PRELAUNCH_PASSWORD is set, so a vanilla instance is
|
|
500
500
|
// unaffected and pays only a closure call per request.
|
|
501
501
|
//
|
|
502
502
|
// It sits ABOVE the metrics middleware deliberately: a refused knock is still
|
|
503
503
|
// a request this instance served, and hiding gate 401s would make the traffic
|
|
504
504
|
// graph lie about how much attention an unannounced instance is getting.
|
|
505
|
-
app.use(
|
|
505
|
+
app.use(prelaunchGate());
|
|
506
506
|
|
|
507
507
|
app.use(metrics.httpMiddleware());
|
|
508
508
|
app.get('/metrics', metrics.metricsHandler());
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.659'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/prelaunch_gate.mjs — the instance pre-launch gate (task 1002775).
|
|
2
2
|
//
|
|
3
3
|
// A gate that is wrong in either direction is worse than no gate: too open and
|
|
4
4
|
// the unannounced instance is readable; too closed and it locks out sign-in,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// gate's whole job is to sit in front of surfaces registered elsewhere — a unit
|
|
7
7
|
// test of the middleware alone could not prove it was mounted early enough.
|
|
8
8
|
//
|
|
9
|
-
// Run: node --test tests/
|
|
9
|
+
// Run: node --test tests/prelaunch_gate.mjs
|
|
10
10
|
|
|
11
11
|
import assert from 'node:assert/strict';
|
|
12
12
|
import { test, before, after } from 'node:test';
|
|
@@ -16,9 +16,9 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
|
|
18
18
|
const PASSWORD = 'a-test-password';
|
|
19
|
-
process.env.
|
|
19
|
+
process.env.PRELAUNCH_PASSWORD = PASSWORD; // set BEFORE the app is built
|
|
20
20
|
|
|
21
|
-
const gate = require('../src/bongos/
|
|
21
|
+
const gate = require('../src/bongos/prelaunch-gate.js');
|
|
22
22
|
const platformMod = require('../src/platform-server.js');
|
|
23
23
|
|
|
24
24
|
let server, port;
|
|
@@ -74,7 +74,7 @@ test('the form leaks nothing about the product it is hiding', async () => {
|
|
|
74
74
|
|
|
75
75
|
test('every human surface is gated, not just the front page', async () => {
|
|
76
76
|
// the old proxy lock covered '/' but not '/projects', so the hub leaked for
|
|
77
|
-
// the whole
|
|
77
|
+
// the whole pre-launch period — that regression must be impossible here
|
|
78
78
|
for (const p of ['/', '/projects', '/create', '/builders', '/docs']) {
|
|
79
79
|
const res = await request(p);
|
|
80
80
|
assert.equal(res.status, 401, `${p} must be gated`);
|
|
@@ -209,9 +209,9 @@ test('a forged or stale cookie does not open it', async () => {
|
|
|
209
209
|
|
|
210
210
|
test('rotating the password invalidates outstanding cookies', async () => {
|
|
211
211
|
const old = validCookie();
|
|
212
|
-
process.env.
|
|
212
|
+
process.env.PRELAUNCH_PASSWORD = 'a-different-password';
|
|
213
213
|
const res = await request('/', { cookie: old });
|
|
214
|
-
process.env.
|
|
214
|
+
process.env.PRELAUNCH_PASSWORD = PASSWORD;
|
|
215
215
|
assert.equal(res.status, 401, 'the token is keyed by the password, so a rotation revokes');
|
|
216
216
|
});
|
|
217
217
|
|
|
@@ -219,16 +219,145 @@ test('an oversized POST body is dropped rather than buffered', async () => {
|
|
|
219
219
|
// a password is not a payload — the handler reads the body itself, so it must
|
|
220
220
|
// bound it or it is a trivial memory sink on an unauthenticated endpoint
|
|
221
221
|
const src = require('node:fs').readFileSync(
|
|
222
|
-
new URL('../src/bongos/
|
|
222
|
+
new URL('../src/bongos/prelaunch-gate.js', import.meta.url), 'utf8');
|
|
223
223
|
assert.match(src, /body\.length > \d+/, 'the body read must be bounded');
|
|
224
224
|
assert.match(src, /req\.destroy\(\)/);
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
test('the gate is inert when no password is configured', () => {
|
|
228
228
|
// the portable default: a vanilla instance has no gate and needs no config
|
|
229
|
-
// to not have one
|
|
229
|
+
// to not have one. BOTH names must be clear — an alias that kept the gate up
|
|
230
|
+
// after the primary was unset would be the same bug in reverse.
|
|
231
|
+
delete process.env.PRELAUNCH_PASSWORD;
|
|
230
232
|
delete process.env.STEALTH_PASSWORD;
|
|
231
233
|
assert.equal(gate.enabled(), false);
|
|
232
|
-
process.env.
|
|
234
|
+
process.env.PRELAUNCH_PASSWORD = PASSWORD;
|
|
233
235
|
assert.equal(gate.enabled(), true);
|
|
234
236
|
});
|
|
237
|
+
|
|
238
|
+
// ── the rename's compatibility surface (task 1003152) ───────────────────────
|
|
239
|
+
//
|
|
240
|
+
// STEALTH_PASSWORD is SET ON LIVE cloudbongos.com right now. A rename that did
|
|
241
|
+
// not read the old name would be an instant, total outage of the instance — so
|
|
242
|
+
// these are the tests that make the rename safe to deploy, not nice-to-haves.
|
|
243
|
+
|
|
244
|
+
test('the DEPRECATED STEALTH_PASSWORD alone still opens the gate', async () => {
|
|
245
|
+
delete process.env.PRELAUNCH_PASSWORD;
|
|
246
|
+
process.env.STEALTH_PASSWORD = PASSWORD;
|
|
247
|
+
try {
|
|
248
|
+
assert.equal(gate.enabled(), true, 'an instance running only the old name must not go dark');
|
|
249
|
+
// and the cookie it mints is the same one, because the HMAC label did not move
|
|
250
|
+
const res = await request('/', { cookie: `${gate.COOKIE}=${gate.expectedToken()}` });
|
|
251
|
+
assert.equal(res.status, 200, 'a cookie held across the rename must still be honoured');
|
|
252
|
+
} finally {
|
|
253
|
+
delete process.env.STEALTH_PASSWORD;
|
|
254
|
+
process.env.PRELAUNCH_PASSWORD = PASSWORD;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('PRELAUNCH_PASSWORD wins when both are set', () => {
|
|
259
|
+
const fromNewNameAlone = gate.expectedToken(); // PRELAUNCH_PASSWORD only
|
|
260
|
+
process.env.STEALTH_PASSWORD = 'the-old-one';
|
|
261
|
+
try {
|
|
262
|
+
assert.equal(gate.expectedToken(), fromNewNameAlone,
|
|
263
|
+
'the primary name is primary — an operator who sets the new one and forgets '
|
|
264
|
+
+ 'to clear the old must get the value they just wrote, not the stale one');
|
|
265
|
+
} finally {
|
|
266
|
+
delete process.env.STEALTH_PASSWORD;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('running on the old name alone warns once, at mount', () => {
|
|
271
|
+
const lines = [];
|
|
272
|
+
const log = (m) => lines.push(String(m));
|
|
273
|
+
delete process.env.PRELAUNCH_PASSWORD;
|
|
274
|
+
process.env.STEALTH_PASSWORD = PASSWORD;
|
|
275
|
+
try {
|
|
276
|
+
assert.equal(gate.warnIfLegacyEnv(log), true, 'the deprecation must be audible');
|
|
277
|
+
assert.match(lines.join('\n'), /PRELAUNCH_PASSWORD/, 'the warning must name the replacement');
|
|
278
|
+
} finally {
|
|
279
|
+
delete process.env.STEALTH_PASSWORD;
|
|
280
|
+
process.env.PRELAUNCH_PASSWORD = PASSWORD;
|
|
281
|
+
}
|
|
282
|
+
// silent once the new name is set — nobody needs a warning they have acted on
|
|
283
|
+
assert.equal(gate.warnIfLegacyEnv(log), false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('both submit paths are accepted, and both are exempt', async () => {
|
|
287
|
+
// A form served before the rename may still be open in somebody's tab. The old
|
|
288
|
+
// path keeps working so their password lands them in, not on a 401.
|
|
289
|
+
assert.equal(gate.SUBMIT_PATH, '/__prelaunch');
|
|
290
|
+
assert.equal(gate.LEGACY_SUBMIT_PATH, '/__stealth');
|
|
291
|
+
for (const p of gate.SUBMIT_PATHS) {
|
|
292
|
+
assert.equal(gate.isExempt(p), true, `${p} must be exempt or the submit is gated behind itself`);
|
|
293
|
+
const res = await request(p, { method: 'POST', body: `password=${encodeURIComponent(PASSWORD)}` });
|
|
294
|
+
assert.equal(res.status, 303, `POST ${p} must accept the password`);
|
|
295
|
+
assert.match(String(res.headers['set-cookie'] || ''), new RegExp(gate.COOKIE),
|
|
296
|
+
`POST ${p} must mint the cookie`);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('the wire format did NOT move — renaming it would log everyone out', () => {
|
|
301
|
+
// Deliberate, and the reason it is pinned: the cookie name and the HMAC label
|
|
302
|
+
// are baked into cookies that are live right now. A later "finish the rename"
|
|
303
|
+
// pass that changes either forces every holder to re-enter the password, to
|
|
304
|
+
// buy nothing — neither string is ever read by a human.
|
|
305
|
+
assert.equal(gate.COOKIE, 'cb_stealth');
|
|
306
|
+
assert.equal(gate.HMAC_LABEL, 'stealth-v1');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ── the vocabulary wall around the rename (check 19d, task 1003152) ─────────
|
|
310
|
+
//
|
|
311
|
+
// ADR 0174's lesson, applied: a naming rule is the easiest kind of check to ship
|
|
312
|
+
// broken — allowlist enough and it passes forever while proving nothing. So the
|
|
313
|
+
// load-bearing tests are the MUTATION tests. A rule that cannot be made to fail
|
|
314
|
+
// is not a rule.
|
|
315
|
+
|
|
316
|
+
const fitness = require('../scripts/gds/fitness.js');
|
|
317
|
+
|
|
318
|
+
test('the pre-launch surface is clean today', () => {
|
|
319
|
+
const r = fitness.checkPrelaunchVocabulary();
|
|
320
|
+
assert.equal(r.ok, true, `check 19d is failing on the real tree:\n${r.violations.join('\n')}`);
|
|
321
|
+
assert.ok(r.note.includes('file(s) scanned'), 'the check must report what it scanned');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('a planted "stealth" on the pre-launch surface REDS the build', () => {
|
|
325
|
+
assert.ok(plantInGate('// the stealth gate mounts here').some((v) => /"stealth"/.test(v)),
|
|
326
|
+
'the retired word must be caught, or the rename un-does itself one comment at a time');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('a documented COMPATIBILITY identifier is not a violation', () => {
|
|
330
|
+
// The four that deliberately stayed. Catching these would force an allowlist,
|
|
331
|
+
// which is what the citation/compat split exists to avoid.
|
|
332
|
+
for (const line of ["const c = 'cb_stealth';", "hmac('stealth-v1');",
|
|
333
|
+
"path === '/__stealth'", 'process.env.STEALTH_PASSWORD']) {
|
|
334
|
+
assert.equal(plantInGate(line).length, 0, `the compat identifier tripped the wall: ${line}`);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('a line CITING the rename or the other meaning may say the word', () => {
|
|
339
|
+
for (const line of ['// renamed from stealth-gate.js (task 1003152)',
|
|
340
|
+
"// platform_identity_projects.visibility = 'stealth' is the OTHER meaning"]) {
|
|
341
|
+
assert.equal(plantInGate(line).length, 0, `a citation tripped the wall: ${line}`);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('the citation does NOT excuse the rest of the file', () => {
|
|
346
|
+
// The header cites task 1003152 on its own line. If the exemption were
|
|
347
|
+
// file-scoped rather than line-scoped, every later line would ride it.
|
|
348
|
+
assert.ok(plantInGate('// bring back the stealth wording').length > 0,
|
|
349
|
+
'an uncited line must still red — a file-wide exemption is an allowlist wearing a regex');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Plant a line into the real gate file, run the real check, restore.
|
|
353
|
+
function plantInGate(line) {
|
|
354
|
+
const fs = require('node:fs');
|
|
355
|
+
const target = new URL('../src/bongos/prelaunch-gate.js', import.meta.url);
|
|
356
|
+
const original = fs.readFileSync(target, 'utf8');
|
|
357
|
+
try {
|
|
358
|
+
fs.writeFileSync(target, `${original}\n${line}\n`);
|
|
359
|
+
return fitness.checkPrelaunchVocabulary().violations;
|
|
360
|
+
} finally {
|
|
361
|
+
fs.writeFileSync(target, original);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// tests/skill_ask_for_help.mjs — the /ask-for-help skill exists and routes the
|
|
2
|
+
// plain-language ask ("ask <builder> to help with task N") to the collab help
|
|
3
|
+
// request rather than to a blocker (task 1003826).
|
|
4
|
+
//
|
|
5
|
+
// The failure it guards: a session hears "ask X to help with N" and files a
|
|
6
|
+
// BLOCKER — the owner's review queue — while the surface the recipient actually
|
|
7
|
+
// reads (/#/collab → GET /help-requests/for-me) stays empty. So the assertions
|
|
8
|
+
// below are about ROUTING and the constraints the table's CHECKs restate, not
|
|
9
|
+
// about prose.
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const SKILL = path.join(ROOT, '.claude', 'skills', 'ask-for-help', 'SKILL.md');
|
|
18
|
+
const NOTES = path.join(ROOT, 'docs', 'file-map.notes.json');
|
|
19
|
+
|
|
20
|
+
// .claude/skills is INSTANCE-materialized; a neutral core checkout carries none
|
|
21
|
+
// (task 1002470) — skip there, with the same guard shape as skill_grade_recover.mjs.
|
|
22
|
+
const skillsDirExists = existsSync(path.join(ROOT, '.claude', 'skills'));
|
|
23
|
+
|
|
24
|
+
test('/ask-for-help files a help request and routes its near-neighbours', (t) => {
|
|
25
|
+
if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
|
|
26
|
+
assert.ok(existsSync(SKILL), '.claude/skills/ask-for-help/SKILL.md must exist (task 1003826)');
|
|
27
|
+
const md = readFileSync(SKILL, 'utf8');
|
|
28
|
+
|
|
29
|
+
// The write path, named literally — this is the whole point of the skill.
|
|
30
|
+
assert.match(md, /POST \/(?:api\/gds\/)?help-requests/, 'names the help-request write route');
|
|
31
|
+
assert.match(md, /--body-file/, 'files via --body-file (api.js drops a positional body)');
|
|
32
|
+
|
|
33
|
+
// Resolve the addressee — never guess a builder id.
|
|
34
|
+
assert.match(md, /GET \/api\/gds\/builders\/directory/, 'resolves a named builder through the directory');
|
|
35
|
+
assert.match(md, /never guess/i, 'forbids guessing a builder id');
|
|
36
|
+
|
|
37
|
+
// The two XOR rules the table's CHECK constraints enforce.
|
|
38
|
+
assert.match(md, /needs_craft/, 'names the craft addressee field');
|
|
39
|
+
assert.match(md, /needs_builder_id/, 'names the person addressee field');
|
|
40
|
+
assert.match(md, /two_addressees/, 'names the both-addressees refusal');
|
|
41
|
+
assert.match(md, /two_contexts/, 'names the both-contexts refusal');
|
|
42
|
+
assert.match(md, /task_id/, 'names the task context field');
|
|
43
|
+
assert.match(md, /goal_id/, 'names the goal context field');
|
|
44
|
+
|
|
45
|
+
// The near-neighbours are routed, not collapsed: a recommendation and a blocker
|
|
46
|
+
// are different acts, and the blocker is complementary rather than an alternative.
|
|
47
|
+
assert.match(md, /POST \/task-recommendations/, 'routes the recommendation surface');
|
|
48
|
+
assert.match(md, /POST \/blockers/, 'routes the blocker surface');
|
|
49
|
+
assert.match(md, /complementary, not alternatives/i, 'says a blocker and a help request can both be right');
|
|
50
|
+
|
|
51
|
+
// ADR 0187 §3 — being asked assigns nobody anything.
|
|
52
|
+
assert.match(md, /never transfer/i, 'states that the collab surfaces never transfer a claim');
|
|
53
|
+
|
|
54
|
+
// The honest limitation: the addressed read is a pull, and help requests have
|
|
55
|
+
// no builder-needs push (task recommendations do).
|
|
56
|
+
assert.match(md, /pull,? not a push/i, 'states the pull-not-push caveat');
|
|
57
|
+
assert.match(md, /help-requests\/for-me/, 'names the addressed read the recipient sees');
|
|
58
|
+
|
|
59
|
+
// Settling is the asker's job — an unsettled pile is noise.
|
|
60
|
+
assert.match(md, /PATCH \/api\/gds\/help-requests\/<id>/, 'names the settle route');
|
|
61
|
+
assert.match(md, /answered/, 'names the answered status');
|
|
62
|
+
assert.match(md, /withdrawn/, 'names the withdrawn status');
|
|
63
|
+
|
|
64
|
+
// No secret ever goes in the ask.
|
|
65
|
+
assert.match(md, /Never put a secret/i, 'forbids putting a secret in what_is_stuck');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('/ask-for-help is described in the generated file map', (t) => {
|
|
69
|
+
if (!skillsDirExists) return t.skip('.claude/skills not materialized in this checkout');
|
|
70
|
+
const notes = JSON.parse(readFileSync(NOTES, 'utf8'));
|
|
71
|
+
assert.ok(notes.skills['ask-for-help'], 'docs/file-map.notes.json must carry a note for ask-for-help');
|
|
72
|
+
});
|