@bongos/core 1.19.709 → 1.19.711

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.
Files changed (35) hide show
  1. package/.bongos-core.json +74 -34
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +16 -0
  4. package/clients/bongos-client/index.cjs +16 -0
  5. package/clients/bongos-client/index.d.ts +24 -0
  6. package/clients/bongos-client/index.mjs +16 -0
  7. package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +2 -0
  8. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +2 -0
  9. package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +56 -0
  10. package/docs/adr/README.md +1 -0
  11. package/docs/api/openapi.json +506 -3
  12. package/docs/api-reference.md +14 -2
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/recipes/ops-gotchas.md +14 -0
  15. package/modules/agents/lib/answer-hold.js +91 -0
  16. package/modules/agents/lib/authoring.js +155 -0
  17. package/modules/agents/lib/fire-budget.js +109 -0
  18. package/modules/agents/lib/gate.js +64 -0
  19. package/modules/agents/routes/agents.js +494 -0
  20. package/modules/agents/spawn.js +12 -0
  21. package/modules/dev-box/app/src/vendor/bongos-client.cjs +16 -0
  22. package/modules/government/catalog.js +10 -0
  23. package/modules/government/migrations/government_013_agent_atoms.sql +66 -0
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/scripts/gds/agent-invoke.js +5 -27
  27. package/scripts/gds/provision-repo.js +181 -0
  28. package/scripts/gds/provision-units.js +33 -5
  29. package/scripts/gds/provision.js +35 -3
  30. package/src/bongos/pool.js +14 -0
  31. package/src/module-api.js +1 -1
  32. package/tests/agents_authoring.mjs +306 -0
  33. package/tests/agents_routes.mjs +76 -15
  34. package/tests/agents_write_routes.mjs +461 -0
  35. package/tests/provision.mjs +227 -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.19.709",
6
- "core_contract": "1.19.709",
7
- "source_commit": "e931b6db287584c48e0e0fbb4f302198e7ad7ae1",
5
+ "core_version": "1.19.711",
6
+ "core_contract": "1.19.711",
7
+ "source_commit": "51bef34ef27a7d94e516e2b1d10623eeebaf80a4",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-13T06:55:41.726Z",
9
+ "built_at": "2026-09-13T07:20:01.517Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 477,
12
+ "docs_redacted": 478,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2140,
14
+ "functional_verbatim": 2147,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2641,
20
- "tree_sha256": "38d60d3f6875f0ae881aeb4942d3a09b3aa7f9d0df66d7ec712d0ead28d6b3dd",
19
+ "file_count": 2649,
20
+ "tree_sha256": "15e6f8276b441266c13fefe89f515bf01b563021cd0be63afe1d36a9cc21ee5c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -367,12 +367,12 @@
367
367
  {
368
368
  "path": "clients/bongos-client/README.md",
369
369
  "mode": "0000644",
370
- "sha256": "cfa105831443d1543f03b217a1673b12ba54e40198d768b57bac1c10459ee35c"
370
+ "sha256": "16978a922f30386ac414172fb81d415ffe2d736e5f2ed2f2b638fb934bdb3f4f"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/bongos-client.global.js",
374
374
  "mode": "0000644",
375
- "sha256": "a418c0eace4c6335d4bd131ce33032b716bdd33f1b7b080c03b76db4259aa069"
375
+ "sha256": "119a98a27af60ad696d1898640f333c78a864b1d5a8c381483382bd03b2d4cff"
376
376
  },
377
377
  {
378
378
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -382,17 +382,17 @@
382
382
  {
383
383
  "path": "clients/bongos-client/index.cjs",
384
384
  "mode": "0000644",
385
- "sha256": "c36be893037b5c271ca116d9a26d66defcde55f1b06a1ea5b6a736b1997be0d4"
385
+ "sha256": "daaef512d980bd8fb84922595bd2e4e6cb863499c8b31b39aa00f45b32e5fe5d"
386
386
  },
387
387
  {
388
388
  "path": "clients/bongos-client/index.d.ts",
389
389
  "mode": "0000644",
390
- "sha256": "4fc17fc7a5a9ecdaa4539d99f6f30dde4a600043b24affd4677018c95fb94b17"
390
+ "sha256": "b824e52697930830717920889fd6a4bef66b3e18763b6acfae4f3dc9840463d3"
391
391
  },
392
392
  {
393
393
  "path": "clients/bongos-client/index.mjs",
394
394
  "mode": "0000644",
395
- "sha256": "83c4a4fc8f4adccb95f15c1943cc40eb6c29d8027c58269271313bb37dbad46c"
395
+ "sha256": "b1de013d0d46b6510323cc1d1d218c373ee9913e70b2738fe12507e8707ea707"
396
396
  },
397
397
  {
398
398
  "path": "clients/bongos-client/package.json",
@@ -1132,7 +1132,7 @@
1132
1132
  {
1133
1133
  "path": "docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md",
1134
1134
  "mode": "0000644",
1135
- "sha256": "1f8311bbe45204ee6f049024e3630cd0586c9bed44c78d4dc1f870b0c0c2381c"
1135
+ "sha256": "1b144566a9166db2a54c07241970d12621273e29691f799799fc5b6a39b3c440"
1136
1136
  },
1137
1137
  {
1138
1138
  "path": "docs/adr/0129-cloudbongos-control-plane-pull-deploy.md",
@@ -1212,7 +1212,7 @@
1212
1212
  {
1213
1213
  "path": "docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md",
1214
1214
  "mode": "0000644",
1215
- "sha256": "53488f1ff783c49467dd7f9bcf17f5a35c8e3161e2840a89ac5f3f571f7471d9"
1215
+ "sha256": "0eaedcf642882a857024cf68e900ecfca457ba379150a8afbd16a374c4a16c82"
1216
1216
  },
1217
1217
  {
1218
1218
  "path": "docs/adr/0146-configurable-reward-mode-cost-plus-only.md",
@@ -1914,20 +1914,25 @@
1914
1914
  "mode": "0000644",
1915
1915
  "sha256": "630f6460b3484f4cc629cce9c431f824bb8363d30a249db9e21e89863995c7ac"
1916
1916
  },
1917
+ {
1918
+ "path": "docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md",
1919
+ "mode": "0000644",
1920
+ "sha256": "96d78083fc4b04b8bdce61dee59e05465ec273759cfac325b160f68c8e5184a4"
1921
+ },
1917
1922
  {
1918
1923
  "path": "docs/adr/README.md",
1919
1924
  "mode": "0000644",
1920
- "sha256": "49273e63c50c201cd70200a4f0db7f233e496827823adb9449638eb5a0c1d030"
1925
+ "sha256": "dd5c5de0fb4c01c355d4adc9cd550717bc6d6c074bedaf6e05fbbd0385fd9138"
1921
1926
  },
1922
1927
  {
1923
1928
  "path": "docs/api-reference.md",
1924
1929
  "mode": "0000644",
1925
- "sha256": "2e05ec4efeab7341561a2230e880f6f34d55ada2d542e53b3dcf87f1efa2062c"
1930
+ "sha256": "0083726ede405f32fedae34b5697155d50dc6fb93afcdd5eb2bee3868dafff7b"
1926
1931
  },
1927
1932
  {
1928
1933
  "path": "docs/api/openapi.json",
1929
1934
  "mode": "0000644",
1930
- "sha256": "7263a9c74173ef0ca46a444310156f5d901ad4bb191fbd181f690387171d755c"
1935
+ "sha256": "3f0d26c7344f6eb9f0041e15d27d2d21faec7318f4555e7996a67bb735bcb880"
1931
1936
  },
1932
1937
  {
1933
1938
  "path": "docs/architecture.md",
@@ -2802,7 +2807,7 @@
2802
2807
  {
2803
2808
  "path": "docs/module-api-changelog.md",
2804
2809
  "mode": "0000644",
2805
- "sha256": "ebca814eb4d786c2958e44b166b23986ef624b9145bc9c5a6b5a0068ef05a411"
2810
+ "sha256": "aaf80c762f49609d71b0aafa23ac9b278a869d2f76e3b3901fee41d65956dd70"
2806
2811
  },
2807
2812
  {
2808
2813
  "path": "docs/modules-contract.md",
@@ -2992,7 +2997,7 @@
2992
2997
  {
2993
2998
  "path": "docs/recipes/ops-gotchas.md",
2994
2999
  "mode": "0000644",
2995
- "sha256": "d9956abefe8496cbbb8bbc94361431d40823c3a9d1fa8f4145e8b48999b54672"
3000
+ "sha256": "dadf37b7916c96aeb6e5a1d068a8b59b7b73648dbf6816f949f63fe1220fad16"
2996
3001
  },
2997
3002
  {
2998
3003
  "path": "docs/recipes/overnight-builder.md",
@@ -3864,6 +3869,26 @@
3864
3869
  "mode": "0000644",
3865
3870
  "sha256": "75a3d09bce8a40b51015054b4f4c77706ce76e7f8cb3fc4a54048c9bd33814b8"
3866
3871
  },
3872
+ {
3873
+ "path": "modules/agents/lib/answer-hold.js",
3874
+ "mode": "0000644",
3875
+ "sha256": "578aa4db41e3429cfb282501ddc4c43629499ce1e84bd26b4b3e4044d44151b5"
3876
+ },
3877
+ {
3878
+ "path": "modules/agents/lib/authoring.js",
3879
+ "mode": "0000644",
3880
+ "sha256": "d6788d803b70195f4d9b8577f36ee3bc296037b43d4e4b299b0ce6ba242e485b"
3881
+ },
3882
+ {
3883
+ "path": "modules/agents/lib/fire-budget.js",
3884
+ "mode": "0000644",
3885
+ "sha256": "c49bf5fc988cc1cc36a73e696dc96025e172bde99fa6f1c0d92b9e928c6d3922"
3886
+ },
3887
+ {
3888
+ "path": "modules/agents/lib/gate.js",
3889
+ "mode": "0000644",
3890
+ "sha256": "b9e298e98c6fc3b29854728e33963cef43d5b289064a24a2c639a14d8a3a1b92"
3891
+ },
3867
3892
  {
3868
3893
  "path": "modules/agents/lib/validate.js",
3869
3894
  "mode": "0000644",
@@ -3882,12 +3907,12 @@
3882
3907
  {
3883
3908
  "path": "modules/agents/routes/agents.js",
3884
3909
  "mode": "0000644",
3885
- "sha256": "31fbc1118ac1f315babb9ba3d0ab4ae634bdce3a6e83cb79defdad73288b2e00"
3910
+ "sha256": "261914d81f3b6cfb9bdb73dbcddf97924577d9ffd4ec61e9ca28d9ce623dd404"
3886
3911
  },
3887
3912
  {
3888
3913
  "path": "modules/agents/spawn.js",
3889
3914
  "mode": "0000644",
3890
- "sha256": "4e92375cbedf3f782583af007314e46262d5776c4c7dc67dc7ff6abda0884ea3"
3915
+ "sha256": "6aa77a39408386f5a08a62204a8cf8436420c61a8a9d7d8253c1b18a2ad88847"
3891
3916
  },
3892
3917
  {
3893
3918
  "path": "modules/autonomy/CLAUDE.md",
@@ -4267,7 +4292,7 @@
4267
4292
  {
4268
4293
  "path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
4269
4294
  "mode": "0000644",
4270
- "sha256": "c36be893037b5c271ca116d9a26d66defcde55f1b06a1ea5b6a736b1997be0d4"
4295
+ "sha256": "daaef512d980bd8fb84922595bd2e4e6cb863499c8b31b39aa00f45b32e5fe5d"
4271
4296
  },
4272
4297
  {
4273
4298
  "path": "modules/dev-box/app/test/unit.mjs",
@@ -4517,7 +4542,7 @@
4517
4542
  {
4518
4543
  "path": "modules/government/catalog.js",
4519
4544
  "mode": "0000644",
4520
- "sha256": "a4bf9be1eadcd77a5a8705208b6d848ec670e9838f48c70899941e1d888aeb3b"
4545
+ "sha256": "e4729cd6482c08f28bbb0f2de2a1b84c7df38747590af9a287d6c58f2d1cc672"
4521
4546
  },
4522
4547
  {
4523
4548
  "path": "modules/government/config.js",
@@ -4629,6 +4654,11 @@
4629
4654
  "mode": "0000644",
4630
4655
  "sha256": "e963028286a7f8a0a55c9fae2f73f203a6ae7c7770f73a265aefdcc026829625"
4631
4656
  },
4657
+ {
4658
+ "path": "modules/government/migrations/government_013_agent_atoms.sql",
4659
+ "mode": "0000644",
4660
+ "sha256": "f1e5ae5cef3f2e9d4196ea6c3f1c436bb4411d5492ca6c42c9b876f8575f754e"
4661
+ },
4632
4662
  {
4633
4663
  "path": "modules/government/module.json",
4634
4664
  "mode": "0000644",
@@ -7787,12 +7817,12 @@
7787
7817
  {
7788
7818
  "path": "package-lock.json",
7789
7819
  "mode": "0000644",
7790
- "sha256": "9e9bbca8f0efb3bed421d30ad97be1b18edf775e80cbc07590a583b2452ea609"
7820
+ "sha256": "048c83c4d1ec0e681d20c4c8da8b65a96e17ddbf88042082e109a0b0fd52e287"
7791
7821
  },
7792
7822
  {
7793
7823
  "path": "package.json",
7794
7824
  "mode": "0000644",
7795
- "sha256": "eb6d8897c8a65579eaee354a12435b412606029b3f182f192cf5628941580075"
7825
+ "sha256": "098001e183920a73e0417d67b50b869c8d381a6d76ef884933d6872fb66e063d"
7796
7826
  },
7797
7827
  {
7798
7828
  "path": "public-docs/index.html",
@@ -7867,7 +7897,7 @@
7867
7897
  {
7868
7898
  "path": "scripts/gds/agent-invoke.js",
7869
7899
  "mode": "0000644",
7870
- "sha256": "ee5b3a261ff1821d81f8d9c44ec3b319cd1c8c6681e28547b4bddb802a6bda56"
7900
+ "sha256": "8996e98b41d7b839ba30d270628cc6ee86a6d302a8898f2339b437b1af462b32"
7871
7901
  },
7872
7902
  {
7873
7903
  "path": "scripts/gds/agents-sync.js",
@@ -8607,17 +8637,17 @@
8607
8637
  {
8608
8638
  "path": "scripts/gds/provision-repo.js",
8609
8639
  "mode": "0000644",
8610
- "sha256": "c87cccdd63ac6fa6d38771c3ad178e5e919f8bee62ad769b9d127819f84e2750"
8640
+ "sha256": "3612ebe8f54da7498a6c4998e0d1d1c4596a80d54f70b5543b4a46dfca2ffbcf"
8611
8641
  },
8612
8642
  {
8613
8643
  "path": "scripts/gds/provision-units.js",
8614
8644
  "mode": "0000644",
8615
- "sha256": "55c394bb932966cd2c53ab44e34d7fc23e855c1157c185456c5a630d8c3e62e1"
8645
+ "sha256": "a59615231f2843a11cf2d9d2f6f07c7aa2dacb6e83fb6126fdfeae9cfdc19abd"
8616
8646
  },
8617
8647
  {
8618
8648
  "path": "scripts/gds/provision.js",
8619
8649
  "mode": "0000644",
8620
- "sha256": "82fb65fea5dd9a33770690c902680cb428eb74b1c2abdaeab7f89f444ae1c11f"
8650
+ "sha256": "007c5cfa3b42567df0016038fc7ec5c7dbdab60ec6ec8ecd9d06386933e5641d"
8621
8651
  },
8622
8652
  {
8623
8653
  "path": "scripts/gds/publish-credential-check.js",
@@ -9412,7 +9442,7 @@
9412
9442
  {
9413
9443
  "path": "src/bongos/pool.js",
9414
9444
  "mode": "0000644",
9415
- "sha256": "c9edcf9cb1f027ca122388beabae488d2b771b9e7aee2cd12fbdb48ea6f0ded8"
9445
+ "sha256": "5c6121de5ba67f23b4609ddf06cad6fcda1dd5e19f09996616a49e23bcca38a1"
9416
9446
  },
9417
9447
  {
9418
9448
  "path": "src/bongos/prelaunch-gate.js",
@@ -9572,7 +9602,7 @@
9572
9602
  {
9573
9603
  "path": "src/module-api.js",
9574
9604
  "mode": "0000644",
9575
- "sha256": "5359ee8368381b16bb0d6e18a13e390dd9366b3a3cdc3965e749cb5746f5cc4f"
9605
+ "sha256": "f53e83493d825804b8b4813d3c22b649d640757d6d10ec5e0d7fd70101510a2a"
9576
9606
  },
9577
9607
  {
9578
9608
  "path": "src/module-loader/catalog.js",
@@ -9684,10 +9714,15 @@
9684
9714
  "mode": "0000644",
9685
9715
  "sha256": "9b90e0caab614f0751c251c5b5812ee273443a74e684d9156a7bffbb28ac7ee2"
9686
9716
  },
9717
+ {
9718
+ "path": "tests/agents_authoring.mjs",
9719
+ "mode": "0000644",
9720
+ "sha256": "eea8f9e0bdf2c84fabc81e16c30e4026a7a336c1e705d614e68b805957b9b74a"
9721
+ },
9687
9722
  {
9688
9723
  "path": "tests/agents_routes.mjs",
9689
9724
  "mode": "0000644",
9690
- "sha256": "4e07055707e77bffe2b60750273d16e92a6b213dbbc999e8ed7e0cc742915f7c"
9725
+ "sha256": "fe8268ed78d1b1c352cb402c47d54bb261501a9ac8626c4c6a38ea8fd09355fa"
9691
9726
  },
9692
9727
  {
9693
9728
  "path": "tests/agents_spawn.mjs",
@@ -9704,6 +9739,11 @@
9704
9739
  "mode": "0000644",
9705
9740
  "sha256": "82c7bcef316f5c0068cb53a43b2ef4a01367ff285939f132f01b0d8633d36353"
9706
9741
  },
9742
+ {
9743
+ "path": "tests/agents_write_routes.mjs",
9744
+ "mode": "0000644",
9745
+ "sha256": "5c413774b0757553ed3d55a9f8d3555c607207cda4065d443412a34ad4d601dc"
9746
+ },
9707
9747
  {
9708
9748
  "path": "tests/api_cli_flags.mjs",
9709
9749
  "mode": "0000644",
@@ -12142,7 +12182,7 @@
12142
12182
  {
12143
12183
  "path": "tests/provision.mjs",
12144
12184
  "mode": "0000644",
12145
- "sha256": "d70cc9bd673dce403787814849abbeef9ed0ea01f9e38a2d3288886879490830"
12185
+ "sha256": "e247471cb6b11000b0e2bcb41166c24adbbe0db902513832f5e40fe1ba655ef8"
12146
12186
  },
12147
12187
  {
12148
12188
  "path": "tests/provision_restart.mjs",
@@ -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
- - 365 operations across 56 resource groups
8
+ - 372 operations across 57 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -100,11 +100,27 @@ function createClient(opts = {}) {
100
100
  // GET /achievements — rank: any-builder — GET /achievements
101
101
  getAchievements: (args) => request("GET", "/achievements", { hasBody: false }, args),
102
102
  },
103
+ "agentRuns": {
104
+ // GET /agent-runs/{id} — rank: any-builder — GET /agent-runs/:id
105
+ getAgentRunsId: (args) => request("GET", "/agent-runs/{id}", { hasBody: false }, args),
106
+ },
103
107
  "agents": {
104
108
  // GET /agents — rank: any-builder — GET /agents
105
109
  getAgents: (args) => request("GET", "/agents", { hasBody: false }, args),
110
+ // POST /agents — rank: metic+archon — POST /agents
111
+ postAgents: (args) => request("POST", "/agents", { hasBody: true }, args),
112
+ // DELETE /agents/{name} — rank: metic+archon — DELETE /agents/:name
113
+ deleteAgentsName: (args) => request("DELETE", "/agents/{name}", { hasBody: false }, args),
106
114
  // GET /agents/{name} — rank: any-builder — GET /agents/:name
107
115
  getAgentsName: (args) => request("GET", "/agents/{name}", { hasBody: false }, args),
116
+ // PATCH /agents/{name} — rank: metic+archon — PATCH /agents/:name
117
+ patchAgentsName: (args) => request("PATCH", "/agents/{name}", { hasBody: true }, args),
118
+ // POST /agents/{name}/disable — rank: metic+archon — POST /agents/:name/disable
119
+ postAgentsNameDisable: (args) => request("POST", "/agents/{name}/disable", { hasBody: true }, args),
120
+ // POST /agents/{name}/enable — rank: metic+archon — POST /agents/:name/enable
121
+ postAgentsNameEnable: (args) => request("POST", "/agents/{name}/enable", { hasBody: true }, args),
122
+ // POST /agents/{name}/invoke — rank: any-builder — POST /agents/:name/invoke
123
+ postAgentsNameInvoke: (args) => request("POST", "/agents/{name}/invoke", { hasBody: true }, args),
108
124
  },
109
125
  "analytics": {
110
126
  // GET /analytics/builder/{id} — rank: any-builder — GET /analytics/builder/:id
@@ -99,11 +99,27 @@ function createClient(opts = {}) {
99
99
  // GET /achievements — rank: any-builder — GET /achievements
100
100
  getAchievements: (args) => request("GET", "/achievements", { hasBody: false }, args),
101
101
  },
102
+ "agentRuns": {
103
+ // GET /agent-runs/{id} — rank: any-builder — GET /agent-runs/:id
104
+ getAgentRunsId: (args) => request("GET", "/agent-runs/{id}", { hasBody: false }, args),
105
+ },
102
106
  "agents": {
103
107
  // GET /agents — rank: any-builder — GET /agents
104
108
  getAgents: (args) => request("GET", "/agents", { hasBody: false }, args),
109
+ // POST /agents — rank: metic+archon — POST /agents
110
+ postAgents: (args) => request("POST", "/agents", { hasBody: true }, args),
111
+ // DELETE /agents/{name} — rank: metic+archon — DELETE /agents/:name
112
+ deleteAgentsName: (args) => request("DELETE", "/agents/{name}", { hasBody: false }, args),
105
113
  // GET /agents/{name} — rank: any-builder — GET /agents/:name
106
114
  getAgentsName: (args) => request("GET", "/agents/{name}", { hasBody: false }, args),
115
+ // PATCH /agents/{name} — rank: metic+archon — PATCH /agents/:name
116
+ patchAgentsName: (args) => request("PATCH", "/agents/{name}", { hasBody: true }, args),
117
+ // POST /agents/{name}/disable — rank: metic+archon — POST /agents/:name/disable
118
+ postAgentsNameDisable: (args) => request("POST", "/agents/{name}/disable", { hasBody: true }, args),
119
+ // POST /agents/{name}/enable — rank: metic+archon — POST /agents/:name/enable
120
+ postAgentsNameEnable: (args) => request("POST", "/agents/{name}/enable", { hasBody: true }, args),
121
+ // POST /agents/{name}/invoke — rank: any-builder — POST /agents/:name/invoke
122
+ postAgentsNameInvoke: (args) => request("POST", "/agents/{name}/invoke", { hasBody: true }, args),
107
123
  },
108
124
  "analytics": {
109
125
  // GET /analytics/builder/{id} — rank: any-builder — GET /analytics/builder/:id
@@ -5,6 +5,7 @@ export declare const DEFAULT_BASE_URL: string;
5
5
 
6
6
  // Request-body + shared types, generated from the OpenAPI components.schemas.
7
7
  export interface ApiErrorObject { code: string; message: string; details?: unknown }
8
+ export interface DeleteAgentsNameResponse { ok: boolean; deleted: unknown }
8
9
  export interface DeleteBoxSshKeyIdResponse { ok: boolean; deleted: unknown }
9
10
  export interface DeleteConnectionsIdResponse { outcome: unknown }
10
11
  export interface DeleteDependenciesResponse { ok: boolean }
@@ -19,6 +20,7 @@ export interface ErrorEnvelope { error: ApiErrorObject }
19
20
  export interface GetAccessRequestsResponse { requests: unknown }
20
21
  export interface GetAccessRequestsStatusResponse { github_login: unknown; status: unknown; admitted: unknown }
21
22
  export interface GetAchievementsResponse { achievements: unknown }
23
+ export interface GetAgentRunsIdResponse { ok: boolean; run: unknown }
22
24
  export interface GetAgentsNameResponse { ok: boolean; agent: unknown }
23
25
  export interface GetAgentsResponse { ok: boolean; agents: unknown; counts: unknown; vocabulary: unknown }
24
26
  export interface GetAuditLogResponse { rows: unknown; page: unknown }
@@ -146,6 +148,7 @@ export interface GetVersionsResponse { versions: unknown; page: unknown }
146
148
  export interface PageInfo { limit: number; offset: number; total?: number }
147
149
  export interface PatchAccessRequestsIdRequest { status: string }
148
150
  export interface PatchAccessRequestsIdResponse { ok: boolean; request: unknown; unchanged?: boolean }
151
+ export interface PatchAgentsNameResponse { ok: boolean; agent: unknown }
149
152
  export interface PatchBoxesBuilderIdBlockRequest { blocked: boolean; reason?: string; close_current?: boolean }
150
153
  export interface PatchBoxesBuilderIdBlockResponse { ok: boolean; blocked: unknown; close_queued: unknown }
151
154
  export interface PatchBuildersIdBudgetRequest { monthly_budget_usd?: unknown }
@@ -191,6 +194,11 @@ export interface PostAccessRequestsInviteRequest { github_login: string }
191
194
  export interface PostAccessRequestsInviteResponse { ok: boolean; already_invited?: boolean; github_login?: unknown; request?: unknown }
192
195
  export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
193
196
  export interface PostAccessRequestsResponse { ok: boolean; request?: unknown; already_pending?: boolean }
197
+ export interface PostAgentsNameDisableResponse { ok: boolean; agent: unknown }
198
+ export interface PostAgentsNameEnableResponse { ok: boolean; agent: unknown }
199
+ export interface PostAgentsNameInvokeRequest { input?: string }
200
+ export interface PostAgentsNameInvokeResponse { ok: boolean; run_id: unknown; poll: unknown; message: unknown }
201
+ export interface PostAgentsResponse { ok: boolean; agent: unknown }
194
202
  export interface PostAuthAppPairApproveRequest { pair_code: string }
195
203
  export interface PostAuthAppPairApproveResponse { ok: boolean }
196
204
  export interface PostAuthAppPairPollRequest { poll_token: string }
@@ -436,11 +444,27 @@ export interface BongosClient {
436
444
  /** GET /achievements — rank: any-builder */
437
445
  getAchievements(args?: RequestArgs): Promise<GetAchievementsResponse>;
438
446
  };
447
+ "agentRuns": {
448
+ /** GET /agent-runs/{id} — rank: any-builder */
449
+ getAgentRunsId(args?: RequestArgs): Promise<GetAgentRunsIdResponse>;
450
+ };
439
451
  "agents": {
440
452
  /** GET /agents — rank: any-builder */
441
453
  getAgents(args?: RequestArgs): Promise<GetAgentsResponse>;
454
+ /** POST /agents — rank: metic+archon */
455
+ postAgents(args?: RequestArgs): Promise<PostAgentsResponse>;
456
+ /** DELETE /agents/{name} — rank: metic+archon */
457
+ deleteAgentsName(args?: RequestArgs): Promise<DeleteAgentsNameResponse>;
442
458
  /** GET /agents/{name} — rank: any-builder */
443
459
  getAgentsName(args?: RequestArgs): Promise<GetAgentsNameResponse>;
460
+ /** PATCH /agents/{name} — rank: metic+archon */
461
+ patchAgentsName(args?: RequestArgs): Promise<PatchAgentsNameResponse>;
462
+ /** POST /agents/{name}/disable — rank: metic+archon */
463
+ postAgentsNameDisable(args?: RequestArgs): Promise<PostAgentsNameDisableResponse>;
464
+ /** POST /agents/{name}/enable — rank: metic+archon */
465
+ postAgentsNameEnable(args?: RequestArgs): Promise<PostAgentsNameEnableResponse>;
466
+ /** POST /agents/{name}/invoke — rank: any-builder */
467
+ postAgentsNameInvoke(args?: RequestArgs & { body?: PostAgentsNameInvokeRequest }): Promise<PostAgentsNameInvokeResponse>;
444
468
  };
445
469
  "analytics": {
446
470
  /** GET /analytics/builder/{id} — rank: any-builder */
@@ -96,11 +96,27 @@ export function createClient(opts = {}) {
96
96
  // GET /achievements — rank: any-builder — GET /achievements
97
97
  getAchievements: (args) => request("GET", "/achievements", { hasBody: false }, args),
98
98
  },
99
+ "agentRuns": {
100
+ // GET /agent-runs/{id} — rank: any-builder — GET /agent-runs/:id
101
+ getAgentRunsId: (args) => request("GET", "/agent-runs/{id}", { hasBody: false }, args),
102
+ },
99
103
  "agents": {
100
104
  // GET /agents — rank: any-builder — GET /agents
101
105
  getAgents: (args) => request("GET", "/agents", { hasBody: false }, args),
106
+ // POST /agents — rank: metic+archon — POST /agents
107
+ postAgents: (args) => request("POST", "/agents", { hasBody: true }, args),
108
+ // DELETE /agents/{name} — rank: metic+archon — DELETE /agents/:name
109
+ deleteAgentsName: (args) => request("DELETE", "/agents/{name}", { hasBody: false }, args),
102
110
  // GET /agents/{name} — rank: any-builder — GET /agents/:name
103
111
  getAgentsName: (args) => request("GET", "/agents/{name}", { hasBody: false }, args),
112
+ // PATCH /agents/{name} — rank: metic+archon — PATCH /agents/:name
113
+ patchAgentsName: (args) => request("PATCH", "/agents/{name}", { hasBody: true }, args),
114
+ // POST /agents/{name}/disable — rank: metic+archon — POST /agents/:name/disable
115
+ postAgentsNameDisable: (args) => request("POST", "/agents/{name}/disable", { hasBody: true }, args),
116
+ // POST /agents/{name}/enable — rank: metic+archon — POST /agents/:name/enable
117
+ postAgentsNameEnable: (args) => request("POST", "/agents/{name}/enable", { hasBody: true }, args),
118
+ // POST /agents/{name}/invoke — rank: any-builder — POST /agents/:name/invoke
119
+ postAgentsNameInvoke: (args) => request("POST", "/agents/{name}/invoke", { hasBody: true }, args),
104
120
  },
105
121
  "analytics": {
106
122
  // GET /analytics/builder/{id} — rank: any-builder — GET /analytics/builder/:id
@@ -39,6 +39,8 @@ Add a single config knob, **`PROVISION_SUDO=1`** (alias `PROVISION_PRIVILEGED=1`
39
39
  - **The dedicated-droplet cloud-init path is deliberately unchanged** (`dedicatedUserData`). It runs as root on a fresh droplet — a different privilege + file-perms model — and is out of the co-tenant/standalone scope this task exercised; escalating it is a separate change.
40
40
  - **Follow-ups noted from the standup:** the live `/etc/caddy/Caddyfile` drifts from `infra/Caddyfile` with no auto-sync (a deploy-time reconcile is worth considering); the migrate-as-postgres readable-checkout precondition should be asserted by the box provisioning, not just documented.
41
41
 
42
+ > **Followed up 2026-09-12 by [ADR 0281](<redacted>.md)** (task [1003369](https://cloudbongos.com/builders#/task/1003369)), which takes the last alternative below from the other end: the *instance* now holds per-instance DB credentials (its own password-authed role) rather than the runner holding app creds. **This ADR is unchanged** — the runner still escalates via `sudo -u postgres` for `createdb`/`migrate`, and `PROVISION_SUDO` still means exactly what it means here.
43
+
42
44
  ## Alternatives considered
43
45
 
44
46
  - **Grant `lars` a PG login role + writable `/etc`.** Rejected: non-portable per-box setup, and it weakens the trust boundary — the runner shouldn't *own* `/etc` or be a standing PG superuser; `sudo -u postgres` is scoped to the two DB commands.
@@ -21,6 +21,8 @@ The co-tenant instances that federate on cloudbongos.com subdomains today (e.g.
21
21
 
22
22
  A `standalone` runs **on the hub box** ([scripts/gds/provision.js](../../scripts/gds/provision.js): `standaloneBase`, control-plane-local IP) as the **same UID as the hub** (`serviceUnit` hardcodes `User=lars`), and its deploy runs `npm ci` (not `--ignore-scripts`) in a **passwordless-sudo-capable** runner context. A malicious owner could ship a `postinstall` and get code execution as `lars` with sudo on the hub box → the hub's signing key, every instance's secrets, the hub DB. This is a pre-existing property of running owner code on the hub box at all, independent of federation, but any free "run your code for you" tier would expose it at scale.
23
23
 
24
+ > **Amended 2026-09-12 ([ADR 0281](<redacted>.md), task [1003369](https://cloudbongos.com/builders#/task/1003369)):** `serviceUnit` no longer hardcodes `User=lars` — each provisioned instance now runs as its own unix account with its own password-authed PG role, so the *running* instance is no longer the same UID as the hub. **Finding 2 stands regardless:** the exposure it names is the DEPLOY, not the steady state — `npm ci` still runs owner `postinstall` code in the passwordless-sudo-capable runner context, before the per-instance account is ever involved. The decision below is unchanged.
25
+
24
26
  The federation protocol itself is sound: the hub binds the minted audience to the instance's exact origin (the `#1002108` fix; asymmetric Ed25519 so one instance can't forge for another). A leaked per-instance client secret is contained to its own origin. The weakness is **placement** (shared cookie domain) and **execution** (owner code on the hub box), not the protocol.
25
27
 
26
28
  ## Decision
@@ -0,0 +1,56 @@
1
+ # ADR 0281 — An instance's identity is its own unix account and its own password-authed Postgres role
2
+
3
+ **Status:** Accepted — implemented 2026-09-12 (task [1003369](https://cloudbongos.com/builders#/task/1003369) · goal 1000094, audit ref `B3` of the 2026-08-29 security audit).
4
+
5
+ ## Context
6
+
7
+ [ADR 0111](<redacted>.md) made instance standup automatic and [ADR 0128](<redacted>.md) settled how the *runner* escalates to perform it. Neither settled who the *instance* is once it is running, and the answer was: everybody is the same person.
8
+
9
+ `serviceUnit` hardcoded `User=lars` / `Group=lars`, and **co-tenant is the default shape** — so on a shared box every instance's Node process ran under one uid. Two consequences, both reachable by any code an instance runs, neither of them a bug in any route:
10
+
11
+ - **Sibling environments.** A unix user may read `/proc/<pid>/environ` of its own processes. Every instance's `EnvironmentFile` carries `GITHUB_APP_PRIVATE_KEY`, `<PREFIX>_HUB_CLIENT_SECRET` and its OAuth client secret ([ADR 0141](<redacted>.md) §4, [ADR 0133](<redacted>.md)). One uid meant every instance could read every sibling's.
12
+ - **Sibling databases.** `src/bongos/pool.js` connected by **passwordless peer auth** over the local socket, with `PGDATABASE` supplied as a systemd `Environment=` default. Peer auth answers "which unix user are you", so any code running as `lars` could open a pool against **any** database on the box by naming it — including the control plane's own `builders` and `builder_sessions`.
13
+
14
+ The per-instance `web.env` (`chmod 600`) and the per-instance database were already correct. They were sound walls with one door, and every instance had the key.
15
+
16
+ ADR 0128 anticipated the DB half in its own alternatives: *"Do createdb/migrate via the app `DATABASE_URL` … Left open for a future instance that wires DB creds into the runner's box.env."* This is that change, from the other end — the instance gets credentials, not the runner.
17
+
18
+ ## Decision
19
+
20
+ **Each instance gets its own unix account and its own password-authenticated Postgres role, both derived from its slug, both created by the provision run.**
21
+
22
+ **Naming.** `instanceUser(inst)` → `bongos-<slug>`, folded to `bongos-<head>-<8 hex of sha256(slug)>` when the slug would push it past utmp's 32 characters. Prefixed, never the bare slug: a project may legally be called `postgres` or `lars`, and a bare-slug scheme would hand it an existing account. The hash suffix is not decoration — two slugs sharing a 16-character prefix would otherwise fold onto one account, which is precisely the shared identity being removed. An invalid slug **throws** rather than producing a name.
23
+
24
+ **The unix account** is a system account with `/usr/sbin/nologin` and a home at `/var/lib/<user>` — deliberately **not** under `/home`, because the unit sets `ProtectHome=read-only` and the server writes inside its config home on first boot (`secret-box.js` provisions `builder-secret.key` there). A `/home` account would make that write `EROFS` and the instance would come up unable to hold a secret.
25
+
26
+ **The unit** carries the hardening block the sibling backup unit already had — `ProtectSystem=strict`, `ProtectHome=read-only`, `PrivateTmp=true`, alongside the existing `NoNewPrivileges=true` — with `ReadWritePaths` as the entire allow-list: the state dir, plus an **optional** `-<workDir>/config` (the modules toggle writes `config/modules.json` at runtime; the `-` keeps a shape without one from failing to mount). **The checkout the service runs is deliberately not writable by it.**
27
+
28
+ **The Postgres role** is created or rotated per run: `LOGIN` with a password, owner of its own database, `REVOKE CONNECT … FROM PUBLIC` — and **not** `SUPERUSER`, `CREATEDB`, `CREATEROLE` or `BYPASSRLS`. The revoke is the half that isolates: Postgres grants `CONNECT` on every database to `PUBLIC` by default, so per-role passwords **alone** would leave a sibling one peer-auth hop from any other instance's data. The grants run in a **second** statement against the instance DB, **after** the migrate, because the privileged migrate runs as `postgres` and therefore owns every table it creates; `ALTER DEFAULT PRIVILEGES` aims the same grant forward so the next migration needs no second visit.
29
+
30
+ **The password** is minted per provision run (32 bytes, base64url) and never stored: the same run sets it on the role and writes it into the `0600` `web.env`, so **every provision rotates the credential** and no plaintext copy outlives the run outside that file. `pool.js` reads `PGUSER`/`PGPASSWORD` **explicitly** rather than leaving them to `pg`'s env defaults — the whole defect was a connection whose identity nothing in the code stated, so the identity is now stated where a test can assert it.
31
+
32
+ **Where the credential may travel is part of the decision, not an afterthought.** Two paths carry the rendered command, and both would have persisted the password somewhere this ADR cannot lock down:
33
+
34
+ - **The runner’s own log.** `makeExec`’s `run()` prints the raw command on both the dry-run and the success path, so the role DDL — which embeds the password — would land in the journal. The call site passes **`shown`**, the same mechanism `makeWriteFile` already uses for the `web.env` body, and `dbRoleCmdShown()` lives beside `dbRoleCmd` so a label cannot outlive the secret it hides. A `shown` call site also suppresses stderr, since a failing shell may echo the line back.
35
+ - **DigitalOcean cloud-init user-data.** DO **retains** user-data and serves it back through its API, its console and the droplet’s own metadata endpoint — a permanent second copy. So the dedicated shape takes **`peerOnly`**: the role, its DB ownership and the `PUBLIC` revoke are all still created, and only the password is omitted. A dedicated droplet is **single-tenant**, so the co-tenancy vector B3 describes does not exist on it, and the role peer-auths to its own unix account. Baking a credential into a retained blob to defend against siblings that are not there would be strictly worse than the thing it protects against.
36
+
37
+ **Absence is the old behavior.** No password ⇒ no `PGUSER`/`PGPASSWORD` lines in `web.env` and no keys in the pool config, so an instance provisioned before this keeps exactly the peer auth it had. This cannot brick a box mid-rollout.
38
+
39
+ ## Consequences
40
+
41
+ - **The two named vectors close.** `/proc/<pid>/environ` is readable only by the process's own uid, so sibling environments are gone; a sibling holds no password for another's role and `PUBLIC` no longer carries `CONNECT`, so sibling databases are gone.
42
+ - **The SQL contains no double quote, and that is load-bearing.** Every literal is dollar-quoted and every identifier goes through `format(%I)`, because these commands are embedded in the dedicated droplet's cloud-init as `- [ bash, -lc, "…" ]` — a YAML **double-quoted** scalar. A plain `"role"` identifier reads fine in `psql` and silently truncates the cloud-init line. `seedFirstVersionCmd` dollar-quotes for the same reason; a test now asserts the absence rather than leaving it to habit.
43
+ - **A partial, stated honestly: on-disk co-tenant reads.** The instance account is added to the app-user group (`usermod -aG`) so it can read the checkout it runs from — without it the unit fails at exec with `EACCES`. That grants exactly the read the app user's group already had, so **file-level** co-tenant separation is not what this change delivers; the process and database boundaries are. Narrowing it further means per-instance ownership of the checkout, which belongs with the shared-checkout question, not here.
44
+ - **A generated secret is only as private as the narrowest channel it crosses.** Both leaks above were found by the ship grade, not by the design, and neither was a typo: each was a call site that never opted into a redaction mechanism the codebase already had. The regression guard is written as an **invariant** rather than an assertion about one line — `provisionInstance` is run and *every* command embedding a dollar-quoted secret must carry a clean `shown` — so the next secret-bearing call site is caught by the same test.
45
+ - **Re-provision is the rotation path**, and it is idempotent throughout: `id -u` short-circuits an existing account, `install -d` re-asserts the state dir, and the role DDL upserts. A re-provision repairs a hand-edited box rather than failing on it.
46
+ - **`provision.js` sits two lines under the 1,500-line ratchet.** The generators and their full rationale live in `provision-repo.js`; the runner carries the wiring and a pointer. The next change there will have to move something out.
47
+ - **The non-privileged path is unchanged in kind.** The DDL escalates through the same `asPostgres()` helper ADR 0128 introduced, so a runner without `PROVISION_SUDO` renders the same commands without `sudo` — as broken, and no more broken, than it already was for `createdb`.
48
+
49
+ ## Alternatives considered
50
+
51
+ - **A `DATABASE_URL` in `web.env` instead of discrete `PG*` vars.** `pool.js` already prefers it, so it would have needed no pool change at all. Rejected: the socket path has to ride as `?host=…` or a percent-encoded authority, and the exact parse is `pg-connection-string`'s business rather than something the test could pin — trading a stated, assertable identity for a URL whose behavior we would be trusting.
52
+ - **Per-role passwords without `REVOKE CONNECT … FROM PUBLIC`.** Rejected on the mechanism: it reads like isolation and is not, because peer auth plus the default `PUBLIC` grant walks straight around the password.
53
+ - **`ProtectSystem=full`, or `strict` with the working directory writable.** Rejected: the first leaves everything outside `/usr` writable, and the second makes the code the service executes writable by the service — an obvious foothold in a change whose whole subject is containment.
54
+ - **Keeping `User=lars` and isolating with a second mechanism** (a PG role alone, or namespaces). Rejected: the shared uid *is* the `/proc` vector; nothing layered on top removes it.
55
+ - **Generating the dedicated droplet’s password on the droplet** (`openssl rand` inside a `runcmd`), instead of `peerOnly`. Rejected: it would have to be written into a `web.env` that cloud-init does not currently produce at all, so it buys a password for a single-tenant box by adding an untested file-writing leg to the one path ADR 0128 explicitly left alone.
56
+ - **Storing the generated password in `provisioning_instances`.** Rejected: it would put a live credential in the control-plane database in plaintext to buy stability the rotation does not need — the role is upserted and the env file rewritten in the same pass, so nothing needs to remember it.
@@ -372,3 +372,4 @@ This keeps the decision history honest and traceable.
372
372
  | 0278 | [**A gated project still takes applications, and the exemption is scoped to the verb** ([task 1003525](https://cloudbongos.com/builders#/task/1003525) · the apply write itself in [task 1003624](https://cloudbongos.com/builders#/task/1003624) · goal 1000106 — *Working area 1, Project creation*; owner decision 2026-09-11). A project's owner sets who may SEE it (`platformVisibility`, [ADR 0192](<redacted>.md)) and who may JOIN it (`joinability`, [ADR 0194](<redacted>.md)) independently — and set to their middle values, members-only AND apply-to-join, the project took no applications at all: the member door refused every cookie-less request with `401` before the public `POST <api>/access-requests` could answer, because that write was not on the exempt list. The two settings composed into **"nobody can apply"**, which nobody chose. It survived because nothing LIED about it — the hub's join box relayed the project's own `401` honestly as `members_only`, and the hall's landing, where the apply form lives, is itself behind the door; the composition was simply unreachable. Found by the R14 proof ([task 1002333](https://cloudbongos.com/builders#/task/1002333)). ADR 0192 §3 had fixed the exempt list at "the door, the manifest, the probes and the downloads — and nothing wider" and left widening it as an owner call, which is what this is. **Decision: yes — and BOTH halves are exempted, each scoped to one path and one verb.** `POST <api>/access-requests` (it grants nothing — an application is a row in a queue the owner still reviews, [ADR 0201](<redacted>.md), already public on every non-gated project) and `GET <api>/access-requests/status` (without it the answer is half an answer: `bongos login` cannot re-poll the device flow after a `not_approved` — the `device_code` is spent — so an applicant would file a request and then wait on an approval they can never observe). `EXEMPT` entries may now be `{ re, methods }` beside the bare `RegExp`s, and `isExempt(path, method)` takes the verb as an OPTIONAL second argument that **fails closed** for a scoped entry when none is given, so the one-argument static callers cannot accidentally widen. **The verb is load-bearing, not tidiness:** the bare `GET` on `<api>/access-requests` is the OWNER'S QUEUE (`requireBuilder` + `access_request.review`), the surface listing would-be builders by name with their vouch state — a path-only exemption would have silently taken the member door off the front of it, leaving one layer where there were two, and the queue's own `requirePermission` still holding is exactly what makes that loss easy to miss. **It opens no oracle the gate was closing:** the status route's boolean twin `GET <api>/auth/web/admission-status` is ALREADY reachable on a gated project inside the `auth/*` subtree §3 exempts whole (§3 records that cost in as many words), and the two share ONE per-IP budget on purpose ([ADR 0209](<redacted>.md)) so neither can be alternated against the other. What it DOES add, stated as the honest cost: applicant detail — `pending`/`dismissed`/`none` over the twin's bare `admitted`. Whether that answer should collapse is ADR 0209's still-open owner question and is deliberately NOT decided here. No hub change: `joinRelayOutcome` maps the RELAYED status, so it carries the project's real answer the moment the `401` stops. Rejected: "gated means gated" — hide *Apply to join* and say so in the manage blurb (coherent, and the call went the other way); exempting the path without the verb; exempting the write alone; collapsing the status response while the route happened to be open (that is how a deferred decision gets made by accident).](<redacted>.md) | project visibility / join door / member door |
373
373
  | 0279 | [**An upgrade is proven by the served version, not by a health check** ([task 1002884](https://cloudbongos.com/builders#/task/1002884) · goal 1000090 — *Working area 4, Bongos Core distribution*; from idea 1000682). On 2026-08-11 the auto-upgrade sweep printed `✓ upgrade complete — core 1.19.13 → 1.19.56`, wrote a success row to `core_upgrades` and exited 0 while live kept serving **1.19.13**. Three shipped checks formed a closed loop that could not see the failure they existed to catch: the `systemctl restart` failed with `Interactive authentication required` (a `User=` unit, no TTY) and `upgrade.js` treated it as a WARNING and fell through; `readInstalledCoreVersion()` then confirmed the version on **disk**, where `npm install` had correctly put it; and `pollHealth()` got a 200 from the **still-running old process**, because a health check confirms a port is served, never *what* serves it. A lying tool is worse than a broken one — nothing goes looking. The damage outlived the incident: `subscribe.js` had already routed the unattended lane around `bongos upgrade` in favour of `go-live.js`, citing this false-pass in a comment. **Decision: a bump is confirmed by asking the running process what version it is.** (1) A failed restart enters the same auto-rollback path as a failed install/migrate/health and exits non-zero — rolling back rather than merely erroring keeps disk and process consistent, since disk-ahead-of-process is the state that made the incident invisible. (2) The served version is read back from `/version` (`coreVersion`, since 1.17.2, prelaunch-gate exempt), defaulting to the `--health-url` origin and overridable via `--version-url` — derived rather than opt-in because every existing call site passes only `--health-url`, and a check you must opt into is off exactly where it is needed. (3) A **mismatch** fails (proof of failure → roll back); an **unreadable** endpoint only warns (absence of proof — refusing every such bump would regress harder than the false-pass), except under an explicit `--version-url`, which asks for proof and therefore gets a failure. That strict mode is what the unattended subscription lane now passes, its roster already carrying the URL. **Escalation is `sudo -n`, not a hand-placed polkit rule:** `restartService()` retries a failed restart through `sudo -n` when not root — the idiom `dev.js`/`dev-lib.js` already use and consistent with `provision.js`, `upgrade.js` having been the one place that restarted without escalating. The live `/etc/polkit-1/rules.d/<redacted>.rules` mitigation is superseded: a rebuilt box inherits code, not hand-placed `/etc` files (polkit route kept in [`docs/recipes/instance-service-restart.md`](../recipes/instance-service-restart.md)). `--no-health-check` stays the single escape hatch and now waives the read-back too. Rejected: comparing `startedAt` (cannot distinguish a restart onto the same old core from one onto the new); making an unreadable endpoint fatal by default; requiring `--version-url` everywhere (absent from every current call site — the same "off where it matters" failure in a new costume); keeping polkit as the answer.](<redacted>.md) | core distribution / upgrade verification |
374
374
  | 0280 | [**The unattended lane may drive a co-tenant, because the roster entry IS the per-instance decision** ([task 1003843](https://cloudbongos.com/builders#/task/1003843) · goal 1000106; from [task 1003521](https://cloudbongos.com/builders#/task/1003521), which built co-tenant mode into `go-live.js` and deliberately left this caller alone). `subscribe.js` keeps a SECOND COPY of go-live's target contract, and it drifted: `GO_LIVE_REQUIRED` still listed `deployTimer` after 1003521 made it optional, so `goLiveTargetFor()` returned `missing: ['deployTimer']` for every provisioned co-tenant, and `rawTopologyFor()`'s fixed field list dropped `pinMode` before the target was built — the same stale-normalizer trap that function's own comment was written to fix, one field later. **1003521 deferred this as a policy judgement call; reading the fallback shows the premise was wrong.** `mapped.ok === false` never meant "skip this instance" — it meant "deploy it the WEAKER way": a bare `bongos upgrade`, no backup, no deploy-timer guard, and a pin COMMITTED AND PUSHED into the co-tenant's own repo, which is exactly what `pinMode` exists to prevent. The lane was already auto-deploying co-tenants; the only open question was whether it did so well. A THIRD defect sat behind the other two and would have turned the silent downgrade into a hard failure had only the first been fixed: the normalizer yields `deployTimer: null`, an explicit null SURVIVES `JSON.stringify` into `--target-json`, and go-live's `validateTarget` runs `isSafeToken(null)` on it and rejects the whole target. A FOURTH sat PAST the go-live call and is what separates "reachable" from "safe": after any successful upgrade the lane runs its own pin net, `commitPin()`, which commits the pin files and PUSHES them to `origin` — a net that predates `pinMode`. Under `local-commit` go-live leaves the tree clean so it no-ops by luck; under **`leave-dirty`** the pin is in the working tree BY DESIGN, so the net committed it and pushed it into the customer's own repo — the lane honouring `pinMode` right up to the moment it undid it. **Decision: the lane may drive a co-tenant and needs no new gate, because listing an instance in `config/update-subscriptions.json` already IS the explicit per-instance operator decision** — the roster ships empty, nothing is touched until an operator lists it, and the routine is autonomy-gated (`requiresAutonomy: true`, default OFF) on top of that; a second consent gate asks the same person the same question twice. So: `GO_LIVE_REQUIRED` now MIRRORS go-live's `REQUIRED_FIELDS`, with a test reading BOTH source files and failing on drift; `deployTimer` moves to the optional-field loop so a co-tenant OMITS the key rather than carrying a null; `rawTopologyFor` carries `pinMode`; `resolveGoLive` gains a `needsCoTenant` probe leg; and `goLiveEngineFor()` makes the decision ONCE for both the dry run and the apply path, which had re-derived it separately. The pin net now obeys the same `pinMode` go-live was given — `commitPin()` takes `push` (default true, so the platform instance is unchanged), `local-commit` commits without pushing, `leave-dirty` skips the net, the rollback tidy follows the same rule, and the "not durable" advice stops telling an operator to `git push` a repo we do not own; its refusal to touch a worktree carrying NON-pin changes is unchanged in every mode. **The probe is CONDITIONAL on purpose** — demanding `pinMode` of every target would strip the go-live path from any instance between the `--target-json` release and 1003521's, trading a live regression for a hypothetical one. Rejected: a second consent gate (an `allowCoTenant` flag); deferring to the unspecced fleet control plane ([task 1001948](https://cloudbongos.com/builders#/task/1001948)) — which does not leave co-tenants alone, it leaves them pushing pins into customer repos; dropping `deployTimer` from `GO_LIVE_REQUIRED` and nothing else (the fix as proposed — trades a silent downgrade for a hard rejection); probing unconditionally; reaching co-tenant mode WITHOUT teaching the durability net about it (nominal support that breaks its own promise seconds later); and teaching `loadSubscriptions()` about `pinMode` instead, since that normalizer ships in the control plane's vendored core that nobody bumps — the exact trap `rawTopologyFor` routes around.](<redacted>.md) | core distribution / unattended deploy |
375
+ | 0281 | [**An instance’s identity is its own unix account and its own password-authed Postgres role** ([task 1003369](https://cloudbongos.com/builders#/task/1003369) · goal 1000094, audit ref `B3` of the 2026-08-29 security audit). [ADR 0111](<redacted>.md) made standup automatic and [ADR 0128](<redacted>.md) settled how the RUNNER escalates; neither settled who the INSTANCE is once it runs, and the answer was that everybody was the same person. `serviceUnit` hardcoded `User=lars`, and **co-tenant is the default shape** — so on a shared box every instance’s process ran under one uid, and `pool.js` connected by **passwordless peer auth**. Two consequences, neither a bug in any route: a unix user may read `/proc/<pid>/environ` of its own processes, so every instance could read every sibling’s `GITHUB_APP_PRIVATE_KEY`, hub client secret and OAuth secret; and peer auth answers “which unix user are you”, so any code as `lars` could open a pool against **any** database on the box by naming it — including the control plane’s own `builders` and `builder_sessions`. The `0600` `web.env` and the per-instance DB were already correct: sound walls with one door, and every instance had the key. **Decision: each instance gets its own unix account and its own password-authed PG role, both derived from its slug, both created by the provision run.** The account is `bongos-<slug>`, hash-folded past utmp’s 32 chars — prefixed because a project may legally be called `postgres`, and hash-suffixed because two slugs sharing a 16-char prefix would otherwise fold onto ONE account, which is the shared identity being removed. Its home is `/var/lib/<user>` and deliberately **not** under `/home`, since the unit sets `ProtectHome=read-only` and the server writes `builder-secret.key` inside its config home on first boot — a `/home` account would make that `EROFS`. The unit gains `ProtectSystem=strict` / `ProtectHome=read-only` / `PrivateTmp=true` with `ReadWritePaths` as the whole allow-list; **the checkout the service runs is deliberately not writable by it**. The role is `LOGIN` + password, owns its own DB, and is NOT superuser/CREATEDB/CREATEROLE — with **`REVOKE CONNECT … FROM PUBLIC` as the half that actually isolates**, because PG grants `CONNECT` to `PUBLIC` by default and per-role passwords alone leave a sibling one peer-auth hop away. Grants run in a SECOND statement **after** the migrate, since the privileged migrate runs as `postgres` and owns every table it creates; `ALTER DEFAULT PRIVILEGES` aims the same grant forward. The password is minted per run and never stored — the same run sets it on the role and rewrites the `0600` env file, so **every provision rotates it** — and `pool.js` reads `PGUSER`/`PGPASSWORD` **explicitly** rather than via `pg`’s env defaults, because the whole defect was a connection whose identity nothing in the code stated. **Absence is the old behavior:** no password ⇒ no credential lines and no pool keys, so an instance provisioned before this keeps its peer auth and the rollout cannot brick a box. Two consequences worth the record. (1) **The generated SQL contains no double quote, and that is load-bearing** — every literal is dollar-quoted and every identifier goes through `format(%I)`, because these commands are embedded in the dedicated droplet’s cloud-init as `- [ bash, -lc, "…" ]`, a YAML DOUBLE-QUOTED scalar: a plain `"role"` identifier reads fine in `psql` and silently truncates the line. A test asserts the absence rather than leaving it to habit. (2) **A partial, stated honestly** — the account is added to the app-user group so it can read the checkout it runs from (without it the unit fails at exec with `EACCES`), so FILE-level co-tenant separation is not what this delivers; the process and database boundaries are. Rejected: a `DATABASE_URL` in `web.env` (needs no pool change, but trades a stated, assertable identity for a socket-path parse that is `pg-connection-string`’s business); per-role passwords WITHOUT the `PUBLIC` revoke (reads like isolation, and peer auth walks around it); `ProtectSystem=full`, or `strict` with the working directory writable (makes the code the service executes writable by the service, in a change whose subject is containment); keeping `User=lars` and isolating with a second mechanism (the shared uid IS the `/proc` vector); storing the password in `provisioning_instances` (a live credential in plaintext, to buy stability the rotation does not need).](<redacted>.md) | tenant isolation / provisioning / DB auth |