@bongos/core 1.19.916 → 1.19.918
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 +54 -24
- package/docs/adr/0314-module-protection-is-per-path-not-per-module.md +10 -5
- package/docs/adr/0317-a-plane-that-cannot-give-an-instance-its-own-account-is-refused.md +148 -0
- package/docs/adr/0318-listing-a-file-for-task-affinity-is-not-owning-it.md +101 -0
- package/docs/adr/README.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/self-host.md +1 -1
- package/migrations/core_252_claims_published_head_sha_idx.sql +33 -0
- package/modules/lifecycle/cascade.js +92 -4
- package/modules/lifecycle/db-ship.js +43 -0
- package/modules/lifecycle/db.js +2 -0
- package/modules/lifecycle/removal-derivation.js +332 -0
- package/modules/lifecycle/routes/lifecycle.js +24 -0
- package/modules/lifecycle/tests/cascade.mjs +21 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision-repo.js +52 -0
- package/scripts/gds/provision.js +7 -1
- package/src/bongos/module-scope-map.js +14 -4
- package/src/module-api.js +1 -1
- package/tests/module-scope-map.mjs +100 -7
- package/tests/provision_account_identity.mjs +135 -0
- package/tests/removal_proposal.mjs +583 -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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.918",
|
|
6
|
+
"core_contract": "1.19.918",
|
|
7
|
+
"source_commit": "d9c3f86944e01f8135ce13bc9476f2ef3c86c00a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-20T18:32:13.181Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 507,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2298,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2831,
|
|
20
|
+
"tree_sha256": "7ecf5ebe792ee91db6d6d40ef4e6b49ea2d375af7a1c100885d66c7e266e5171",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2062,7 +2062,7 @@
|
|
|
2062
2062
|
{
|
|
2063
2063
|
"path": "docs/adr/0314-module-protection-is-per-path-not-per-module.md",
|
|
2064
2064
|
"mode": "0000644",
|
|
2065
|
-
"sha256": "
|
|
2065
|
+
"sha256": "d408c3f7bee29bbdf5f537ec7ed4559b88fae5d8074a8d58e556dc9e65bff46d"
|
|
2066
2066
|
},
|
|
2067
2067
|
{
|
|
2068
2068
|
"path": "docs/adr/0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md",
|
|
@@ -2074,10 +2074,20 @@
|
|
|
2074
2074
|
"mode": "0000644",
|
|
2075
2075
|
"sha256": "d0597a50d8f9a23c8d6c81a595fe6af73640cf574ae32f64730078ac7e19c7a5"
|
|
2076
2076
|
},
|
|
2077
|
+
{
|
|
2078
|
+
"path": "docs/adr/0317-a-plane-that-cannot-give-an-instance-its-own-account-is-refused.md",
|
|
2079
|
+
"mode": "0000644",
|
|
2080
|
+
"sha256": "4dd5bbb9c14e4ba19568646ceea2d29b66c46b99555d6c236a25b7b40097756c"
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
"path": "docs/adr/0318-listing-a-file-for-task-affinity-is-not-owning-it.md",
|
|
2084
|
+
"mode": "0000644",
|
|
2085
|
+
"sha256": "d5364169236db8c8d1a7968051fba352bb368aca53b19e554f172e0c3dc90d5d"
|
|
2086
|
+
},
|
|
2077
2087
|
{
|
|
2078
2088
|
"path": "docs/adr/README.md",
|
|
2079
2089
|
"mode": "0000644",
|
|
2080
|
-
"sha256": "
|
|
2090
|
+
"sha256": "9cebfdfef7de4b2c0ede697b30f387121f2a4a0df695b21b205f16acea84bad9"
|
|
2081
2091
|
},
|
|
2082
2092
|
{
|
|
2083
2093
|
"path": "docs/api-reference.md",
|
|
@@ -2647,7 +2657,7 @@
|
|
|
2647
2657
|
{
|
|
2648
2658
|
"path": "docs/module-api-changelog.md",
|
|
2649
2659
|
"mode": "0000644",
|
|
2650
|
-
"sha256": "
|
|
2660
|
+
"sha256": "824b753887223eeba6c21424a6a1f0984235e33165799114ac4a40993bb9b080"
|
|
2651
2661
|
},
|
|
2652
2662
|
{
|
|
2653
2663
|
"path": "docs/modules-contract.md",
|
|
@@ -2867,7 +2877,7 @@
|
|
|
2867
2877
|
{
|
|
2868
2878
|
"path": "docs/recipes/self-host.md",
|
|
2869
2879
|
"mode": "0000644",
|
|
2870
|
-
"sha256": "
|
|
2880
|
+
"sha256": "a5c98b8ee01461669a5f4ee5fa361e85e11915eb8326e060c4cf20841c35c07e"
|
|
2871
2881
|
},
|
|
2872
2882
|
{
|
|
2873
2883
|
"path": "docs/recipes/server-bring-up.md",
|
|
@@ -3754,6 +3764,11 @@
|
|
|
3754
3764
|
"mode": "0000644",
|
|
3755
3765
|
"sha256": "75bb644b32a70b52ac544edb901abff91aad07701848a470cc52103dc7e74234"
|
|
3756
3766
|
},
|
|
3767
|
+
{
|
|
3768
|
+
"path": "migrations/core_252_claims_published_head_sha_idx.sql",
|
|
3769
|
+
"mode": "0000644",
|
|
3770
|
+
"sha256": "4c7a47e03192ee2ed1e885d14a06854af40acb6472f52af40bcb36ad3e3ddb17"
|
|
3771
|
+
},
|
|
3757
3772
|
{
|
|
3758
3773
|
"path": "modules/agents/CLAUDE.md",
|
|
3759
3774
|
"mode": "0000644",
|
|
@@ -5492,7 +5507,7 @@
|
|
|
5492
5507
|
{
|
|
5493
5508
|
"path": "modules/lifecycle/cascade.js",
|
|
5494
5509
|
"mode": "0000644",
|
|
5495
|
-
"sha256": "
|
|
5510
|
+
"sha256": "52e63c329e4546f0a8ce2ca75157b9974b23eeaabf155f7ea02ead4d8f9bb8a8"
|
|
5496
5511
|
},
|
|
5497
5512
|
{
|
|
5498
5513
|
"path": "modules/lifecycle/check-rollup.js",
|
|
@@ -5587,7 +5602,7 @@
|
|
|
5587
5602
|
{
|
|
5588
5603
|
"path": "modules/lifecycle/db-ship.js",
|
|
5589
5604
|
"mode": "0000644",
|
|
5590
|
-
"sha256": "
|
|
5605
|
+
"sha256": "d280c54d35721d0a24090ab9cc3ac35d311d253d59d4ea57fb542e1232f5c744"
|
|
5591
5606
|
},
|
|
5592
5607
|
{
|
|
5593
5608
|
"path": "modules/lifecycle/db-tasks.js",
|
|
@@ -5602,7 +5617,7 @@
|
|
|
5602
5617
|
{
|
|
5603
5618
|
"path": "modules/lifecycle/db.js",
|
|
5604
5619
|
"mode": "0000644",
|
|
5605
|
-
"sha256": "
|
|
5620
|
+
"sha256": "2dea49342bf4a9be1279e662240075e57c31855804c8407a212ea9c33934f95e"
|
|
5606
5621
|
},
|
|
5607
5622
|
{
|
|
5608
5623
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -5759,6 +5774,11 @@
|
|
|
5759
5774
|
"mode": "0000644",
|
|
5760
5775
|
"sha256": "6176e109575f02c681240b809b85b272c438c7284a7fdd290cd87b46abdb9e7c"
|
|
5761
5776
|
},
|
|
5777
|
+
{
|
|
5778
|
+
"path": "modules/lifecycle/removal-derivation.js",
|
|
5779
|
+
"mode": "0000644",
|
|
5780
|
+
"sha256": "ef63843d5d00230a90063c454a5ad7a9dc170ce26cedc99377807ae1ced3f7c0"
|
|
5781
|
+
},
|
|
5762
5782
|
{
|
|
5763
5783
|
"path": "modules/lifecycle/rot.js",
|
|
5764
5784
|
"mode": "0000644",
|
|
@@ -5827,7 +5847,7 @@
|
|
|
5827
5847
|
{
|
|
5828
5848
|
"path": "modules/lifecycle/routes/lifecycle.js",
|
|
5829
5849
|
"mode": "0000644",
|
|
5830
|
-
"sha256": "
|
|
5850
|
+
"sha256": "f4b080286870b6cd52cd59c5cb7c5a5127c3b0a90df50b7d87495d684767203b"
|
|
5831
5851
|
},
|
|
5832
5852
|
{
|
|
5833
5853
|
"path": "modules/lifecycle/routes/mingle.js",
|
|
@@ -5927,7 +5947,7 @@
|
|
|
5927
5947
|
{
|
|
5928
5948
|
"path": "modules/lifecycle/tests/cascade.mjs",
|
|
5929
5949
|
"mode": "0000644",
|
|
5930
|
-
"sha256": "
|
|
5950
|
+
"sha256": "fc8c5cf14265515faf8dc63b17b80de1acbb7558a08d26808960244aec6e4377"
|
|
5931
5951
|
},
|
|
5932
5952
|
{
|
|
5933
5953
|
"path": "modules/lifecycle/version-autoclose.js",
|
|
@@ -7832,12 +7852,12 @@
|
|
|
7832
7852
|
{
|
|
7833
7853
|
"path": "package-lock.json",
|
|
7834
7854
|
"mode": "0000644",
|
|
7835
|
-
"sha256": "
|
|
7855
|
+
"sha256": "9842c68958d709653d9006a15332aa271621aaa6e4fac8de8411896a6c103042"
|
|
7836
7856
|
},
|
|
7837
7857
|
{
|
|
7838
7858
|
"path": "package.json",
|
|
7839
7859
|
"mode": "0000644",
|
|
7840
|
-
"sha256": "
|
|
7860
|
+
"sha256": "31b797c1d71820a20e2744debb104990294e44cf943d24aeed1b77b35e2f1b2a"
|
|
7841
7861
|
},
|
|
7842
7862
|
{
|
|
7843
7863
|
"path": "public-docs/index.html",
|
|
@@ -8807,7 +8827,7 @@
|
|
|
8807
8827
|
{
|
|
8808
8828
|
"path": "scripts/gds/provision-repo.js",
|
|
8809
8829
|
"mode": "0000644",
|
|
8810
|
-
"sha256": "
|
|
8830
|
+
"sha256": "dd912cf4446f572257b0a9cc7380a331b8ae7e3d834ccff828fddc13ae5bcb19"
|
|
8811
8831
|
},
|
|
8812
8832
|
{
|
|
8813
8833
|
"path": "scripts/gds/provision-teardown.js",
|
|
@@ -8822,7 +8842,7 @@
|
|
|
8822
8842
|
{
|
|
8823
8843
|
"path": "scripts/gds/provision.js",
|
|
8824
8844
|
"mode": "0000644",
|
|
8825
|
-
"sha256": "
|
|
8845
|
+
"sha256": "784400e0a2912fb4c9bd14107dca7d50cc4a10d77b4a849cd73fe853a6cf3966"
|
|
8826
8846
|
},
|
|
8827
8847
|
{
|
|
8828
8848
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -9647,7 +9667,7 @@
|
|
|
9647
9667
|
{
|
|
9648
9668
|
"path": "src/bongos/module-scope-map.js",
|
|
9649
9669
|
"mode": "0000644",
|
|
9650
|
-
"sha256": "
|
|
9670
|
+
"sha256": "40f2196830c746ed440ba91b938bece8ba48fdf225285c6a6d5dccd109d47d3d"
|
|
9651
9671
|
},
|
|
9652
9672
|
{
|
|
9653
9673
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -9837,7 +9857,7 @@
|
|
|
9837
9857
|
{
|
|
9838
9858
|
"path": "src/module-api.js",
|
|
9839
9859
|
"mode": "0000644",
|
|
9840
|
-
"sha256": "
|
|
9860
|
+
"sha256": "250d1791c44836efe9407a5a524a0171bf7ad0c9b337c0cfaa6dd4c491b76d7c"
|
|
9841
9861
|
},
|
|
9842
9862
|
{
|
|
9843
9863
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12342,7 +12362,7 @@
|
|
|
12342
12362
|
{
|
|
12343
12363
|
"path": "tests/module-scope-map.mjs",
|
|
12344
12364
|
"mode": "0000644",
|
|
12345
|
-
"sha256": "
|
|
12365
|
+
"sha256": "0a6878a91c21b5e8c31f0c06518177969f907cb0a980733c2ec1a89f8621bcb8"
|
|
12346
12366
|
},
|
|
12347
12367
|
{
|
|
12348
12368
|
"path": "tests/module_api.mjs",
|
|
@@ -12824,6 +12844,11 @@
|
|
|
12824
12844
|
"mode": "0000644",
|
|
12825
12845
|
"sha256": "642ba54a20f08df1514f94114d39cdb70427f93542f4c060c83251ad1f46c80a"
|
|
12826
12846
|
},
|
|
12847
|
+
{
|
|
12848
|
+
"path": "tests/provision_account_identity.mjs",
|
|
12849
|
+
"mode": "0000644",
|
|
12850
|
+
"sha256": "d56cf2553c0b5efa6a17df3d578b7b5766ff62504af6c226688f73ac417df3c6"
|
|
12851
|
+
},
|
|
12827
12852
|
{
|
|
12828
12853
|
"path": "tests/provision_caddy_wiring.mjs",
|
|
12829
12854
|
"mode": "0000644",
|
|
@@ -13099,6 +13124,11 @@
|
|
|
13099
13124
|
"mode": "0000644",
|
|
13100
13125
|
"sha256": "a7a0a61260a4ee78f5452b76ee6d8ef732104cf9c32a3c05bfa25517bf883aeb"
|
|
13101
13126
|
},
|
|
13127
|
+
{
|
|
13128
|
+
"path": "tests/removal_proposal.mjs",
|
|
13129
|
+
"mode": "0000644",
|
|
13130
|
+
"sha256": "b447287dca1ffe65a926715e0a5f07c0a05e3503066febf85b7f6d317b805c8f"
|
|
13131
|
+
},
|
|
13102
13132
|
{
|
|
13103
13133
|
"path": "tests/rename_history_restraint.mjs",
|
|
13104
13134
|
"mode": "0000644",
|
|
@@ -111,11 +111,16 @@ new derivation mechanism. Filed separately:
|
|
|
111
111
|
structural path test — directories under `scripts/` are territory and still
|
|
112
112
|
protect), narrowing the derived set from twelve to the nine predicted below.
|
|
113
113
|
Nothing was unprotected to achieve it and the path wall did not move.
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
- ~~**`builder-settings`, deliberately out of scope.**~~ **Resolved** by
|
|
115
|
+
[ADR 0318](0318-listing-a-file-for-task-affinity-is-not-owning-it.md)
|
|
116
|
+
([task 1004110](https://cloudbongos.com/builders#/task/1004110)), and by neither
|
|
117
|
+
of the two routes predicted here. Its overlap was `src/bongos/routes/me.js`
|
|
118
|
+
caught by the blanket `src/bongos/routes/` prefix — but that glob was a task
|
|
119
|
+
AFFINITY listing, not an ownership claim, on a file `MODULE_GLOBS` itself
|
|
120
|
+
called a core facade. Ownership moved to `kernel`, where the aggregator
|
|
121
|
+
belongs; the blanket glob was left alone (all eighteen route files are
|
|
122
|
+
protected by it and nothing else) and no strangler move was needed (the
|
|
123
|
+
aggregator resolves ten module ports). The derived set narrows nine to eight.
|
|
119
124
|
- **Nine protected paths are owned by no module at all** — `gen-repo-map.js`,
|
|
120
125
|
`gen-file-map.js`, `gen-session-index.js`, `install-git-hooks.js`, `cli-lib.js`,
|
|
121
126
|
`run-unit-tests.js`, `git-merge-regen.js`, `scripts/gds/hooks/` and
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ADR 0317 — A plane that cannot give an instance its own account is refused, not given a shared uid
|
|
2
|
+
|
|
3
|
+
**Status:** accepted · **Date:** 2026-09-20 · **Task:** [task 1004018](https://cloudbongos.com/builders#/task/1004018) (goal 1000106 — BONGOS-V2)
|
|
4
|
+
|
|
5
|
+
**Related:** [ADR 0281](0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md) (the per-instance account this one guards) · [ADR 0315](0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md) (the sibling refusal, and the ordering this one extends) · [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) (the supervision seam that deferred this question here) · [ADR 0128](0128-provisioning-runner-privilege-and-db-auth-model.md) (the `PROVISION_SUDO` escalation the probe tests)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
ADR 0281 made an instance's identity its own unix account: provisioning step 1c runs
|
|
10
|
+
`ensureInstanceUserCmd` → `useradd --system`, and the unit's `User=`/`Group=` is what
|
|
11
|
+
stops one co-tenant instance reading a sibling's `/proc/<pid>/environ` — where every
|
|
12
|
+
instance's `EnvironmentFile` carries its GitHub App private key and its OAuth client
|
|
13
|
+
secret. That was audit finding B3, and closing it is the whole point of the account.
|
|
14
|
+
|
|
15
|
+
`useradd` needs uid 0. The self-host image is `node:22-bookworm-slim`, runs `USER node`,
|
|
16
|
+
and installs `postgresql-client bash tini curl ca-certificates git` — **no sudo**. On that
|
|
17
|
+
plane the account cannot be created at all, and ADR 0306 named this as the piece its seam
|
|
18
|
+
does not cover: *"`useradd` cannot run under `USER node` — that is task 1004018."*
|
|
19
|
+
|
|
20
|
+
**Two failure shapes, neither of them honest.**
|
|
21
|
+
|
|
22
|
+
- **Today it crashes, late.** `boxExec(ensureInstanceUserCmd(...))` is not `allowFail`, so
|
|
23
|
+
step 1c throws — *after* the capacity, ingress and wiring preflights have passed, after
|
|
24
|
+
the status flip to `provisioning`, and after the standalone shape has already pulled its
|
|
25
|
+
repo and run `npm ci`. The runner records a crash where the honest answer is a refusal,
|
|
26
|
+
and it records it with the box half-changed.
|
|
27
|
+
- **Tomorrow it goes quiet.** ADR 0315 refuses a proxyless plane, so a compose self-host
|
|
28
|
+
currently stops at the ingress question and never reaches step 1c. That is coincidence,
|
|
29
|
+
not protection: 0315 says in its own consequences that *"a future ADR that adds [a front
|
|
30
|
+
door] replaces this refusal rather than extending it."* The moment someone gives the
|
|
31
|
+
container a published port, the identity gap is live again — and `provision-process.js`'s
|
|
32
|
+
supervisord program block writes `user=<instanceUser>` for an account that was never
|
|
33
|
+
created, which supervisord reports as a program that will not start rather than as a
|
|
34
|
+
security regression.
|
|
35
|
+
|
|
36
|
+
The task asked for a posture, and it is worth naming the one that was rejected: *accept
|
|
37
|
+
that a self-host container is single-tenant and document that instances there are not
|
|
38
|
+
isolated.* It buys a working container standup by re-opening B3, and an operator has no
|
|
39
|
+
way to see which posture their box is running.
|
|
40
|
+
|
|
41
|
+
## Decision
|
|
42
|
+
|
|
43
|
+
**A plane where the runner cannot become root is refused before anything is attempted, and
|
|
44
|
+
the refusal names what a shared uid would cost.**
|
|
45
|
+
|
|
46
|
+
`accountPlaneGap(exec, { privileged })` in `scripts/gds/provision-repo.js` — beside the
|
|
47
|
+
account it guards — probes with `${sudo}sh -c 'test "$(id -u)" = 0'`. It returns `null`
|
|
48
|
+
when the runner reaches root, or a string naming the missing privilege.
|
|
49
|
+
`accountPreflight({ exec, apply, log, shape, privileged })` wraps it with the narration
|
|
50
|
+
its sibling preflights carry, and `provisionInstance` returns `{ok:false, error: 'Account
|
|
51
|
+
preflight failed: …'}`.
|
|
52
|
+
|
|
53
|
+
**The probe runs the same escalation the mutation will,** so a pass is evidence rather than
|
|
54
|
+
inference: `privileged` (i.e. `PROVISION_SUDO=1`) renders `sudo sh -c …`, exactly the
|
|
55
|
+
prefix `ensureInstanceUserCmd` will use a few steps later.
|
|
56
|
+
|
|
57
|
+
**`sh -c`, not a bare `test`, and that is load-bearing.** Under sudo the *outer*,
|
|
58
|
+
unprivileged shell would expand `$(id -u)` before sudo ever ran, and the probe would
|
|
59
|
+
cheerfully answer about the wrong user — passing on precisely the plane this refuses. The
|
|
60
|
+
command substitution has to happen inside the escalation. For the same reason there is no
|
|
61
|
+
pipeline: a pipeline's exit status is its last stage's, which would erase the distinction
|
|
62
|
+
below.
|
|
63
|
+
|
|
64
|
+
**A shell that ran and answered is the only failure that proves the plane cannot.** Under
|
|
65
|
+
remote exec `boxExec` is `ssh <host> bash -s`, so a briefly-unreachable co-hosting box
|
|
66
|
+
throws here too — and answering that with "this plane cannot create accounts, provision
|
|
67
|
+
elsewhere" would be a confident wrong diagnosis burning the intent's retry budget on a
|
|
68
|
+
network blip. Exit 1 (`test` said no) and 127 (`sudo` is not installed) are read as proof;
|
|
69
|
+
ssh reports its own failures as 255, so both still mean what they say across the hop.
|
|
70
|
+
Anything else returns *"could not confirm the runner can create a per-instance unix
|
|
71
|
+
account on this plane"* with the real cause leading. This is ADR 0315's rule, applied to a
|
|
72
|
+
different question.
|
|
73
|
+
|
|
74
|
+
**The refusal is ordered third, after capacity and ingress and before ADR 0303's wiring
|
|
75
|
+
check.** Those first three are questions about the *plane* — has it room, has it a proxy,
|
|
76
|
+
can it hand out an identity — and 0303's is the one question about *this instance's*
|
|
77
|
+
domain. ADR 0315's capacity → ingress → wiring order is extended, not rearranged: all four
|
|
78
|
+
still return before anything on the box changes.
|
|
79
|
+
|
|
80
|
+
**No container-shaped identity model, and no "single-tenant" acceptance.** The symmetry
|
|
81
|
+
with ADR 0306 would be a seam with a container implementation. There is nothing to put in
|
|
82
|
+
it: a per-instance account inside the container needs the image to run privileged, a
|
|
83
|
+
container-per-instance needs a docker socket and an orchestrator that does not exist here,
|
|
84
|
+
and unprivileged user namespaces are not reliably available. The honest encoding of "this
|
|
85
|
+
plane cannot give a child its own identity" is a refusal — the same reasoning ADR 0315
|
|
86
|
+
used to reject a no-op ingress backend.
|
|
87
|
+
|
|
88
|
+
## Consequences
|
|
89
|
+
|
|
90
|
+
- **A compose self-host still cannot provision a child instance — and now it says so for
|
|
91
|
+
its own reason, not by accident.** This refusal does not depend on ADR 0315's firing
|
|
92
|
+
first, so giving the container a front door later cannot silently re-open B3. Whoever
|
|
93
|
+
builds that front door has to answer this question too, and the error text is where they
|
|
94
|
+
will find it.
|
|
95
|
+
|
|
96
|
+
- **The failure moved from step 1c to the preflight.** A plane without root used to hard-
|
|
97
|
+
error after the status flip and, for a standalone, after a repo pull and an `npm ci`.
|
|
98
|
+
It now returns `{ok:false}` before any mutation, which is the pending-retry path an
|
|
99
|
+
environment problem earns rather than the crash it was getting.
|
|
100
|
+
|
|
101
|
+
- **One extra `sh -c` per applied standup.** Read-only, no mutation, and it runs after the
|
|
102
|
+
capacity measurement, so a full box still refuses without it. A dry run never probes, and
|
|
103
|
+
a dedicated droplet is skipped — cloud-init runs as root there, and ADR 0281's account
|
|
104
|
+
steps are baked into its user-data unchanged.
|
|
105
|
+
|
|
106
|
+
- **The live box is untouched.** The control-plane runner unit sets `PROVISION_SUDO=1`, so
|
|
107
|
+
the probe renders `sudo sh -c …`, passes, and every command after it is byte-identical.
|
|
108
|
+
The regression is pinned by a test that provisions a root-reaching plane and asserts
|
|
109
|
+
`useradd --system` still runs, after the probe.
|
|
110
|
+
|
|
111
|
+
- **Root, but no `useradd`, is deliberately not covered.** An Alpine-style plane with
|
|
112
|
+
`adduser` instead would pass the probe and fail at step 1c as it does today. The gap this
|
|
113
|
+
ADR closes is privilege, which is the one the planes we actually run exhibit; inventing a
|
|
114
|
+
second probe for a plane nobody has would be speculation, and it would fail loudly rather
|
|
115
|
+
than quietly in any case.
|
|
116
|
+
|
|
117
|
+
- **Teardown is not covered,** for ADR 0315's reason: a refusal on the teardown path would
|
|
118
|
+
strand a row instead of cleaning it.
|
|
119
|
+
|
|
120
|
+
- **This is a capability question, not a configuration knob.** There is no
|
|
121
|
+
`PROVISION_ALLOW_SHARED_UID`. A runner either reaches root on this plane or it does not,
|
|
122
|
+
and an operator who wants provisioning to proceed needs privilege, not a flag that says
|
|
123
|
+
to pretend.
|
|
124
|
+
|
|
125
|
+
## Alternatives considered
|
|
126
|
+
|
|
127
|
+
- **Accept it: document a self-host container as single-tenant with instances sharing one
|
|
128
|
+
uid.** Rejected. It is a silent downgrade of the exact vector ADR 0281 closed, visible
|
|
129
|
+
nowhere on the running box, and "single-tenant" is not what a container hosting several
|
|
130
|
+
child projects is. A posture an operator cannot observe is not a posture.
|
|
131
|
+
|
|
132
|
+
- **An identity seam with a container implementation** (mirroring ADR 0306). Rejected: no
|
|
133
|
+
implementation exists to put behind it. A seam whose second backend is "do nothing" is
|
|
134
|
+
the no-op backend ADR 0315 already rejected, wearing a seam's clothes.
|
|
135
|
+
|
|
136
|
+
- **Probe `useradd --help`, or `command -v useradd`.** Rejected on the mechanism: both
|
|
137
|
+
succeed as an unprivileged user on the very image this refuses. They test the binary; the
|
|
138
|
+
missing thing is the privilege to run it.
|
|
139
|
+
|
|
140
|
+
- **`${sudo}id -u` and compare the output to `0`.** Rejected: it decides on captured stdout
|
|
141
|
+
rather than on exit status, which every sibling preflight and every provisioning test
|
|
142
|
+
fake treats as the contract — the fakes answer commands with an empty `stdout`, so the
|
|
143
|
+
probe would have failed on hermetic planes that are fine, and the fix would have been to
|
|
144
|
+
teach a dozen harnesses about one probe.
|
|
145
|
+
|
|
146
|
+
- **Run the probe inside `ensureInstanceUserCmd` and let step 1c refuse itself.** Rejected:
|
|
147
|
+
it keeps the refusal after the status flip and after the standalone repo sync, which is
|
|
148
|
+
most of what makes the current failure a bad one.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# ADR 0318 — Listing a file for task affinity is not claiming to own it
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted — **implemented** by [task 1004110](https://cloudbongos.com/builders#/task/1004110) (2026-09-20)
|
|
4
|
+
- **Date:** 2026-09-20
|
|
5
|
+
- **Task:** [task 1004110](https://cloudbongos.com/builders#/task/1004110), goal 1000095 — BONGOS-V2
|
|
6
|
+
- **Extends** [ADR 0314](0314-module-protection-is-per-path-not-per-module.md), which left this case open by name.
|
|
7
|
+
- **Supersedes nothing.**
|
|
8
|
+
|
|
9
|
+
## The question
|
|
10
|
+
|
|
11
|
+
After [ADR 0314](0314-module-protection-is-per-path-not-per-module.md), nine
|
|
12
|
+
modules derive `protected` — making a goal scoped to them Archon-only to create.
|
|
13
|
+
`builder-settings` was one of them, and it earned that on exactly one glob:
|
|
14
|
+
`src/bongos/routes/me.js`, caught by the blanket `src/bongos/routes/` prefix in
|
|
15
|
+
`PROTECTED_GLOBS`.
|
|
16
|
+
|
|
17
|
+
ADR 0314 named this and left it, because clearing it looked like it needed one of
|
|
18
|
+
two expensive things: finishing the strangler move of `me.js` into
|
|
19
|
+
`modules/builder-settings/`, or narrowing a blanket protected glob. Both were
|
|
20
|
+
scoped out. The task that picked this up was written the same way — those two
|
|
21
|
+
options, and *"the choice is the deliverable"*.
|
|
22
|
+
|
|
23
|
+
Both are wrong, and measuring them is what shows a third thing was actually broken.
|
|
24
|
+
|
|
25
|
+
## What the measurements said
|
|
26
|
+
|
|
27
|
+
**Narrowing the blanket prefix removes the only wall that exists.** All eighteen
|
|
28
|
+
files under `src/bongos/routes/` are protected by that prefix *and nothing else* —
|
|
29
|
+
no route file has a second protecting glob. Narrowing it unwalls `auth.js`,
|
|
30
|
+
`builders.js`, `backup.js` and `core-update.js` in the same stroke. This is now
|
|
31
|
+
pinned as a test rather than left as a caution, so the next reader does not have
|
|
32
|
+
to re-derive it.
|
|
33
|
+
|
|
34
|
+
**Moving `me.js` into `modules/builder-settings/` inverts the port architecture.**
|
|
35
|
+
`GET /me` is a deliberate aggregator: it resolves **ten** module ports —
|
|
36
|
+
`lifecycle`, `reward`, `onboarding`, `government`, `specialities`, `grade`,
|
|
37
|
+
`box.hasEverConnected`, `art.sharedKeyConfigured`, `art.getBuilderGeminiKeyRow`,
|
|
38
|
+
`discord.isLinked` — and `builder-settings` is one of the ten. Hosting it inside
|
|
39
|
+
one module would mean that module resolving nine foreign ports from the inside,
|
|
40
|
+
which is the `core -> module` edge [ADR 0083](0083-modular-architecture-strangler-build-plan.md)
|
|
41
|
+
rule 1 exists to forbid. A core facade is precisely what prevents that.
|
|
42
|
+
|
|
43
|
+
Three places already said so, in the code, before this task was written: the file
|
|
44
|
+
header (*"This file kept only the read aggregation"*), `module.json` (*"the
|
|
45
|
+
`GET /me` aggregator stays a thin core facade"*), and `MODULE_GLOBS` itself.
|
|
46
|
+
|
|
47
|
+
## The decision
|
|
48
|
+
|
|
49
|
+
**`MODULE_GLOBS` listed `src/bongos/routes/me.js` under `builder-settings` for
|
|
50
|
+
task affinity — and said so in the same comment — while `isProtected()` read it
|
|
51
|
+
as an ownership claim.** Affinity is a convenience for authoring; ownership is a
|
|
52
|
+
statement about territory. One listing, read as the other, made a whole module
|
|
53
|
+
Archon-only on the strength of a file its own comment called core.
|
|
54
|
+
|
|
55
|
+
So: **ownership follows what the file is.** `src/bongos/routes/me.js` moves to
|
|
56
|
+
`kernel`, which already owns `src/bongos/routes/security.js` and is the home of
|
|
57
|
+
the core route surface. `builder-settings` keeps its own directory and its
|
|
58
|
+
pref-mirroring CLI, and stops being protected.
|
|
59
|
+
|
|
60
|
+
This is [ADR 0314](0314-module-protection-is-per-path-not-per-module.md) one step
|
|
61
|
+
on. That decision said *owning the command is not owning the authority*, for a CLI
|
|
62
|
+
entrypoint that merely lives near a module. The same error wearing different
|
|
63
|
+
clothes: **being listed next to a module is not owning it either.** 0314 solved
|
|
64
|
+
its case structurally, with a path test (`isCommandPath`). This one is solved by
|
|
65
|
+
correcting the data, because there is exactly one such listing — see below.
|
|
66
|
+
|
|
67
|
+
## Why not a general affinity tier
|
|
68
|
+
|
|
69
|
+
The obvious generalisation is to mark affinity entries in `MODULE_GLOBS` so
|
|
70
|
+
`isProtected()` skips them, mirroring `isCommandPath`. Rejected, because the
|
|
71
|
+
audit does not support it. Of the nine protected modules, three others
|
|
72
|
+
(`kernel`, `devsecops`, `methodology`) are protected only by paths outside their
|
|
73
|
+
own directory — but in every one of those the foreign path genuinely *is* the
|
|
74
|
+
domain: `kernel` **is** `src/bongos/auth.js`, `devsecops` **is** `.github/`,
|
|
75
|
+
`methodology` **is** `CLAUDE.md` and `docs/`.
|
|
76
|
+
|
|
77
|
+
`builder-settings` was the only module protected by a path its own source
|
|
78
|
+
described as belonging elsewhere. A new tier in `MODULE_GLOBS` would be a new
|
|
79
|
+
concept, and a new place for a hole to hide, built for a population of one.
|
|
80
|
+
|
|
81
|
+
## Consequences
|
|
82
|
+
|
|
83
|
+
- `protectedModules()` goes **9 -> 8**. `builder-settings` leaves the set.
|
|
84
|
+
- **The wall that matters does not move.** This decides who may CREATE a goal
|
|
85
|
+
whose territory includes `/me`. It does not decide who may WRITE it: a
|
|
86
|
+
sub-Metic builder touching `src/bongos/routes/me.js` is still refused by
|
|
87
|
+
`checkPermissionPaths`, which the tests assert directly rather than assume.
|
|
88
|
+
- A `builder-settings`-scoped goal can no longer author a task touching `/me`;
|
|
89
|
+
that needs `kernel` scope. Correct, rather than a loss — the aggregator spans
|
|
90
|
+
ten modules and belongs to none of them.
|
|
91
|
+
- `src/bongos/routes/` now has **two** of eighteen files owned, up from two with
|
|
92
|
+
one of them wrong. The other sixteen remain authorable by no goal;
|
|
93
|
+
[task 1004116](https://cloudbongos.com/builders#/task/1004116) holds that
|
|
94
|
+
decision, and this change removes the builder-settings overlap that made its
|
|
95
|
+
option (a) awkward.
|
|
96
|
+
|
|
97
|
+
## Not built here
|
|
98
|
+
|
|
99
|
+
- **The other sixteen route files.** Still unowned, still
|
|
100
|
+
[task 1004116](https://cloudbongos.com/builders#/task/1004116). The blanket-prefix
|
|
101
|
+
tripwire added here is the measurement that task needs, landed early.
|
package/docs/adr/README.md
CHANGED
|
@@ -408,3 +408,5 @@ This keeps the decision history honest and traceable.
|
|
|
408
408
|
| 0314 | [**A module owns a protected path without the whole module becoming protected** ([task 1004070](https://cloudbongos.com/builders#/task/1004070), goal 1000095 — BONGOS-V2). Three modules derive `protected` — and so are Archon-only to scope a goal to — because of ONE file that is not their domain: `onboarding` on `scripts/gds/setup.js`, `autonomy` on `scripts/gds/conductor-dispatch.js`, `copy-desk` on `scripts/gds/copy-inventory.js`. The owner chose *narrow what counts as protected*; the task set the fork and required the choice be justified before any glob moved. **Deleting the glob is out**: `MODULE_GLOBS` is also the territory deciding who may AUTHOR a task on a path, and the shared floor is only `tests/`, so an unowned path is authorable by no goal at all. That left reassigning each CLI to an already-protected module — **measured, not reasoned about**, by applying the edit to the source and re-requiring in a fresh process (`MODULE_SCOPE_MAP` is frozen and built from a COPY of `MODULE_GLOBS` at load, so an in-process mutation proves nothing and passes green). The measurement kills it: `copy-desk` and `autonomy` lose the ability to author on their own generator and their own conductor, and — the row that decides it — `devsecops` as the new owner **cannot author the generator together with the two artifacts it writes**, because `docs/copy-registry.json` and `docs/copy-inventory.md` stay `copy-desk`'s. So *change a UI string, regenerate the registry* becomes authorable by no single goal, and that is the ship-check freshness gate every UI task hits. Decision: **protection becomes per-path at the goal-scope wall, as it already is at the other three** — `matchProtected` decides task-authoring, the grader and gate decide ship, the post-push audit decides the dishonest case; only goal-creation asked about the MODULE, and that inconsistency is the whole defect. The derivation stays computed from `PROTECTED_GLOBS` and no flag is hand-set ([ADR 0086](0086-goal-scoped-work-hierarchy.md) §7 / SEC #863), and the Metic floor on those paths does not move. **Corrects the task's own numbers**: 12 of 26 modules derive protected, not 11, and `agents` (protected by `modules/agents/`, itself a protected glob) is absent from its list of seven — which is why the one thing this ADR SHIPS is a test pinning the full protected key set **by name**, a count assertion passing on the wrong twelve being exactly how that drifted. Implementation is deferred to task 1004109, per the task's own instruction to stop after the ADR. Also filed: `builder-settings` (task 1004110), whose overlap rides the blanket `src/bongos/routes/` prefix and so needs either the `me.js` strangler move or its own security ADR; and **nine protected paths owned by no module at all** (task 1004111) — the freshness generators, the merge-driver pair and `cli-lib.js` among them — found while checking whether deletion was viable, a pre-existing hole rather than one this opens. Does not decide: the floor a protected path carries, whether `scripts/gds/` deserves a shared-floor entry the way `tests/` has one, or how the nine unowned paths get a home.](0314-module-protection-is-per-path-not-per-module.md) | modules / permissions |
|
|
409
409
|
| 0316 | [**The ignite floor is a scheduling dial, not the trusted tier** ([task 1004084](https://cloudbongos.com/builders#/task/1004084), goal 1000095 — BONGOS-V2). BV2 decision 3 makes the ignite floor — who may turn a quick idea straight into claimable work — a per-instance setting, and decision 16 adds the descent decay rate beside it, the two to share one home. Both the spec and the seeded task described the mechanism the same way: the floor IS the `METIC_PLUS` constant in `modules/ideas/routing.js`, so this is "a setting, not a refactor". Tracing the code before building refuted that: `METIC_PLUS` is spelled once but READ for four semantically different gates — the two `decideLanding` bug branches (land at `ready` or at `backlog`), `mayProposeCriterion` (who may file a `kind='criterion-proposal'`, a planning-scope write), and the protected-scope wall in `authorizeRoutedLanding` (who may route work into the permission / ship-grade / deploy / migration core). Only the first two are the ignite floor, so making the CONSTANT configurable would mean an instance setting its floor to `xenos` — exactly the "project of one wants every thought claimable" case the owner asked for — ALSO gaining the right to file criterion-proposals and to breach the protected core. The codebase already said so in three places nobody had read into the spec: the constant's own header ("It grants nothing — the server-side rank floor and the goal's scope wall are separate, load-bearing gates"), the protected-scope branch ("the two are layers, not alternatives"), and `tests/filing_abuse_matrix.mjs`, which models the widening as an ATTACK and pins the literal. Decision: a separate dial. `modules/ideas/config.js` + `config/ideas.neutral.json` carry both knobs on the `modules/government/config.js` precedence (env -> instance root `config/ideas.json` -> core root neutral; the ADR 0268 dual-root split matters, since one root reads the host pack out of `node_modules` where `npm ci` erases it). `igniteFloor` defaults to `metic`, resolving to exactly the `METIC_PLUS` set, and is threaded into `decideLanding` as a ready-made `trustedRanks` **Set** by the callers — `routing.js` is TOLD the floor and never reads config, staying pure and DB-free — while `METIC_PLUS` stays a FIXED constant for the other two gates, neither of which takes a floor argument (pinned structurally). `descentDecayRate` defaults to 0.5 and ships with no production caller, because R11 is its intended reader and depends on the unbuilt R08 — the same deliberate slice boundary `modules/government/config.js` documents for itself, and what decision 16 asked for. Everything fails to VANILLA rather than dead: a malformed pack, an unknown rank or an out-of-band rate falls back, and an unrecognised floor yields the DEFAULT set, never an empty one (failing closed would stop every ignition on a typo). Rejected: configuring `METIC_PLUS` as written (a security widening in a config file's clothes); splitting it into four named constants (the other three want the same fixed tier, and naming them invites configuring them); having `routing.js` read the config itself (makes the landing matrix impure and untestable at a different floor). Spec decision 3's "spelled in one place" describes the intent, not the mechanism; the spec was not edited and the task description was amended in flight.](0316-the-ignite-floor-is-not-the-trusted-tier.md) | ideas / permissions |
|
|
410
410
|
| 0315 | [**A plane with no proxy is refused, not given a no-op ingress** ([task 1004017](https://cloudbongos.com/builders#/task/1004017), goal 1000106 — BONGOS-V2). The piece [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) explicitly deferred: the self-host image ships no proxy, so provisioning step 6's `<slug>.caddy` snippet write and `systemctl reload caddy` cannot work there and a child instance gets no front door. **Two failure shapes, only one visible**: a DOMAINED child already half-refused, because `caddy adapt` fails with no binary and [ADR 0303](0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)'s own catch turns that into a gap — but the message told the operator to add a directive to `/etc/caddy/Caddyfile`, a file the container does not have and that ADR 0303 §1 forbids provisioning from creating; a DOMAINLESS child skipped that preflight entirely (it is gated on `inst.domain`), reached step 6 and died on the `sudo` write, an uncaught throw that bypasses the `{ok:false}` pending-retry path and hard-errors the instance on attempt one — a crash recorded where a refusal was the honest answer. Decision: `ingressPlaneGap(exec)` probes the plane with `caddy version` — one cheap unprivileged read, the same posture as `checkCaddyWiring` — and both standup legs refuse with `Ingress preflight failed: …`, deliberately NOT domain-gated, because the domainless child is the one 0303 could not see. **Only a missing binary proves there is no proxy**: under remote exec `boxExec` is `ssh <host> bash -s`, so an unreachable co-hosting box throws here too, and diagnosing a network blip as 'this plane has no proxy, provision elsewhere' would be a confident wrong answer that burns the intent's retry budget — exit 127 is read as proof, anything else answers 'could not confirm a reverse proxy on this plane' with the real cause leading. The three refusals are ordered **capacity → ingress → wiring**, cheapest and most fundamental first and all before any mutation, which moves 0303's preflight behind the ADR 0285 capacity measurement; both still refuse before anything changes, so only the precedence between two refusals moves. **No ingress seam and no no-op backend**, despite the obvious symmetry with 0306: a `none` implementation whose `writeSite`/`reload` are no-ops lets the standup SUCCEED while leaving the child unreachable — neither half of the task's done-when, and exactly the silently-broken instance it exists to prevent. Does not decide: what a child's front door IS on a container plane (a published port, a bundled proxy, an explicit localhost-only shape) — a future ADR that adds one replaces this refusal rather than extending it. Teardown is left alone on purpose: it already reloads `allowFail`, and refusing there would strand a row instead of cleaning it.](0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md) | provisioning / ingress |
|
|
411
|
+
| 0317 | [**A plane that cannot give an instance its own account is refused, not given a shared uid** ([task 1004018](https://cloudbongos.com/builders#/task/1004018), goal 1000106 — BONGOS-V2). The last of the three container gaps [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) deferred, and the sibling of [ADR 0315](0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md). [ADR 0281](0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md) makes an instance's identity its own unix account, and the unit's `User=`/`Group=` is what stops one co-tenant reading a sibling's `/proc/<pid>/environ` — where every instance's `EnvironmentFile` carries its GitHub App private key and OAuth client secret (audit B3). `useradd` needs uid 0; the self-host image is `node:22-bookworm-slim` running `USER node` with **no sudo**, so there the account cannot be created at all. **Two failure shapes, neither honest**: today step 1c's `boxExec(ensureInstanceUserCmd(...))` is not `allowFail`, so it throws AFTER every preflight passed, AFTER the status flip to `provisioning`, and — for a standalone — after a repo pull and an `npm ci`, recording a crash with the box half-changed; and tomorrow it goes quiet, because a compose self-host only stops short of step 1c by ADR 0315's coincidence, and 0315 says in its own consequences that a future front door **replaces** that refusal. Decision: `accountPlaneGap(exec, { privileged })` in `provision-repo.js` probes with `${sudo}sh -c 'test "$(id -u)" = 0'` and `provisionInstance` refuses with `Account preflight failed: …` before any mutation. **The probe runs the SAME escalation the mutation will** (`PROVISION_SUDO=1` renders the same `sudo ` prefix `ensureInstanceUserCmd` uses), so a pass is evidence rather than inference. **`sh -c`, not a bare `test`, is load-bearing**: under sudo the outer unprivileged shell would expand `$(id -u)` first and answer about the wrong user, passing on exactly the plane this refuses — and for the same reason there is no pipeline, whose exit status is its last stage's. **Only a shell that ran and answered proves it**: `boxExec` is `ssh <host> bash -s` under remote exec, so exit 1 (`test` said no) and 127 (no sudo binary) are read as proof while ssh's own 255 answers "could not confirm" with the real cause leading — ADR 0315's rule applied to a different question. Ordered **third**, after capacity and ingress and before 0303's wiring check: the first three ask about the PLANE, 0303 asks about this instance's DOMAIN, and 0315's order is extended rather than rearranged. **Rejected: accepting it** — documenting a self-host container as single-tenant with a shared uid is a silent downgrade of the vector 0281 closed, observable nowhere on the running box, and "single-tenant" is not what a container hosting several child projects is; **an identity seam with a container implementation**, because nothing exists to put behind it and a seam whose second backend does nothing is 0315's no-op wearing a seam's clothes; **probing `useradd --help` / `command -v useradd`**, which succeed unprivileged on the very image this refuses; **`${sudo}id -u` compared against `0`**, which decides on captured stdout where every sibling preflight and every test fake treats exit status as the contract; and **refusing inside step 1c**, which keeps the refusal after the status flip. Not covered, deliberately: root-but-no-`useradd` (an Alpine-style plane fails loudly at step 1c as today), and teardown (0315's reason — a refusal there strands a row instead of cleaning it). No `PROVISION_ALLOW_SHARED_UID`: a runner either reaches root on this plane or it does not.](0317-a-plane-that-cannot-give-an-instance-its-own-account-is-refused.md) | provisioning / identity |
|
|
412
|
+
| 0318 | [**Listing a file for task affinity is not claiming to own it** ([task 1004110](https://cloudbongos.com/builders#/task/1004110), goal 1000095 — BONGOS-V2). The case [ADR 0314](0314-module-protection-is-per-path-not-per-module.md) named and left open, closed by neither of the two routes it predicted. `builder-settings` derived `protected` — Archon-only to scope a goal to — on exactly one glob, `src/bongos/routes/me.js`, caught by the blanket `src/bongos/routes/` prefix. **That glob was never an ownership claim:** `MODULE_GLOBS` listed it for task AFFINITY and said in the same comment that the `GET /me` aggregator stays a thin core facade, and `isProtected()` cannot tell the two apart. **Both predicted routes were measured and rejected.** Narrowing the blanket prefix: all eighteen files under `src/bongos/routes/` are protected by that prefix AND NOTHING ELSE, so narrowing unwalls `auth.js`, `builders.js`, `backup.js` and `core-update.js` together — now pinned as a tripwire test rather than left as a caution. Moving `me.js` into `modules/builder-settings/`: the aggregator resolves TEN module ports (`lifecycle`, `reward`, `onboarding`, `government`, `specialities`, `grade`, `box`, `art` ×2, `discord`) and `builder-settings` is one of them, so hosting it inside one module means resolving nine foreign ports from the inside — the `core -> module` edge [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 forbids. Decision: **ownership follows what the file is** — `me.js` moves to `kernel`, which already owns `src/bongos/routes/security.js`. This is 0314 one step on: owning the command is not owning the authority, and **being listed next to a module is not owning it either**. **Rejected: a general affinity tier** in `MODULE_GLOBS` mirroring `isCommandPath` — the audit does not support it. Three other modules (`kernel`, `devsecops`, `methodology`) are also protected only by paths outside their own directory, but in each the foreign path genuinely IS the domain; `builder-settings` was the only module protected by a path its own source called core, so a new tier would be a new place for a hole to hide, built for a population of one. `protectedModules()` goes 9 -> 8. **The wall that matters does not move:** a sub-Metic builder touching `/me` is still refused by `checkPermissionPaths`, asserted directly rather than assumed. The sixteen still-unowned route files remain [task 1004116](https://cloudbongos.com/builders#/task/1004116), whose option (a) this de-risks.](0318-listing-a-file-for-task-affinity-is-not-owning-it.md) | permissions / module scope |
|
|
@@ -2306,5 +2306,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2306
2306
|
landed since 1.19.914 with no explicit bump. run 35524223153. (task 1002620)
|
|
2307
2307
|
1.19.916 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2308
2308
|
landed since 1.19.915 with no explicit bump. run 35525694524. (task 1002620)
|
|
2309
|
+
1.19.917 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2310
|
+
landed since 1.19.916 with no explicit bump. run 35526196428. (task 1002620)
|
|
2311
|
+
1.19.918 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2312
|
+
landed since 1.19.917 with no explicit bump. run 35529342372. (task 1002620)
|
|
2309
2313
|
---------------------------------------------------------------------------
|
|
2310
2314
|
```
|
|
@@ -39,7 +39,7 @@ Author `config/branding.json` from the neutral starter — see [the branding con
|
|
|
39
39
|
|
|
40
40
|
**Or scaffold it with `bongos init`** ([task #1204](https://example.com/builders#/task/1204)): a short interview (or `bongos init --from spec.json` for a hands-free / CI run) that writes both config files, **seeds your first version + its done-when goal**, and **files the human-only kickoff steps** (register a GitHub OAuth app, first owner sign-in, repo binding, admission) as real tracked tasks — so a fresh instance has a claimable queue on day one instead of an empty hall. It never invents your branding (it points you at the fill prompt for that). Preview the whole plan with `bongos init --dry-run`.
|
|
41
41
|
|
|
42
|
-
**Production hosting, optional (the `provisioning` module — [ADR 0111](../adr/0111-instance-hosting-provisioning-module.md) §3).** If you enable the `provisioning` module and answer the interview's hosting questions (shape — co-tenant `$0` or a dedicated droplet — bring-your-own domain, credential ref, tier), init turns those answers into a real **provisioning request** (a queued `provision` intent the control-plane runner drains) instead of only filing a manual "production topology" task. **A queued request does nothing until a runner drains it:** on the control plane that is a systemd timer running `node scripts/gds/provision.js run-intents --apply`. This Docker image now carries the tools the scaffold needs — git, and a core it can package out of its own `.git`-less checkout (task 1003995) — and supervision is no longer systemd-only: the runner picks a process manager, falling back to supervisord where systemd is not running (task 1003997, [ADR 0306](../adr/0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md)). Three standup legs still assume a systemd box, so on a compose self-host a new project cannot stand up: the container has no reverse proxy for a child instance's front door, cannot create the per-instance unix account that isolates one instance from another
|
|
42
|
+
**Production hosting, optional (the `provisioning` module — [ADR 0111](../adr/0111-instance-hosting-provisioning-module.md) §3).** If you enable the `provisioning` module and answer the interview's hosting questions (shape — co-tenant `$0` or a dedicated droplet — bring-your-own domain, credential ref, tier), init turns those answers into a real **provisioning request** (a queued `provision` intent the control-plane runner drains) instead of only filing a manual "production topology" task. **A queued request does nothing until a runner drains it:** on the control plane that is a systemd timer running `node scripts/gds/provision.js run-intents --apply`. This Docker image now carries the tools the scaffold needs — git, and a core it can package out of its own `.git`-less checkout (task 1003995) — and supervision is no longer systemd-only: the runner picks a process manager, falling back to supervisord where systemd is not running (task 1003997, [ADR 0306](../adr/0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md)). Three standup legs still assume a systemd box, so on a compose self-host a new project cannot stand up: the container has no reverse proxy for a child instance's front door, cannot create the per-instance unix account that isolates one instance from another, and installs its backup dir as a hardcoded founder user (task 1002719). **Two of the three now refuse instead of failing opaquely, each for its own reason** — so fixing one cannot silently re-open the other. **The ingress leg now refuses instead of failing opaquely** (task 1004017, [ADR 0315](../adr/0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md)): provisioning probes the plane with `caddy version` and returns `Ingress preflight failed: …` naming the missing proxy, before it writes a Caddy snippet or reloads anything. Previously a domained child was told to edit an `/etc/caddy/Caddyfile` the container does not have, and a domainless one crashed on the snippet write. Giving the container a real front door is still unbuilt — the refusal closes the failure mode, not the capability. **The identity leg refuses too** (task 1004018, [ADR 0317](../adr/0317-a-plane-that-cannot-give-an-instance-its-own-account-is-refused.md)): provisioning probes whether the runner can reach root on this plane and returns `Account preflight failed: …` before any mutation, because `useradd` cannot run under `USER node` and every child there would otherwise share one uid — the sibling-secret vector [ADR 0281](../adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md) exists to close. It is a capability question, not a knob: there is no setting that accepts a shared uid. Note supervisord has no timer concept either, so a container-hosted instance gets no automatic DB backup — the standup log prints the `crontab` line to add instead. The two irreducibly-human legs — the GitHub OAuth app and the first owner sign-in — stay guided/manual. If the module isn't enabled on the target instance, init falls back to the manual production-topology task, so nothing is silently skipped.
|
|
43
43
|
|
|
44
44
|
## Configuration surface
|
|
45
45
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- core_252 — index claims.published_head_sha for REVERSE lookup (task 1004090 / BV2.R15)
|
|
2
|
+
--
|
|
3
|
+
-- migrations/171 added the column and every reader since has gone FORWARD:
|
|
4
|
+
-- getClaimPublishedSha(taskId) reads one row by task_id, which the existing
|
|
5
|
+
-- task_id index serves. BV2.R15 introduced the first reader that goes BACKWARD —
|
|
6
|
+
-- shippedTasksByPublishedSha(shas) asks "which shipped tasks published these
|
|
7
|
+
-- commits", i.e. `WHERE published_head_sha = ANY($1::text[])` — and nothing
|
|
8
|
+
-- indexed that direction, so it planned a full scan of `claims` on every ship
|
|
9
|
+
-- that deleted a previously shipped file.
|
|
10
|
+
--
|
|
11
|
+
-- That is on the ship path, which is exactly where a seq scan must not be: the
|
|
12
|
+
-- table grows one row per claim forever, and the cost would arrive quietly as
|
|
13
|
+
-- ships gradually slowing rather than as anything failing.
|
|
14
|
+
--
|
|
15
|
+
-- PARTIAL, on NOT NULL. The column is nullable and most historical rows are null
|
|
16
|
+
-- (rows predating 171, and publishes that rode another branch), so a partial
|
|
17
|
+
-- index is both smaller and a better match for the query — `= ANY(...)` never
|
|
18
|
+
-- matches NULL, so the excluded rows could not have been returned anyway.
|
|
19
|
+
--
|
|
20
|
+
-- Additive and idempotent: adds an index, touches no data, no column and no
|
|
21
|
+
-- constraint. Safe to run forward against a live database, and safe for an older
|
|
22
|
+
-- release to run against — an unused index changes no behaviour.
|
|
23
|
+
--
|
|
24
|
+
-- NOT a lifecycle_* module migration: `claims` is a CORE table, and a module
|
|
25
|
+
-- migration may only touch its own namespaced tables (fitness.js
|
|
26
|
+
-- checkModuleMigrationsAdditive, ADR 0083 Decision #5).
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS claims_published_head_sha_idx
|
|
29
|
+
ON claims (published_head_sha)
|
|
30
|
+
WHERE published_head_sha IS NOT NULL;
|
|
31
|
+
|
|
32
|
+
COMMENT ON INDEX claims_published_head_sha_idx IS
|
|
33
|
+
'Reverse lookup sha -> claim for BV2.R15 removal attribution (db-ship.shippedTasksByPublishedSha). Partial: = ANY() never matches NULL.';
|