@bongos/core 1.19.82 → 1.19.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.82",
6
- "core_contract": "1.19.82",
7
- "source_commit": "9ca69eb29ee6b3ee602e17cbf1d1ef1ebc9df5f9",
5
+ "core_version": "1.19.83",
6
+ "core_contract": "1.19.83",
7
+ "source_commit": "fff49f72b2b345f6b610b37c943bc45b963f2b40",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-12T23:14:31.232Z",
9
+ "built_at": "2026-08-12T23:29:40.091Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 295,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1227,
14
+ "functional_verbatim": 1228,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1543,
20
- "tree_sha256": "253f8de922c6d8cc2023e6a2d28b363fafa2cfdec666f96afc3a0e5d879a918f",
19
+ "file_count": 1544,
20
+ "tree_sha256": "ec5d11dea2b4d13543a2593e7b1c13583eefea55e3ffca3b42cd9835cfdd134a",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -707,7 +707,7 @@
707
707
  {
708
708
  "path": "docs/adr/0055-server-mediated-branch-publish.md",
709
709
  "mode": "0000644",
710
- "sha256": "a574107524d7b0c85130df49b2b165b94dc1d1b0efe1493b7952e49c053c8a53"
710
+ "sha256": "6f51c67939eacaf510774894625d0a09e6883e4c8e1d8fdea2cf35f8d9f58ea4"
711
711
  },
712
712
  {
713
713
  "path": "docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md",
@@ -3412,7 +3412,7 @@
3412
3412
  {
3413
3413
  "path": "modules/lifecycle/github-push.js",
3414
3414
  "mode": "0000644",
3415
- "sha256": "1a7ef193f9f2829668f5dd612972e6cca43fd84dec1e13d2b1c20f7fd99766f9"
3415
+ "sha256": "0534cf4522f5a644301946abf6a703852375fe269401902dfae10d7b9192c3d5"
3416
3416
  },
3417
3417
  {
3418
3418
  "path": "modules/lifecycle/goal-advisory.js",
@@ -4057,12 +4057,12 @@
4057
4057
  {
4058
4058
  "path": "package-lock.json",
4059
4059
  "mode": "0000644",
4060
- "sha256": "b41abda8346dddd1c07e48067f2d9d0127aaad506519710b08cea5373a993b98"
4060
+ "sha256": "4e476d4e4a1ce4f6f0065ee8a18903571b5c89783f90929d62b5b5d9b4afe385"
4061
4061
  },
4062
4062
  {
4063
4063
  "path": "package.json",
4064
4064
  "mode": "0000644",
4065
- "sha256": "0ed0a2fa31ababaeef64de35d41cba12b4308fef35fe2d831a0532ac503581c8"
4065
+ "sha256": "db68efb63158fb5e544d9afc252161739f7c54c832509dc03fa45878675cb990"
4066
4066
  },
4067
4067
  {
4068
4068
  "path": "public-docs/index.html",
@@ -5039,6 +5039,11 @@
5039
5039
  "mode": "0000644",
5040
5040
  "sha256": "7ad384495370312a57f7c1f0dab4770888c83afd08b89a8dc27aebae408abfd1"
5041
5041
  },
5042
+ {
5043
+ "path": "scripts/gds/publish-credential-check.js",
5044
+ "mode": "0000644",
5045
+ "sha256": "0d700716747617c43e3cf410ef9395cc2f28fff342feef400a8ae45209e893c1"
5046
+ },
5042
5047
  {
5043
5048
  "path": "scripts/gds/publish-manifest.js",
5044
5049
  "mode": "0000644",
@@ -5687,7 +5692,7 @@
5687
5692
  {
5688
5693
  "path": "src/module-api.js",
5689
5694
  "mode": "0000644",
5690
- "sha256": "793ab7f173efc8c4137e658cae4617f09a9c0df1e1783cd28396337eac6aa83b"
5695
+ "sha256": "95dd72f2698cf288bc35875f4c9a0503393024525c6c2918902e6b22ce1f93f5"
5691
5696
  },
5692
5697
  {
5693
5698
  "path": "src/module-loader/loader.js",
@@ -7232,7 +7237,7 @@
7232
7237
  {
7233
7238
  "path": "tests/publish_branch_route.mjs",
7234
7239
  "mode": "0000644",
7235
- "sha256": "e3f843387deb2d2a8f2668490bc740f7291d141b2de70c1556a84cb429bdc21d"
7240
+ "sha256": "ea46629917c3236b9dfb7133af643a9fecc9a205323c345e16671b5093606fd6"
7236
7241
  },
7237
7242
  {
7238
7243
  "path": "tests/publish_manifest.mjs",
@@ -38,6 +38,10 @@ A **fine-grained GitHub PAT** scoped to the single repo: `Contents: read/write`
38
38
 
39
39
  > **Update (2026-06-13, task 1028 — GitHub App):** the preferred server credential is now a **GitHub App**, not a standing PAT. The server holds only the App private key (`GITHUB_APP_ID` + `GITHUB_APP_INSTALLATION_ID` + `GITHUB_APP_PRIVATE_KEY`) and mints a short-lived (~1h), repo-scoped **installation token** per push window — so there's no standing push secret at rest. `github-push.js resolveToken()` prefers the App and falls back to the PAT (`GITHUB_PUSH_TOKEN`) during cutover; `isConfigured()` is true if **either** is set, so the route's inert-safe `503` is unchanged. The App JWT is RS256-signed with `node:crypto` (no new dependency). Same single-repo scopes (Contents r/w + PR r/w + Actions read) and the same can't-touch-`main` guarantee. **LIVE on prod as of 2026-06-13 (task 1043 cutover, blocker 31 resolved):** the App (App ID 4047551, installation 140135657, `repository_selection=selected`) is installed and its key is set in `/etc/example/web-source.env`; verified that the server mints + uses an installation token (`publish-status` → 200, and a broken key fails loud as `503`/error rather than falling back). The old `GITHUB_PUSH_TOKEN` was **removed** — there is now **no standing push secret at rest**, only the App private key.
40
40
 
41
+ > **Update (2026-08-12, task 1002733 — verify `Actions: read`, and verify WHICH credential is live):** the 2026-06-13 note above says the PAT was removed and the App is live. **On cloudbongos.com that is no longer true.** Checked on the host this session: <redacted> sets `GITHUB_PUSH_TOKEN` and **none** of `GITHUB_APP_ID` / `GITHUB_APP_INSTALLATION_ID` / `GITHUB_APP_PRIVATE_KEY`. So `appConfigured()` is false and `resolveToken()` falls through to the **PAT** — the App is not the running credential here. Whichever one an instance is actually on, **that** one needs `Actions: read`; don't assume from this ADR's history. If the live credential lacks it, `getPublishStatus`'s deploy probe cannot see workflow runs and reports a merged PR as still deploying **forever**, which stranded task 1002695 at `confirmed` across four reconciler sweeps on 2026-08-12. That failure is invisible from the outside: on a private repo GitHub returns 404 for both "no such workflow" and "not yours", so a single probe cannot tell a legitimately workflow-less instance (the ADR 0108 pin-bump shape) from a blind credential.
42
+ >
43
+ > Check it with `node scripts/gds/publish-credential-check.js` on the host — it lists the repo's workflows as a control call first, so `blind` and `readable_no_workflow` are distinguishable. Task 1002724 makes a blind probe non-fatal (the reconciler falls back to the git-history land proof), but a blind credential means the deploy gate is **not being enforced**, only bypassed.
44
+
41
45
  ## Consequences
42
46
 
43
47
  - A dev-box builder ships end-to-end: `/builder-reauth` → claim → edit → `/builder-stage` ([ADR 0046](<redacted>.md)) → `/builder-ship`, with the GDS doing the push. The box holds no GitHub credential, so the [ADR 0053](0053-scoped-dev-box-session.md) blast-radius argument still holds.
@@ -492,6 +492,87 @@ async function getPublishStatus({ branch }, deps = {}) {
492
492
  };
493
493
  }
494
494
 
495
+ // probeDeployVisibility (task 1002733) — can this instance's publish credential READ
496
+ // the Actions API at all?
497
+ //
498
+ // WHY THIS EXISTS. getPublishStatus's deploy probe answers a question about ONE
499
+ // workflow, and on a private repo GitHub deliberately blurs "not there" and "not
500
+ // yours" — both can surface as 404. So a single call cannot distinguish "this repo
501
+ // has no deploy-prod.yml" (fine; the merge is the land, ADR 0108) from "this token
502
+ // cannot see Actions" (a silent misconfiguration that made task 1002695 strand at
503
+ // confirmed across 4 sweeps on 2026-08-12). Until task 1002724 there was no way to
504
+ // tell them apart without a shell on the host and a hand-run of the API.
505
+ //
506
+ // The fix is a CONTROL call: list the repo's workflows. If that succeeds we can read
507
+ // Actions, so a 404 on deploy-prod.yml genuinely means the file is absent. If the
508
+ // list itself is refused, the credential is blind and every deploy verdict it gives
509
+ // is meaningless.
510
+ //
511
+ // ADR 0055 specifies this credential as Contents rw + Pull requests rw + Actions:
512
+ // read. NOTE resolveToken precedence is deps.token > App installation token > PAT, so
513
+ // an instance runs on ONE of them and the other is irrelevant: a correctly-scoped PAT
514
+ // can be masked by a narrower App grant, and equally an App that was never configured
515
+ // means the PAT is live no matter what the ADR's history says. cloudbongos.com is the
516
+ // second case — checked 2026-08-12, web.env sets GITHUB_PUSH_TOKEN and no GITHUB_APP_*.
517
+ // The remediation string below resolves which one is live rather than guessing.
518
+ async function probeDeployVisibility(deps = {}) {
519
+ const token = await resolveToken(deps);
520
+ if (!token) {
521
+ return { state: 'no_credential', http: null, detail: 'no push credential configured (GITHUB_PUSH_TOKEN / App)', remediation: null };
522
+ }
523
+ const repoInfo = deps.repoInfo || loadRepoInfo();
524
+ if (!repoInfo || repoInfo.error) {
525
+ return { state: 'no_repo_info', http: null, detail: 'repo owner/name could not be resolved', remediation: null };
526
+ }
527
+ const { owner, repo } = repoInfo;
528
+ const doFetch = deps.fetchImpl || fetch;
529
+ const api = `https://api.github.com/repos/${owner}/${repo}`;
530
+ // Name the credential that is ACTUALLY live, so the remediation points at the right
531
+ // one. resolveToken() prefers the App and falls back to GITHUB_PUSH_TOKEN, so an
532
+ // instance can be on either — and cloudbongos.com is on the PAT despite ADR 0055's
533
+ // history saying the App replaced it (task 1002733).
534
+ const live = appConfigured() ? 'the GitHub App (GITHUB_APP_* are set — it wins over any PAT)' : 'the PAT in GITHUB_PUSH_TOKEN (no GITHUB_APP_* configured)';
535
+ const GRANT = `Grant 'Actions: read' on ${owner}/${repo} to ${live} (ADR 0055).`;
536
+
537
+ let listRes;
538
+ try {
539
+ listRes = await doFetch(`${api}/actions/workflows?per_page=1`, { headers: ghHeaders(token) });
540
+ } catch (e) {
541
+ return { state: 'unknown', http: null, detail: `workflows list threw: ${e && e.message}`, remediation: null };
542
+ }
543
+ if (!listRes.ok) {
544
+ return {
545
+ state: 'blind',
546
+ http: listRes.status,
547
+ detail: `cannot list workflows (HTTP ${listRes.status}) — every deploy verdict from this credential is unreliable`,
548
+ remediation: GRANT,
549
+ };
550
+ }
551
+
552
+ // Actions is readable. Now the specific workflow: present, or genuinely absent?
553
+ let runsRes;
554
+ try {
555
+ runsRes = await doFetch(`${api}/actions/workflows/deploy-prod.yml/runs?branch=main&per_page=1`, { headers: ghHeaders(token) });
556
+ } catch (e) {
557
+ return { state: 'unknown', http: null, detail: `workflow-runs threw: ${e && e.message}`, remediation: null };
558
+ }
559
+ if (runsRes.ok) {
560
+ return { state: 'readable', http: runsRes.status, detail: 'Actions readable; deploy-prod.yml present — the deploy gate is real', remediation: null };
561
+ }
562
+ if (runsRes.status === 404) {
563
+ return {
564
+ state: 'readable_no_workflow', http: 404,
565
+ detail: 'Actions readable and deploy-prod.yml is genuinely absent — the merge IS the land (ADR 0108 pin-bump publishing)',
566
+ remediation: null,
567
+ };
568
+ }
569
+ return {
570
+ state: 'blind', http: runsRes.status,
571
+ detail: `workflows list succeeded but deploy-prod.yml runs returned HTTP ${runsRes.status}`,
572
+ remediation: GRANT,
573
+ };
574
+ }
575
+
495
576
  // task 1547 — does the PR matched by getPublishStatus actually belong to THIS task?
496
577
  // The robust land proof is published-tip containment; this is the fallback used when
497
578
  // no published tip was recorded (a legacy row, or a publish in flight across the
@@ -1145,6 +1226,7 @@ module.exports = {
1145
1226
  GATE_OWNER_APPROVED_CONTEXT,
1146
1227
  landConfirmed,
1147
1228
  deployProbeBlind,
1229
+ probeDeployVisibility,
1148
1230
  landProvenForTask,
1149
1231
  prReferencesTask,
1150
1232
  findTaskPullRequest,
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.82",
3
+ "version": "1.19.83",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.82",
9
+ "version": "1.19.83",
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.82",
3
+ "version": "1.19.83",
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",
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/publish-credential-check.js — can the publish credential SEE Actions?
3
+ // (task 1002733, promoted from idea 1000705.)
4
+ //
5
+ // Run ON THE HOST, where GITHUB_PUSH_TOKEN / the GitHub App env lives:
6
+ // node scripts/gds/publish-credential-check.js
7
+ //
8
+ // WHY. On 2026-08-12 task 1002695 stranded at 'confirmed' across four reconciler
9
+ // sweeps: its PR was MERGED with green checks, but the server's deploy probe kept
10
+ // answering 'pending' because the credential could not read the Actions API. That
11
+ // blindness was invisible — the only way to find it was to notice a strand and
12
+ // hand-run getPublishStatus against the live server. This turns a silent
13
+ // misconfiguration into a one-command answer.
14
+ //
15
+ // Task 1002724 stopped the blindness from stranding work (a blind probe no longer
16
+ // vetoes a git-history-proven land). This tells you whether you are relying on that
17
+ // fallback instead of a real deploy gate.
18
+ //
19
+ // Exit codes: 0 = credential can read Actions (gate is real, or the workflow is
20
+ // legitimately absent), 1 = BLIND (remediation printed), 2 = nothing to check.
21
+
22
+ const githubPush = require('../../modules/lifecycle/github-push');
23
+ const { cliExit } = require('./cli-lib');
24
+
25
+ const C = process.stdout.isTTY
26
+ ? { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', b: '\x1b[1m', x: '\x1b[0m' }
27
+ : { g: '', y: '', r: '', b: '', x: '' };
28
+
29
+ (async () => {
30
+ if (!githubPush.isConfigured()) {
31
+ console.log(`${C.y}!${C.x} No publish credential configured on this machine.`);
32
+ console.log(' This check belongs on the server/host that holds GITHUB_PUSH_TOKEN or the GitHub App env.');
33
+ return cliExit(2);
34
+ }
35
+
36
+ let probe;
37
+ try {
38
+ probe = await githubPush.probeDeployVisibility();
39
+ } catch (e) {
40
+ console.log(`${C.r}✖${C.x} probe failed: ${e && e.message}`);
41
+ return cliExit(1);
42
+ }
43
+
44
+ const http = probe.http ? ` (HTTP ${probe.http})` : '';
45
+ switch (probe.state) {
46
+ case 'readable':
47
+ console.log(`${C.g}✓${C.x} ${C.b}Actions readable${C.x}${http} — ${probe.detail}`);
48
+ return cliExit(0);
49
+ case 'readable_no_workflow':
50
+ console.log(`${C.g}✓${C.x} ${C.b}Actions readable${C.x}${http} — ${probe.detail}`);
51
+ return cliExit(0);
52
+ case 'blind':
53
+ console.log(`${C.r}✖${C.x} ${C.b}BLIND${C.x}${http} — ${probe.detail}`);
54
+ console.log('');
55
+ console.log(` ${C.b}Fix:${C.x} ${probe.remediation}`);
56
+ console.log(' Until then the reconciler falls back to the git-history land proof (task 1002724),');
57
+ console.log(' so work still lands — but the deploy gate is not actually being enforced.');
58
+ return cliExit(1);
59
+ default:
60
+ console.log(`${C.y}!${C.x} ${probe.state}${http} — ${probe.detail}`);
61
+ return cliExit(2);
62
+ }
63
+ })();
package/src/module-api.js CHANGED
@@ -591,8 +591,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
591
591
  // landed since 1.19.80 with no explicit bump. run 31649705242. (task 1002620)
592
592
  // 1.19.82 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
593
593
  // landed since 1.19.81 with no explicit bump. run 31650108283. (task 1002620)
594
+ // 1.19.83 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
595
+ // landed since 1.19.82 with no explicit bump. run 31651046759. (task 1002620)
594
596
  // ---------------------------------------------------------------------------
595
- const CORE_VERSION = '1.19.82'; // CI auto-patch carrier (ADR 0161); see changelog above
597
+ const CORE_VERSION = '1.19.83'; // CI auto-patch carrier (ADR 0161); see changelog above
596
598
 
597
599
  // A namespaced logger so a module's log lines are attributable + consistent.
598
600
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -570,5 +570,74 @@ await t('landConfirmed: unconfigured deploy counts as landed, but ONLY when merg
570
570
  assert.equal(gp.landConfirmed({ merged: true, deploy_status: 'none' }), false, 'none ≠ unconfigured — no PR at all is not a land');
571
571
  });
572
572
 
573
+ // ===========================================================================
574
+ // task 1002733 — probeDeployVisibility: is the publish credential BLIND?
575
+ //
576
+ // On a private repo GitHub deliberately blurs "not there" and "not yours" — both
577
+ // can surface as 404 — so one call cannot tell "this repo has no deploy-prod.yml"
578
+ // (fine) from "this credential cannot read Actions" (the silent misconfiguration
579
+ // that stranded task 1002695 across 4 sweeps). The CONTROL call is listing the
580
+ // repo's workflows: if that works, a 404 on the specific workflow is genuine.
581
+ // ===========================================================================
582
+
583
+ await t('probeDeployVisibility: workflows list 403 → blind, with remediation', async () => {
584
+ const fetchImpl = async (url) => {
585
+ if (url.includes('/actions/workflows?')) return jsonRes(403, { message: 'Resource not accessible' });
586
+ throw new Error('must not probe the specific workflow once the list is refused: ' + url);
587
+ };
588
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
589
+ assert.equal(p.state, 'blind');
590
+ assert.equal(p.http, 403);
591
+ assert.match(p.remediation, /Actions: read/i);
592
+ // The remediation must name the credential that is ACTUALLY live rather than
593
+ // guessing: resolveToken prefers the App and falls back to GITHUB_PUSH_TOKEN, and an
594
+ // instance runs on exactly one of them. Pointing at the wrong one sends the operator
595
+ // to widen permissions on a credential that is not in play (cloudbongos.com is on
596
+ // the PAT, despite ADR 0055's history saying the App replaced it — task 1002733).
597
+ assert.match(
598
+ p.remediation, /GITHUB_APP_\*|GITHUB_PUSH_TOKEN/,
599
+ 'must identify which credential resolveToken will actually use'
600
+ );
601
+ });
602
+
603
+ await t('probeDeployVisibility: list OK + runs 404 → readable_no_workflow (NOT blind)', async () => {
604
+ const fetchImpl = async (url) => {
605
+ if (url.includes('/actions/workflows?')) return jsonRes(200, { workflows: [{ id: 1, path: '.github/workflows/unit.yml' }] });
606
+ if (url.includes('deploy-prod.yml/runs')) return jsonRes(404, { message: 'Not Found' });
607
+ throw new Error('unexpected ' + url);
608
+ };
609
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
610
+ assert.equal(p.state, 'readable_no_workflow', 'a 404 behind a readable list means the workflow is genuinely absent');
611
+ assert.equal(p.remediation, null, 'nothing to fix — this is the ADR 0108 pin-bump shape');
612
+ });
613
+
614
+ await t('probeDeployVisibility: list OK + runs OK → readable (the deploy gate is real)', async () => {
615
+ const fetchImpl = async (url) => {
616
+ if (url.includes('/actions/workflows?')) return jsonRes(200, { workflows: [] });
617
+ if (url.includes('deploy-prod.yml/runs')) return jsonRes(200, { workflow_runs: [] });
618
+ throw new Error('unexpected ' + url);
619
+ };
620
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
621
+ assert.equal(p.state, 'readable');
622
+ assert.equal(p.remediation, null);
623
+ });
624
+
625
+ await t('probeDeployVisibility: list OK but runs 403 → still blind', async () => {
626
+ const fetchImpl = async (url) => {
627
+ if (url.includes('/actions/workflows?')) return jsonRes(200, { workflows: [] });
628
+ if (url.includes('deploy-prod.yml/runs')) return jsonRes(403, { message: 'nope' });
629
+ throw new Error('unexpected ' + url);
630
+ };
631
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
632
+ assert.equal(p.state, 'blind');
633
+ assert.equal(p.http, 403);
634
+ });
635
+
636
+ await t('probeDeployVisibility: no credential → no_credential, not a false alarm', async () => {
637
+ const p = await gp.probeDeployVisibility({ token: null, repoInfo: REPO, fetchImpl: async () => { throw new Error('must not fetch'); } });
638
+ assert.equal(p.state, 'no_credential');
639
+ assert.equal(p.remediation, null);
640
+ });
641
+
573
642
  console.log(`\n${pass} passed, ${fail} failed`);
574
643
  process.exit(fail === 0 ? 0 : 1);