@bongos/core 1.19.1077 → 1.19.1079

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.
@@ -17712,6 +17712,215 @@
17712
17712
  "security": []
17713
17713
  }
17714
17714
  },
17715
+ "/store/entitlements": {
17716
+ "get": {
17717
+ "operationId": "get_store_entitlements",
17718
+ "tags": [
17719
+ "store"
17720
+ ],
17721
+ "summary": "GET /store/entitlements",
17722
+ "description": "GET /api/bongos/store/entitlements and /store/modules/:key/entitlement — the read side of the entitlement record (task 1003813, ADR 0338 D1). Own-scoped like /me/sessions: the holder is ALWAYS the caller (holder_kind 'builder', holder_ref = req.builder.id), never named in the query or path, so requireBuilder alone is the gate and one builder cannot read another's holdings. Granting has no route yet: install (task 1003785) grants a free module, area 8's buy action grants a paid one — both through src/bongos/module-entitlements.js.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
17723
+ "x-rank": "any-builder",
17724
+ "x-source": "src/bongos/routes/modules.js",
17725
+ "responses": {
17726
+ "200": {
17727
+ "description": "Success.",
17728
+ "content": {
17729
+ "application/json": {
17730
+ "schema": {
17731
+ "$ref": "#/components/schemas/GetStoreEntitlementsResponse"
17732
+ }
17733
+ }
17734
+ }
17735
+ },
17736
+ "400": {
17737
+ "$ref": "#/components/responses/BadRequest"
17738
+ },
17739
+ "401": {
17740
+ "$ref": "#/components/responses/Unauthorized"
17741
+ },
17742
+ "403": {
17743
+ "$ref": "#/components/responses/Forbidden"
17744
+ }
17745
+ },
17746
+ "security": [
17747
+ {
17748
+ "builderSession": []
17749
+ }
17750
+ ]
17751
+ }
17752
+ },
17753
+ "/store/modules/{key}/acquire": {
17754
+ "post": {
17755
+ "operationId": "post_store_modules_key_acquire",
17756
+ "tags": [
17757
+ "store"
17758
+ ],
17759
+ "summary": "POST /store/modules/:key/acquire",
17760
+ "description": "---- install: acquire → download → record (task 1003785, ADR 0338 D1) ---- The server end of `bongos module install`. Three steps, so the store never records a version as acquired that the caller did not verify and place: 1. POST acquire — resolve the version, and take the entitlement: a free module (no price, or a zero price) is granted here with source 'free'; a priced one needs the buy action, which is area 8's (tasks 1003788/1003789) and answers 402 until then. 2. GET tarball — served only to a LIVE entitlement holder, never for a delisted module (ADR 0338 D2: no further versions served). 3. POST acquired — the CLI has verified the bytes with verifyModuleArtifact and placed them; recordAcquired stamps the exact version. Own-scoped like the entitlement reads: the holder is always the caller (holder_kind 'builder'), never named in the request, so requireBuilder is the gate — any signed-in builder may take a free module for themselves, and nothing here changes what runs on this instance (install never enables; the CLI places files on the CALLER's checkout). Channels: every version is on the general channel until the channel table (task 1003815) and \"install honours the channel\" (task 1003805) land; those add the channel check to step 1 and step 2, beside the entitlement check.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
17761
+ "x-rank": "any-builder",
17762
+ "x-source": "src/bongos/routes/modules.js",
17763
+ "parameters": [
17764
+ {
17765
+ "name": "key",
17766
+ "in": "path",
17767
+ "required": true,
17768
+ "schema": {
17769
+ "type": "string"
17770
+ },
17771
+ "description": "Path parameter `key`."
17772
+ }
17773
+ ],
17774
+ "requestBody": {
17775
+ "required": false,
17776
+ "content": {
17777
+ "application/json": {
17778
+ "schema": {
17779
+ "$ref": "#/components/schemas/PostStoreModulesKeyAcquireRequest"
17780
+ }
17781
+ }
17782
+ },
17783
+ "x-validated": true
17784
+ },
17785
+ "responses": {
17786
+ "200": {
17787
+ "description": "Success.",
17788
+ "content": {
17789
+ "application/json": {
17790
+ "schema": {
17791
+ "$ref": "#/components/schemas/PostStoreModulesKeyAcquireResponse"
17792
+ }
17793
+ }
17794
+ }
17795
+ },
17796
+ "400": {
17797
+ "$ref": "#/components/responses/ValidationFailed"
17798
+ },
17799
+ "401": {
17800
+ "$ref": "#/components/responses/Unauthorized"
17801
+ },
17802
+ "403": {
17803
+ "$ref": "#/components/responses/Forbidden"
17804
+ },
17805
+ "404": {
17806
+ "$ref": "#/components/responses/NotFound"
17807
+ }
17808
+ },
17809
+ "security": [
17810
+ {
17811
+ "builderSession": []
17812
+ }
17813
+ ]
17814
+ }
17815
+ },
17816
+ "/store/modules/{key}/acquired": {
17817
+ "post": {
17818
+ "operationId": "post_store_modules_key_acquired",
17819
+ "tags": [
17820
+ "store"
17821
+ ],
17822
+ "summary": "POST /store/modules/:key/acquired",
17823
+ "description": "**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
17824
+ "x-rank": "any-builder",
17825
+ "x-source": "src/bongos/routes/modules.js",
17826
+ "parameters": [
17827
+ {
17828
+ "name": "key",
17829
+ "in": "path",
17830
+ "required": true,
17831
+ "schema": {
17832
+ "type": "string"
17833
+ },
17834
+ "description": "Path parameter `key`."
17835
+ }
17836
+ ],
17837
+ "requestBody": {
17838
+ "required": true,
17839
+ "content": {
17840
+ "application/json": {
17841
+ "schema": {
17842
+ "$ref": "#/components/schemas/PostStoreModulesKeyAcquiredRequest"
17843
+ }
17844
+ }
17845
+ },
17846
+ "x-validated": true
17847
+ },
17848
+ "responses": {
17849
+ "200": {
17850
+ "description": "Success.",
17851
+ "content": {
17852
+ "application/json": {
17853
+ "schema": {
17854
+ "$ref": "#/components/schemas/PostStoreModulesKeyAcquiredResponse"
17855
+ }
17856
+ }
17857
+ }
17858
+ },
17859
+ "400": {
17860
+ "$ref": "#/components/responses/ValidationFailed"
17861
+ },
17862
+ "401": {
17863
+ "$ref": "#/components/responses/Unauthorized"
17864
+ },
17865
+ "403": {
17866
+ "$ref": "#/components/responses/Forbidden"
17867
+ },
17868
+ "404": {
17869
+ "$ref": "#/components/responses/NotFound"
17870
+ }
17871
+ },
17872
+ "security": [
17873
+ {
17874
+ "builderSession": []
17875
+ }
17876
+ ]
17877
+ }
17878
+ },
17879
+ "/store/modules/{key}/entitlement": {
17880
+ "get": {
17881
+ "operationId": "get_store_modules_key_entitlement",
17882
+ "tags": [
17883
+ "store"
17884
+ ],
17885
+ "summary": "GET /store/modules/:key/entitlement",
17886
+ "description": "**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
17887
+ "x-rank": "any-builder",
17888
+ "x-source": "src/bongos/routes/modules.js",
17889
+ "parameters": [
17890
+ {
17891
+ "name": "key",
17892
+ "in": "path",
17893
+ "required": true,
17894
+ "schema": {
17895
+ "type": "string"
17896
+ },
17897
+ "description": "Path parameter `key`."
17898
+ }
17899
+ ],
17900
+ "responses": {
17901
+ "200": {
17902
+ "description": "Success."
17903
+ },
17904
+ "400": {
17905
+ "$ref": "#/components/responses/BadRequest"
17906
+ },
17907
+ "401": {
17908
+ "$ref": "#/components/responses/Unauthorized"
17909
+ },
17910
+ "403": {
17911
+ "$ref": "#/components/responses/Forbidden"
17912
+ },
17913
+ "404": {
17914
+ "$ref": "#/components/responses/NotFound"
17915
+ }
17916
+ },
17917
+ "security": [
17918
+ {
17919
+ "builderSession": []
17920
+ }
17921
+ ]
17922
+ }
17923
+ },
17715
17924
  "/store/modules/{key}/versions": {
17716
17925
  "post": {
17717
17926
  "operationId": "post_store_modules_key_versions",
@@ -17780,6 +17989,60 @@
17780
17989
  ]
17781
17990
  }
17782
17991
  },
17992
+ "/store/modules/{key}/versions/{version}/tarball": {
17993
+ "get": {
17994
+ "operationId": "get_store_modules_key_versions_version_tarball",
17995
+ "tags": [
17996
+ "store"
17997
+ ],
17998
+ "summary": "GET /store/modules/:key/versions/:version/tarball",
17999
+ "description": "**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
18000
+ "x-rank": "any-builder",
18001
+ "x-source": "src/bongos/routes/modules.js",
18002
+ "parameters": [
18003
+ {
18004
+ "name": "key",
18005
+ "in": "path",
18006
+ "required": true,
18007
+ "schema": {
18008
+ "type": "string"
18009
+ },
18010
+ "description": "Path parameter `key`."
18011
+ },
18012
+ {
18013
+ "name": "version",
18014
+ "in": "path",
18015
+ "required": true,
18016
+ "schema": {
18017
+ "type": "string"
18018
+ },
18019
+ "description": "Path parameter `version`."
18020
+ }
18021
+ ],
18022
+ "responses": {
18023
+ "200": {
18024
+ "description": "Success."
18025
+ },
18026
+ "400": {
18027
+ "$ref": "#/components/responses/BadRequest"
18028
+ },
18029
+ "401": {
18030
+ "$ref": "#/components/responses/Unauthorized"
18031
+ },
18032
+ "403": {
18033
+ "$ref": "#/components/responses/Forbidden"
18034
+ },
18035
+ "404": {
18036
+ "$ref": "#/components/responses/NotFound"
18037
+ }
18038
+ },
18039
+ "security": [
18040
+ {
18041
+ "builderSession": []
18042
+ }
18043
+ ]
18044
+ }
18045
+ },
17783
18046
  "/task-recommendations": {
17784
18047
  "post": {
17785
18048
  "operationId": "post_task_recommendations",
@@ -22341,6 +22604,15 @@
22341
22604
  "public_key_b64"
22342
22605
  ]
22343
22606
  },
22607
+ "GetStoreEntitlementsResponse": {
22608
+ "type": "object",
22609
+ "properties": {
22610
+ "entitlements": {}
22611
+ },
22612
+ "required": [
22613
+ "entitlements"
22614
+ ]
22615
+ },
22344
22616
  "GetTaskRecommendationsForMeResponse": {
22345
22617
  "type": "object",
22346
22618
  "properties": {
@@ -26855,6 +27127,69 @@
26855
27127
  "avatar_url"
26856
27128
  ]
26857
27129
  },
27130
+ "PostStoreModulesKeyAcquireRequest": {
27131
+ "type": "object",
27132
+ "properties": {
27133
+ "version": {
27134
+ "type": "string",
27135
+ "maxLength": 32
27136
+ }
27137
+ },
27138
+ "additionalProperties": false
27139
+ },
27140
+ "PostStoreModulesKeyAcquireResponse": {
27141
+ "type": "object",
27142
+ "properties": {
27143
+ "ok": {
27144
+ "type": "boolean"
27145
+ },
27146
+ "module_key": {},
27147
+ "version": {},
27148
+ "core_version": {},
27149
+ "tarball_sha256": {},
27150
+ "tree_sha256": {},
27151
+ "tarball_bytes": {},
27152
+ "channel": {},
27153
+ "entitlement": {}
27154
+ },
27155
+ "required": [
27156
+ "ok",
27157
+ "module_key",
27158
+ "version",
27159
+ "core_version",
27160
+ "tarball_sha256",
27161
+ "tree_sha256",
27162
+ "tarball_bytes",
27163
+ "channel",
27164
+ "entitlement"
27165
+ ]
27166
+ },
27167
+ "PostStoreModulesKeyAcquiredRequest": {
27168
+ "type": "object",
27169
+ "properties": {
27170
+ "version": {
27171
+ "type": "string",
27172
+ "maxLength": 32
27173
+ }
27174
+ },
27175
+ "required": [
27176
+ "version"
27177
+ ],
27178
+ "additionalProperties": false
27179
+ },
27180
+ "PostStoreModulesKeyAcquiredResponse": {
27181
+ "type": "object",
27182
+ "properties": {
27183
+ "ok": {
27184
+ "type": "boolean"
27185
+ },
27186
+ "entitlement": {}
27187
+ },
27188
+ "required": [
27189
+ "ok",
27190
+ "entitlement"
27191
+ ]
27192
+ },
26858
27193
  "PostStoreModulesKeyVersionsResponse": {
26859
27194
  "type": "object",
26860
27195
  "properties": {
@@ -27737,9 +28072,9 @@
27737
28072
  "description": "A required dependency/feature is not configured or is temporarily down."
27738
28073
  }
27739
28074
  },
27740
- "x-endpoint-count": 434,
27741
- "x-schema-count": 466,
28075
+ "x-endpoint-count": 439,
28076
+ "x-schema-count": 471,
27742
28077
  "x-undocumented-bodies": 11,
27743
- "x-response-schemas": 319,
28078
+ "x-response-schemas": 322,
27744
28079
  "x-generated-by": "scripts/gds/gen-api-docs.js"
27745
28080
  }
@@ -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. 434 endpoints across 75 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 439 endpoints across 75 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`.
@@ -694,11 +694,16 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
694
694
  | GET | `/api/bongos/sso/pubkey` | `public` | — | GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). |
695
695
  | POST | `/api/bongos/sso/token` | `public` | `client_id`, `client_secret`, `code`, `redirect_uri` | POST /sso/token — server-to-server code redemption. |
696
696
 
697
- ## `store` (1)
697
+ ## `store` (6)
698
698
 
699
699
  | Method | Path | Rank | Body | Description |
700
700
  |---|---|---|---|---|
701
+ | 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 … |
702
+ | 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`. |
703
+ | POST | `/api/bongos/store/modules/:key/acquired` | `any-builder` | `version` | |
704
+ | GET | `/api/bongos/store/modules/:key/entitlement` | `any-builder` | — | |
701
705
  | POST | `/api/bongos/store/modules/:key/versions` | `metic+archon` | _undocumented_ | POST /api/bongos/store/modules/:key/versions — publish one module version to the store (task 1004271, ADR 0338 D1). |
706
+ | GET | `/api/bongos/store/modules/:key/versions/:version/tarball` | `any-builder` | — | |
702
707
 
703
708
  ## `task-recommendations` (4)
704
709
 
@@ -233,6 +233,22 @@ store_module_prices(id, module_key, price_cents, price_credits, set_by, set_at)
233
233
  -- in that currency, at least one must be set.
234
234
  ```
235
235
 
236
+ Module entitlements (migration `core_259`, task 1003813 — who holds which module, at which version; the record install, update and the telemetry signals consult; it records ownership, never payment — area 8 owns money):
237
+
238
+ ```
239
+ module_entitlements(id, module_key, holder_kind IN (builder|instance), holder_ref,
240
+ source IN (free|core|grant|purchase), payment_ref (opaque, area 8's),
241
+ version exact X.Y.Z | NULL, acquired_at, granted_by, granted_at,
242
+ revoked_at, revoked_reason, UNIQUE(module_key, holder_kind, holder_ref))
243
+ -- module_key is NOT an FK: a core-bundled module has no store row and
244
+ -- holds an entitlement like any other. Ownership is per module; an update
245
+ -- moves `version` forward, never asks for a second purchase. A delist never
246
+ -- touches it (ADR 0338 D2); only revoke (refund, task 1003789) ends a hold,
247
+ -- keeping the row.
248
+ ```
249
+
250
+ Code: `src/bongos/module-entitlements.js` (`grantEntitlement`, `recordAcquired`, `revokeEntitlement`, `checkEntitlement`, `listEntitlements`). Read routes (own-scoped, `requireBuilder`): `GET /store/entitlements`, `GET /store/modules/:key/entitlement`. Install (task 1003785) grants a free module through `POST /store/modules/:key/acquire`; the buy action (area 8) will grant a paid one.
251
+
236
252
  Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again in 014 so the new `discipline` column flows through), `version_progress` (rebuilt in 006 to count only leaf tasks for forward-compat with task hierarchy), `tasks_session_fit` (new in 006: derives `fits_session_modes[]` and `credits_per_minute`; rebuilt in 014 for the same SELECT t.* refresh), `task_grades_with_context` (new in 016: joins task_grades × tasks × builders × cost_log so the V4 optimizer has quality-per-dollar in one read).
237
253
 
238
254
  **Bongos API (`/api/bongos/*`, mounted in the same Node process):**
@@ -245,6 +261,7 @@ Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again
245
261
  - Authenticated CLI/web: `GET /me` (returns `rank`), `GET /versions[/progress]`, `GET /tasks[?version=&status=&kind=&discipline=]`, `GET /tasks/claimable[?version=&discipline=]`, `GET /tasks/:id`, `POST /tasks` (accepts `discipline` + `criterion_ids`; **metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)), `PATCH /tasks/:id` (supports `parent_task_id`, `kind`, `discipline`, `goal_id` — (re)assign the task's goal; must be a goal in the task's own version, task 1763; **metic+**), `POST /tasks/:id/promote` (**metic+**), `POST /claims`, `POST /claims/:id/resolve`, `POST /cost`.
246
262
  - Criterion rollup + task↔criterion links ([ADR 0025](adr/0025-structured-criterion-task-link.md), [#435](https://example.com/builders#/task/435)/[#438](https://example.com/builders#/task/438)): `GET /versions/:id/progress` rolls up each done-when criterion → its gating tasks (via `task_criteria`) → live status counts + the not-yet-shipped `remaining[]` + `unattributed_tasks` (the read behind the `/status` skill, in `src/bongos/done-when.js criterionProgress`). The link CRUD mirrors the dependency endpoints: `GET /tasks/:id/criteria`, `POST /tasks/:id/criteria` (**metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)) and `DELETE /tasks/:id/criteria/:criterionId` (**metic+**). A criterion ref is a numeric `done_when_criteria.id`, a positional `"Cn"` token, or a `criterion_id` slug, resolved against the task's version (`POST /tasks` links at create time so `/status` counts the task with no backfill).
247
263
  - Module store publish (ADR 0338 D1, task 1004271): `POST /store/modules/:key/versions` (`requireBuilder` + `requirePermission('module.submit')`, Metic floor). The body is the raw gzip tarball `bongos module publish` builds (own `express.raw` parser, 5 MB cap; 16 MB / 2000 files unpacked). `scripts/gds/module-artifact.js` re-verifies every hash and the publish gate, then `src/bongos/module-store.js` keeps the file under `var/module-store/` and inserts the `store_module_versions` row in one transaction. The first publish makes the caller the author; later versions are author-only, newer than the last and never overwritten; a delisted key takes none.
264
+ - Module store install (ADR 0338 D1, task 1003785), all `requireBuilder` and own-scoped (the holder is the caller): `POST /store/modules/:key/acquire` (`{version?}`; grants a free entitlement — no price row, or a zero price, is free — or `402 payment_required` for a priced module; refuses a delisted one), `GET /store/modules/:key/versions/:version/tarball` (served only to a live entitlement holder; `X-Tarball-Sha256`), `POST /store/modules/:key/acquired` (`{version}` → `recordAcquired`). Read side in `src/bongos/module-store.js` (`resolveAcquirable`, `isFreePrice`, `versionArtifactFile`). No channel check yet (tasks 1003815, 1003805).
248
265
  - Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a checkout with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). `branch` is optional: without it the server finds the task's own PR by its `task <id>: …` title, and the answer names the `branch` it used and its `branch_source` (`query` / `task_pr`, or `null` with a `branch_hint` when there is no such PR) — never a `400` (task 1003764). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). Because the server's credential is the PR's AUTHOR, publish also **assigns the PR to the task's claim holder** and names them in the body (`modules/lifecycle/pr-assign.js`, task 1003991) — best-effort: the 201 carries `pr_assignee` (the login, or `null` when GitHub would not assign it) and a failed assignment never fails the publish. `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (no authenticated `gh` on the machine, or `<PREFIX>_PUSH_VIA_SERVER=1`); a machine with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
249
266
  - Archon monitoring reads backing the `/watch` page (ADR 0036): `GET /builders/roster`, `GET /grades/by-builder[?days=N]` (**archon-only**, [#726](https://example.com/builders#/task/726) — per-builder grade breakdown for the MARKS section; the project-wide aggregate stays public at `/public/grades`), `GET /audit-log`, `GET /override-requests`, `GET /access-requests`, `GET /security/reports`. Personal-prefs writes backing `/settings`: `GET/PATCH /me/skill-prefs`, `PATCH /me/disciplines`.
250
267
  - Per-builder "needs" + own Gemini key ([ADR 0073](adr/0073-builder-needs-signal-and-byok-gemini-key.md), [#1013](https://example.com/builders#/task/1013)): `GET /me` now also carries `needs` ({items, action_needed_count} — the consistent "the system needs an input from you" signal; `modules/builder-settings/builder-needs.js` is the SSOT — carved out in BV1.R80, resolved by `GET /me` via the `builder-settings` kernel port — rendered by the hall Standing card + the CLI `printNeedsNudge`). The own-key (pragmatic BYOK) endpoints, all own-scoped: `GET /me/art-key/own` (masked meta — last4 only), `PUT /me/art-key/own` (validate-on-save via a Google list-models call → encrypt with `src/bongos/secret-box.js` → store; `503 storage_not_configured` until `BUILDER_SECRET_KEY` is set, `422 invalid_key` on a bad key), `DELETE /me/art-key/own`. The existing `GET /me/art-key` (shared-key delivery) is extended to also deliver the decrypted own key, which `scripts/gds/fetch-art-key.js` syncs into the local `gemini_api_key` slot (own key wins in the `gen_api.py` cascade).
@@ -2645,5 +2645,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2645
2645
  landed since 1.19.1075 with no explicit bump. run 36523068301. (task 1002620)
2646
2646
  1.19.1077 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2647
2647
  landed since 1.19.1076 with no explicit bump. run 36558431394. (task 1002620)
2648
+ 1.19.1078 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2649
+ landed since 1.19.1077 with no explicit bump. run 36600137276. (task 1002620)
2650
+ 1.19.1079 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2651
+ landed since 1.19.1078 with no explicit bump. run 36602720244. (task 1002620)
2648
2652
  ---------------------------------------------------------------------------
2649
2653
  ```
@@ -357,6 +357,16 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
357
357
  - 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.
358
358
  - Gate: Metic+ (`module.submit`) for now. This is not `bongos module submit`, which proposes a module *into* core.
359
359
 
360
+ ### 7. Install from the store
361
+
362
+ `bongos module install <key> [--version X.Y.Z]` acquires a module from the store and places it under the instance's `modules/`. Three calls: `POST /store/modules/:key/acquire` takes your entitlement and names the version (the newest unless you pick one), `GET /store/modules/:key/versions/:version/tarball` downloads it, and once the files are placed, `POST /store/modules/:key/acquired` records the exact version.
363
+
364
+ - The download is checked with `verifyModuleArtifact`, the same checker the store runs on publish, and must match the version and hashes the store described. A failed check places nothing.
365
+ - A module with no price, or a zero price, is free and granted on the spot. A priced one answers 402 until buying exists (area 8). An entitlement is kept for good, even if a price is added later (ADR 0338 D2).
366
+ - Install never enables. Switch the module on from the hall's Modules tab.
367
+ - A module already on disk is refused; moving one to a newer version is `update`. A module this core cannot mount is refused before download.
368
+ - A delisted module cannot be acquired or downloaded. Channels are not checked yet: every version is general until the channel tasks land.
369
+
360
370
  ## Carving an existing core domain (the `db.js` per-domain pattern)
361
371
 
362
372
  Most BONGOS-V1 module work is not scaffolding a *new* module — it's **carving an existing core domain out of the monolith** (ADR 0091, criterion C7). The reference carve is the kernel slice `src/bongos/db-kernel.js` (BV1.R69); the feature precedents are `modules/discord/db.js` + `modules/game/db.js`. Every later carve (memory R72, grading R73, …) copies this recipe. The invariant is **OTB byte-identical** at every step — you are *moving* code, not changing behavior.
@@ -0,0 +1,58 @@
1
+ -- core_259_module_entitlements.sql — who holds which module, and at which version.
2
+ --
3
+ -- task 1003813 (goal 1000091, working area 5). ADR 0338 D1: the store checks an
4
+ -- entitlement before it serves a tarball, and install (1003785), update (1003786)
5
+ -- and the install/reliability signals (1003793) all read it. This is the durable
6
+ -- meaning of "bought" — and deliberately NOT the act of paying. Granting an
7
+ -- entitlement is area 5's; taking money, balances and payout are area 8's (goal
8
+ -- 1000092) and are not modelled here. The one seam is `payment_ref`: an opaque
9
+ -- text pointer area 8 can fill when it wires the buy action, never read by us.
10
+ --
11
+ -- module_entitlements one row per (module, holder). The holder is a builder or
12
+ -- an instance, named by (holder_kind, holder_ref).
13
+ -- `source` says how it came to be held: 'free' (a zero-price
14
+ -- store module), 'core' (bundled with core), 'grant' (a
15
+ -- person gave it, no payment), 'purchase' (area 8 will
16
+ -- write this). Every source is the same row, so free and
17
+ -- core modules go through exactly the record a paid one does.
18
+ -- `version` is the exact X.Y.Z the holder last acquired —
19
+ -- NULL until the first acquisition. Ownership is per MODULE:
20
+ -- an update moves `version` forward and never asks for a
21
+ -- second purchase (task 1003786).
22
+ --
23
+ -- module_key is deliberately NOT a foreign key to store_modules: a core-bundled
24
+ -- module has no store row, and it must hold an entitlement like any other.
25
+ -- A delist never touches this table (ADR 0338 D2: an entitlement is never revoked
26
+ -- by a delist); the only thing that ends one is `revoked_at`, set by the refund
27
+ -- path (task 1003789). A revoked row is kept, not deleted, so history stays
28
+ -- answerable, and a re-grant clears `revoked_at` on the same row.
29
+ --
30
+ -- Additive only (a new table), so forward-safe. Idempotent: safe to re-run.
31
+
32
+ CREATE TABLE IF NOT EXISTS module_entitlements (
33
+ id bigserial PRIMARY KEY,
34
+ module_key text NOT NULL
35
+ CHECK (module_key ~ '^[a-z][a-z0-9-]*$'),
36
+ holder_kind text NOT NULL
37
+ CHECK (holder_kind IN ('builder', 'instance')),
38
+ holder_ref text NOT NULL CHECK (length(holder_ref) > 0),
39
+ source text NOT NULL
40
+ CHECK (source IN ('free', 'core', 'grant', 'purchase')),
41
+ payment_ref text,
42
+ version text
43
+ CHECK (version IS NULL OR version ~ '^[0-9]+\.[0-9]+\.[0-9]+$'),
44
+ acquired_at timestamptz,
45
+ granted_by bigint REFERENCES builders(id),
46
+ granted_at timestamptz NOT NULL DEFAULT now(),
47
+ revoked_at timestamptz,
48
+ revoked_reason text,
49
+ UNIQUE (module_key, holder_kind, holder_ref),
50
+ CHECK ((version IS NULL) = (acquired_at IS NULL)),
51
+ CHECK ((revoked_at IS NULL) = (revoked_reason IS NULL))
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_module_entitlements_holder
55
+ ON module_entitlements (holder_kind, holder_ref);
56
+
57
+ INSERT INTO schema_migrations (version) VALUES ('core_259_module_entitlements')
58
+ ON CONFLICT (version) DO NOTHING;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1077",
3
+ "version": "1.19.1079",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1077",
9
+ "version": "1.19.1079",
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.1077",
3
+ "version": "1.19.1079",
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",
@@ -7855,5 +7855,17 @@
7855
7855
  "id": "1004357",
7856
7856
  "text": "A project's hall on one of our addresses can no longer read the sign-in of a hub builder who visits it. Hub sign-in cookies now stay on the exact address that set them. builders. and status. still sign you in automatically thr"
7857
7857
  }
7858
+ ],
7859
+ "1.19.1078": [
7860
+ {
7861
+ "id": "1003813",
7862
+ "text": "The store now has a durable record of who owns which module and at which version, so install and update can check it. Free and bundled modules use the same record; nothing charges money yet."
7863
+ }
7864
+ ],
7865
+ "1.19.1079": [
7866
+ {
7867
+ "id": "1003785",
7868
+ "text": "You can now install a module from the store with one command. It checks the download is exactly what was published and never switches the module on by itself. Modules without a price are free."
7869
+ }
7858
7870
  ]
7859
7871
  }
@@ -141,7 +141,10 @@ function packModule(key, { modulesDir, now = () => new Date(), modeOf } = {}) {
141
141
  // denylist runs again. Resolves { ok: true, ... } or { ok: false, code, message }.
142
142
  // Async so the inflate runs off the event loop (readTarAsync); the rest is one pass
143
143
  // over at most READ_LIMITS.maxUnpackedBytes.
144
- async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES } = {}) {
144
+ // `includeFiles: true` (install, task 1003785) also returns `files`: the module's own
145
+ // files — [{ path, mode, buf }], the inner manifest left out — from the SAME parse the
146
+ // hashes were checked against, so a caller that places them never re-reads the bytes.
147
+ async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES, includeFiles = false } = {}) {
145
148
  const fail = (code, message) => ({ ok: false, code, message });
146
149
  if (!Buffer.isBuffer(tgz) || tgz.length === 0) return fail('empty_artifact', 'the upload is empty');
147
150
  if (tgz.length > maxBytes) return fail('artifact_too_large', `the tarball is over ${maxBytes} bytes`);
@@ -202,11 +205,15 @@ async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES } =
202
205
  return fail('bad_manifest', 'the manifest version does not match module.json');
203
206
  }
204
207
 
205
- return {
208
+ const out = {
206
209
  ok: true, manifest, moduleJson,
207
210
  version: moduleJson.version, coreVersion: moduleJson.coreVersion,
208
211
  treeSha256: treeSha, tarballSha256: sha256hex(tgz), bytes: tgz.length,
209
212
  };
213
+ if (includeFiles) {
214
+ out.files = [...byName.entries()].map(([p, e]) => ({ path: p, mode: normalizeMode(e.mode.toString(8)), buf: e.buf }));
215
+ }
216
+ return out;
210
217
  }
211
218
 
212
219
  module.exports = { MAX_TARBALL_BYTES, packModule, verifyModuleArtifact };