@bongos/core 1.21.126 → 1.21.127

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 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.126",
6
- "core_contract": "1.21.126",
7
- "source_commit": "94a48727259da5ad6269d8e1982648c5a263116b",
5
+ "core_version": "1.21.127",
6
+ "core_contract": "1.21.127",
7
+ "source_commit": "ea6cb74e9eba8805eb016b8ac0420841e429c290",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-15T15:32:06.327Z",
9
+ "built_at": "2026-10-15T15:58:58.689Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 606,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3133,
14
+ "functional_verbatim": 3138,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3768,
20
- "tree_sha256": "aaa23a52f299e82601309b1dc11007fe842e9686674c9c5cc96991f5412286e3",
19
+ "file_count": 3773,
20
+ "tree_sha256": "2af5f5b320f78e843791c36c370aa8f198a28c9bd3b085c1875f39869558c2cd",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -1887,7 +1887,7 @@
1887
1887
  {
1888
1888
  "path": "docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md",
1889
1889
  "mode": "0000644",
1890
- "sha256": "6043bb19c2b18074c132750b0480b03f627733536c3263ab79c049f40f314e57"
1890
+ "sha256": "957ee2f99f8b8ac8a22723c7af73798540499a0ea859f010decb87b09cf0a02f"
1891
1891
  },
1892
1892
  {
1893
1893
  "path": "docs/adr/0286-a-gated-project-s-manifest-tells-a-stranger-how-to-sign-in-and-nothing-else.md",
@@ -2232,7 +2232,7 @@
2232
2232
  {
2233
2233
  "path": "docs/adr/0354-a-builder-hosts-one-project-free-and-more-takes-the-fleet-permission.md",
2234
2234
  "mode": "0000644",
2235
- "sha256": "962d868c0887244c53b53c58754f7232715a20df6feec694f30cbda707f9e17e"
2235
+ "sha256": "47b6fbbe4da97fd83f1b7e9914f102bbd5924d88b02dca6d87a4e2cf44af8664"
2236
2236
  },
2237
2237
  {
2238
2238
  "path": "docs/adr/0355-a-soft-limit-is-a-rule-with-a-reason-and-nobody-enforces-it.md",
@@ -2922,7 +2922,7 @@
2922
2922
  {
2923
2923
  "path": "docs/module-api-changelog.md",
2924
2924
  "mode": "0000644",
2925
- "sha256": "8b70cfa6cac3da1e08d0f969f6d2a3f0c43ca0533b28d7e8a58ca12044165c26"
2925
+ "sha256": "de498901b8143d15779aafd30051f97e04ae01f32c880902da3a8234a6e262e7"
2926
2926
  },
2927
2927
  {
2928
2928
  "path": "docs/modules-contract.md",
@@ -10482,12 +10482,12 @@
10482
10482
  {
10483
10483
  "path": "package-lock.json",
10484
10484
  "mode": "0000644",
10485
- "sha256": "341426e0e35db5113dd017485f4479dc0674db124643388aeec9d883e113f7e3"
10485
+ "sha256": "efe64aff087c9fac228bb0502df35fdb706ea66215ba9a7b20a16575b4568a6d"
10486
10486
  },
10487
10487
  {
10488
10488
  "path": "package.json",
10489
10489
  "mode": "0000644",
10490
- "sha256": "f5522e5c98564bbf8ffd3a4817e094732239f369a1850e1b743030a4b03d00b2"
10490
+ "sha256": "65dbc0fd1642caa7dd0907f8ca5f8d6e3808087637f31aaff0b5b3b2e22da1da"
10491
10491
  },
10492
10492
  {
10493
10493
  "path": "public-docs/index.html",
@@ -10507,7 +10507,7 @@
10507
10507
  {
10508
10508
  "path": "release-notes.json",
10509
10509
  "mode": "0000644",
10510
- "sha256": "4fbea8b3dd2714eb5ec49751e18f66a0b995e94d64163edfbbf5fa837b196d81"
10510
+ "sha256": "b3c808cc70f6d027e4dc58fad374f2dff423b73416eee5c5a96e8779ddd2d04f"
10511
10511
  },
10512
10512
  {
10513
10513
  "path": "scripts/cron/style_guide_audit.sh",
@@ -11022,7 +11022,7 @@
11022
11022
  {
11023
11023
  "path": "scripts/gds/exec-path-guard.js",
11024
11024
  "mode": "0000644",
11025
- "sha256": "cda9e125cfb2cb354c7d4e20065c36712a75955d3981203012c2334d0ce637ae"
11025
+ "sha256": "5b5f2b8b40d62dab10a0ea9b43768944dcbe60a8af550b35229ce278deaceffc"
11026
11026
  },
11027
11027
  {
11028
11028
  "path": "scripts/gds/feedback-send.js",
@@ -11097,7 +11097,7 @@
11097
11097
  {
11098
11098
  "path": "scripts/gds/fitness.js",
11099
11099
  "mode": "0000644",
11100
- "sha256": "88a99a1a1455477dd9f7987c0f6c48f20464cc4ab98cc2f224c1a5536483fd2d"
11100
+ "sha256": "e2f483de1afd806d77fc5b62d6f1b08d762ea4fec9eaddb91d2ef1f25f00112e"
11101
11101
  },
11102
11102
  {
11103
11103
  "path": "scripts/gds/gate-review.js",
@@ -11404,6 +11404,11 @@
11404
11404
  "mode": "0000644",
11405
11405
  "sha256": "b21c85714a8b93e43857c26237eec9dca9830abb53f44146957525f262fb8e7b"
11406
11406
  },
11407
+ {
11408
+ "path": "scripts/gds/module-error-report.js",
11409
+ "mode": "0000644",
11410
+ "sha256": "dc8e6794d0c09479891136c8b0ab4da7ce73c1797522e03b9457db113b2534e2"
11411
+ },
11407
11412
  {
11408
11413
  "path": "scripts/gds/module-test-runner.js",
11409
11414
  "mode": "0000644",
@@ -11422,7 +11427,7 @@
11422
11427
  {
11423
11428
  "path": "scripts/gds/module.js",
11424
11429
  "mode": "0000644",
11425
- "sha256": "b02d07e9e7e1d2e2852676383e054f13d8e423755534fe23ed3e10cbfefb4086"
11430
+ "sha256": "9391cc7d6881150cd5d196aa5e63245db032093063ce90e1e1ae5c26dafcb49c"
11426
11431
  },
11427
11432
  {
11428
11433
  "path": "scripts/gds/move-escalation.js",
@@ -11549,6 +11554,11 @@
11549
11554
  "mode": "0000644",
11550
11555
  "sha256": "2102af404ac985f0d7b037da973600234b2ef8f90b6c689ca164b9ac1f46262f"
11551
11556
  },
11557
+ {
11558
+ "path": "scripts/gds/provision-caps.js",
11559
+ "mode": "0000644",
11560
+ "sha256": "0dee61336a309cf3eda2cb715bb53680773c17d65186864384e3cb0bc9dadaba"
11561
+ },
11552
11562
  {
11553
11563
  "path": "scripts/gds/provision-config.js",
11554
11564
  "mode": "0000644",
@@ -11587,7 +11597,7 @@
11587
11597
  {
11588
11598
  "path": "scripts/gds/provision-process.js",
11589
11599
  "mode": "0000644",
11590
- "sha256": "6a784f63078e5cb2c0f85475672b02b18ded67a36a8041b1ba3643b227a465d0"
11600
+ "sha256": "4689867d4925672e3e12c56620ef2e4e6ad2474dfaaa309571da9533af49b997"
11591
11601
  },
11592
11602
  {
11593
11603
  "path": "scripts/gds/provision-render.js",
@@ -11617,12 +11627,12 @@
11617
11627
  {
11618
11628
  "path": "scripts/gds/provision-units.js",
11619
11629
  "mode": "0000644",
11620
- "sha256": "eb6cf09ce1eb97330b20fe97c61068e2b7cc8bb4556a54df39e3a3978fe6ffa2"
11630
+ "sha256": "02173656cffc347b23b68fa5036826d16bfedbb0e490161b8e5853b5cbb9b8a9"
11621
11631
  },
11622
11632
  {
11623
11633
  "path": "scripts/gds/provision.js",
11624
11634
  "mode": "0000644",
11625
- "sha256": "ea11cc49dbc298c8b3ed4ad88e25a3acfbba65fcd65727c9b47c43ee41e24437"
11635
+ "sha256": "bf16a2ef196d64088c7755e3d6ccbdd8f7917eb907efdbb8ec132e7df9694a52"
11626
11636
  },
11627
11637
  {
11628
11638
  "path": "scripts/gds/publish-credential-check.js",
@@ -12847,17 +12857,22 @@
12847
12857
  {
12848
12858
  "path": "src/module-api.js",
12849
12859
  "mode": "0000644",
12850
- "sha256": "05cbabab7a15ea12d403cdce32d0e9accfb56fe1dc44c691dc5e8920933edaf0"
12860
+ "sha256": "8ff71d0200dd2b11afa13be91155669caee7ba000a537df94e37426a6485e7f1"
12851
12861
  },
12852
12862
  {
12853
12863
  "path": "src/module-loader/catalog.js",
12854
12864
  "mode": "0000644",
12855
12865
  "sha256": "633042ab3228780fd18f96ce6a2dd0d84452a124948720678d4484c29df0acd8"
12856
12866
  },
12867
+ {
12868
+ "path": "src/module-loader/error-tally.js",
12869
+ "mode": "0000644",
12870
+ "sha256": "00f37bb206c47a75c5ee1420831781a38eb88b9446a9ce1cc3e8ee5a47e09334"
12871
+ },
12857
12872
  {
12858
12873
  "path": "src/module-loader/loader.js",
12859
12874
  "mode": "0000644",
12860
- "sha256": "04458c645d876011324e5ae11011774699c06e39ff2fa1ba1cf58a2d2775f887"
12875
+ "sha256": "4520d7cb0a91dd3f6cb52c52ced8caf2039126fa7c942e3f0714e91600cebb80"
12861
12876
  },
12862
12877
  {
12863
12878
  "path": "src/module-loader/manifest-schema.js",
@@ -12892,7 +12907,7 @@
12892
12907
  {
12893
12908
  "path": "src/platform-server.js",
12894
12909
  "mode": "0000644",
12895
- "sha256": "773372a16a32d3936aa53d0ca5ec0e19833e45cadcc16688102b5e81c117e5bf"
12910
+ "sha256": "435a89e5f5becab510ee5ad3f4246f34ee6a6abac859d49823edc74fd1bcdf4f"
12896
12911
  },
12897
12912
  {
12898
12913
  "path": "src/role-responsibilities.js",
@@ -16409,6 +16424,11 @@
16409
16424
  "mode": "0000644",
16410
16425
  "sha256": "50e4d45bf544efe755dda538a93eafc40dcd9caa071e987d8833d97ecf1b7e0f"
16411
16426
  },
16427
+ {
16428
+ "path": "tests/module_error_counts.mjs",
16429
+ "mode": "0000644",
16430
+ "sha256": "7c5e8009a416c72d5659677c0a23d90408001f33ff6b56b121ed06a2beceb739"
16431
+ },
16412
16432
  {
16413
16433
  "path": "tests/module_exports_unique_keys.mjs",
16414
16434
  "mode": "0000644",
@@ -17132,7 +17152,7 @@
17132
17152
  {
17133
17153
  "path": "tests/provision.mjs",
17134
17154
  "mode": "0000644",
17135
- "sha256": "2c31c74c71147c5fe96beb8e6a53f8793ea3f42870512a6005f43df7c6fb5382"
17155
+ "sha256": "dc30483813143d76ed6597f4b68d61db3dd2ae9bf34d15b9ce8ddd57caf0f79f"
17136
17156
  },
17137
17157
  {
17138
17158
  "path": "tests/provision_account_identity.mjs",
@@ -17169,6 +17189,11 @@
17169
17189
  "mode": "0000644",
17170
17190
  "sha256": "f99d0a4a54fa9d846efc772148156e06023d3ad4e2afbddce8dd1d9a62821b96"
17171
17191
  },
17192
+ {
17193
+ "path": "tests/provision_resource_caps.mjs",
17194
+ "mode": "0000644",
17195
+ "sha256": "491c29cbeee40a677a01ee71f1f54a787017456b4baa8416219480f337e3c3e3"
17196
+ },
17172
17197
  {
17173
17198
  "path": "tests/provision_restart.mjs",
17174
17199
  "mode": "0000644",
@@ -80,3 +80,9 @@ The measured cost is the same **unit** for `co-tenant` and `standalone`. What se
80
80
  - **+** Billing has a unit cost for a shared slot.
81
81
  - **−** The page-cache reserve is an allowance, not a measurement. If a ceiling is ever approached, re-measure `MemAvailable` then instead of trusting the 250 MB.
82
82
  - **−** These are **idle-fleet** numbers. A project with real traffic or large data will sit above 80 MB, so the ceiling is an upper bound for quiet projects. Re-measure the same way (`Pss` + `SwapPss` per unit, `memory.peak`, `pg_stat_activity`) as projects gain users.
83
+
84
+ ## Amendment — 2026-10-15 (task 1004414): each hosted project is capped
85
+
86
+ **The owner's numbers**, sized from the measurement above: `MemoryHigh=160M`, `MemoryMax=256M`, `CPUQuota=50%`, `TasksMax=256` on every hosted project's unit. A project idles at 45–70 MB and its boot peak is 118–134 MB with page cache, so `MemoryHigh` sits above the boot peak and makes the kernel reclaim before `MemoryMax` kills. A killed project comes back through `Restart=always`. Half a core is about a thousand times the measured idle use, and 256 tasks is far above what one Node process starts. The caps bound one runaway project; they do not change the ceiling, which still counts ~80 MB per project.
87
+
88
+ New units render the caps (`serviceUnit`). An existing unit takes them with `systemctl set-property`, live and without a restart, through `node scripts/gds/provision.js apply-caps --apply` on the control plane. The unit file is not rewritten, because a project made before the per-instance accounts (task 1003369) would be moved onto an account that does not exist. The numbers live in `scripts/gds/provision-caps.js`, and `tests/provision_resource_caps.mjs` pins them, so changing one is a decision. A project with real traffic or an adopted repo with heavy dependencies may need more; re-measure as above before raising them.
@@ -36,7 +36,7 @@ The count and the insert are two separate steps, so two requests sent together w
36
36
  ## Consequences
37
37
 
38
38
  - New users can create a project from the wizard again. Each project costs us about $0.80 a month (ADR 0285).
39
- - A stranger's code now runs on the shared box, on the same server as the hub. It runs in its own locked account (task 1003369), and the hub's sign-in cookie no longer reaches project halls (task 1004357). **Nothing caps a hosted project's memory or CPU**, so one misbehaving project can slow every hall and the hub. That was already true of every hosted project; this decision widens who can have one.
39
+ - A stranger's code now runs on the shared box, on the same server as the hub. It runs in its own locked account (task 1003369), and the hub's sign-in cookie no longer reaches project halls (task 1004357). **Nothing caps a hosted project's memory or CPU**, so one misbehaving project can slow every hall and the hub. That was already true of every hosted project; this decision widens who can have one. (Closed by task 1004414: each hosted project is now capped. See the ADR 0285 amendment.)
40
40
  - Each first-free request holds one database connection from its count to its insert. Creates are rare, so the pool can carry it.
41
41
 
42
42
  ## Alternatives considered
@@ -3089,5 +3089,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3089
3089
  landed since 1.21.124 with no explicit bump. run 38759002066. (task 1002620)
3090
3090
  1.21.126 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3091
3091
  landed since 1.21.125 with no explicit bump. run 38761052564. (task 1002620)
3092
+ 1.21.127 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3093
+ landed since 1.21.126 with no explicit bump. run 38764985266. (task 1002620)
3092
3094
  ---------------------------------------------------------------------------
3093
3095
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.126",
3
+ "version": "1.21.127",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.126",
9
+ "version": "1.21.127",
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.126",
3
+ "version": "1.21.127",
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",
@@ -9750,5 +9750,15 @@
9750
9750
  "id": "1004738",
9751
9751
  "text": "A goal's page is much shorter now: task dependencies sit in a small card at the top where Conflicts used to be, and the full dependency picture and conflict list are one click away."
9752
9752
  }
9753
+ ],
9754
+ "1.21.127": [
9755
+ {
9756
+ "id": "1004637",
9757
+ "text": "Projects now count each store module's errors per day and send the count to the store, so a module's reliability score comes from real use. It sends with a builder's sign-in on that machine and respects the reporting switch in"
9758
+ },
9759
+ {
9760
+ "id": "1004414",
9761
+ "text": "Each hosted project now has a memory and CPU limit, so one runaway project can no longer slow down every other project or the hub."
9762
+ }
9753
9763
  ]
9754
9764
  }
@@ -103,7 +103,7 @@ const EXEMPTIONS = [
103
103
  + 'absorbed into this check.',
104
104
  },
105
105
  {
106
- at: 'src/module-loader/loader.js:347',
106
+ at: 'src/module-loader/loader.js:350',
107
107
  why: 'The poller twin of the route require at :196 — same DEFAULT_ROOTS bound, same plugin-by-'
108
108
  + 'design rationale, same ADR 0107 / task 1003400 follow-on. Listed on its own line so a '
109
109
  + 'change to either call re-opens only that one.',
@@ -524,6 +524,7 @@ const KERNEL_FILES = [
524
524
  'src/module-loader/loader.js', // the disk loader (discover → validate → mount)
525
525
  'src/module-loader/manifest-schema.js', // the manifest validator
526
526
  'src/module-loader/semver.js', // coreVersion compatibility
527
+ 'src/module-loader/error-tally.js', // per-module runtime-error counts the loader feeds (task 1004637)
527
528
  'src/bongos/module-scope-map.js', // the module→path registry (it NAMES every module — exempt below)
528
529
  // trust primitives (ADR 0091 §1) — the authz boundary + data substrate
529
530
  'src/bongos/auth.js', // authz gates (requireBuilder/requireRank, ADR 0016)
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ //
4
+ // scripts/gds/module-error-report.js — send this instance's daily runtime-error
5
+ // count for each store module to the store, with the builder sign-in on this box
6
+ // (task 1004637, follow-up to task 1003793; ADR 0343 D1).
7
+ //
8
+ // WHY. The store scores a module's Reliability from daily error counts, and nothing
9
+ // sent one. The server keeps the tally (src/module-loader/error-tally.js writes
10
+ // module-error-counts.json in the instance config dir); this script sends every
11
+ // finished day it has not sent yet — including a 0, because a clean day is what
12
+ // Reliability measures — and remembers which days went in module-error-sent.json.
13
+ //
14
+ // WHO SENDS (owner, 2026-10-15): a builder's sign-in on this box, because the store's
15
+ // report routes take a builder and no project-to-store credential exists (that option
16
+ // is parked as task 1004740). No sign-in here means nothing is sent, quietly.
17
+ //
18
+ // PRIVACY (owner, 2026-10-01): counts only, on by default, with an opt-out. If the
19
+ // signed-in builder switched reporting off in Settings, nothing is sent and the days
20
+ // are marked done; the store also drops a report from an opted-out holder, so the
21
+ // rule holds whichever client sends.
22
+ //
23
+ // The server runs this every few hours (error-tally.js startDailySend); it is safe to
24
+ // run by hand too:
25
+ // node scripts/gds/module-error-report.js [--dry-run] [--quiet]
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ const { readTally, utcDay, FILE_NAME } = require('../../src/module-loader/error-tally');
31
+
32
+ const SENT_FILE = 'module-error-sent.json';
33
+ // The store takes days up to 30 back (module-usage-reports.js ERROR_DAY_WINDOW).
34
+ const WINDOW_DAYS = 30;
35
+ const ENTRY_RE = /^([a-z0-9][a-z0-9-]*)@(\d+\.\d+\.\d+)$/;
36
+
37
+ function daysAgo(today, n) { return utcDay(Date.parse(`${today}T00:00:00Z`) - n * 86_400_000); }
38
+
39
+ // The finished days still to send, oldest first. Pure.
40
+ function pendingDays(days, sent, today) {
41
+ const oldest = daysAgo(today, WINDOW_DAYS);
42
+ return Object.keys(days || {}).filter((d) => d < today && d >= oldest && !(sent && sent[d])).sort();
43
+ }
44
+
45
+ // Send what is pending. `call(method, path, { json })` is the store client (module.js
46
+ // storeRequest). Never throws for a store answer: a day is marked done only when
47
+ // every count in it got a final answer — recorded, or refused for good (4xx: the store
48
+ // does not have that version, or this builder does not hold the module). A 5xx or an
49
+ // unreachable store leaves the day for the next run. Returns a summary.
50
+ async function sendPending({ days, sent = {}, today, call, dryRun = false }) {
51
+ const out = { pending: pendingDays(days, sent, today), sentDays: [], posted: 0, skipped: 0, optedOut: false, retryLater: false };
52
+ if (!out.pending.length) return out;
53
+
54
+ const pref = await call('GET', '/store/usage-reporting');
55
+ if (!pref.ok) { out.retryLater = true; return out; }
56
+ if (pref.data && pref.data.enabled === false) {
57
+ out.optedOut = true;
58
+ if (!dryRun) out.sentDays = out.pending.slice();
59
+ return out;
60
+ }
61
+
62
+ const entries = new Set();
63
+ for (const d of out.pending) for (const e of Object.keys(days[d] || {})) if (ENTRY_RE.test(e)) entries.add(e);
64
+ const keys = [...new Set([...entries].map((e) => e.match(ENTRY_RE)[1]))];
65
+ let known = new Set();
66
+ if (keys.length) {
67
+ const res = await call('GET', `/store/modules/latest?keys=${keys.map(encodeURIComponent).join(',')}`);
68
+ if (!res.ok) { out.retryLater = true; return out; }
69
+ known = new Set(Object.keys((res.data && res.data.latest) || {}));
70
+ }
71
+
72
+ for (const day of out.pending) {
73
+ let done = true;
74
+ for (const [e, count] of Object.entries(days[day] || {})) {
75
+ const m = e.match(ENTRY_RE);
76
+ if (!m || !known.has(m[1]) || !Number.isInteger(count) || count < 0) { out.skipped += 1; continue; }
77
+ if (dryRun) { out.posted += 1; continue; }
78
+ let res;
79
+ try {
80
+ res = await call('POST', `/store/modules/${encodeURIComponent(m[1])}/versions/${encodeURIComponent(m[2])}/error-count`, { json: { day, count } });
81
+ } catch { res = { ok: false, status: 0 }; }
82
+ if (res.ok) out.posted += 1;
83
+ else if (res.status >= 400 && res.status < 500) out.skipped += 1;
84
+ else done = false;
85
+ }
86
+ if (done && !dryRun) out.sentDays.push(day);
87
+ else if (!done) out.retryLater = true;
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function readSent(file) {
93
+ try {
94
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
95
+ return parsed && parsed.sent && typeof parsed.sent === 'object' ? parsed.sent : {};
96
+ } catch { return {}; }
97
+ }
98
+
99
+ // Record the days sent, dropping any older than the store's window.
100
+ function writeSent(file, sent, newDays, today) {
101
+ const oldest = daysAgo(today, WINDOW_DAYS + 1);
102
+ const next = {};
103
+ for (const d of [...Object.keys(sent), ...newDays]) if (d >= oldest) next[d] = true;
104
+ fs.mkdirSync(path.dirname(file), { recursive: true });
105
+ const tmp = `${file}.${process.pid}.tmp`;
106
+ fs.writeFileSync(tmp, JSON.stringify({ sent: next }, null, 2));
107
+ fs.renameSync(tmp, file);
108
+ }
109
+
110
+ async function main(argv = process.argv.slice(2), {
111
+ log = console.log, request, tallyFile, sentFile, today = utcDay(),
112
+ } = {}) {
113
+ const quiet = argv.includes('--quiet');
114
+ const dryRun = argv.includes('--dry-run');
115
+ const say = (m) => { if (!quiet) log(m); };
116
+ const { configPath } = require('../../src/instance-config');
117
+ const tFile = tallyFile || configPath(FILE_NAME);
118
+ const sFile = sentFile || configPath(SENT_FILE);
119
+
120
+ const days = readTally(tFile);
121
+ const sent = readSent(sFile);
122
+ if (!pendingDays(days, sent, today).length) { say('module error counts: nothing to send.'); return 0; }
123
+
124
+ let call = request;
125
+ if (!call) {
126
+ try { call = require('./module').storeRequest(); }
127
+ catch { say('module error counts: no Bongos sign-in on this machine, so nothing was sent.'); return 0; }
128
+ }
129
+
130
+ let out;
131
+ try { out = await sendPending({ days, sent, today, call, dryRun }); }
132
+ catch (e) { say(`module error counts: the store could not be reached (${e.message}); will try again later.`); return 1; }
133
+ if (out.sentDays.length) writeSent(sFile, sent, out.sentDays, today);
134
+
135
+ if (out.optedOut) say('module error counts: reporting is switched off in your Settings, so nothing was sent.');
136
+ else say(`module error counts: ${dryRun ? 'would send' : 'sent'} ${out.posted} count(s) for ${out.pending.length} day(s); ${out.skipped} skipped (not a store module you hold)${out.retryLater ? '; some will be retried later' : ''}.`);
137
+ return out.retryLater ? 1 : 0;
138
+ }
139
+
140
+ module.exports = { pendingDays, sendPending, main };
141
+
142
+ if (require.main === module) {
143
+ // Set the exit code and drain, never a hard exit after a request (task 1002734).
144
+ Promise.resolve(main()).then(
145
+ (code) => { process.exitCode = code; },
146
+ (e) => { console.error(e && e.message ? e.message : e); process.exitCode = 1; },
147
+ );
148
+ }
@@ -1350,6 +1350,7 @@ async function main(argv) {
1350
1350
 
1351
1351
  module.exports = {
1352
1352
  main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
1353
+ storeRequest,
1353
1354
  gatherCatalog, formatCatalog, ownProjectName,
1354
1355
  scaffoldModule, gatherUpgrade, evaluateUpgrade, gatherUpdates, evaluateUpdates, buildManifest,
1355
1356
  formatMaintenanceHint, isPastRemoveAfter,
@@ -0,0 +1,63 @@
1
+ // scripts/gds/provision-caps.js — the resource caps every hosted project's service carries
2
+ // (task 1004414). A cloud-host project runs its owner's code on the box that also serves the
3
+ // hub, and ADR 0354 lets any signed-in builder host one, so a runaway project must not be
4
+ // able to starve the others or the hub.
5
+ //
6
+ // The numbers are the owner's (2026-10-15), sized from ADR 0285's measurement: a project
7
+ // idles at ~45-70 MB and peaks at 118-134 MB at boot (page cache included), and uses a
8
+ // fraction of a percent of one core. MemoryHigh throttles and reclaims before MemoryMax
9
+ // kills; a killed project comes back through the unit's Restart=always. Too tight a cap
10
+ // takes a healthy project down, so change these only as a decision.
11
+ //
12
+ // A NEW unit renders them (serviceUnit). An EXISTING unit takes them with `systemctl
13
+ // set-property`, which applies live and persists a drop-in under /etc/systemd/system.control/,
14
+ // so nothing restarts and no unit file is rewritten — rewriting would move a project made
15
+ // before the per-instance accounts (task 1003369) onto an account that does not exist.
16
+ // systemd only: the supervisord image has no cgroups to cap.
17
+
18
+ 'use strict';
19
+
20
+ const RESOURCE_CAPS = Object.freeze({
21
+ MemoryHigh: '160M',
22
+ MemoryMax: '256M',
23
+ CPUQuota: '50%',
24
+ TasksMax: '256',
25
+ });
26
+
27
+ function capDirectives() {
28
+ return Object.entries(RESOURCE_CAPS).map(([key, value]) => `${key}=${value}`);
29
+ }
30
+
31
+ // The slug is interpolated into a root shell command, so it is held to the create route's
32
+ // charset here too rather than trusted from the row.
33
+ const SAFE_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
34
+
35
+ function setCapsCmd(inst, { sudoP = '' } = {}) {
36
+ if (!SAFE_SLUG.test(String(inst.slug))) throw new Error(`refusing to cap an unsafe slug: ${JSON.stringify(inst.slug)}`);
37
+ return `${sudoP}systemctl set-property ${inst.slug}.service ${capDirectives().join(' ')}`;
38
+ }
39
+
40
+ // `provision.js apply-caps [--apply]` — cap every running hosted project now. A cloud-host
41
+ // always runs on the control plane (capacity.placementBox), so this uses controlExec. One
42
+ // project that refuses never stops the rest; the result names it.
43
+ async function cmdApplyCaps(deps) {
44
+ const { db, log, privileged } = deps;
45
+ const exec = deps.controlExec || deps.exec;
46
+ const sudoP = privileged ? 'sudo ' : '';
47
+ const { rows } = await db.query(
48
+ `SELECT slug FROM provisioning_instances
49
+ WHERE hosting_shape = 'cloud-host' AND status = 'active'
50
+ ORDER BY slug`);
51
+ const capped = [], failed = [];
52
+ for (const { slug } of rows) {
53
+ // makeExec answers an allowFail'd failure ok:true + softFailed:true (softFailResult)
54
+ let r;
55
+ try { r = exec(setCapsCmd({ slug }, { sudoP }), { allowFail: true }); } catch { r = null; }
56
+ (!r || r.ok === false || r.softFailed ? failed : capped).push(slug);
57
+ }
58
+ log(`${deps.apply ? 'capped' : 'would cap (dry-run — add --apply)'} ${capped.length} of ${rows.length} active hosted project(s) at ${capDirectives().join(' ')}`);
59
+ if (failed.length) log(` ✖ could not cap: ${failed.join(', ')} — re-run once each unit is loaded`);
60
+ return { ok: failed.length === 0, capped, failed };
61
+ }
62
+
63
+ module.exports = { RESOURCE_CAPS, capDirectives, setCapsCmd, cmdApplyCaps };
@@ -59,6 +59,9 @@ const systemd = {
59
59
  exec(`${sudoP}rm -f ${serviceUnitPath(inst)}`, { allowFail: true });
60
60
  exec(`${sudoP}systemctl disable --now ${backupUnitName(inst)}.timer`, { allowFail: true });
61
61
  exec(`${sudoP}rm -f ${backupServicePath(inst)} ${backupTimerPath(inst)}`, { allowFail: true });
62
+ // the caps drop-in `provision.js apply-caps` wrote (task 1004414, provision-caps.js)
63
+ exec(`${sudoP}rm -f /etc/systemd/system.control/${inst.slug}.service.d/50-*.conf`, { allowFail: true });
64
+ exec(`${sudoP}rmdir /etc/systemd/system.control/${inst.slug}.service.d`, { allowFail: true });
62
65
  exec(`${sudoP}systemctl daemon-reload`, { allowFail: true });
63
66
  exec(`${sudoP}systemctl reset-failed ${inst.slug}.service ${backupUnitName(inst)}.service`, { allowFail: true });
64
67
  },
@@ -12,6 +12,7 @@ const fs = require('node:fs');
12
12
  const path = require('node:path');
13
13
  const { CONFIG, REPO_ROOT } = require('./provision-config.js');
14
14
  const { CORE_PKG_DIR, appUser, dbName, instanceDbRole, instanceStateDir, instanceUser, parseTargetRef, standaloneRoot } = require('./provision-repo.js');
15
+ const { capDirectives } = require('./provision-caps.js');
15
16
 
16
17
  function serviceUnit(inst, { etcBase = CONFIG.etcBase } = {}) {
17
18
  const envFile = path.posix.join(etcBase, inst.slug, 'web.env');
@@ -61,6 +62,8 @@ ProtectSystem=strict
61
62
  ProtectHome=read-only
62
63
  PrivateTmp=true
63
64
  ReadWritePaths=${stateDir} -${workDir}/config
65
+ # Resource caps (task 1004414; the numbers and their reasons are in provision-caps.js).
66
+ ${capDirectives().join('\n')}
64
67
 
65
68
  [Install]
66
69
  WantedBy=multi-user.target
@@ -1348,6 +1348,7 @@ Commands:
1348
1348
  teardown <slug> tear down one instance by slug
1349
1349
  reconcile re-enqueue instances stuck mid-flight with no open intent
1350
1350
  status list provisioned instances + estimated run-rate
1351
+ apply-caps cap every active hosted project's memory/CPU/tasks live (task 1004414)
1351
1352
  dns-check <domain> PREFLIGHT: verify the control-plane Cloudflare token can reach
1352
1353
  the domain's zone BEFORE the address step (network-only, no DB)
1353
1354
 
@@ -1397,7 +1398,7 @@ async function main() {
1397
1398
  process.exit(1);
1398
1399
  }
1399
1400
 
1400
- const known = ['run-intents', 'provision', 'teardown', 'reconcile', 'status'];
1401
+ const known = ['run-intents', 'provision', 'teardown', 'reconcile', 'status', 'apply-caps'];
1401
1402
  if (!known.includes(cmd)) { console.error(`unknown command: ${cmd}\n`); usage(); process.exit(2); }
1402
1403
  const slug = args[1] && !args[1].startsWith('--') ? args[1] : null;
1403
1404
  if ((cmd === 'provision' || cmd === 'teardown') && !slug) { console.error(`${cmd} needs a <slug>`); process.exit(2); }
@@ -1432,6 +1433,7 @@ async function main() {
1432
1433
  else if (cmd === 'teardown') { const inst = await provisioning.getInstanceBySlug(pool, slug); if (!inst) { console.error(`no instance with slug '${slug}'`); await pool.end().catch(() => {}); process.exit(1); } result = await teardownInstance(inst, deps); }
1433
1434
  else if (cmd === 'reconcile') result = await cmdReconcile(deps);
1434
1435
  else if (cmd === 'status') result = await cmdStatus(deps);
1436
+ else if (cmd === 'apply-caps') result = await require('./provision-caps.js').cmdApplyCaps(deps);
1435
1437
  } catch (e) {
1436
1438
  console.error('fatal:', e && e.message ? e.message : e);
1437
1439
  await pool.end().catch(() => {});
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.126'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.127'; // 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');