@bongos/core 1.21.74 → 1.21.75
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 +32 -22
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +12 -0
- package/docs/api/openapi.json +32 -1
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/module-listing.js +85 -0
- package/src/bongos/routes/modules.js +15 -0
- package/src/module-api.js +1 -1
- package/tests/module_store_listing.mjs +156 -0
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.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.75",
|
|
6
|
+
"core_contract": "1.21.75",
|
|
7
|
+
"source_commit": "fdb1b2948dda051d6bd0b5b5e79a1382a7e93f63",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-08T17:
|
|
9
|
+
"built_at": "2026-10-08T17:33:55.022Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 589,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2982,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3600,
|
|
20
|
+
"tree_sha256": "9485028ef2ecfaf25ef937e1315cca15d5526ff1d11ed8a280e78a28177fd26e",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -312,12 +312,12 @@
|
|
|
312
312
|
{
|
|
313
313
|
"path": "clients/bongos-client/README.md",
|
|
314
314
|
"mode": "0000644",
|
|
315
|
-
"sha256": "
|
|
315
|
+
"sha256": "cdea685186e07a106fcd43e603d006648d5d681f68895989915f3a38a54f3073"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
319
319
|
"mode": "0000644",
|
|
320
|
-
"sha256": "
|
|
320
|
+
"sha256": "d244bccc1ec1ced97ebdc3219dbe130d9a615d12be060e8479e46368a95862bc"
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -327,17 +327,17 @@
|
|
|
327
327
|
{
|
|
328
328
|
"path": "clients/bongos-client/index.cjs",
|
|
329
329
|
"mode": "0000644",
|
|
330
|
-
"sha256": "
|
|
330
|
+
"sha256": "17bbcc0e24a99a7e8e6cba5f04db7fc210baf2fc119863c51698f69fd04771c4"
|
|
331
331
|
},
|
|
332
332
|
{
|
|
333
333
|
"path": "clients/bongos-client/index.d.ts",
|
|
334
334
|
"mode": "0000644",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "88bcc5e74a08fb574b1f1a81d40355a69ffd8271a75e9fdd25038c60a67b6ff3"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.mjs",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "042c25ee1600192bdaa6c7c817ff94558a884a428a7ab5b06dbf2b665fc89101"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/package.json",
|
|
@@ -2172,7 +2172,7 @@
|
|
|
2172
2172
|
{
|
|
2173
2173
|
"path": "docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md",
|
|
2174
2174
|
"mode": "0000644",
|
|
2175
|
-
"sha256": "
|
|
2175
|
+
"sha256": "db41e62093dd09e5ed3f9bb4628c98ea0255b487bb5b94191bbb4adc0651636b"
|
|
2176
2176
|
},
|
|
2177
2177
|
{
|
|
2178
2178
|
"path": "docs/adr/0344-a-tester-is-a-project-that-opts-in-once.md",
|
|
@@ -2312,12 +2312,12 @@
|
|
|
2312
2312
|
{
|
|
2313
2313
|
"path": "docs/api-reference.md",
|
|
2314
2314
|
"mode": "0000644",
|
|
2315
|
-
"sha256": "
|
|
2315
|
+
"sha256": "9d29a5b16d79cbf4da889fd3d58c7164a8f70f511fd9de75b94f321ce7e01f1c"
|
|
2316
2316
|
},
|
|
2317
2317
|
{
|
|
2318
2318
|
"path": "docs/api/openapi.json",
|
|
2319
2319
|
"mode": "0000644",
|
|
2320
|
-
"sha256": "
|
|
2320
|
+
"sha256": "2f5f417f06c5b9e99e02b830fe9309c55a901cff059be909a18078a4b3ad0dee"
|
|
2321
2321
|
},
|
|
2322
2322
|
{
|
|
2323
2323
|
"path": "docs/architecture.md",
|
|
@@ -2857,12 +2857,12 @@
|
|
|
2857
2857
|
{
|
|
2858
2858
|
"path": "docs/module-api-changelog.md",
|
|
2859
2859
|
"mode": "0000644",
|
|
2860
|
-
"sha256": "
|
|
2860
|
+
"sha256": "244e5150f6cb37861f017cf08c10d4396d641be31e7c27836ff278045b8c7365"
|
|
2861
2861
|
},
|
|
2862
2862
|
{
|
|
2863
2863
|
"path": "docs/modules-contract.md",
|
|
2864
2864
|
"mode": "0000644",
|
|
2865
|
-
"sha256": "
|
|
2865
|
+
"sha256": "55a964846d438df4265777244e046b5b86deaebda3a6081c6f10a4a5a7a0dbf3"
|
|
2866
2866
|
},
|
|
2867
2867
|
{
|
|
2868
2868
|
"path": "docs/onboarding/diagrams/01-task-lifecycle.mmd",
|
|
@@ -10057,12 +10057,12 @@
|
|
|
10057
10057
|
{
|
|
10058
10058
|
"path": "package-lock.json",
|
|
10059
10059
|
"mode": "0000644",
|
|
10060
|
-
"sha256": "
|
|
10060
|
+
"sha256": "b55138ff35c924085a5d9b12fa4651c403dea3a2f1c4d3ee9e2d1a5fcb931314"
|
|
10061
10061
|
},
|
|
10062
10062
|
{
|
|
10063
10063
|
"path": "package.json",
|
|
10064
10064
|
"mode": "0000644",
|
|
10065
|
-
"sha256": "
|
|
10065
|
+
"sha256": "e99024f875cb0765bdf60139021c5bf984d616c954eec42444666a0b79395c94"
|
|
10066
10066
|
},
|
|
10067
10067
|
{
|
|
10068
10068
|
"path": "public-docs/index.html",
|
|
@@ -10082,7 +10082,7 @@
|
|
|
10082
10082
|
{
|
|
10083
10083
|
"path": "release-notes.json",
|
|
10084
10084
|
"mode": "0000644",
|
|
10085
|
-
"sha256": "
|
|
10085
|
+
"sha256": "c545e162df4719ea3f8712009774705b406674c314973d49487b2d31b71185dd"
|
|
10086
10086
|
},
|
|
10087
10087
|
{
|
|
10088
10088
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -12069,6 +12069,11 @@
|
|
|
12069
12069
|
"mode": "0000644",
|
|
12070
12070
|
"sha256": "917f364b804f25437dea7fe507abe50e9b64cac79b6cc29ac786de0e24350d33"
|
|
12071
12071
|
},
|
|
12072
|
+
{
|
|
12073
|
+
"path": "src/bongos/module-listing.js",
|
|
12074
|
+
"mode": "0000644",
|
|
12075
|
+
"sha256": "15047b229ce7c3633d74597fb8f971db00fac2ddae5fb0038666fa0896b25574"
|
|
12076
|
+
},
|
|
12072
12077
|
{
|
|
12073
12078
|
"path": "src/bongos/module-overrides.js",
|
|
12074
12079
|
"mode": "0000644",
|
|
@@ -12237,7 +12242,7 @@
|
|
|
12237
12242
|
{
|
|
12238
12243
|
"path": "src/bongos/routes/modules.js",
|
|
12239
12244
|
"mode": "0000644",
|
|
12240
|
-
"sha256": "
|
|
12245
|
+
"sha256": "6c20741df0b1395355abf58146ad6753197dcfde928e532b43f69ecf1ce7a325"
|
|
12241
12246
|
},
|
|
12242
12247
|
{
|
|
12243
12248
|
"path": "src/bongos/routes/my-sessions.js",
|
|
@@ -12317,7 +12322,7 @@
|
|
|
12317
12322
|
{
|
|
12318
12323
|
"path": "src/module-api.js",
|
|
12319
12324
|
"mode": "0000644",
|
|
12320
|
-
"sha256": "
|
|
12325
|
+
"sha256": "e2dc4fbb86019edff6c023c9b78afdd70fa04010249e28eee821da8383b2252e"
|
|
12321
12326
|
},
|
|
12322
12327
|
{
|
|
12323
12328
|
"path": "src/module-loader/catalog.js",
|
|
@@ -15674,6 +15679,11 @@
|
|
|
15674
15679
|
"mode": "0000644",
|
|
15675
15680
|
"sha256": "43828c1a95652180d558e7676cc2053d0599a61c0edf4cd1ab100820fc7de493"
|
|
15676
15681
|
},
|
|
15682
|
+
{
|
|
15683
|
+
"path": "tests/module_store_listing.mjs",
|
|
15684
|
+
"mode": "0000644",
|
|
15685
|
+
"sha256": "289478dfc8666527e919a28db3127807aefe87c19849001f133780bb8a8ac816"
|
|
15686
|
+
},
|
|
15677
15687
|
{
|
|
15678
15688
|
"path": "tests/module_store_publish.mjs",
|
|
15679
15689
|
"mode": "0000644",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 526 operations across 75 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -1149,6 +1149,8 @@ function createClient(opts = {}) {
|
|
|
1149
1149
|
"store": {
|
|
1150
1150
|
// GET /store/entitlements — rank: any-builder — GET /store/entitlements
|
|
1151
1151
|
getStoreEntitlements: (args) => request("GET", "/store/entitlements", { hasBody: false }, args),
|
|
1152
|
+
// GET /store/modules — rank: any-builder — GET /store/modules
|
|
1153
|
+
getStoreModules: (args) => request("GET", "/store/modules", { hasBody: false }, args),
|
|
1152
1154
|
// POST /store/modules/{key}/acquire — rank: any-builder — POST /store/modules/:key/acquire
|
|
1153
1155
|
postStoreModulesKeyAcquire: (args) => request("POST", "/store/modules/{key}/acquire", { hasBody: true }, args),
|
|
1154
1156
|
// POST /store/modules/{key}/acquired — rank: any-builder — POST /store/modules/:key/acquired
|
|
@@ -1148,6 +1148,8 @@ function createClient(opts = {}) {
|
|
|
1148
1148
|
"store": {
|
|
1149
1149
|
// GET /store/entitlements — rank: any-builder — GET /store/entitlements
|
|
1150
1150
|
getStoreEntitlements: (args) => request("GET", "/store/entitlements", { hasBody: false }, args),
|
|
1151
|
+
// GET /store/modules — rank: any-builder — GET /store/modules
|
|
1152
|
+
getStoreModules: (args) => request("GET", "/store/modules", { hasBody: false }, args),
|
|
1151
1153
|
// POST /store/modules/{key}/acquire — rank: any-builder — POST /store/modules/:key/acquire
|
|
1152
1154
|
postStoreModulesKeyAcquire: (args) => request("POST", "/store/modules/{key}/acquire", { hasBody: true }, args),
|
|
1153
1155
|
// POST /store/modules/{key}/acquired — rank: any-builder — POST /store/modules/:key/acquired
|
|
@@ -1649,6 +1649,8 @@ export interface BongosClient {
|
|
|
1649
1649
|
"store": {
|
|
1650
1650
|
/** GET /store/entitlements — rank: any-builder */
|
|
1651
1651
|
getStoreEntitlements(args?: RequestArgs): Promise<GetStoreEntitlementsResponse>;
|
|
1652
|
+
/** GET /store/modules — rank: any-builder */
|
|
1653
|
+
getStoreModules(args?: RequestArgs): Promise<ApiResponse>;
|
|
1652
1654
|
/** POST /store/modules/{key}/acquire — rank: any-builder */
|
|
1653
1655
|
postStoreModulesKeyAcquire(args?: RequestArgs & { body?: PostStoreModulesKeyAcquireRequest }): Promise<PostStoreModulesKeyAcquireResponse>;
|
|
1654
1656
|
/** POST /store/modules/{key}/acquired — rank: any-builder */
|
|
@@ -1145,6 +1145,8 @@ export function createClient(opts = {}) {
|
|
|
1145
1145
|
"store": {
|
|
1146
1146
|
// GET /store/entitlements — rank: any-builder — GET /store/entitlements
|
|
1147
1147
|
getStoreEntitlements: (args) => request("GET", "/store/entitlements", { hasBody: false }, args),
|
|
1148
|
+
// GET /store/modules — rank: any-builder — GET /store/modules
|
|
1149
|
+
getStoreModules: (args) => request("GET", "/store/modules", { hasBody: false }, args),
|
|
1148
1150
|
// POST /store/modules/{key}/acquire — rank: any-builder — POST /store/modules/:key/acquire
|
|
1149
1151
|
postStoreModulesKeyAcquire: (args) => request("POST", "/store/modules/{key}/acquire", { hasBody: true }, args),
|
|
1150
1152
|
// POST /store/modules/{key}/acquired — rank: any-builder — POST /store/modules/:key/acquired
|
|
@@ -66,6 +66,18 @@ couldn't have yet. The floor and the ranking decide how New modules are placed (
|
|
|
66
66
|
Price is never an input. The Metic+ override (task 1003796) and delist (task 1003797) sit on
|
|
67
67
|
top of the computed score and never replace how it is computed. Every override is recorded.
|
|
68
68
|
|
|
69
|
+
## Implementation note — the floor (task 1003807, 2026-10-03)
|
|
70
|
+
|
|
71
|
+
The owner set the floor on 2026-10-01: **50 out of 100**, fixed in Bongos itself
|
|
72
|
+
(`QUALITY_FLOOR` in `src/bongos/module-listing.js`). It is not a live setting and no new
|
|
73
|
+
permission moves it; changing it is an owner-approved, reviewed task, so no author can
|
|
74
|
+
move it. `GET /store/modules` lists each listed module's newest version by its current
|
|
75
|
+
score: the Security gate must have passed (failed or not yet run means not listed); a
|
|
76
|
+
version still labelled New (D5) is listed under "new", never ranked, and the floor does
|
|
77
|
+
not apply to it until it has a real score; any other version is listed only at or above
|
|
78
|
+
the floor. Below the bar is not listed at all. Authors read the rule in
|
|
79
|
+
`docs/modules-contract.md` ("Getting listed").
|
|
80
|
+
|
|
69
81
|
## Consequences
|
|
70
82
|
|
|
71
83
|
- **Day one, only Tests counts toward the overall score**, behind the Security gate. The number
|
package/docs/api/openapi.json
CHANGED
|
@@ -21492,6 +21492,37 @@
|
|
|
21492
21492
|
]
|
|
21493
21493
|
}
|
|
21494
21494
|
},
|
|
21495
|
+
"/store/modules": {
|
|
21496
|
+
"get": {
|
|
21497
|
+
"operationId": "get_store_modules",
|
|
21498
|
+
"tags": [
|
|
21499
|
+
"store"
|
|
21500
|
+
],
|
|
21501
|
+
"summary": "GET /store/modules",
|
|
21502
|
+
"description": "The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each listed module's newest version, if it passed the Security gate and scores at or above the floor, or under \"new\" if it has no real-world data yet. Below the bar is not listed at all. The floor is fixed in module-listing.js and returned here. rank: any-builder — a read of public store metadata, like /store/modules/latest: no entitlement taken, nothing changed.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
21503
|
+
"x-rank": "any-builder",
|
|
21504
|
+
"x-source": "src/bongos/routes/modules.js",
|
|
21505
|
+
"responses": {
|
|
21506
|
+
"200": {
|
|
21507
|
+
"description": "Success."
|
|
21508
|
+
},
|
|
21509
|
+
"400": {
|
|
21510
|
+
"$ref": "#/components/responses/BadRequest"
|
|
21511
|
+
},
|
|
21512
|
+
"401": {
|
|
21513
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
21514
|
+
},
|
|
21515
|
+
"403": {
|
|
21516
|
+
"$ref": "#/components/responses/Forbidden"
|
|
21517
|
+
}
|
|
21518
|
+
},
|
|
21519
|
+
"security": [
|
|
21520
|
+
{
|
|
21521
|
+
"builderSession": []
|
|
21522
|
+
}
|
|
21523
|
+
]
|
|
21524
|
+
}
|
|
21525
|
+
},
|
|
21495
21526
|
"/store/modules/latest": {
|
|
21496
21527
|
"get": {
|
|
21497
21528
|
"operationId": "get_store_modules_latest",
|
|
@@ -33747,7 +33778,7 @@
|
|
|
33747
33778
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
33748
33779
|
}
|
|
33749
33780
|
},
|
|
33750
|
-
"x-endpoint-count":
|
|
33781
|
+
"x-endpoint-count": 526,
|
|
33751
33782
|
"x-schema-count": 567,
|
|
33752
33783
|
"x-undocumented-bodies": 12,
|
|
33753
33784
|
"x-response-schemas": 376,
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 526 endpoints across 93 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -814,11 +814,12 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
814
814
|
| GET | `/api/bongos/sso/pubkey` | `public` | — | GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). |
|
|
815
815
|
| POST | `/api/bongos/sso/token` | `public` | `client_id`, `client_secret`, `code`, `redirect_uri` | POST /sso/token — server-to-server code redemption. |
|
|
816
816
|
|
|
817
|
-
## `store` (
|
|
817
|
+
## `store` (11)
|
|
818
818
|
|
|
819
819
|
| Method | Path | Rank | Body | Description |
|
|
820
820
|
|---|---|---|---|---|
|
|
821
821
|
| GET | `/api/bongos/store/entitlements` | `any-builder` | — | GET /api/bongos/store/entitlements and /store/modules/:key/entitlement — the read side of the entitlement record (task 1003813, ADR 0338 … |
|
|
822
|
+
| GET | `/api/bongos/store/modules` | `any-builder` | — | The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each listed module's newest version, if it passed the Security … |
|
|
822
823
|
| POST | `/api/bongos/store/modules/:key/acquire` | `any-builder` | `version` | ---- install: acquire → download → record (task 1003785, ADR 0338 D1) ---- The server end of `bongos module install`. |
|
|
823
824
|
| POST | `/api/bongos/store/modules/:key/acquired` | `any-builder` | `version` | |
|
|
824
825
|
| POST | `/api/bongos/store/modules/:key/delist` | `metic+archon` | `reason` | POST /api/bongos/store/modules/:key/delist — a Metic+ person takes a module out of the store (task 1003797, ADR 0338 D2). |
|
|
@@ -2980,5 +2980,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2980
2980
|
landed since 1.21.72 with no explicit bump. run 37803458577. (task 1002620)
|
|
2981
2981
|
1.21.74 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2982
2982
|
landed since 1.21.73 with no explicit bump. run 37815719494. (task 1002620)
|
|
2983
|
+
1.21.75 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2984
|
+
landed since 1.21.74 with no explicit bump. run 37817454172. (task 1002620)
|
|
2983
2985
|
---------------------------------------------------------------------------
|
|
2984
2986
|
```
|
package/docs/modules-contract.md
CHANGED
|
@@ -362,6 +362,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
|
|
|
362
362
|
- The publish denylist applies (ADR 0098), plus a refusal of credential-named files (`.env*`, `*.pem`, `*.key`, `id_rsa`, …) anywhere in the module. The store re-checks every hash and the denylist itself.
|
|
363
363
|
- Gate: Metic+ (`module.submit`) for now. This is not `bongos module submit`, which proposes a module *into* core.
|
|
364
364
|
- **A how-to is required** ([ADR 0347](adr/0347-every-store-module-ships-a-how-to.md)). `modules/<key>/HOWTO.md` is what a buyer reads — the store shows it as the module's page — so it is written for the person who installs the module, not for an AI working inside it (that is `CLAUDE.md`). It is plain Markdown: any AI or person can write it. Publish refuses the version unless it has these five level-2 headings, in any order and any case, each with some text under it: `## What it does`, `## Install and enable`, `## How to use it` (with one worked example), `## Configuration` ("None." is fine) and `## Limits and known issues` ("None known." is fine). HTML comments don't count as text, so the scaffold's prompts must be replaced. The refusal names each failing section. The check is one function, `checkHowto` in `scripts/gds/module-artifact.js`; the CLI runs it before uploading and the store runs it again on upload, so a hand-built upload can't skip it. `bongos module check` reports it as advice only — an upstream submit is not gated by it. Install and update don't re-check it, so a version published before the gate still installs. The file travels in the tarball, so its hash pins it to the version. An optional `howto.artifactUrl` in `module.json` (an `https://` link on `claude.ai`) is shown as an extra, never graded.
|
|
365
|
+
- **Getting listed** (task 1003807, [ADR 0343](adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md)). Publishing a version does not by itself put it in the store's catalog, `GET /store/modules`. The catalog shows each module's newest version, judged by its current score. It must pass the Security gate first; a version that failed it, or has not been checked yet, is not listed. A new version with no install or reliability data yet is listed with a **New** label and is not ranked; the floor does not apply to it until it has a real score. Every other version is listed only if its overall score is **at least 50 out of 100**. Below that it is not listed at all. The floor is fixed in Bongos itself (`QUALITY_FLOOR` in `src/bongos/module-listing.js`) and returned in the catalog response; no author or instance setting moves it, and changing it is an owner-approved, reviewed task.
|
|
365
366
|
|
|
366
367
|
### 7. Install from the store
|
|
367
368
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.75",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.75",
|
|
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.21.
|
|
3
|
+
"version": "1.21.75",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9254,5 +9254,11 @@
|
|
|
9254
9254
|
"id": "1004628",
|
|
9255
9255
|
"text": "Projects now have a Ship mode setting. Auto, the default, means the AI ships finished work on its own. Manual means it asks first. Owners can switch it on the project settings page."
|
|
9256
9256
|
}
|
|
9257
|
+
],
|
|
9258
|
+
"1.21.75": [
|
|
9259
|
+
{
|
|
9260
|
+
"id": "1003807",
|
|
9261
|
+
"text": "The module store now only shows modules that are safe and good enough: each must pass the security check, and once it has real-world data it needs a score of at least 50 out of 100. Brand-new modules show with a 'New' label un"
|
|
9262
|
+
}
|
|
9257
9263
|
]
|
|
9258
9264
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/bongos/module-listing.js — which store modules the catalog lists, under the
|
|
2
|
+
// quality floor (task 1003807, criterion wa5-curated-listing, ADR 0343 D2/D5).
|
|
3
|
+
//
|
|
4
|
+
// One module appears once, as its NEWEST published version (semver order), judged by
|
|
5
|
+
// that version's CURRENT score: the newest module_assessment_scores row for it
|
|
6
|
+
// (computed_at DESC, id DESC), whether computed or a Metic+ override (core_265).
|
|
7
|
+
// placeVersion is the whole rule and is pure, so tests drive it directly:
|
|
8
|
+
//
|
|
9
|
+
// excluded the Security gate has not passed (false, or not run yet), or the
|
|
10
|
+
// version has no score row at all. The gate is a hard exclusion (D2).
|
|
11
|
+
// new passed the gate but has no real-world data yet (is_new, D5). Listed
|
|
12
|
+
// with a "New" label and never ranked: the floor does not apply until
|
|
13
|
+
// it has a real score (owner decision, 2026-10-01).
|
|
14
|
+
// listed passed the gate and its overall score is at least the floor.
|
|
15
|
+
// excluded otherwise: scored below the floor, or no overall to compare.
|
|
16
|
+
//
|
|
17
|
+
// A delisted module (ADR 0338 D2) is never in the catalog, whatever its score.
|
|
18
|
+
|
|
19
|
+
const { parseVersion, cmp } = require('../module-loader/semver');
|
|
20
|
+
|
|
21
|
+
// The quality floor, 0-100. It lives here and nowhere else: it is part of Bongos
|
|
22
|
+
// itself, not a live setting, so no author, builder or instance can move it.
|
|
23
|
+
// Changing it is an owner-approved, reviewed task (owner decision, 2026-10-01).
|
|
24
|
+
// Authors read the rule in docs/modules-contract.md ("Getting listed").
|
|
25
|
+
const QUALITY_FLOOR = 50;
|
|
26
|
+
|
|
27
|
+
// Where one version stands. `score` is its current score row, or null.
|
|
28
|
+
// Returns { placement: 'listed' | 'new' | 'excluded', reason }.
|
|
29
|
+
function placeVersion(score, floor = QUALITY_FLOOR) {
|
|
30
|
+
if (!score) return { placement: 'excluded', reason: 'not_assessed' };
|
|
31
|
+
if (score.security_passed !== true) return { placement: 'excluded', reason: 'security_not_passed' };
|
|
32
|
+
if (score.is_new) return { placement: 'new', reason: 'new' };
|
|
33
|
+
if (score.overall == null) return { placement: 'excluded', reason: 'no_score' };
|
|
34
|
+
if (score.overall < floor) return { placement: 'excluded', reason: 'below_floor' };
|
|
35
|
+
return { placement: 'listed', reason: 'at_or_above_floor' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The newest version row per module key, in semver order (a version that does not
|
|
39
|
+
// parse is skipped, as latestVersions does in module-store.js).
|
|
40
|
+
function newestPerModule(rows) {
|
|
41
|
+
const out = new Map();
|
|
42
|
+
for (const r of rows) {
|
|
43
|
+
const next = parseVersion(r.version);
|
|
44
|
+
if (!next) continue;
|
|
45
|
+
const have = out.get(r.module_key);
|
|
46
|
+
if (have && cmp(parseVersion(have.version), next) >= 0) continue;
|
|
47
|
+
out.set(r.module_key, r);
|
|
48
|
+
}
|
|
49
|
+
return [...out.values()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The catalog: { floor, listed: [...], new: [...] }, each entry
|
|
53
|
+
// { key, title, version, published_at, overall, is_new }. Excluded versions are left
|
|
54
|
+
// out entirely: a module below the bar is not listed at all. Both lists are in key
|
|
55
|
+
// order; ranking is a separate task (1003808) and does not happen here.
|
|
56
|
+
async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
|
|
57
|
+
const pool = db || require('./pool').pool;
|
|
58
|
+
const { rows: versions } = await pool.query(
|
|
59
|
+
`SELECT m.module_key, m.title, v.id AS version_id, v.version, v.published_at
|
|
60
|
+
FROM store_modules m JOIN store_module_versions v ON v.module_key = m.module_key
|
|
61
|
+
WHERE m.status = 'listed'`);
|
|
62
|
+
const newest = newestPerModule(versions);
|
|
63
|
+
const scores = new Map();
|
|
64
|
+
if (newest.length) {
|
|
65
|
+
const { rows } = await pool.query(
|
|
66
|
+
`SELECT DISTINCT ON (version_id) version_id, overall, security_passed, is_new
|
|
67
|
+
FROM module_assessment_scores
|
|
68
|
+
WHERE version_id = ANY($1)
|
|
69
|
+
ORDER BY version_id, computed_at DESC, id DESC`, [newest.map((r) => r.version_id)]);
|
|
70
|
+
for (const s of rows) scores.set(String(s.version_id), s);
|
|
71
|
+
}
|
|
72
|
+
const out = { floor, listed: [], new: [] };
|
|
73
|
+
for (const r of newest.sort((a, b) => a.module_key.localeCompare(b.module_key))) {
|
|
74
|
+
const score = scores.get(String(r.version_id)) || null;
|
|
75
|
+
const { placement } = placeVersion(score, floor);
|
|
76
|
+
if (placement === 'excluded') continue;
|
|
77
|
+
out[placement].push({
|
|
78
|
+
key: r.module_key, title: r.title, version: r.version, published_at: r.published_at,
|
|
79
|
+
overall: placement === 'new' ? null : score.overall, is_new: placement === 'new',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { QUALITY_FLOOR, placeVersion, listCatalog };
|
|
@@ -97,6 +97,7 @@ const log = require('../logger').logger.child({ src: 'modules' });
|
|
|
97
97
|
// property access on a CommonJS namespace, so reaching these through one made them read
|
|
98
98
|
// as unused exports and tripped the dead-code ratchet that held this branch out of main.
|
|
99
99
|
const { listOverrides, setOverride } = require('../module-overrides');
|
|
100
|
+
const { listCatalog } = require('../module-listing');
|
|
100
101
|
|
|
101
102
|
// A packaged submission embeds every file's content (base64) — cap the total
|
|
102
103
|
// so a runaway/misbuilt module can't bloat the review-queue table. Generous
|
|
@@ -692,6 +693,20 @@ module.exports = function buildModulesRouter() {
|
|
|
692
693
|
res.json({ latest });
|
|
693
694
|
}));
|
|
694
695
|
|
|
696
|
+
// The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each
|
|
697
|
+
// listed module's newest version, if it passed the Security gate and scores at or
|
|
698
|
+
// above the floor, or under "new" if it has no real-world data yet. Below the bar
|
|
699
|
+
// is not listed at all. The floor is fixed in module-listing.js and returned here.
|
|
700
|
+
//
|
|
701
|
+
// rank: any-builder — a read of public store metadata, like /store/modules/latest:
|
|
702
|
+
// no entitlement taken, nothing changed.
|
|
703
|
+
router.get('/store/modules', auth.requireBuilder,
|
|
704
|
+
asyncHandler('GET /store/modules', async (_req, res) => {
|
|
705
|
+
const listing = await listCatalog();
|
|
706
|
+
res.set('Cache-Control', 'no-store');
|
|
707
|
+
res.json(listing);
|
|
708
|
+
}));
|
|
709
|
+
|
|
695
710
|
router.post('/store/modules/:key/acquired', auth.requireBuilder,
|
|
696
711
|
asyncHandler('POST /store/modules/:key/acquired', async (req, res) => {
|
|
697
712
|
const { key } = req.params;
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.75'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// tests/module_store_listing.mjs — the store catalog under the quality floor (task
|
|
2
|
+
// 1003807, criterion wa5-curated-listing, ADR 0343 D2/D5). Two halves, no port and
|
|
3
|
+
// no real DB:
|
|
4
|
+
// - the rule: placeVersion, pure — the Security gate excludes, "New" is listed
|
|
5
|
+
// unranked, a real score must reach the floor;
|
|
6
|
+
// - the route: the real GET /store/modules handler off the real router, with
|
|
7
|
+
// pool.js replaced in the require cache (the module_upgrade_updates.mjs harness).
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/module_store_listing.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
// ---- the fake registry the route reads ----
|
|
18
|
+
const state = { modules: {}, versions: [], scores: [] };
|
|
19
|
+
const pool = {
|
|
20
|
+
async query(sql, params = []) {
|
|
21
|
+
if (/FROM store_modules m JOIN store_module_versions v/.test(sql)) {
|
|
22
|
+
return {
|
|
23
|
+
rows: state.versions
|
|
24
|
+
.filter((v) => state.modules[v.module_key] && state.modules[v.module_key].status === 'listed')
|
|
25
|
+
.map((v) => ({ ...v, version_id: v.id, title: state.modules[v.module_key].title })),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (/FROM module_assessment_scores/.test(sql)) {
|
|
29
|
+
const ids = params[0].map(String);
|
|
30
|
+
const newest = new Map();
|
|
31
|
+
for (const s of state.scores) {
|
|
32
|
+
if (!ids.includes(String(s.version_id))) continue;
|
|
33
|
+
const have = newest.get(s.version_id);
|
|
34
|
+
if (!have || s.computed_at > have.computed_at || (s.computed_at === have.computed_at && s.id > have.id)) newest.set(s.version_id, s);
|
|
35
|
+
}
|
|
36
|
+
return { rows: [...newest.values()] };
|
|
37
|
+
}
|
|
38
|
+
return { rows: [] };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
42
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
|
|
43
|
+
|
|
44
|
+
const buildModulesRouter = require('../src/bongos/routes/modules.js');
|
|
45
|
+
const { QUALITY_FLOOR, placeVersion } = require('../src/bongos/module-listing.js');
|
|
46
|
+
|
|
47
|
+
function handlerFor(router, method, p) {
|
|
48
|
+
const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
|
|
49
|
+
assert.ok(layer, `no ${method.toUpperCase()} ${p} on the router`);
|
|
50
|
+
const stack = layer.route.stack;
|
|
51
|
+
return { handle: stack[stack.length - 1].handle, names: stack.map((s) => s.name) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function invoke(handle, req) {
|
|
55
|
+
const res = {
|
|
56
|
+
statusCode: 200, body: undefined, headers: {},
|
|
57
|
+
json(b) { this.body = b; return this; },
|
|
58
|
+
status(c) { this.statusCode = c; return this; },
|
|
59
|
+
set(k, v) { this.headers[k] = v; return this; },
|
|
60
|
+
fail(code, opts) { this.statusCode = opts.status; this.body = { error: code, ...opts }; return this; },
|
|
61
|
+
};
|
|
62
|
+
let nextErr = null;
|
|
63
|
+
await handle(req, res, (err) => { nextErr = err; });
|
|
64
|
+
if (nextErr) throw nextErr;
|
|
65
|
+
return res;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const score = (extra = {}) => ({ overall: 70, security_passed: true, is_new: false, ...extra });
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// the rule (pure)
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
test('the floor is 50 out of 100 (owner decision, 2026-10-01)', () => {
|
|
75
|
+
assert.equal(QUALITY_FLOOR, 50);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('placeVersion: the Security gate must have PASSED — false or not run yet excludes', () => {
|
|
79
|
+
assert.equal(placeVersion(score({ security_passed: false })).placement, 'excluded');
|
|
80
|
+
assert.equal(placeVersion(score({ security_passed: null })).placement, 'excluded');
|
|
81
|
+
assert.equal(placeVersion(score({ security_passed: false, is_new: true })).placement, 'excluded');
|
|
82
|
+
assert.equal(placeVersion(score({ security_passed: false })).reason, 'security_not_passed');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('placeVersion: no score row at all is excluded, not failed silently', () => {
|
|
86
|
+
assert.deepEqual(placeVersion(null), { placement: 'excluded', reason: 'not_assessed' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('placeVersion: a New version that passed the gate is listed as new, whatever its number', () => {
|
|
90
|
+
assert.equal(placeVersion(score({ is_new: true, overall: 10 })).placement, 'new');
|
|
91
|
+
assert.equal(placeVersion(score({ is_new: true, overall: null })).placement, 'new');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('placeVersion: a scored version must reach the floor — at the floor is in, one below is out', () => {
|
|
95
|
+
assert.equal(placeVersion(score({ overall: 50 })).placement, 'listed');
|
|
96
|
+
assert.equal(placeVersion(score({ overall: 100 })).placement, 'listed');
|
|
97
|
+
assert.deepEqual(placeVersion(score({ overall: 49 })), { placement: 'excluded', reason: 'below_floor' });
|
|
98
|
+
assert.equal(placeVersion(score({ overall: null })).placement, 'excluded');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// the route
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
test('GET /store/modules: any signed-in builder, no extra permission', () => {
|
|
106
|
+
const { names } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
107
|
+
assert.equal(names.length, 2, `expected requireBuilder + handler, got ${names.join(', ')}`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('GET /store/modules: lists by the newest version\'s current score, drops what falls short', async () => {
|
|
111
|
+
state.modules = {
|
|
112
|
+
good: { status: 'listed', title: 'Good' },
|
|
113
|
+
fresh: { status: 'listed', title: 'Fresh' },
|
|
114
|
+
low: { status: 'listed', title: 'Low' },
|
|
115
|
+
unsafe: { status: 'listed', title: 'Unsafe' },
|
|
116
|
+
unscored: { status: 'listed', title: 'Unscored' },
|
|
117
|
+
gone: { status: 'delisted', title: 'Gone' },
|
|
118
|
+
fixed: { status: 'listed', title: 'Fixed' },
|
|
119
|
+
};
|
|
120
|
+
state.versions = [
|
|
121
|
+
{ id: 1, module_key: 'good', version: '1.9.0', published_at: 't1' },
|
|
122
|
+
{ id: 2, module_key: 'good', version: '1.10.0', published_at: 't2' },
|
|
123
|
+
{ id: 3, module_key: 'fresh', version: '0.1.0', published_at: 't3' },
|
|
124
|
+
{ id: 4, module_key: 'low', version: '1.0.0', published_at: 't4' },
|
|
125
|
+
{ id: 5, module_key: 'unsafe', version: '1.0.0', published_at: 't5' },
|
|
126
|
+
{ id: 6, module_key: 'unscored', version: '1.0.0', published_at: 't6' },
|
|
127
|
+
{ id: 7, module_key: 'gone', version: '1.0.0', published_at: 't7' },
|
|
128
|
+
{ id: 8, module_key: 'fixed', version: '2.0.0', published_at: 't8' },
|
|
129
|
+
];
|
|
130
|
+
state.scores = [
|
|
131
|
+
{ id: 1, version_id: 1, computed_at: 1, ...score({ overall: 20 }) }, // an OLDER version scored low
|
|
132
|
+
{ id: 2, version_id: 2, computed_at: 1, ...score({ overall: 80 }) },
|
|
133
|
+
{ id: 3, version_id: 3, computed_at: 1, ...score({ is_new: true, overall: 30 }) },
|
|
134
|
+
{ id: 4, version_id: 4, computed_at: 1, ...score({ overall: 49 }) },
|
|
135
|
+
{ id: 5, version_id: 5, computed_at: 1, ...score({ overall: 95, security_passed: false }) },
|
|
136
|
+
{ id: 6, version_id: 7, computed_at: 1, ...score({ overall: 99 }) },
|
|
137
|
+
{ id: 7, version_id: 8, computed_at: 1, ...score({ overall: 40 }) }, // superseded by the newer row
|
|
138
|
+
{ id: 8, version_id: 8, computed_at: 2, ...score({ overall: 60 }) },
|
|
139
|
+
];
|
|
140
|
+
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
141
|
+
const res = await invoke(handle, {});
|
|
142
|
+
assert.equal(res.statusCode, 200);
|
|
143
|
+
assert.equal(res.headers['Cache-Control'], 'no-store');
|
|
144
|
+
assert.equal(res.body.floor, 50);
|
|
145
|
+
assert.deepEqual(res.body.listed.map((m) => [m.key, m.version, m.overall]), [['fixed', '2.0.0', 60], ['good', '1.10.0', 80]]);
|
|
146
|
+
assert.deepEqual(res.body.new.map((m) => [m.key, m.is_new, m.overall]), [['fresh', true, null]]);
|
|
147
|
+
const all = [...res.body.listed, ...res.body.new].map((m) => m.key);
|
|
148
|
+
for (const k of ['low', 'unsafe', 'unscored', 'gone']) assert.ok(!all.includes(k), `${k} must not be listed`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('GET /store/modules: an empty store answers empty lists and the floor', async () => {
|
|
152
|
+
state.modules = {}; state.versions = []; state.scores = [];
|
|
153
|
+
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
154
|
+
const res = await invoke(handle, {});
|
|
155
|
+
assert.deepEqual(res.body, { floor: 50, listed: [], new: [] });
|
|
156
|
+
});
|