@bongos/core 1.19.734 → 1.19.736
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 +27 -17
- package/docs/adr/0134-private-first-npm-distribution.md +1 -1
- package/docs/adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md +53 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/private-npm-distribution.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +2 -2
- package/scripts/gds/package-core.js +61 -26
- package/src/module-api.js +1 -1
- package/tests/leak_scan_fail_closed.mjs +108 -0
- package/tests/package_core.mjs +27 -10
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.736",
|
|
6
|
+
"core_contract": "1.19.736",
|
|
7
|
+
"source_commit": "917ef53331537c93796fb9db9a8b77be72cffe22",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-13T17:30:35.415Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 479,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2108,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2611,
|
|
20
|
+
"tree_sha256": "4cd2b02aa80c6f20919a54413b4983dd572af10e199bf5401e6e75ee8914b543",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -1157,7 +1157,7 @@
|
|
|
1157
1157
|
{
|
|
1158
1158
|
"path": "docs/adr/0134-private-first-npm-distribution.md",
|
|
1159
1159
|
"mode": "0000644",
|
|
1160
|
-
"sha256": "
|
|
1160
|
+
"sha256": "47b4d52804d291c8fd8952d8a2e8ad673f173a965cf0b5fe100aa6b93d850cd6"
|
|
1161
1161
|
},
|
|
1162
1162
|
{
|
|
1163
1163
|
"path": "docs/adr/0135-module-upstream-submission-interim-queue.md",
|
|
@@ -1919,10 +1919,15 @@
|
|
|
1919
1919
|
"mode": "0000644",
|
|
1920
1920
|
"sha256": "1cd5710392b4226c27af4ce4f661ee6228f80ce16e9f9adb1fb43c3ebb9b5d24"
|
|
1921
1921
|
},
|
|
1922
|
+
{
|
|
1923
|
+
"path": "docs/adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md",
|
|
1924
|
+
"mode": "0000644",
|
|
1925
|
+
"sha256": "7060acdff9fde6dbe4831ea1873e5f75bc25acf6e3207a084ac3ddb33ee1a2ff"
|
|
1926
|
+
},
|
|
1922
1927
|
{
|
|
1923
1928
|
"path": "docs/adr/README.md",
|
|
1924
1929
|
"mode": "0000644",
|
|
1925
|
-
"sha256": "
|
|
1930
|
+
"sha256": "872911d695cd9e3bf27b5bb4995acb2e814c73bbb7cabac2f7852ad10e3e1710"
|
|
1926
1931
|
},
|
|
1927
1932
|
{
|
|
1928
1933
|
"path": "docs/api-reference.md",
|
|
@@ -2487,7 +2492,7 @@
|
|
|
2487
2492
|
{
|
|
2488
2493
|
"path": "docs/module-api-changelog.md",
|
|
2489
2494
|
"mode": "0000644",
|
|
2490
|
-
"sha256": "
|
|
2495
|
+
"sha256": "8dc183ec4dc010d5897f8cca67dd0f55bb6abe5620e358388e9ed07a2f6521a8"
|
|
2491
2496
|
},
|
|
2492
2497
|
{
|
|
2493
2498
|
"path": "docs/modules-contract.md",
|
|
@@ -2697,7 +2702,7 @@
|
|
|
2697
2702
|
{
|
|
2698
2703
|
"path": "docs/recipes/private-npm-distribution.md",
|
|
2699
2704
|
"mode": "0000644",
|
|
2700
|
-
"sha256": "
|
|
2705
|
+
"sha256": "33e4f7f3083b8ac5d10f117d4d93e78b9d8b7b4d082087051b69698cb67b03b1"
|
|
2701
2706
|
},
|
|
2702
2707
|
{
|
|
2703
2708
|
"path": "docs/recipes/redteam-patrol.md",
|
|
@@ -7532,12 +7537,12 @@
|
|
|
7532
7537
|
{
|
|
7533
7538
|
"path": "package-lock.json",
|
|
7534
7539
|
"mode": "0000644",
|
|
7535
|
-
"sha256": "
|
|
7540
|
+
"sha256": "014dc18dcf764c17d7927fe5475436f4d6e2acf18e7baa6f16b7852306dc89e0"
|
|
7536
7541
|
},
|
|
7537
7542
|
{
|
|
7538
7543
|
"path": "package.json",
|
|
7539
7544
|
"mode": "0000644",
|
|
7540
|
-
"sha256": "
|
|
7545
|
+
"sha256": "96f124d7dee1ff76d006c102798790ab7b59b27c0fbacdb36c545d658c185f39"
|
|
7541
7546
|
},
|
|
7542
7547
|
{
|
|
7543
7548
|
"path": "public-docs/index.html",
|
|
@@ -8327,7 +8332,7 @@
|
|
|
8327
8332
|
{
|
|
8328
8333
|
"path": "scripts/gds/package-core.js",
|
|
8329
8334
|
"mode": "0000644",
|
|
8330
|
-
"sha256": "
|
|
8335
|
+
"sha256": "89ec2a080f397e1af5c0dede40a7eb22a2e3343d219da4b4a4ba903337bcc420"
|
|
8331
8336
|
},
|
|
8332
8337
|
{
|
|
8333
8338
|
"path": "scripts/gds/paste-token.js",
|
|
@@ -9337,7 +9342,7 @@
|
|
|
9337
9342
|
{
|
|
9338
9343
|
"path": "src/module-api.js",
|
|
9339
9344
|
"mode": "0000644",
|
|
9340
|
-
"sha256": "
|
|
9345
|
+
"sha256": "ad6748bac62f397b729ead21e7f591bc8e03fe771fff22855eb3813b066663da"
|
|
9341
9346
|
},
|
|
9342
9347
|
{
|
|
9343
9348
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11374,6 +11379,11 @@
|
|
|
11374
11379
|
"mode": "0000644",
|
|
11375
11380
|
"sha256": "f6097301300cdb559d3620a5c602b763b70f1acc14816a7e70e1cdb61bb42d95"
|
|
11376
11381
|
},
|
|
11382
|
+
{
|
|
11383
|
+
"path": "tests/leak_scan_fail_closed.mjs",
|
|
11384
|
+
"mode": "0000644",
|
|
11385
|
+
"sha256": "e5968a1df544680db05c66eeec803ebdbbbe98932bfec31a5287617c6dc01466"
|
|
11386
|
+
},
|
|
11377
11387
|
{
|
|
11378
11388
|
"path": "tests/lib_sh_resolution.mjs",
|
|
11379
11389
|
"mode": "0000644",
|
|
@@ -11742,7 +11752,7 @@
|
|
|
11742
11752
|
{
|
|
11743
11753
|
"path": "tests/package_core.mjs",
|
|
11744
11754
|
"mode": "0000644",
|
|
11745
|
-
"sha256": "
|
|
11755
|
+
"sha256": "a8278101d0de34bb8214f4c7a5a68760b5546099df27044c20d5c360fdc4442f"
|
|
11746
11756
|
},
|
|
11747
11757
|
{
|
|
11748
11758
|
"path": "tests/parallel-load.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0134 — Private-first npm distribution for the core + client (paid scoped, then public at launch)
|
|
2
2
|
|
|
3
|
-
- **Status:**
|
|
3
|
+
- **Status:** **Superseded.** The **core half** — the private-by-default posture — is superseded by [ADR 0283](0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md) ([task 1003872](https://cloudbongos.com/builders#/task/1003872), 2026-09-13): `@bongos/core` now publishes **public**. This ADR's own "public at launch" step is the thing that happened; what remains accurate here is the *reasoning* for the pre-launch posture and the scoped-package mechanics. The **client half** was already superseded by [task 1003742](https://cloudbongos.com/builders#/task/1003742) (2026-09-09) — see the note under Decision. Publish path built in task 2089.
|
|
4
4
|
- **Date:** 2026-07-07
|
|
5
5
|
- **Deciders:** example-owner (owner/Archon), Claude. Owner asked to have the npm/CLI onboarding commands ready but kept private pre-launch, choosing paid private npm over the free GitHub Packages option.
|
|
6
6
|
- **Task:** [#2089](https://example.com/builders#/task/2089) (publish path + private config) · [#2092](https://example.com/builders#/task/2092) (the `@cloudbongos` → `@bongos` scope rename + first real publish) · **Goal:** 35 (project-creation flow), criterion 165.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ADR 0283 — The core publishes publicly, because access was never the thing protecting it
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-13
|
|
5
|
+
- **Task:** [1003872](https://cloudbongos.com/builders#/task/1003872) (goal 1000090, working area 4 — Bongos Core distribution). Gated on [1003869](https://cloudbongos.com/builders#/task/1003869) (sweep the literals), [1003870](https://cloudbongos.com/builders#/task/1003870) (fail-closed leak scan), [1003871](https://cloudbongos.com/builders#/task/1003871) (fixture confirmation). Records the owner decision behind [1003502](https://cloudbongos.com/builders#/task/1003502) ("public repo, public npm, or wait for the mirror" — **public npm**). That task is *not* closed by this ADR: its remaining work is repointing the gate's open-source link, which lives in a separate static site with no auto-deploy, and should follow once the registry flip is live.
|
|
6
|
+
- **Supersedes:** [ADR 0134](0134-private-first-npm-distribution.md)'s **core half** — the pre-launch private-by-default posture. (Its client half was already superseded by task 1003742.)
|
|
7
|
+
- **Adjacent:** [ADR 0258](0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md) (the public CLI, which exists *because* the core was private) · [ADR 0136](0136-update-channel-subscription-policy.md) (the subscription lane this unblocks) · [ADR 0099](0099-delayed-redacted-mirror-export.md) (the mirror pipeline this path deliberately does not use) · [ADR 0161](0161-publish-on-merge.md) (the lane that performs the publish).
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
ADR 0134 chose **private-first**: `@bongos/core` publishes as a restricted scoped package on a paid npm org, with "public at launch" named as a later, deliberate step. That was the right call pre-launch. This ADR is that later step.
|
|
12
|
+
|
|
13
|
+
The posture had a cost that grew quietly, because every symptom looked like a different bug:
|
|
14
|
+
|
|
15
|
+
- **A tokenless `npm install` returns a flat E404** — indistinguishable from "no such package". Every co-tenant and downstream instance upgrade needs a credential to move at all.
|
|
16
|
+
- **The nightly auto-upgrade sweep died on 2026-08-25 and reported `failed=0`.** Its systemd unit passed no `NPM_TOKEN`, the instance's env-fed `.npmrc` expanded to empty, and the private read 404'd. A skip is not a failure in that script's accounting, so nothing anywhere complained while the instance stopped receiving core updates.
|
|
17
|
+
- **Any Anthropic-hosted cloud session** working on an instance repo cannot install the dependency at all.
|
|
18
|
+
- **A newly scaffolded instance's README step 1 is `npm install`** — which is also what installs the `bongos` command. Without a token the owner does not get a stale instance; they get no working instance.
|
|
19
|
+
- **[ADR 0258](0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md) exists only because of this.** `@cloudbongos/cli` was built, and its require-closure painstakingly delazified, so that a new builder could reach the board without a private-registry credential.
|
|
20
|
+
|
|
21
|
+
The decisive point is that **access control was never what protected this artifact.** Three gates decide what is *in* the tarball, and all three now hold:
|
|
22
|
+
|
|
23
|
+
1. The in-build **no-leak gate** (branding-derived literals + live credential shapes) — fail-closed, 2,624 files clean.
|
|
24
|
+
2. The **prior-instance leak scan** — fatal by default since task 1003870, with `--no-leak-scan` the only escape; a real build passes with `✓ no prior-instance literals in the built artifact`.
|
|
25
|
+
3. The **fixture confirmation** of task 1003871 — all 14 credential-shape files checked against each provider's real key format (11 too short to have ever been valid, 3 alphabet sequences or Amazon's own published documentation example).
|
|
26
|
+
|
|
27
|
+
`--access restricted` decides *who may fetch* the artifact. Those three decide *what is in it*. Conflating them is what made the private posture feel like a security control rather than a launch-timing choice.
|
|
28
|
+
|
|
29
|
+
## Decision
|
|
30
|
+
|
|
31
|
+
**`@bongos/core` publishes as a public scoped package.**
|
|
32
|
+
|
|
33
|
+
- `buildPackageJson` sets `publishConfig.access: "public"`, and `publishArgs` passes `--access public`. Both, deliberately: the belt-and-braces of the restricted era, pointed the other way, so neither drifting alone can silently take the package back to private.
|
|
34
|
+
- **`.github/workflows/publish.yml` passes `--access public` too.** This is the site that actually releases — `package-core --publish` is the manual owner path. Flipping only `package-core.js` would have left every real release restricted while the code claimed otherwise: precisely the failure shape of task 1003870, where the workflow did not arm the gate its own header comment advertised. A test now reads the workflow and fails if `restricted` reappears anywhere in the lane.
|
|
35
|
+
- **The one-time registry flip is a separate, deliberate owner step:** `npm access set status=public @bongos/core`. `--access` on publish governs what *that publish* declares, and has historically been a no-op when re-publishing an already-private package. The package read `private` on the registry when this ADR was written, so the flag alone cannot be assumed sufficient — the flip is verified by a tokenless install from a clean `HOME`, not by the flag's presence.
|
|
36
|
+
- **Provenance stays off.** The *package* is public; the *source repository* is not. npm refuses to generate provenance from a private repo, so `NPM_CONFIG_PROVENANCE: 'false'` remains required until the source repository itself goes public. Turning it on before then fails every publish.
|
|
37
|
+
- **The npm path does not run ADR 0099's mirror pipeline, by design.** `package-core` performs its own redaction (476 docs redacted, 24 agent-docs stubbed, `package.json` synthesized). That is why gates 1–3 carry the whole safety burden here, and why they had to land first.
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
- `npm install @bongos/core` works from any machine with no credential. Co-tenant upgrades, the nightly sweep, cloud sessions and fresh scaffolds all stop needing a token they were never given.
|
|
42
|
+
- The sweep's `NPM_TOKEN` dependency becomes vestigial for reads. It should be removed from that path rather than left as a trap that appears to work.
|
|
43
|
+
- **[ADR 0258](0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md) is largely redundant and should be revisited, not deleted reflexively.** `@cloudbongos/cli` is published, works, and has a deliberately narrow verb set; the question is whether a public core makes it a maintenance burden or a still-useful thin entry point.
|
|
44
|
+
- **This does not fix the 22 co-tenants.** Twenty of them pin `file:vendor/bongos-core-<v>.tgz` — a local path with no registry channel to follow. Public access is necessary but not sufficient; converting them is separate work.
|
|
45
|
+
- The source repository stays private. Public package ≠ public repo, and nothing here changes who can read the source repository.
|
|
46
|
+
- Going public is effectively **one-way**. A published tarball is mirrored and cached beyond our control; `npm unpublish` has a 72-hour window and does not reach mirrors. This is why the flip is an owner action and why gates 1–3 were non-negotiable prerequisites.
|
|
47
|
+
|
|
48
|
+
## Rejected
|
|
49
|
+
|
|
50
|
+
- **A rank-gated registry proxy** — the server holds the npm token, builders authenticate with their existing Bongos CLI session, and the server proxies to npmjs.org after a per-request rank check. This is the correct design *if the core must stay private*: nothing is handed out, revocation is instant and per-builder, and every fetch is auditable. It was rejected because it manages the problem that going public deletes, and because it would entrench the private posture the owner chose to leave.
|
|
51
|
+
- **Handing builders the npm read token, gated by rank.** Rank-gating the handout does not rank-gate the use: a token, once held, is held forever and can be passed on, and revocation means rotating it for everyone. It is access control in name only.
|
|
52
|
+
- **Keeping the vendored-tarball path as the answer.** It is what co-tenants do today, and it is exactly why twenty of them are frozen on the version current at their provisioning date.
|
|
53
|
+
- **Making the repository public in the same change.** A larger decision with its own review, and not required for any of the benefits above. Keeping them separate also keeps the provenance question honest.
|
package/docs/adr/README.md
CHANGED
|
@@ -374,3 +374,4 @@ This keeps the decision history honest and traceable.
|
|
|
374
374
|
| 0280 | [**The unattended lane may drive a co-tenant, because the roster entry IS the per-instance decision** ([task 1003843](https://cloudbongos.com/builders#/task/1003843) · goal 1000106; from [task 1003521](https://cloudbongos.com/builders#/task/1003521), which built co-tenant mode into `go-live.js` and deliberately left this caller alone). `subscribe.js` keeps a SECOND COPY of go-live's target contract, and it drifted: `GO_LIVE_REQUIRED` still listed `deployTimer` after 1003521 made it optional, so `goLiveTargetFor()` returned `missing: ['deployTimer']` for every provisioned co-tenant, and `rawTopologyFor()`'s fixed field list dropped `pinMode` before the target was built — the same stale-normalizer trap that function's own comment was written to fix, one field later. **1003521 deferred this as a policy judgement call; reading the fallback shows the premise was wrong.** `mapped.ok === false` never meant "skip this instance" — it meant "deploy it the WEAKER way": a bare `bongos upgrade`, no backup, no deploy-timer guard, and a pin COMMITTED AND PUSHED into the co-tenant's own repo, which is exactly what `pinMode` exists to prevent. The lane was already auto-deploying co-tenants; the only open question was whether it did so well. A THIRD defect sat behind the other two and would have turned the silent downgrade into a hard failure had only the first been fixed: the normalizer yields `deployTimer: null`, an explicit null SURVIVES `JSON.stringify` into `--target-json`, and go-live's `validateTarget` runs `isSafeToken(null)` on it and rejects the whole target. A FOURTH sat PAST the go-live call and is what separates "reachable" from "safe": after any successful upgrade the lane runs its own pin net, `commitPin()`, which commits the pin files and PUSHES them to `origin` — a net that predates `pinMode`. Under `local-commit` go-live leaves the tree clean so it no-ops by luck; under **`leave-dirty`** the pin is in the working tree BY DESIGN, so the net committed it and pushed it into the customer's own repo — the lane honouring `pinMode` right up to the moment it undid it. **Decision: the lane may drive a co-tenant and needs no new gate, because listing an instance in `config/update-subscriptions.json` already IS the explicit per-instance operator decision** — the roster ships empty, nothing is touched until an operator lists it, and the routine is autonomy-gated (`requiresAutonomy: true`, default OFF) on top of that; a second consent gate asks the same person the same question twice. So: `GO_LIVE_REQUIRED` now MIRRORS go-live's `REQUIRED_FIELDS`, with a test reading BOTH source files and failing on drift; `deployTimer` moves to the optional-field loop so a co-tenant OMITS the key rather than carrying a null; `rawTopologyFor` carries `pinMode`; `resolveGoLive` gains a `needsCoTenant` probe leg; and `goLiveEngineFor()` makes the decision ONCE for both the dry run and the apply path, which had re-derived it separately. The pin net now obeys the same `pinMode` go-live was given — `commitPin()` takes `push` (default true, so the platform instance is unchanged), `local-commit` commits without pushing, `leave-dirty` skips the net, the rollback tidy follows the same rule, and the "not durable" advice stops telling an operator to `git push` a repo we do not own; its refusal to touch a worktree carrying NON-pin changes is unchanged in every mode. **The probe is CONDITIONAL on purpose** — demanding `pinMode` of every target would strip the go-live path from any instance between the `--target-json` release and 1003521's, trading a live regression for a hypothetical one. Rejected: a second consent gate (an `allowCoTenant` flag); deferring to the unspecced fleet control plane ([task 1001948](https://cloudbongos.com/builders#/task/1001948)) — which does not leave co-tenants alone, it leaves them pushing pins into customer repos; dropping `deployTimer` from `GO_LIVE_REQUIRED` and nothing else (the fix as proposed — trades a silent downgrade for a hard rejection); probing unconditionally; reaching co-tenant mode WITHOUT teaching the durability net about it (nominal support that breaks its own promise seconds later); and teaching `loadSubscriptions()` about `pinMode` instead, since that normalizer ships in the control plane's vendored core that nobody bumps — the exact trap `rawTopologyFor` routes around.](0280-the-unattended-lane-may-drive-a-co-tenant-because-the-roster-is-the-decision.md) | core distribution / unattended deploy |
|
|
375
375
|
| 0281 | [**An instance’s identity is its own unix account and its own password-authed Postgres role** ([task 1003369](https://cloudbongos.com/builders#/task/1003369) · goal 1000094, audit ref `B3` of the 2026-08-29 security audit). [ADR 0111](0111-instance-hosting-provisioning-module.md) made standup automatic and [ADR 0128](0128-provisioning-runner-privilege-and-db-auth-model.md) settled how the RUNNER escalates; neither settled who the INSTANCE is once it runs, and the answer was that everybody was the same person. `serviceUnit` hardcoded `User=lars`, and **co-tenant is the default shape** — so on a shared box every instance’s process ran under one uid, and `pool.js` connected by **passwordless peer auth**. Two consequences, neither a bug in any route: a unix user may read `/proc/<pid>/environ` of its own processes, so every instance could read every sibling’s `GITHUB_APP_PRIVATE_KEY`, hub client secret and OAuth secret; and peer auth answers “which unix user are you”, so any code as `lars` could open a pool against **any** database on the box by naming it — including the control plane’s own `builders` and `builder_sessions`. The `0600` `web.env` and the per-instance DB were already correct: sound walls with one door, and every instance had the key. **Decision: each instance gets its own unix account and its own password-authed PG role, both derived from its slug, both created by the provision run.** The account is `bongos-<slug>`, hash-folded past utmp’s 32 chars — prefixed because a project may legally be called `postgres`, and hash-suffixed because two slugs sharing a 16-char prefix would otherwise fold onto ONE account, which is the shared identity being removed. Its home is `/var/lib/<user>` and deliberately **not** under `/home`, since the unit sets `ProtectHome=read-only` and the server writes `builder-secret.key` inside its config home on first boot — a `/home` account would make that `EROFS`. The unit gains `ProtectSystem=strict` / `ProtectHome=read-only` / `PrivateTmp=true` with `ReadWritePaths` as the whole allow-list; **the checkout the service runs is deliberately not writable by it**. The role is `LOGIN` + password, owns its own DB, and is NOT superuser/CREATEDB/CREATEROLE — with **`REVOKE CONNECT … FROM PUBLIC` as the half that actually isolates**, because PG grants `CONNECT` to `PUBLIC` by default and per-role passwords alone leave a sibling one peer-auth hop away. Grants run in a SECOND statement **after** the migrate, since the privileged migrate runs as `postgres` and owns every table it creates; `ALTER DEFAULT PRIVILEGES` aims the same grant forward. The password is minted per run and never stored — the same run sets it on the role and rewrites the `0600` env file, so **every provision rotates it** — and `pool.js` reads `PGUSER`/`PGPASSWORD` **explicitly** rather than via `pg`’s env defaults, because the whole defect was a connection whose identity nothing in the code stated. **Absence is the old behavior:** no password ⇒ no credential lines and no pool keys, so an instance provisioned before this keeps its peer auth and the rollout cannot brick a box. Two consequences worth the record. (1) **The generated SQL contains no double quote, and that is load-bearing** — every literal is dollar-quoted and every identifier goes through `format(%I)`, because these commands are embedded in the dedicated droplet’s cloud-init as `- [ bash, -lc, "…" ]`, a YAML DOUBLE-QUOTED scalar: a plain `"role"` identifier reads fine in `psql` and silently truncates the line. A test asserts the absence rather than leaving it to habit. (2) **A partial, stated honestly** — the account is added to the app-user group so it can read the checkout it runs from (without it the unit fails at exec with `EACCES`), so FILE-level co-tenant separation is not what this delivers; the process and database boundaries are. Rejected: a `DATABASE_URL` in `web.env` (needs no pool change, but trades a stated, assertable identity for a socket-path parse that is `pg-connection-string`’s business); per-role passwords WITHOUT the `PUBLIC` revoke (reads like isolation, and peer auth walks around it); `ProtectSystem=full`, or `strict` with the working directory writable (makes the code the service executes writable by the service, in a change whose subject is containment); keeping `User=lars` and isolating with a second mechanism (the shared uid IS the `/proc` vector); storing the password in `provisioning_instances` (a live credential in plaintext, to buy stability the rotation does not need).](0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md) | tenant isolation / provisioning / DB auth |
|
|
376
376
|
| 0282 | [**A module that spends money for its callers DECLARES so, and the core refuses to enable it where anyone can sign up and nobody is on the hook** ([task 1003884](https://cloudbongos.com/builders#/task/1003884), owner decision 2026-09-13). A fire from `modules/agents` costs ~$0.90, `POST /agents/:name/invoke` answers **202** so the caller does not even wait, and the route is gated at `requireBuilder` and nothing else — deliberately, because a v1 agent is hard-read-only by plumbing. That bounds CAPABILITY, not SPEND, and `lib/fire-budget.js` says so in its own header: it is a blast radius, explicitly NOT a budget, and it defers the real cap to `agents_runs.cost_usd`. The cap did not exist. The exposure is three reasonable facts composed: a fire is expensive and async; invoke admits any **any-builder**; and with the join door `open`, any GitHub account IS one seconds after sign-up. **Decision: the MODULE declares `"spend": { "requiresPayer": true }` and the CORE refuses to enable it when the composed join door (ADR 0247 — the door, never the `joinability` knob alone) is `open` with no configured payer.** The declaration lives in the manifest because fitness Check 10 forbids the kernel from naming a discovered module key — a constraint that turns out to be the better design, since the next metered module inherits the guard by declaring one line rather than by someone remembering to widen a list. The refusal is at **config load**, so the routes never mount and the migrations never run: a per-request check would let the module mount, advertise its routes and read as enabled everywhere the UI looks, then refuse callers one at a time — a broken feature, not a closed door. It is applied AFTER config, env and default alike, because `<PREFIX>_MODULE_<KEY>` is exactly the knob a hurried deploy reaches for and a wall env could outrank is a suggestion. An UNREADABLE posture refuses too, inverting this file’s usual fail-to-unchanged default: "I could not tell whether strangers can sign up" has to mean "assume they can", and the cost of being wrong is one module resolving off. `spend_payer` (`none|project|builder`) joins `SETTINGS_VOCAB` on ADR 0241’s transport, and `agents_runs` gains `payer_kind` + `payer_builder_id` — **stored, not derived**, because the setting is a live knob and deriving would make last month’s bill change when an owner flips it. `none` is NOT a third payer: a run always has a real one and `none` attributes to the project, so a CLOSED-door instance is unchanged and `none` is fatal only where it is also dangerous. An EVENT fire has no caller, so it bills the project rather than nobody — otherwise the event path is the one unbounded route. The per-payer monthly ceiling (`lib/spend-ceiling.js`) is a SECOND wall that must never merge into the first: a rate ceiling cannot bound a bill (~$650/month/builder at 10 fires an hour) and a spend ceiling cannot bound a loop (it reads spend already landed). It records NOTHING — the ledger is the tally, and a second tally is the disagreeing answer fire-budget’s header refuses to become. It honours the finding of abandoned [task 1001393](https://cloudbongos.com/builders#/task/1001393), which caught the autonomy brake reading month-to-date spend from a DB it could not reach and **failing open**: every unknown refuses (no cap, non-finite cap, no payer, a read that threw, a sum absent or unparseable), answering **402** rather than fire-budget’s 429 so "the money is gone" and "too fast" stay distinguishable. Writing the absent-sum clause down was not paranoia — the first draft admitted the fire on a null sum because `Number(null)` is `0`, and the test for that clause is what caught it. The overshoot is STATED: in-flight fires are invisible to the read, bounded by concurrency × ~$0.90, and what bounds concurrency is fire-budget — each wall’s blind spot is the other’s window. The cost-needs-a-payer constraint is added `NOT VALID` so it binds every future write without failing the migration of an instance whose history predates the column. **What it does not do:** nobody is billed; `builder` attributes spend, it does not collect.](0282-a-module-that-spends-for-its-callers-declares-a-payer-and-the-core-refuses-it-without-one.md) | modules / spend attribution / agents |
|
|
377
|
+
| 0283 | [**The core publishes publicly, because access was never the thing protecting it** ([task 1003872](https://cloudbongos.com/builders#/task/1003872) · goal 1000090 — *Working area 4, Bongos Core distribution*; owner decision 2026-09-13, resolving [task 1003502](https://cloudbongos.com/builders#/task/1003502)). [ADR 0134](0134-private-first-npm-distribution.md) chose private-first and named "public at launch" as a later deliberate step; this is that step. The restricted posture had a cost that grew quietly because every symptom read as a different bug: a tokenless `npm install` returns a flat **E404**, indistinguishable from "no such package", so every co-tenant and downstream upgrade needed a credential to move at all; the nightly auto-upgrade sweep **died on 2026-08-25 and reported `failed=0`** (its unit passed no `NPM_TOKEN`, the env-fed `.npmrc` expanded to empty, and a skip is not a failure in that script's accounting, so nothing complained while the instance stopped receiving core updates); an Anthropic-hosted cloud session could not install the dependency at all; and a fresh scaffold's README step 1 IS `npm install`, which is also what installs the `bongos` command — so without a token an owner got no working instance rather than a stale one. [ADR 0258](0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md)'s `@cloudbongos/cli`, require-closure delazification and all, exists ONLY because of this. **The decisive point is that access control was never what protected the artifact:** three gates decide what is IN the tarball — the in-build no-leak gate (branding literals + credential shapes, fail-closed, 2,624 files clean), the prior-instance leak scan (**fatal by default since [task 1003870](https://cloudbongos.com/builders#/task/1003870)**, `--no-leak-scan` the only escape), and the fixture confirmation of [task 1003871](https://cloudbongos.com/builders#/task/1003871) (all 14 credential-shape files checked against each provider's real key format: 11 too short to have ever been valid, 3 alphabet sequences or Amazon's own published doc example). `--access restricted` decides who may FETCH it; those three decide what is IN it, and conflating them is what made a launch-timing choice feel like a security control. **Decision: publish public — at all three sites.** `publishConfig.access` and `publishArgs` both say `public` (belt-and-braces of the restricted era pointed the other way, so neither drifting alone reverts it), **and so does `publish.yml`** — the site that ACTUALLY releases, `package-core --publish` being the manual owner path. Flipping only `package-core.js` would have left every real release restricted while the code claimed otherwise: exactly task 1003870's shape, where the workflow did not arm the gate its own header comment advertised. A test reads the workflow and fails if `restricted` reappears anywhere in the lane. **The one-time registry flip stays a separate owner step** (`npm access set status=public`): `--access` governs what THAT publish declares and has historically been a no-op when re-publishing an already-private package — the package read `private` on the registry when this was written — so it is verified by a tokenless install from a clean `HOME`, never by the flag's presence. **Provenance stays off:** the PACKAGE is public, the SOURCE REPO is not, and npm blocks provenance on the repo's visibility — turning it on before the source repository goes public fails every publish. The npm path deliberately does not run [ADR 0099](0099-delayed-redacted-mirror-export.md)'s mirror pipeline (`package-core` runs its own redaction: 476 docs redacted, 24 agent-docs stubbed), which is why gates 1–3 carry the whole safety burden and had to land first. **Going public is effectively one-way** — a published tarball is mirrored and cached beyond our control and `npm unpublish` has a 72-hour window that does not reach mirrors. **Does NOT fix the 22 co-tenants:** twenty pin `file:vendor/bongos-core-<v>.tgz`, a local path with no registry channel to follow, so public access is necessary but not sufficient. Rejected: a **rank-gated registry proxy** (server holds the token, builders authenticate with their existing Bongos session, per-request rank check — the RIGHT design if the core must stay private, rejected because it manages the problem going public deletes); **handing builders the read token gated by rank** (rank-gating the handout does not rank-gate the use — a token once held is held forever and revocation means rotating it for everyone: access control in name only); keeping the vendored tarball as the answer (it is why twenty co-tenants are frozen at their provisioning date); making the REPO public in the same change (a larger decision, not required for any of this, and separating them keeps the provenance question honest).](0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md) | core distribution / npm access |
|
|
@@ -1942,5 +1942,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1942
1942
|
landed since 1.19.732 with no explicit bump. run 34766569496. (task 1002620)
|
|
1943
1943
|
1.19.734 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1944
1944
|
landed since 1.19.733 with no explicit bump. run 34767120578. (task 1002620)
|
|
1945
|
+
1.19.735 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1946
|
+
landed since 1.19.734 with no explicit bump. run 34768136326. (task 1002620)
|
|
1947
|
+
1.19.736 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1948
|
+
landed since 1.19.735 with no explicit bump. run 34771773838. (task 1002620)
|
|
1945
1949
|
---------------------------------------------------------------------------
|
|
1946
1950
|
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **This is one leg of three.** For the whole path from a change on core `main` to a live instance — and for when publishing to npm is *not* what moves an instance — start at [the core release pipeline](core-release-pipeline.md).
|
|
4
4
|
>
|
|
5
|
-
>
|
|
5
|
+
> **⚠️ The private half of this recipe is HISTORY as of 2026-09-13.** `@bongos/core` publishes **public** — [ADR 0283](../adr/0283-the-core-publishes-publicly-because-access-was-never-the-thing-protecting-it.md) ([task 1003872](https://cloudbongos.com/builders#/task/1003872)) supersedes [ADR 0134](../adr/0134-private-first-npm-distribution.md)'s core half. A tokenless `npm install @bongos/core` now works, so **anything below telling you to set `NPM_TOKEN` for a READ is obsolete**; a token is still needed to *publish*. The build, redaction, manifest-pin and no-leak-gate mechanics are unchanged and still accurate. The daily command is `bongos` regardless of package name.
|
|
6
6
|
>
|
|
7
7
|
> **`@bongos/client` is no longer part of this.** It was published once (2026-07-07, 0.0.2), never updated while the API moved 500+ core versions, and had **zero** consumers the whole time — every consumer vendors a copy instead. The owner removed it from npm on 2026-09-09; ADR 0134's client half is superseded ([task 1003742](https://cloudbongos.com/builders#/task/1003742)). The generated client still exists and still regenerates — it is simply not a package.
|
|
8
8
|
>
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.736",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.736",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.736",
|
|
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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
|
-
"access": "
|
|
24
|
+
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"express": "^4.21.2",
|
|
@@ -59,16 +59,20 @@
|
|
|
59
59
|
// (task 1002218 / audit H3) — deterministic + network-free.
|
|
60
60
|
// --self-verify-boot ALSO `npm install <tgz>` into a throwaway consumer + boot /healthz
|
|
61
61
|
// (reuses tests/consumer_layout_boot.mjs; needs npm/registry access).
|
|
62
|
-
// --leak-scan
|
|
63
|
-
//
|
|
62
|
+
// --no-leak-scan THE ONLY ESCAPE from the post-build leak scan: skips it entirely.
|
|
63
|
+
// The scan is FAIL-CLOSED by default — an un-allowlisted prior-instance
|
|
64
|
+
// literal in the built artifact aborts the build (task 1003870 / H6).
|
|
65
|
+
// --leak-scan-strict accepted as an explicit no-op; fatal IS the default since task
|
|
66
|
+
// 1003870. Kept so CI can state the intent it relies on out loud.
|
|
64
67
|
// Outputs (nothing is committed — dist/ is gitignored):
|
|
65
68
|
// dist/bongos-core-<version>/ the redacted core tree (inspectable)
|
|
66
69
|
// dist/bongos-core-<version>.tgz the installable unit
|
|
67
70
|
// dist/bongos-core-<version>.manifest.json the PIN (tree hash + per-file hashes)
|
|
68
|
-
// --publish after building, `npm publish` the .tgz as a
|
|
69
|
-
// package (--access
|
|
70
|
-
// (task
|
|
71
|
-
//
|
|
71
|
+
// --publish after building, `npm publish` the .tgz as a PUBLIC scoped
|
|
72
|
+
// package (--access public). DRY-RUN unless --live is added
|
|
73
|
+
// (task 1003872 / ADR 0283, superseding ADR 0134's restricted
|
|
74
|
+
// posture). Needs `npm login` as a member of the @bongos org;
|
|
75
|
+
// this script holds no token.
|
|
72
76
|
|
|
73
77
|
const fs = require('node:fs');
|
|
74
78
|
const os = require('node:os');
|
|
@@ -303,13 +307,18 @@ function buildPackageJson({ version, sourceDeps, sourceOverrides }) {
|
|
|
303
307
|
'./module-seams': './src/module-seams.js',
|
|
304
308
|
},
|
|
305
309
|
engines: { node: '>=22' },
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
310
|
+
// Distribution posture: PUBLIC (task 1003872, ADR 0283 — supersedes ADR 0134's
|
|
311
|
+
// pre-launch `restricted`). The private posture existed so a pre-launch artifact
|
|
312
|
+
// could not leak; what it actually cost was that a tokenless `npm install` 404s,
|
|
313
|
+
// which broke every co-tenant upgrade, the nightly auto-upgrade sweep (silently,
|
|
314
|
+
// since 2026-08-25), and every newly scaffolded instance.
|
|
315
|
+
//
|
|
316
|
+
// The safety this replaces is NOT "nobody can read it" — it is the three gates
|
|
317
|
+
// that now run on every build: the branding/credential no-leak gate, the
|
|
318
|
+
// prior-instance leak scan (fatal by default since task 1003870), and the
|
|
319
|
+
// fixture confirmation of task 1003871. Those decide what is IN the tarball;
|
|
320
|
+
// access decides who may fetch it. Do not re-conflate them.
|
|
321
|
+
publishConfig: { access: 'public' },
|
|
313
322
|
dependencies,
|
|
314
323
|
};
|
|
315
324
|
if (sourceOverrides && sourceOverrides.undici) pkg.overrides = { undici: sourceOverrides.undici };
|
|
@@ -551,12 +560,20 @@ function arg(name, fallback = null) {
|
|
|
551
560
|
// npm is `npm.cmd` on Windows and can't be execFile'd bare; unix uses `npm`.
|
|
552
561
|
const NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
553
562
|
|
|
554
|
-
// Pure — the exact `npm publish` argv for the built core tarball.
|
|
555
|
-
//
|
|
556
|
-
// pass `--access
|
|
557
|
-
//
|
|
563
|
+
// Pure — the exact `npm publish` argv for the built core tarball. The package
|
|
564
|
+
// publishes PUBLIC via publishConfig.access:"public" (buildPackageJson); we ALSO
|
|
565
|
+
// pass `--access public` so the posture is stated at the call site and a stray
|
|
566
|
+
// `publishConfig` edit can't silently take it back to restricted — the same
|
|
567
|
+
// belt-and-braces the restricted era used, pointed the other way (task 1003872).
|
|
568
|
+
//
|
|
569
|
+
// NOTE for whoever reads this after a failed rollout: `--access` on publish governs
|
|
570
|
+
// what THIS publish declares. A package that is already `private` on the registry
|
|
571
|
+
// also needs the one-time, registry-side `npm access set status=public @bongos/core`
|
|
572
|
+
// — the flag alone has historically been a no-op on re-publish. ADR 0283 records
|
|
573
|
+
// that flip as a separate, deliberate owner step for exactly that reason.
|
|
574
|
+
// `--dry-run` unless `live` (the permanent owner step).
|
|
558
575
|
function publishArgs(tgzPath, live) {
|
|
559
|
-
const a = ['publish', tgzPath, '--access', '
|
|
576
|
+
const a = ['publish', tgzPath, '--access', 'public'];
|
|
560
577
|
if (!live) a.push('--dry-run');
|
|
561
578
|
return a;
|
|
562
579
|
}
|
|
@@ -629,6 +646,18 @@ function scanKindOf(pathRel) {
|
|
|
629
646
|
return 'functional';
|
|
630
647
|
}
|
|
631
648
|
|
|
649
|
+
// The post-build leak scan's POLICY, split out of main() so a test can plant a literal and prove
|
|
650
|
+
// the build refuses without running a full 2-minute package (task 1003870). Pure: argv in, verdict
|
|
651
|
+
// out. Fail-closed — any un-allowlisted prior-instance literal is fatal unless the operator has
|
|
652
|
+
// explicitly said --no-leak-scan, which skips the scan altogether rather than downgrading it.
|
|
653
|
+
// `--leak-scan-strict` is honoured as a no-op: it named this behaviour before it became default.
|
|
654
|
+
function leakScanVerdict(remainingLiteralHits, argv = process.argv) {
|
|
655
|
+
if (argv.includes('--no-leak-scan')) return { run: false, fatal: false, reason: 'skipped' };
|
|
656
|
+
const n = (remainingLiteralHits || []).length;
|
|
657
|
+
if (n === 0) return { run: true, fatal: false, reason: 'clean' };
|
|
658
|
+
return { run: true, fatal: true, reason: 'prior-instance-literals' };
|
|
659
|
+
}
|
|
660
|
+
|
|
632
661
|
// Independent post-build leak scan over the built core file set (task 1002220 / audit H6). Two layers:
|
|
633
662
|
// • INSTANCE LITERALS — findLeaks() over PRIOR_INSTANCE_LITERALS (+ the public-IP floor) across
|
|
634
663
|
// EVERY shipped file: brand/owner/history that must not ship in a portable core. This is the
|
|
@@ -942,14 +971,14 @@ function main() {
|
|
|
942
971
|
|
|
943
972
|
// Post-build leak scan (task 1002220 / audit H6): independent verification that the BUILT
|
|
944
973
|
// artifact carries no prior-instance identity, + a warn-level secret-shape scan over functional
|
|
945
|
-
// files. Runs on every build
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
// secret-shape hit
|
|
974
|
+
// files. Runs on every build and is FAIL-CLOSED: an un-allowlisted prior-instance literal aborts
|
|
975
|
+
// the build. The trunk sweep (task 1003869) cleared the residue those literals used to represent,
|
|
976
|
+
// so warning instead of failing would only let it creep back (task 1003870). --no-leak-scan is
|
|
977
|
+
// the sole escape. A secret-shape hit stays a non-fatal warning — those 14 files were each
|
|
978
|
+
// confirmed a test fixture (task 1003871) and the R88 functional-verbatim fix is preserved.
|
|
949
979
|
if (!process.argv.includes('--no-leak-scan')) {
|
|
950
980
|
const scanFiles = staged.map((s) => ({ path: s.name.startsWith(TAR_ROOT) ? s.name.slice(TAR_ROOT.length) : s.name, content: s.buf }));
|
|
951
981
|
const scan = scanArtifactForLeaks(scanFiles);
|
|
952
|
-
const strict = process.argv.includes('--leak-scan-strict');
|
|
953
982
|
// Named, reasoned exceptions (task 1003869). A detector must carry the pattern it
|
|
954
983
|
// detects and a "public IPs are flagged" test must be handed a public IP, so those
|
|
955
984
|
// hits are real but not leaks. Each is listed per (file, literal) with its reason in
|
|
@@ -966,17 +995,21 @@ function main() {
|
|
|
966
995
|
for (const e of stale) console.error(` ${e.path} [${e.literal}]`);
|
|
967
996
|
}
|
|
968
997
|
scan.literalHits = remaining;
|
|
998
|
+
const verdict = leakScanVerdict(scan.literalHits);
|
|
969
999
|
if (scan.literalHits.length) {
|
|
970
1000
|
const byLit = {};
|
|
971
1001
|
for (const h of scan.literalHits) byLit[h.sample] = (byLit[h.sample] || 0) + 1;
|
|
972
1002
|
const summary = Object.entries(byLit).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}×${n}`).join(', ');
|
|
973
1003
|
const files = [...new Set(scan.literalHits.map((h) => h.path))];
|
|
974
1004
|
console.error('');
|
|
975
|
-
console.error(`[leak-scan]
|
|
1005
|
+
console.error(`[leak-scan] FAILED — ${scan.literalHits.length} prior-instance literal hit(s) across ${files.length} shipped file(s): ${summary}`);
|
|
976
1006
|
for (const p of files.slice(0, 30)) console.error(` ${p}`);
|
|
977
1007
|
if (files.length > 30) console.error(` … +${files.length - 30} more file(s)`);
|
|
978
|
-
|
|
979
|
-
console.error('[leak-scan]
|
|
1008
|
+
console.error('[leak-scan] refusing to certify a leaky artifact. Either remove the literal at SOURCE (check what');
|
|
1009
|
+
console.error('[leak-scan] REGENERATES the file — a generator re-leaks on every run, task 1003911), or, if the hit is');
|
|
1010
|
+
console.error('[leak-scan] a detector carrying its own pattern, add a named (path, class) reason to');
|
|
1011
|
+
console.error('[leak-scan] scripts/gds/leak-scan-allowlist.js. --no-leak-scan skips this gate entirely.');
|
|
1012
|
+
if (verdict.fatal) process.exit(1);
|
|
980
1013
|
} else {
|
|
981
1014
|
console.error('[leak-scan] ✓ no prior-instance literals in the built artifact.');
|
|
982
1015
|
}
|
|
@@ -1021,6 +1054,8 @@ module.exports = {
|
|
|
1021
1054
|
selfVerify, reproducePinFromTree,
|
|
1022
1055
|
// task 1002220 (audit H6) — post-build leak scan (prior-instance literals + functional secret shapes)
|
|
1023
1056
|
scanArtifactForLeaks, PRIOR_INSTANCE_LITERALS,
|
|
1057
|
+
// task 1003870 — the fail-closed verdict the build acts on (pure, so a test can plant a literal)
|
|
1058
|
+
leakScanVerdict,
|
|
1024
1059
|
// task 1002217 (audit H4) — the fixed neutral floor injected into the doc-redaction + fail-closed gate
|
|
1025
1060
|
PRIOR_INSTANCE_FLOOR,
|
|
1026
1061
|
};
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.736'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// tests/leak_scan_fail_closed.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1003870 — the post-build leak scan must FAIL CLOSED. The detector (task 1002220) and
|
|
4
|
+
// the neutral floor (task 1002217) both landed, but strictness was opt-in and the publish lane
|
|
5
|
+
// never opted in, so a leaky artifact could have gone to npm while the workflow's own header
|
|
6
|
+
// comment claimed a fail-closed gate. This locks both halves:
|
|
7
|
+
// 1. the POLICY — planting a prior-instance literal makes the build refuse, with no flag,
|
|
8
|
+
// 2. the WIRING — the publish lane still arms the gate and never disarms it.
|
|
9
|
+
// (2) is the half that was actually broken, and a policy test alone would not have caught it.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/leak_scan_fail_closed.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { scanArtifactForLeaks, leakScanVerdict, PRIOR_INSTANCE_FLOOR } = require('../scripts/gds/package-core.js');
|
|
21
|
+
|
|
22
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
|
|
24
|
+
// This file is itself published, so it DERIVES the literal it plants instead of spelling it
|
|
25
|
+
// out — the self-leak invariant the allowlist and mirror-redact both hold about themselves.
|
|
26
|
+
// It also means a future rename of the prior instance cannot quietly gut this test: the plant
|
|
27
|
+
// follows the floor, rather than pinning a string that would stop matching.
|
|
28
|
+
const TOKEN = PRIOR_INSTANCE_FLOOR.tokens[0];
|
|
29
|
+
const OWNER = PRIOR_INSTANCE_FLOOR.owners[0];
|
|
30
|
+
|
|
31
|
+
let passed = 0, failed = 0;
|
|
32
|
+
function t(name, fn) {
|
|
33
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
34
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('\nthe planted literal makes the build refuse (no flag required):');
|
|
38
|
+
|
|
39
|
+
t('a planted prior-instance token in a functional file is FATAL by default', () => {
|
|
40
|
+
const files = [{ path: 'src/some-module.js', content: `const host = "${TOKEN}";\n` }];
|
|
41
|
+
const scan = scanArtifactForLeaks(files);
|
|
42
|
+
assert.ok(scan.literalHits.length > 0, 'the scan must see the planted token');
|
|
43
|
+
const v = leakScanVerdict(scan.literalHits, ['node', 'package-core.js']);
|
|
44
|
+
assert.equal(v.run, true, 'the scan runs when no escape flag is passed');
|
|
45
|
+
assert.equal(v.fatal, true, 'THE LOAD-BEARING ASSERTION: a leak aborts the build with no flag');
|
|
46
|
+
assert.equal(v.reason, 'prior-instance-literals');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
t('a planted prior-instance OWNER is fatal too, not just the token', () => {
|
|
50
|
+
const files = [{ path: 'scripts/thing.js', content: `// maintained by ${OWNER}\n` }];
|
|
51
|
+
const scan = scanArtifactForLeaks(files);
|
|
52
|
+
assert.ok(scan.literalHits.length > 0, 'the scan must see the planted owner');
|
|
53
|
+
assert.equal(leakScanVerdict(scan.literalHits, ['node', 'x']).fatal, true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
t('a clean artifact is NOT fatal (the gate does not fire on everything)', () => {
|
|
57
|
+
const files = [{ path: 'src/clean.js', content: 'const x = 1;\n' }];
|
|
58
|
+
const scan = scanArtifactForLeaks(files);
|
|
59
|
+
assert.equal(scan.literalHits.length, 0);
|
|
60
|
+
const v = leakScanVerdict(scan.literalHits, ['node', 'x']);
|
|
61
|
+
assert.equal(v.fatal, false);
|
|
62
|
+
assert.equal(v.reason, 'clean');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
t('--leak-scan-strict is a no-op: it cannot be the thing that makes it fatal', () => {
|
|
66
|
+
const files = [{ path: 'src/some-module.js', content: `x = "${TOKEN}"\n` }];
|
|
67
|
+
const hits = scanArtifactForLeaks(files).literalHits;
|
|
68
|
+
const withFlag = leakScanVerdict(hits, ['node', 'x', '--leak-scan-strict']);
|
|
69
|
+
const without = leakScanVerdict(hits, ['node', 'x']);
|
|
70
|
+
assert.deepEqual(withFlag, without, 'strict must add nothing — fatal is the default now');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t('--no-leak-scan is the only escape, and it skips rather than downgrades', () => {
|
|
74
|
+
const files = [{ path: 'src/some-module.js', content: `x = "${TOKEN}"\n` }];
|
|
75
|
+
const hits = scanArtifactForLeaks(files).literalHits;
|
|
76
|
+
const v = leakScanVerdict(hits, ['node', 'x', '--no-leak-scan']);
|
|
77
|
+
assert.equal(v.run, false, 'the scan does not run at all');
|
|
78
|
+
assert.equal(v.fatal, false);
|
|
79
|
+
assert.equal(v.reason, 'skipped');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log('\nthe publish lane arms the gate (the half that was actually broken):');
|
|
83
|
+
|
|
84
|
+
const publishYml = readFileSync(join(ROOT, '.github/workflows/publish.yml'), 'utf8');
|
|
85
|
+
const packLine = publishYml.split('\n').find((l) => l.includes('package-core.js') && l.includes('--ref HEAD'));
|
|
86
|
+
|
|
87
|
+
t('publish.yml still invokes package-core to build the release artifact', () => {
|
|
88
|
+
assert.ok(packLine, 'no package-core invocation found — the lane was rewired, re-check this test');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
t('the publish invocation arms the leak gate explicitly', () => {
|
|
92
|
+
assert.ok(packLine.includes('--leak-scan-strict'),
|
|
93
|
+
'the publish lane must state the gate it depends on, even though fatal is the default');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
t('nothing the publish lane RUNS disarms the leak scan', () => {
|
|
97
|
+
// Executable lines only: a YAML/shell comment may legitimately name the escape flag while
|
|
98
|
+
// explaining why the lane never uses it, and matching prose would fail on its own docs.
|
|
99
|
+
const executable = publishYml
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter((l) => !/^\s*#/.test(l))
|
|
102
|
+
.join('\n');
|
|
103
|
+
assert.ok(!executable.includes('--no-leak-scan'),
|
|
104
|
+
'the one lane that makes an artifact permanent and public must never skip the scan');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log(`\n${passed} passed, ${failed} failed\n`);
|
|
108
|
+
if (failed) process.exit(1);
|
package/tests/package_core.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { strict as assert } from 'node:assert';
|
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import zlib from 'node:zlib';
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
19
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { tmpdir } from 'node:os';
|
|
21
21
|
import { join, dirname } from 'node:path';
|
|
22
22
|
|
|
@@ -385,18 +385,35 @@ t('buildPackageJson: prunes to express+pg+pino, keeps the source ranges + the un
|
|
|
385
385
|
assert.equal(pkg.exports['./module-loader/loader'], './src/module-loader/loader.js');
|
|
386
386
|
assert.equal(pkg.exports['./modules'], './src/modules.js');
|
|
387
387
|
assert.equal(pkg.exports['./module-seams'], './src/module-seams.js');
|
|
388
|
-
// task
|
|
389
|
-
|
|
388
|
+
// task 1003872 / ADR 0283 — the synthesized package publishes PUBLIC
|
|
389
|
+
// (superseding ADR 0134's pre-launch `restricted`).
|
|
390
|
+
assert.deepEqual(pkg.publishConfig, { access: 'public' });
|
|
390
391
|
});
|
|
391
392
|
|
|
392
|
-
t('publishArgs:
|
|
393
|
+
t('publishArgs: public publish, dry-run unless live (task 1003872 / ADR 0283)', () => {
|
|
393
394
|
const tgz = 'dist/bongos-core-1.14.0.tgz';
|
|
394
|
-
// Default: dry-run
|
|
395
|
-
assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', '
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
assert.ok(!publishArgs(tgz, true).includes('
|
|
395
|
+
// Default: dry-run — building must never be a real upload, whatever the access.
|
|
396
|
+
assert.deepEqual(publishArgs(tgz, false), ['publish', tgz, '--access', 'public', '--dry-run']);
|
|
397
|
+
assert.deepEqual(publishArgs(tgz, true), ['publish', tgz, '--access', 'public']);
|
|
398
|
+
// The posture is stated at the call site AND in publishConfig, so neither one
|
|
399
|
+
// drifting alone can silently take the package back to private.
|
|
400
|
+
assert.ok(!publishArgs(tgz, true).includes('restricted'));
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// THE PUBLISH LANE IS THE PATH THAT ACTUALLY RUNS (task 1003872). package-core's
|
|
404
|
+
// own `--publish` is the manual owner path; CI publishes the tarball itself, with
|
|
405
|
+
// its own `--access` flag. Flipping only package-core.js would have left the real
|
|
406
|
+
// releases restricted — the same shape as task 1003870, where the workflow did not
|
|
407
|
+
// arm the gate its own header comment claimed.
|
|
408
|
+
t('the publish workflow publishes PUBLIC, matching publishArgs', () => {
|
|
409
|
+
const yml = readFileSync(new URL('../.github/workflows/publish.yml', import.meta.url), 'utf8');
|
|
410
|
+
const m = yml.match(/npm publish "\$TGZ" --access (\w+)/);
|
|
411
|
+
assert.ok(m, 'publish.yml must publish the packaged tarball with an explicit --access');
|
|
412
|
+
assert.equal(m[1], 'public', 'the lane that actually releases must publish public, not restricted');
|
|
413
|
+
assert.ok(
|
|
414
|
+
!/--access restricted/.test(yml),
|
|
415
|
+
'no restricted publish may survive anywhere in the lane',
|
|
416
|
+
);
|
|
400
417
|
});
|
|
401
418
|
|
|
402
419
|
t('buildPackageJson: omits `overrides` when the source has none', () => {
|