@bongos/core 1.21.139 → 1.21.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.139",
6
- "core_contract": "1.21.139",
7
- "source_commit": "9c3b6a3669e6d8122345fde40f98189a34d6809d",
5
+ "core_version": "1.21.140",
6
+ "core_contract": "1.21.140",
7
+ "source_commit": "59d16cd8522221c5bba608d77c3b8ed6912e0b1e",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-16T15:50:00.989Z",
9
+ "built_at": "2026-10-16T17:43:19.059Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 608,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3793,
20
- "tree_sha256": "442b7a9383f6629ccb8a89fca8867a4ced006e1d7e334673b517e6f11c9eea5c",
20
+ "tree_sha256": "c80dcd5ef2a6891e7c9f449cd7bed3222290d97f4f4f4dc8bc21f077c3611616",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2102,7 +2102,7 @@
2102
2102
  {
2103
2103
  "path": "docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md",
2104
2104
  "mode": "0000644",
2105
- "sha256": "8a59c1aa7f81d335e96c6305bb4f22689c4a0fee627eecdec9585e76a3477bea"
2105
+ "sha256": "0a40681d78739b99df16e4207e71283d15d4b3895bf24c757c9894060254a03f"
2106
2106
  },
2107
2107
  {
2108
2108
  "path": "docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md",
@@ -2377,7 +2377,7 @@
2377
2377
  {
2378
2378
  "path": "docs/adr/README.md",
2379
2379
  "mode": "0000644",
2380
- "sha256": "c5e8e4a27cf07799da677172e50a0c06d9be9bdf31f511416b40e392d268ffc0"
2380
+ "sha256": "7efe6a2db56553a970553c66cec08e8c93aebecaa3d6fbe91702f7ab2b85cebc"
2381
2381
  },
2382
2382
  {
2383
2383
  "path": "docs/api-reference.md",
@@ -2927,7 +2927,7 @@
2927
2927
  {
2928
2928
  "path": "docs/module-api-changelog.md",
2929
2929
  "mode": "0000644",
2930
- "sha256": "bdd801f3f3af2b93f31a37b2f2eb1b9ac3b394ab8ef980400e6aaa2ae8e5c476"
2930
+ "sha256": "eefe6cfabd50b62d1420d34cac09a8bb32161e188d3923e095686cfd02757a4c"
2931
2931
  },
2932
2932
  {
2933
2933
  "path": "docs/modules-contract.md",
@@ -10532,12 +10532,12 @@
10532
10532
  {
10533
10533
  "path": "package-lock.json",
10534
10534
  "mode": "0000644",
10535
- "sha256": "6c35ef513a1c02f3f73e27af737b43d8d444be0c810bbbab5400d408562b1d24"
10535
+ "sha256": "0d967d05871a6ab5242c834b0310ede209ca0b1eadbeb008d9d563393880efcc"
10536
10536
  },
10537
10537
  {
10538
10538
  "path": "package.json",
10539
10539
  "mode": "0000644",
10540
- "sha256": "cbb5b48ee0147aeb0e05c4cf38add512a7df4efed3b521bd3e069d99102f9cb7"
10540
+ "sha256": "63e77bb62eeb77dc0ca023b4878027d732bb96b152b3cb3fb7e22693d7dfd7d0"
10541
10541
  },
10542
10542
  {
10543
10543
  "path": "public-docs/index.html",
@@ -10557,7 +10557,7 @@
10557
10557
  {
10558
10558
  "path": "release-notes.json",
10559
10559
  "mode": "0000644",
10560
- "sha256": "8d3cfe743193bd2308275eb71e3bee00e0467d19861ea18413605f796af9ab0f"
10560
+ "sha256": "521fe34eafc9d15830e7aa1d6b5081050e53a479ecd863bd095dbb45aa6501ca"
10561
10561
  },
10562
10562
  {
10563
10563
  "path": "scripts/cron/style_guide_audit.sh",
@@ -12922,7 +12922,7 @@
12922
12922
  {
12923
12923
  "path": "src/module-api.js",
12924
12924
  "mode": "0000644",
12925
- "sha256": "6751283b546731a5e4ce9df1826bc81ec8c71d7df3784b23692c38fd079bb979"
12925
+ "sha256": "05f5d90316a6a2f0c0ecc290ff187db3906d088917b141f61794e443685964aa"
12926
12926
  },
12927
12927
  {
12928
12928
  "path": "src/module-loader/catalog.js",
@@ -1,6 +1,6 @@
1
1
  # ADR 0328 — A hosted project's authority is its membership row, not its owner column
2
2
 
3
- - **Status:** accepted
3
+ - **Status:** accepted. **Amended 2026-10-16:** Phase 2 (admitting `member`) is suspended because a `member` row is written at every hub sign-in and is not a grant from the project. The gate stays owner-or-archon. See the *Amendment* at the end ([task 1004754](https://cloudbongos.com/builders#/task/1004754)).
4
4
  - **Date:** 2026-09-22
5
5
  - **Task:** [task 1003882](https://cloudbongos.com/builders#/task/1003882) (goal 1000094 — BONGOS-V2)
6
6
  - **Deciders:** the scoping call — "every API action must name its project; a non-member may only apply" — is the owner's, recorded 2026-09-13 on the task. This ADR is the mechanism that carries it out: where the underlying membership data already lives, what a route must call to consult it, and what a non-member is told.
@@ -46,6 +46,8 @@ No new header, no session binding, no new CLI convention. `/provisioning/instanc
46
46
 
47
47
  **Phase 1 (consolidate three implementations into one, and correct the inconsistency between them — task 1004159, no schema change, no new port).** Provisioning already holds `owner_builder_id` directly; it needs no cross-module port to check its own column. Extract one local helper, `requireProjectOwner(loadFn, { archonOverride = true } = {})`, mirroring `requireProjectInviter`'s composed-gate shape (mounted after `requireBuilder`, loads the row, admits on ownership match OR — by default — a plain **rank** check, `req.builder.rank === 'archon'`, denies 404 `instance_not_found` fail-closed on every other path). **This must be the exact rank test `instanceAccessDenied` and `resolveOwnInstance` already run — not `requirePermission('provisioning.fleet.manage')`.** The two are not interchangeable: that permission's floor is `metic` (ADR 0157 lowered it from `archon`, `provisioning.js:252-253`), used only by the separately-gated `force-teardown` route; wiring it into `requireProjectOwner` instead of the plain rank test would silently widen all 14 routes — teardown and disconnect included — from archon-only to metic-and-up, an unstated widening this ADR's whole posture forbids. With the correct rank test, all 14 routes point at the one helper: the 11 that already resolve to "owner OR archon" today (via `instanceAccessDenied`, `resolveOwnInstance`, or `disconnect.js`'s inline copy) get a mechanical, behavior-preserving swap; the 3 that resolve only to "owner" today (`teardown`, `domain`, `github-app`) get the SAME rank-based default the other 11 already carry — an explicit, named widening, not an accident: it makes those three routes consistent with the fleet-support access every sibling route in the same module already grants, closing the gap `force-teardown`'s own existence already implies was intended. **Every archon action taken on a project the archon does not own — through the widened routes or the existing ones — must write a `provisioning_events` row naming the archon actor**, the same `actor: 'api:archon:<id>'` convention the existing routes already use for their own-vs-archon audit trail (`provisioning.js:712,883,1048`); task 1004159's acceptance criteria pin this for the 3 newly-widened routes specifically, since they have no such event today.
48
48
 
49
+ > **Suspended 2026-10-16.** The premise below is wrong: the hub's sign-in default is itself `member`, so the rows this phase would admit include everyone who has signed in. Do not build this phase from this text. See the *Amendment* at the end.
50
+
49
51
  **Phase 2 (the multi-member widening — task 1004161).** Extend the same helper to resolve a NEW port, `platform-identity.projectMembershipKind(clientId, githubId) → Promise<'owner'|'member'|'visitor'|'pending'|null>` — a thin wrapper over the already-correct `getMembership()` — via `resolveOptional`, admitting on `owner` or `member`. **This is only as trustworthy as who may write a `member` row, which this ADR consumes and does not change or re-audit.** The one write path that sets `membership_kind` to a value other than the sign-in default (`POST /sso/membership/check-in`, `modules/platform-identity/routes/sso.js`) is authenticated by the PROJECT's own `client_id`/`client_secret` — not a builder's session — and is update-only against a row that already carries a hub-witnessed sign-in (`touchMembership`'s `WHERE ... signin_verified_at IS NOT NULL`). So a `member` row on the hub reflects what the FEDERATED PROJECT's own server reported about its own builder, under the trust model ADR 0141/0201/0247 already established for federated check-ins generally. This ADR's gate inherits that existing trust boundary as-is for provisioning's admission decision; it neither tightens nor loosens it, and does not itself re-verify the check-in path's authorization. **The fallback is scoped narrowly, and the two failure shapes are NOT the same decision:** when the port is unregistered (a self-hosted instance without `platform-identity`), fall back to the Phase 1 direct-column check — the ADR 0083 "graceful degradation on a module-less instance" contract, and the correct answer there, since such an instance has no fleet of *other people's* projects to protect. When the port IS registered but the call throws or resolves an unrecognized value (a hub where the port should be answering and is not), **deny** — do not silently widen the fallback to cover a runtime fault on infrastructure that is supposed to be present. Task 1004161's acceptance criteria must test both paths separately.
50
52
 
51
53
  This is a **new, parallel axis**, not a fifth branch bolted onto `requirePermission` — exactly as the existing ownership axis (`authorizeOwned`/`authorizeOwnedOrAny`, [ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100) is kept structurally separate from the plain permission resolve, for the same reason: a fact `requireBuilder` cannot know (which concrete row, and who stands on it) must be supplied by the handler's own loaded row, and folding it into `requirePermission` would silently drop that second fact for every other caller of that gate.
@@ -82,7 +84,7 @@ The task's own point 5 asks for migration order "for the 96 write routes." This
82
84
 
83
85
  ## Consequences
84
86
 
85
- - **A team can share one paid hosted instance for the first time**, once task 1004161 lands — `member`-kind rows already exist in the schema and are already correct; they are simply not consulted yet by the surface that would need them.
87
+ - ~~**A team can share one paid hosted instance for the first time**, once task 1004161 lands.~~ *Withdrawn 2026-10-16:* `member`-kind rows are not a project grant (see the *Amendment*). Sharing an instance waits on an owner-made grant ([task 1004753](https://cloudbongos.com/builders#/task/1004753)).
86
88
  - **An archon gains fleet-support reach on `teardown`, `domain`, and `github-app` that does not exist today**, once task 1004159 lands — a stated widening, matching the access every other project-scoped route in the same module already grants, not a side effect discovered later.
87
89
  - **Three redundant local implementations (`instanceAccessDenied`, `resolveOwnInstance`, `disconnect.js`'s inline copy) collapse into one function**, auditable by `route-rank-check.js` the same way a missing rank check already is — the exact mechanism "Why now" says does not exist today.
88
90
  - **A self-hosted instance running `provisioning` without `platform-identity` installed is unaffected**: the port's `resolveOptional` fallback is the existing owner-only check, verbatim, kept as the fallback branch instead of the only branch — and a hub where the port SHOULD be answering and errors instead denies rather than silently falling back (Decision 2, Phase 2).
@@ -96,3 +98,28 @@ The task's own point 5 asks for migration order "for the 96 write routes." This
96
98
  - **Fold project authority into `requirePermission` as a fifth axis.** Rejected for the same reason the ownership axis is its own entry point ([ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100): project standing is parameterized by a concrete row `requirePermission` is never handed, so admitting on a bare permission key would collapse per-project standing into "holds the key everywhere" — precisely the widening the ownership axis exists to prevent for `.own` keys.
97
99
  - **Migrate all 96 `any-builder` write routes under one project-scope umbrella.** Rejected: the other ~82 routes act on a single-tenant instance's own Bongos data, which has no project dimension per ADR 0100 — applying this decision there would be inventing multi-tenancy for a system deliberately built single-tenant-per-repo, a far larger and unrelated call the owner's one-line decision does not ask for.
98
100
  - **Drop the 404 existence-hiding and return 403 for every unauthorized instance id.** Rejected: a stranger with no standing on a project should learn nothing about whether it exists, exactly as `invite-authz.js`'s existing ownership refusal already guarantees. Only a caller who already has a membership row (however weak) is told anything more, because the row itself is proof they already know the project exists.
101
+
102
+ ## Amendment — 2026-10-16: Phase 2 is suspended, because a `member` row is not a grant
103
+
104
+ [Task 1004754](https://cloudbongos.com/builders#/task/1004754). This is the owner's decision of 2026-10-16. It was made when the session claiming [task 1004161](https://cloudbongos.com/builders#/task/1004161) stopped before writing code.
105
+
106
+ **What Phase 2 got wrong.** Decision 2 says the only write that sets `membership_kind` "to a value other than the sign-in default" is the project's own check-in, so a `member` row reflects what the project reported. That is true, but the sign-in default is itself `member`:
107
+
108
+ - `recordFederatedSignin` (`modules/platform-identity/platform-identity.js`) inserts `membership_kind = 'member'`, with the hub's witness, on every completed hub sign-in.
109
+ - It runs at `POST /sso/token`, which completes before the instance's own admission gate runs (`src/bongos/auth-idp.js` `completeIdpFlow` checks `firstTimeAdmissionBlocked` only after the code exchange). A person the project refuses still gets a witnessed `member` row.
110
+ - It runs at `POST /sso/device/poll`. `POST /sso/device/start` takes only a typed origin and needs no sign-in, so any GitHub user can run the device flow against any federated project and get a witnessed `member` row. The project's server is never asked.
111
+ - Instance side, `membershipKind` (`src/bongos/auth-admission.js`) reports `owner` for the first admin and `member` for every other builder. On an instance with `admission.policy: open`, every GitHub sign-up is a builder.
112
+
113
+ So `member` means "has signed in here". It does not mean "the owner trusts this person with the hosting". Admitting it on the 14 routes would let any GitHub user tear down, re-domain, disconnect, restart or reconfigure any federated hosted project. The witness (`signin_verified_at`) proves that the sign-in happened, not that anyone approved it.
114
+
115
+ **What changes.**
116
+
117
+ 1. Phase 2 is suspended. `requireProjectOwner` stays the Phase 1 owner-or-archon gate. Task 1004161 is parked in the backlog and depends on the design task below.
118
+ 2. A person other than the owner can reach a project's provisioning routes only through a grant that the project's owner (or an archon) makes on purpose. That grant must be stored apart from the sign-in default, and must have one named writer. [Task 1004753](https://cloudbongos.com/builders#/task/1004753) designs it: who writes it, which routes a grantee may use (reads and writes separately; repo-private and the github-app pick-up stay owner-only because they act with the owner's own GitHub token), whether the hub's sign-in default should stop being `member`, and the audit actor a grantee's write carries (neither `api:self` nor `api:archon:<id>`).
119
+ 3. Task 1004162's 403 split assumed this vocabulary, so it is re-checked against the new design before it is built.
120
+
121
+ **What still holds, verified for task 1004161:**
122
+
123
+ - `client_id = slug` is a sound join key. `scripts/gds/provision-net.js` registers each hosted instance's credentialed client with `clientId = inst.slug`. `provisioning_instances.slug` is `UNIQUE` and is never reused. A catalog-only client cannot check in.
124
+ - `(client_id, github_id)` is already indexed by the table's own `UNIQUE (client_id, github_id)` constraint (`platform_identity_003_project_memberships.sql`). The per-request read needs no new index.
125
+ - Decision 2 has two failure shapes, and they still apply to whatever port carries the grant. If the port is unregistered, fall back to the Phase 1 owner-or-archon check. If the port is registered but throws or returns an unknown value, deny.
@@ -446,7 +446,7 @@ These 20 numbers are each shared by exactly two files. They are **accepted histo
446
446
  | 0325 | [**The managed fleet control plane is the hub we already run** ([task 1001948](https://cloudbongos.com/builders#/task/1001948), goal 1000106 — BONGOS-V2). The spike was written against [ADR 0111](0111-instance-hosting-provisioning-module.md) §5/§7, which deferred the managed offering as **"genuinely greenfield (no fleet registry exists)"** behind two gates — Mercury and a real `bongos upgrade`. Both gates are shipped and **the premise is not**: measuring the call sites first changed the answer, as it did for [task 1004141](https://cloudbongos.com/builders#/task/1004141). The registry is `provisioning_instances` (owner, shape, domain, database, port, lifecycle, cost estimate, module selection, the liveness overlay and the served/available core-version snapshot), with `provisioning_intents` as the queue and `provisioning_events` as the audit trail; the operator is `scripts/gds/provision.js`, the single token-holder draining that queue behind ADR 0111 §2's trust boundary; oversight is `GET /provisioning/fleet` and `/fleet/cost-ledger`; and the update channel is not one mechanism but two shipped ones — the owner's per-instance deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md), health-gated with auto-rollback) and the opt-in patch-only sweep ([ADR 0136](0136-update-channel-subscription-policy.md)). **Decision: there is no greenfield fleet control plane to design, and none will be built — the hub IS it.** It never needed to sit *above* the per-repo databases [ADR 0100](0100-two-repo-instance-model.md) §3 locks: the roster is a hub table and the operator has never read one. The genuinely missing leg is a **platform leg** — `grep -rl RENDER_API scripts modules src` returns nothing, and after [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) a `cloud-host` is a Render service — and this ADR **deliberately specifies none of it**, because that is [task 1004142](https://cloudbongos.com/builders#/task/1004142)'s with those files open and a second spec for one implementation is how two drift. The fleet **driven** and the fleet **observed** are different populations: `byo-host` is in the registry and operated by nobody, its liveness the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull, so every operator surface must state which it counts — a roster that mixes them reports a fleet-wide upgrade as complete while an unknown number were never asked. The operator holds **exactly one** credential and it is **its own** (the platform account token, root-only on the control plane, never the web tier, never per-customer); ADR 0323 D6's "names, never values" rule is about the CUSTOMER's secrets and is unchanged. Two gaps are filed rather than fixed here: the subscription roster living in `config/update-subscriptions.json` is the one fleet fact the roster route cannot report and a restored backup can lose independently of the rows it describes ([task 1004150](https://cloudbongos.com/builders#/task/1004150), a recommendation and not a ruling), and **`costEstimateUsd` returns 0 for every shape but `dedicated`** — measured live, all 24 fleet rows report `$0`, confidently and with no error, for projects about to cost ~\$14/month each ([task 1004148](https://cloudbongos.com/builders#/task/1004148)). **No fleet-wide "upgrade everything" button**: both channels are per-instance by construction, and one action that moves the whole fleet is the shape most likely to take every customer down at once. AGPL posture unchanged — ADR 0111 §5's one-sentence rule survives its own table. Rejected: building the separate control plane §5 described (a second registry and a second operator holding the same rows with nothing keeping them equal); specifying Render here; reading per-instance databases for fleet reporting; and continuing to defer the whole layer, which is now the reason the ledger reads \$0 for a fleet about to cost real money.](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) | hosting / fleet / provisioning |
447
447
  | 0326 | [**An upgrade may ADD a deny rule to a file the instance owns** ([task 1004156](https://cloudbongos.com/builders#/task/1004156), goal 1000090 — BONGOS-V2). Task 1003966 gave the instance starter its deny rules — no reading `.env` files, keys or session tokens, no shelving with the repo-global stash — and they reached **nobody who was already here**. The starter is written only when `.claude/settings.json` is ABSENT, and `bongos upgrade` materializes from the published package, which excludes that file on purpose; so an upgrade always takes the starter branch, always finds a settings.json already there, and left it alone. Every instance on the allow-only starter from task 1003962 — the live one included — kept it forever. That was deliberate ("rewriting a file the instance owns is the line task 1003962 drew"), and the recorded remedy was manual: re-run `bongos init`, or delete the file. **A manual remedy for a security gap protects the instances whose owners read release notes, which is not the set that needs protecting.** The owner's call: ADD the missing deny rules and NAME each one in the run output. The argument is that a deny rule is **monotonic in the safe direction** — an allow rule widens what the local agent may do, a deny rule can only narrow, so appending one cannot make any instance less safe than it was a moment earlier. That is what makes this the one merge worth doing without asking per-instance, and it extends [ADR 0121](0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md) §2's already-licensed "merge OR leave-and-report" from `--adopt` to every materialize. "Never clobber" survives in the sense that was load-bearing: **nothing the instance wrote is removed, reordered or rewritten** — their allow rules, own denies, `ask` list, hooks, env and every key outside `permissions` come back exactly as they were, with new rules appended after. Deliberately NOT done: **no provenance**, so an owner who deletes a rule we add gets it back next upgrade and (deny beating allow in Claude Code's order) cannot override it with an allow — rejected as speculative machinery for a case nobody has hit, revisit on a real complaint; a file that will not parse, or whose `permissions`/`permissions.deny` is not the expected shape, is left BYTE-FOR-BYTE alone and reported, the posture task 1004023 took for the core's own file, because a half-understood rewrite of a permissions file is worse than the gap; and **the allow half is never topped up** — the safety argument is directionality and does not survive being applied to a rule that grants. `materializeClaude` gains `summary.settingsDenyAdded` + `settingsDenyUncheckable`, both printed through `log()` since `upgrade.js` does not iterate `summary.notes`. The test asserting an instance's settings.json comes back byte-identical is superseded by a strictly stronger one — every rule still there, unmoved, deny-only growth — the weaker spelling being what froze the gap in place. **AMENDED 2026-09-22 by [task 1004158](https://cloudbongos.com/builders#/task/1004158): the two shelf rules are now starter-only and the top-up asks for the CREDENTIAL half alone.** 0326 itself named this exclusion as the remedy if the rule ever bit; the owner took it ahead of that, because the monotonicity argument licenses a rule about reading a secret and does not reach one that guards a workflow — with no provenance and deny beating allow, an instance that loses the shelf cannot give it back. A fresh instance still gets both. The top-up only ever appends, so this is **forward-looking only**: an instance that already took them keeps them, live included.](0326-an-upgrade-may-add-a-deny-rule-to-a-file-the-instance-owns.md) | security / instance onboarding |
448
448
  | 0327 | [**A `cloud-host` runs on the owner's own account, and the key is borrowed, not stored** ([task 1004175](https://cloudbongos.com/builders#/task/1004175), goal 1000106 — BONGOS-V2). [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) moved a `cloud-host` to Render to stop being a landlord but left **whose account** unexamined; [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 then ruled the platform token is ours and "never per-customer". Standing it up disproved the premise: a Render API key is issued against a **user** and carries their rights in **every workspace they belong to** — the owner's reached three, one of them another builder's, holding their live production. The same check found `mercury` **already on Render**, run by its own owner, which 0323 would have migrated OFF a correct setup onto our bill. The remaining landlord relationship was never the servers; it was the **account**. **Decisions:** (1) each project brings its own platform key — we hold no billing relationship for anyone's compute, and a leaked key's blast radius is exactly its own owner's account, a property that comes from the decision and not from cryptography; (2) the key is **borrowed for standup and cleared**, never stored, because nothing afterwards needs it — Render auto-deploys from Git, the deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md)) is a push, liveness is the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull — so it rides the queue as a short-lived value encrypted with the existing `secret-box.js`, the [ADR 0133](0133-github-app-manifest-onboarding-signin.md) handshake's shape; (3) we **never destroy a customer's infrastructure** — teardown disconnects, the owner deletes their own services, a safety property first and the reason (2) works; (4) **two shapes, not three** (`dedicated-cloud-host` removed — on someone's own account a bigger plan is a setting, finishing 0323 §1's own "plan size, not a second code path"), and the **free tier is free**, 0323 §7's cap retired with its premise. **Deliberately NOT built, because nothing is stored:** an asymmetric sealing module, per-row credential binding, rotation/revocation, and the separate-unix-user precondition — each designed in full, then deleted when the retention question was asked instead of assumed. **Stated honestly:** encrypting the in-transit value protects it from a database dump, NOT from a compromised web tier, because the owner's key arrives there in plaintext first; that defence was never available at any retention length. **§5 records a measurement, fixed elsewhere:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §2's boundary is enforced by the CODE, not the kernel — `cloudbongos.service` runs `User=lars` with no sandboxing and `/etc/cloudbongos/box.env` is mode 600 owned by `lars`, so the web tier CAN read the DigitalOcean, Cloudflare and platform tokens; 0325 §4's "root-only" is not what is on the box. (A second finding in the same section — a "mode 777" key file — was **void and is corrected in place**: the path is a symlink, whose mode display is meaningless, to a 600 target. It was inferred from an `ls` rather than an attempted read.) **Resolved 2026-09-23 by [task 1004176](https://cloudbongos.com/builders#/task/1004176)**, and more narrowly than §5 proposed — not the unix split (which would mean relocating the checkout, config dir, `web.env` and the secret-box master key the web tier legitimately needs), but a systemd drop-in with `InaccessiblePaths`, verified as the service user; proving it as **root** reports "no effect" against a working config, because `CAP_DAC_OVERRIDE` bypasses the mode-000 over-mount. It is emphatically NOT a precondition here — that is the point of storing nothing, and had we sealed per-project keys with a private key in that same file the two would have had to land together. **Consequences:** the migration shrinks to an **adoption**; the cost ledger's question inverts (our spend on a `cloud-host` is ~\$0 and correct — what an owner is worth telling is THEIR Render cost, a different column, [task 1004148](https://cloudbongos.com/builders#/task/1004148)); and metering loses its forcing function, cost *visibility* surviving as a feature while billing for hosting does not ([task 1001949](https://cloudbongos.com/builders#/task/1001949)). Rejected: storing each key sealed with a key pair (three days plus a live-site infra change, to keep a value needed once); one platform account holding every project (0325 §4 as written — keeps us the billing landlord and makes one credential reach every customer); generating a Blueprint and having the owner click once in Render (simplest, but the click is the exact friction `cloud-host` exists to remove); and keeping the key in the browser to call Render directly (standup is a runner job with retries and an audit trail, not a page that stays open).](0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md) | hosting / provisioning / credentials |
449
- | 0328 | [**A hosted project's authority is its membership row, not its owner column** ([task 1003882](https://cloudbongos.com/builders#/task/1003882), goal 1000094 — BONGOS-V2). The owner's decision (2026-09-13): every API action must name its project, and a non-member may only apply. Two "project" concepts exist and answer different questions: `provisioning_instances` (the hub's fleet registry, single `owner_builder_id`, [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md)) and `<redacted>` (`client_id, github_id, membership_kind ∈ {owner,member,visitor,pending}`, already correct, already backing `GET /my-projects`). The composition this needs already exists once, in the opposite direction: `provisioning.projectOwnedBy` is a published port (task 1003580) that `platform-identity/invite-authz.js` already consumes via `resolveOptional` to ask "does this builder own this project" without importing provisioning (ADR 0083). **The gap is that the module which owns the fact does not use it on itself, and has grown three separate local copies of it instead of one** — `instanceAccessDenied` (6 routes), `resolveOwnInstance` (4 routes) and `disconnect.js`'s own inline copy (1 route) all implement "owner OR archon", while 3 routes (`teardown`, `domain`, `github-app`) implement only "owner" with no archon override at all — an existing inconsistency between sibling routes in the same module, not just duplication, and none of the three implementations is visible to `route-rank-check.js`. Decision: (1) project scope stays the existing path segment, resolved through `slug` (the `client_id = slug` join key [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) already establishes); (2) Phase 1 extracts one local `requireProjectOwner` helper replacing all three implementations — a behavior-preserving swap for 11 routes and a stated, intentional archon-override widening for the 3 that lacked it — no new port needed since provisioning already holds the column; Phase 2 adds a new port `platform-identity.projectMembershipKind` and widens the same helper to admit `owner` OR `member`, falling back to the Phase 1 column check when platform-identity isn't installed (ADR 0083's degrade-gracefully contract) — a NEW parallel axis, not a fifth branch on `requirePermission`, mirroring why the ownership axis ([ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100) is kept separate; (3) a caller with NO membership row still gets 404 `instance_not_found` (existence-hidden, unchanged), but a `pending`/`visitor` row gets 403 `project_membership_required` naming `POST /my-projects/join` (ADR 0141/0201/0247's existing apply flow) — the "apply to join it" the owner's decision names, routed into the flow that already ships rather than a second one; (4) `route-rank-check.js` gains a `project_scope_missing` finding (blocker on write, concerns on read) for any `/provisioning/instances/:id...` route missing the gate, the same structural read it already gives `requirePermission`; (5) migration order is the 14 evidenced routes only — the task's own "96 write routes" figure is the whole app's census, and the other ~82 act on a single-tenant instance's own Bongos board, which [ADR 0100](0100-two-repo-instance-model.md) §3 already isolates one-database-per-repo and has no project dimension to name. Filed: [task 1004159](https://cloudbongos.com/builders#/task/1004159) (the helper) · [task 1004160](https://cloudbongos.com/builders#/task/1004160) (route-rank-check.js, depends on 1004159) · [task 1004161](https://cloudbongos.com/builders#/task/1004161) (the membership port, depends on 1004159) · [task 1004162](https://cloudbongos.com/builders#/task/1004162) (the 403/404 split, depends on 1004161). Rejected: a second `provisioning_instance_members` table (the membership table already exists and joins on the same key — a second one repeats the mistake ADR 0325 already refused); folding project authority into `requirePermission` as a fifth axis (same reasoning R100 already applied to `.own` keys); migrating all 96 write routes (re-litigates ADR 0100's single-tenant model, not this decision); dropping 404 existence-hiding entirely (a total stranger should learn nothing, exactly as `invite-authz.js` already guarantees).](0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md) | security / authorization / provisioning |
449
+ | 0328 | [**A hosted project's authority is its membership row, not its owner column** ([task 1003882](https://cloudbongos.com/builders#/task/1003882), goal 1000094 — BONGOS-V2). **Amended 2026-10-16 ([task 1004754](https://cloudbongos.com/builders#/task/1004754)):** Phase 2 is suspended. The hub writes a `member` row at every sign-in, including the device flow anyone can start, so admitting `member` would hand any GitHub user any hosted project. The gate stays owner-or-archon until an owner-made grant is designed ([task 1004753](https://cloudbongos.com/builders#/task/1004753)). The owner's decision (2026-09-13): every API action must name its project, and a non-member may only apply. Two "project" concepts exist and answer different questions: `provisioning_instances` (the hub's fleet registry, single `owner_builder_id`, [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md)) and `<redacted>` (`client_id, github_id, membership_kind ∈ {owner,member,visitor,pending}`, already correct, already backing `GET /my-projects`). The composition this needs already exists once, in the opposite direction: `provisioning.projectOwnedBy` is a published port (task 1003580) that `platform-identity/invite-authz.js` already consumes via `resolveOptional` to ask "does this builder own this project" without importing provisioning (ADR 0083). **The gap is that the module which owns the fact does not use it on itself, and has grown three separate local copies of it instead of one** — `instanceAccessDenied` (6 routes), `resolveOwnInstance` (4 routes) and `disconnect.js`'s own inline copy (1 route) all implement "owner OR archon", while 3 routes (`teardown`, `domain`, `github-app`) implement only "owner" with no archon override at all — an existing inconsistency between sibling routes in the same module, not just duplication, and none of the three implementations is visible to `route-rank-check.js`. Decision: (1) project scope stays the existing path segment, resolved through `slug` (the `client_id = slug` join key [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) already establishes); (2) Phase 1 extracts one local `requireProjectOwner` helper replacing all three implementations — a behavior-preserving swap for 11 routes and a stated, intentional archon-override widening for the 3 that lacked it — no new port needed since provisioning already holds the column; Phase 2 adds a new port `platform-identity.projectMembershipKind` and widens the same helper to admit `owner` OR `member`, falling back to the Phase 1 column check when platform-identity isn't installed (ADR 0083's degrade-gracefully contract) — a NEW parallel axis, not a fifth branch on `requirePermission`, mirroring why the ownership axis ([ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100) is kept separate; (3) a caller with NO membership row still gets 404 `instance_not_found` (existence-hidden, unchanged), but a `pending`/`visitor` row gets 403 `project_membership_required` naming `POST /my-projects/join` (ADR 0141/0201/0247's existing apply flow) — the "apply to join it" the owner's decision names, routed into the flow that already ships rather than a second one; (4) `route-rank-check.js` gains a `project_scope_missing` finding (blocker on write, concerns on read) for any `/provisioning/instances/:id...` route missing the gate, the same structural read it already gives `requirePermission`; (5) migration order is the 14 evidenced routes only — the task's own "96 write routes" figure is the whole app's census, and the other ~82 act on a single-tenant instance's own Bongos board, which [ADR 0100](0100-two-repo-instance-model.md) §3 already isolates one-database-per-repo and has no project dimension to name. Filed: [task 1004159](https://cloudbongos.com/builders#/task/1004159) (the helper) · [task 1004160](https://cloudbongos.com/builders#/task/1004160) (route-rank-check.js, depends on 1004159) · [task 1004161](https://cloudbongos.com/builders#/task/1004161) (the membership port, depends on 1004159) · [task 1004162](https://cloudbongos.com/builders#/task/1004162) (the 403/404 split, depends on 1004161). Rejected: a second `provisioning_instance_members` table (the membership table already exists and joins on the same key — a second one repeats the mistake ADR 0325 already refused); folding project authority into `requirePermission` as a fifth axis (same reasoning R100 already applied to `.own` keys); migrating all 96 write routes (re-litigates ADR 0100's single-tenant model, not this decision); dropping 404 existence-hiding entirely (a total stranger should learn nothing, exactly as `invite-authz.js` already guarantees).](0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md) | security / authorization / provisioning |
450
450
  | 0329 | [**The rendered fit check is a kit floor and an advisory ship-check row, not a grader worker** ([task 1004203](https://cloudbongos.com/builders#/task/1004203), goal 1000095 — UI quality). Nothing in the pipeline rendered a page, so titles clipped by character count ran past their cards (task 1003480, task 1004204) and a second `.mSec` child would have landed on the first (task 1003078), each found only in a browser. Two new kit floors, `text-fit` (text measured word by word past its box; SVG text past its card rect) and `sibling-overlap` (in-flow, block-level, text-bearing siblings), run in every `render.js` render; `scripts/gds/render-check.js` renders the pages a branch changed at 1280 + 390 and `/ship-check` shows them as an ADVISORY row that never flips its exit. Not a grader worker: that would put a browser, fixtures and a harness on the grader host for signal the builder already saw; the artist gate (ADR 0241) stays the human look before deploy.](0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md) | ui-design / kit + ship-check |
451
451
  | 0330 | [**An ask is answered where it was asked** ([task 1004216](https://cloudbongos.com/builders#/task/1004216), goal 1000095 — BONGOS-V2), amending [ADR 0187](0187-collab-four-decisions.md) §4a. `lifecycle_004` let a builder ASK for help and let the **asker** mark their own row answered; the person being **asked** had no write at all, so the answer happened somewhere Bongos cannot see and the row sat open until its author remembered it — six asks addressed to the owner had been open between four and thirteen days on 2026-09-23, every one of them answerable. That was not an oversight: §4a deferred conversation to the Agora and a test named *"there is deliberately NO reply thread"* enforced it. **The deferral lost its addressee** — task 1002946 is ABANDONED and goal 1000064 ARCHIVED (verified against the live DB), so the rule had stopped meaning "wait for the Agora" and started meaning "an ask stays unanswerable forever to keep a promise to something that does not exist". **Decisions:** (1) an ask carries a thread of replies, APPEND-ONLY in the SCHEMA rather than by convention — no `updated_at`, `edited_by`, `deleted_at` or `parent_reply_id`, no reactions, no mentions, and a trigger that refuses UPDATE and DELETE outright, because a thing you can edit, nest, react to and address at people is a chat and that is what §4a declined; any builder may reply, since the board read is already project-wide and the good case is somebody you did not ask knowing the answer; (2) the **ADDRESSEE** may close an ask (`answered`), widening an asker-only UPDATE that was coherent only while nothing could be said on the row — but `withdrawn` does NOT widen, since "I no longer need this" is the asker's sentence alone and widening it would be a way to make an ask vanish unanswered; `settled_by` records which of the two closed it, and ADR 0187 §3's consent posture is untouched — closing ends a question, it does not accept work; (3) `/collab-review` posts the reply and **leaves the ask open** (owner decision), since an answer the reader has not read is exactly what an unattended run must halt on. **Consequences:** a reply cannot be taken back, by design; the FK **RESTRICTS rather than cascades** — the first draft cascaded, which reads right and was wrong, because a cascade is a plain DELETE the trigger refuses, so deleting an answered ask failed outright (caught by applying the migration to a real Postgres, not by reading it); the old test was REPLACED, not deleted, so the record that the restraint was deliberate survives; and replies inherit the pull-not-push posture, so nothing still tells an asker they were answered. Rejected: a single `answer` column (cheapest and unambiguously not a chat, but a follow-up would have nowhere to go except a second ask, splitting one conversation across two rows); waiting for the Agora (abandoned — waiting means forever); letting anyone close any ask (an open question is somebody's, and a stranger ending it should not be one click away).](0330-an-ask-is-answered-where-it-was-asked.md) | collab / lifecycle / schema |
452
452
  | 0331 | [**The sky is the ideator's home, and the project sky takes the goal map's page** ([task 1004229](https://cloudbongos.com/builders#/task/1004229), goal 1000095 — BONGOS-V2), amending [ADR 0227](0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md) and decision 11 of `docs/specs/<redacted>.md`. The owner reviewed an interactive prototype of the ideator sky and the project sky over five rounds on 2026-09-24 and adopted it AS IS — copy, layout, colours, tour text, toggles, interactions — which contradicts two things already written down, so they are rewritten rather than leaving the build graded against a contract nobody means any more. **Decisions:** (1) the sky is the ideator's HOME surface — it replaces *Your thinking*, and the project sky replaces the goal map page — so "never load-bearing" and "arrives last" are DROPPED as a sequencing hedge from before there was a design to look at; the rule that survives at full force is the accessibility half, that the sky is never the ONLY path to anything it shows (the list drawer — the room — is one key away, and every drawn thing is a row in it); (2) ADR 0227 is AMENDED, not reversed — its measured refusal of a whole-corpus task map stands, and the project sky is §1's own prescription rather than its refusal: the goal graph is the entry point, tasks are drawn only INSIDE one goal past a zoom threshold (the per-goal drill-down §10.5 measured readable at 85.2px pitch worst case), below that they are dust density on the rings with NO identities, and the goal map's table survives as the sky's ledger drawer (§6's table equivalent kept); the nav slot the goal map was deliberately denied is GRANTED to the project sky, under Goals (owner, 2026-09-24), so part 4 changes the page and the test asserting no slot together; (3) the five states and the two channels are UNTOUCHED — criterion `ideator-sky-tells-the-truth` names a model, not a picture, and this amendment changes the picture's standing only; (4) the two HEURISTICS in the prototype — an idea's origin guessed from its text, and "born of a thought" found by scanning text for idea ids — must not ship as ledger facts: parts 3 and 4 hide both layers, part 5 replaces them with a `filed_via` column and a real born-of read. **Consequences:** `docs/design/mocks/sky/sky.template.html` is the PIXEL CONTRACT for parts 2–5, and every deviation a gate or the file ratchet forces is listed in that part's ship notes; no snapshot JSON or built page is ever committed (it carries real task and idea titles and this repo is public); ADR 0264's deleted goal "Living project sky" is superseded by this work under Working area 3. Rejected: shipping the sky as an extra and keeping "never load-bearing" (makes the adopted home surface unroutable); reversing ADR 0227 (its refusal is measured and still correct — the sky needs its drill-down shape, not its repeal); shipping the heuristics behind a disclaimer (a labelled guess is still a guess in a picture whose promise is that it cannot be faked).](0331-the-sky-is-the-ideators-home.md) | design / ideator / sky |
@@ -3115,5 +3115,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3115
3115
  landed since 1.21.137 with no explicit bump. run 38920999785. (task 1002620)
3116
3116
  1.21.139 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3117
3117
  landed since 1.21.138 with no explicit bump. run 38927010049. (task 1002620)
3118
+ 1.21.140 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3119
+ landed since 1.21.139 with no explicit bump. run 38941756016. (task 1002620)
3118
3120
  ---------------------------------------------------------------------------
3119
3121
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.139",
3
+ "version": "1.21.140",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.139",
9
+ "version": "1.21.140",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.139",
3
+ "version": "1.21.140",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9836,5 +9836,11 @@
9836
9836
  "id": "1004752",
9837
9837
  "text": "Provisioning's own tests now run in the automated checks, so they can't quietly break again."
9838
9838
  }
9839
+ ],
9840
+ "1.21.140": [
9841
+ {
9842
+ "id": "1004754",
9843
+ "text": "Stopped a planned change that would have let anyone who signs in to a hosted project manage it. The design record now says sharing a project needs the owner to grant access on purpose; that design is filed as its own task."
9844
+ }
9839
9845
  ]
9840
9846
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.139'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.140'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');