@cldmv/gh-broker 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +165 -41
  2. package/config.example.json +38 -9
  3. package/config.sample.json +45 -0
  4. package/deploy/README.md +76 -17
  5. package/deploy/install.mjs +171 -104
  6. package/deploy/install.sh +0 -0
  7. package/deploy/lib/config-merge.mjs +174 -16
  8. package/deploy/lib/credentials.mjs +143 -0
  9. package/deploy/lib/install-args.mjs +144 -0
  10. package/deploy/lib/npm-setup.mjs +164 -0
  11. package/deploy/lib/secret-store.mjs +20 -2
  12. package/deploy/lib/system.mjs +26 -2
  13. package/dist/index.mjs +5864 -1809
  14. package/dist/index.mjs.map +4 -4
  15. package/package.json +18 -8
  16. package/.configs/fix-headers.json +0 -3
  17. package/.configs/vitest.config.mjs +0 -44
  18. package/.github/dependabot.yml +0 -111
  19. package/.github/workflows/branch-retention.yml +0 -44
  20. package/.github/workflows/bundle-size.yml +0 -52
  21. package/.github/workflows/ci.yml +0 -389
  22. package/.github/workflows/cla.yml +0 -66
  23. package/.github/workflows/codeql.yml +0 -87
  24. package/.github/workflows/dependabot-auto-merge.yml +0 -62
  25. package/.github/workflows/dependabot-recreate.yml +0 -67
  26. package/.github/workflows/dependency-review.yml +0 -46
  27. package/.github/workflows/feature-pr.yml +0 -75
  28. package/.github/workflows/hotfix-redirector.yml +0 -67
  29. package/.github/workflows/hotfixes-release.yml +0 -70
  30. package/.github/workflows/labeler.yml +0 -48
  31. package/.github/workflows/master-commit-audit.yml +0 -56
  32. package/.github/workflows/member-auto-merge.yml +0 -75
  33. package/.github/workflows/next-release.yml +0 -76
  34. package/.github/workflows/next-reset.yml +0 -50
  35. package/.github/workflows/pr-notify.yml +0 -44
  36. package/.github/workflows/pr-title-normalizer.yml +0 -52
  37. package/.github/workflows/provenance.yml +0 -51
  38. package/.github/workflows/publish.yml +0 -146
  39. package/.github/workflows/release-merge.yml +0 -104
  40. package/.github/workflows/release-notify.yml +0 -41
  41. package/.github/workflows/scorecard.yml +0 -64
  42. package/.github/workflows/stale.yml +0 -50
  43. package/.github/workflows/tag-health.yml +0 -70
  44. package/.github/workflows/update-major-version-tags.yml +0 -94
  45. package/.github/workflows/v4-bootstrap.yml +0 -115
  46. package/.github/workflows/welcome.yml +0 -42
  47. package/docs/TOOLS.md +0 -347
  48. package/docs/agent-user-model.md +0 -89
  49. package/docs/changelog/v0/v0.2.0.md +0 -26
  50. package/docs/changelog/v0/v0.2.1.md +0 -23
  51. package/docs/changelog/v0/v0.2.2.md +0 -23
  52. package/docs/changelog/v0/v0.2.3.md +0 -33
  53. package/docs/changelog/v0/v0.2.4.md +0 -25
  54. package/docs/changelog/v0/v0.2.5.md +0 -29
  55. package/docs/changelog/v0/v0.3.0.md +0 -84
  56. package/docs/changelog/v0/v0.3.1.md +0 -30
  57. package/docs/changelog/v0/v0.3.2.md +0 -24
  58. package/docs/changelog/v0/v0.3.3.md +0 -28
  59. package/docs/changelog/v0/v0.4.0.md +0 -58
  60. package/docs/changelog/v0/v0.4.1.md +0 -33
  61. package/docs/changelog/v0/v0.4.2.md +0 -45
  62. package/docs/changelog/v0/v0.5.0.md +0 -39
  63. package/docs/changelog/v0/v0.5.1.md +0 -28
  64. package/docs/changelog/v0/v0.6.0.md +0 -52
  65. package/docs/changelog/v0/v0.6.1.md +0 -48
  66. package/docs/changelog/v0/v0.7.0.md +0 -71
  67. package/docs/deploy-architecture.md +0 -82
  68. package/src/appauth.ts +0 -56
  69. package/src/audit.ts +0 -34
  70. package/src/catalog.ts +0 -165
  71. package/src/config.ts +0 -130
  72. package/src/index.ts +0 -107
  73. package/src/paths.ts +0 -60
  74. package/src/router.ts +0 -258
  75. package/src/secrets.ts +0 -137
  76. package/src/server.ts +0 -91
  77. package/src/types.ts +0 -104
  78. package/src/upstream.ts +0 -85
  79. package/src/verbs/issue_labels.ts +0 -329
  80. package/src/verbs/link_issue.ts +0 -84
  81. package/src/verbs/release-analysis.ts +0 -210
  82. package/src/verbs/repo-tier.ts +0 -90
  83. package/src/verbs/report_ci_status.ts +0 -358
  84. package/src/verbs/report_release_prs.ts +0 -238
  85. package/src/verbs/report_security_alerts.ts +0 -442
  86. package/src/verbs/report_unlabeled.ts +0 -194
  87. package/src/verbs/rest-client.ts +0 -181
  88. package/src/verbs/review_thread.ts +0 -138
  89. package/tests/appauth.test.vitest.mts +0 -76
  90. package/tests/audit.test.vitest.mts +0 -48
  91. package/tests/catalog.test.vitest.mts +0 -180
  92. package/tests/config-merge.test.vitest.mts +0 -58
  93. package/tests/config.test.vitest.mts +0 -314
  94. package/tests/issue-labels.test.vitest.mts +0 -347
  95. package/tests/paths.test.vitest.mts +0 -52
  96. package/tests/release-prs.test.vitest.mts +0 -237
  97. package/tests/rest-client.test.vitest.mts +0 -115
  98. package/tests/router.test.vitest.mts +0 -399
  99. package/tests/run-vitest.mjs +0 -51
  100. package/tests/secret-store.test.vitest.mts +0 -106
  101. package/tests/secrets.test.vitest.mts +0 -155
  102. package/tests/security-alerts.test.vitest.mts +0 -328
  103. package/tests/server.test.vitest.mts +0 -20
  104. package/tests/upstream.test.vitest.mts +0 -91
  105. package/tests/verb-helpers.test.vitest.mts +0 -331
  106. package/tests/verbs.test.vitest.mts +0 -624
  107. package/tsconfig.json +0 -21
@@ -1,52 +0,0 @@
1
- # gh-broker v0.6.0 Changelog
2
-
3
- **Release Date**: October 2026
4
- **Release Type**: Minor
5
- **Branch**: `release/0.6.0`
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- v0.6.0 makes the two report verbs from v0.5.0 usable on a whole org. `report_security_alerts` can now return a compact per-repo roll-up instead of about 700 KB of alerts, filter by severity and repo, and tag each repo with its flow tier. `report_release_prs` can now judge whether each release is ready for its changelog, including whether an existing changelog is still up to date. All additions are opt-in parameters on verbs that already exist, so nothing changes for existing callers and no config change is needed.
12
-
13
- ---
14
-
15
- ## ✨ Features
16
-
17
- ### Per-repo roll-up, filters and flow tier for `report_security_alerts` ([#99](https://github.com/CLDMV/gh-broker/pull/99))
18
-
19
- - **`byRepo` on every result.** One row per repo with at least one alert, worst first (criticals, then highs, then total), with per-type counts and critical / high / medium / low counts.
20
- - **`summaryOnly`.** Returns just the roll-up and the counts. An org run drops from about 700 KB to a few KB, and `alertsOmitted` says how many alerts were left out.
21
- - **`minSeverity` and `repos`.** Keep only alerts at or above a severity (secret-scanning alerts have no severity and are always kept), or only the named repos.
22
- - **`classify`.** Adds each repo's flow tier to its row: `v4` (carries the v4 release workflow), `dev-trunk` (a v4 repo whose real code lives on a persistent `dev` branch while `next` holds only the scaffold) or `non-v4` (no release workflow on the default branch, even if `next` and `hotfixes` branches exist). The raw facts behind the tier are returned as `tierSignals`, because the tier is a structural signal and not a verdict. It costs about three calls per repo that has alerts.
23
-
24
- ### Release readiness for `report_release_prs` ([#99](https://github.com/CLDMV/gh-broker/pull/99))
25
-
26
- `analyze: true` adds an `analysis` block to each release PR and a `verdicts` tally:
27
-
28
- - **Open PRs against the release branch**, classified as doc, dependency, feature or draft.
29
- - **Changelog freshness.** The release PR's commits give the PRs that actually shipped; the changelog is checked for each one. The status is `current`, `minor-gap` (only CI, chore, style or test PRs missing), `stale` (a user-facing PR missing), `version-drift` (the heading names another version) or `thin` (a stub). The README What's New block is checked for the version too.
30
- - **A verdict:** `done`, `in-flight` (a doc PR is already open), `wait` (feature PRs still open), `gray` (only dependency PRs or drafts open), `write`, `update` or `inspect`.
31
-
32
- With `analyze`, `onlyMissing` lists every release PR whose verdict is not `done`, so a changelog that exists but is out of date is not hidden. It costs about five more calls per release PR, so raise `maxApiCalls` for a whole org.
33
-
34
- ## 🔧 Dependencies
35
-
36
- All dev-only; no runtime dependency changed and the lockfile moves only these two packages.
37
-
38
- - `@cldmv/fix-headers` `^2.1.1` to `^2.2.0` (locked 2.1.1 to 2.2.0), in two steps: 2.1.4 ([#97](https://github.com/CLDMV/gh-broker/pull/97), no headers needed restamping) and then 2.2.0 ([#104](https://github.com/CLDMV/gh-broker/pull/104)). From 2.2.0 the `@Last modified by` tag follows content edits only, so a header-only rewrite keeps the recorded editor. Running it added the standard header block to `src/verbs/release-analysis.ts` and `src/verbs/repo-tier.ts`, the two files from the verb work above that had none; no existing header or code line changed.
39
- - `@cldmv/configs` `^1.2.0` to `^1.2.4` (locked 1.2.0 to 1.2.4) ([#104](https://github.com/CLDMV/gh-broker/pull/104)). Version 1.2.4 sets `forceAuthorUpdate` and `forceLastModifiedAuthorUpdate` to `false`, so `@Author` is never rewritten. Re-running `fix:headers` under the new config restamped no files.
40
-
41
- ## 📚 Documentation
42
-
43
- - **NEW:** [docs/changelog/v0/v0.6.0.md](./v0.6.0.md): this changelog.
44
- - **NEW:** backfilled [v0.3.3](./v0.3.3.md), [v0.4.1](./v0.4.1.md), [v0.4.2](./v0.4.2.md), [v0.5.0](./v0.5.0.md) and [v0.5.1](./v0.5.1.md), which shipped without a changelog file.
45
- - The new parameters, the tier definitions and the verdict table are documented in `docs/TOOLS.md`.
46
-
47
- ---
48
-
49
- ## Upgrade notes
50
-
51
- - No breaking changes. The new parameters are optional, and the output of an existing call only gains a `byRepo` field.
52
- - Redeploy and reconnect your client to use them. A broker older than this version rejects the new parameters as unknown properties.
@@ -1,48 +0,0 @@
1
- # gh-broker v0.6.1 Changelog
2
-
3
- **Release Date**: October 2026
4
- **Release Type**: Patch
5
- **Branch**: `release/0.6.1`
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- v0.6.1 updates the MCP SDK the broker is built on, `@modelcontextprotocol/sdk` from 1.31.0 to 1.32.0, and refreshes the development toolchain: vitest 5.0.3, `@cldmv/vitest-runner` 1.5.3 and `@types/node` 26.6.4.
12
-
13
- No broker source changed. The net diff against v0.6.0 is `package.json` and `package-lock.json` only, and the SDK is the single runtime package that moved: its own dependency ranges are identical between the two versions, so no new runtime package is installed. The SDK's 1.32.0 release does change how its HTTP client follows redirects (see Upgrade notes), and the broker uses that client to reach the upstream GitHub MCP endpoint, so the SDK bump is a runtime-affecting update even though no line of broker code differs.
14
-
15
- ---
16
-
17
- ## 🔧 Dependencies
18
-
19
- ### Runtime
20
-
21
- - `@modelcontextprotocol/sdk` `^1.31.0` → `^1.32.0` (locked 1.31.0 → 1.32.0) ([#108](https://github.com/CLDMV/gh-broker/pull/108)). The 1.32.0 release notes list these changes:
22
- - **Redirects (upgrade note).** The HTTP client transports now follow a redirect only when it stays on the same origin (same scheme, host and port; http to https on the same host is allowed). `redirectPolicy: 'follow'` on `StreamableHTTPClientTransport` restores the old behavior.
23
- - **Two new options, off unless set:** `maxToolInputElements` on `McpServer` and `expectedResource` on `requireBearerAuth`. The broker uses neither: it builds its server on the SDK's low-level `Server` class (with a stdio transport bound to a local socket) and does not use the SDK's bearer-auth middleware.
24
- - **Server fixes:** `tools/call` and `prompts/get` requests that omit `arguments` are accepted, and the in-memory task store keeps tasks within the session that created them. Neither was exercised by a broker code path that was changed.
25
- - Documentation and example changes in the SDK repository (SECURITY.md, example session handling).
26
-
27
- ### Development
28
-
29
- - `@cldmv/vitest-runner` `^1.5.1` → `^1.5.3` (locked 1.5.1 → 1.5.3) ([#109](https://github.com/CLDMV/gh-broker/pull/109)). Version 1.5.3 makes the runner's CommonJS entry fail with a clear error on a Node.js version without `require(esm)` and raises the runner's `engines.node` from `>=20.19.0` to `>=22.12.0`. The locked package's license field also changes from MIT to Apache-2.0. The runner's `chalk` dependency moves from 5.x to 6, so the locked `chalk` goes 5.6.2 → 6.0.1 and now requires Node.js 22 (`engines` `>=22`). The broker's own `engines.node` was already `>=22.12.0` and the CI matrix already starts at 22.12.0, so nothing about supported Node.js versions changes here.
30
- - `vitest` `^5.0.1` → `^5.0.3` (locked 5.0.2 → 5.0.3) ([#106](https://github.com/CLDMV/gh-broker/pull/106)), with `@vitest/coverage-v8`, `@vitest/mocker` and `@vitest/spy` following to 5.0.3 (the `@vitest/coverage-v8` range stays `^5.0.2`). Transitive, lockfile only: `vite` 8.3.1 → 8.3.2, `rolldown` and its platform bindings 1.2.11 → 1.2.12, `@oxc-project/types` 0.151.0 → 0.152.0, and `why-is-node-running` 3.2.2 → 3.2.1 (vitest 5.0.3 pins the older release).
31
- - `@types/node` `^26.6.3` → `^26.6.4` (locked 26.6.3 → 26.6.4) ([#109](https://github.com/CLDMV/gh-broker/pull/109)).
32
-
33
- `esbuild`, `typescript`, `@octokit/auth-app` and `@octokit/core` are unchanged.
34
-
35
- ---
36
-
37
- ## 📚 Documentation
38
-
39
- - **NEW:** [docs/changelog/v0/v0.6.1.md](./v0.6.1.md), this changelog.
40
- - README `✨ What's New` is promoted to v0.6.1.
41
-
42
- ---
43
-
44
- ## Upgrade notes
45
-
46
- - No breaking changes to the broker's verbs, configuration or installer; the source is identical to v0.6.0.
47
- - If the `upstream` URL in your config (the default is `https://api.githubcopilot.com/mcp/x/all`) ever answers with a redirect to a different origin, the SDK will no longer follow it. Point `upstream` at the final URL. It was not verified here whether the default endpoint redirects at all; if it does not, nothing changes for you.
48
- - Redeploy to pick up the new SDK; the development changes do not affect the installed broker.
@@ -1,71 +0,0 @@
1
- # gh-broker v0.7.0 Changelog
2
-
3
- **Release Date**: October 2026
4
- **Release Type**: Minor
5
- **Branch**: `release/0.7.0`
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- v0.7.0 adds `issue_labels`, a write verb that edits an issue's or PR's labels one at a time. The hosted MCP's `issue_write` treats `labels` as a full replacement, so changing one status label through it silently drops every other label on the issue ([#117](https://github.com/CLDMV/gh-broker/issues/117)). `issue_labels` uses GitHub's per-label endpoints instead and only touches the labels it is given. The advertised `issue_write` description now carries a warning about the replacement behavior and points at the new verb.
12
-
13
- The rest of the release is dependency updates. The only runtime packages that changed are `@modelcontextprotocol/sdk` 1.32.0 → 1.32.1 (documentation-only upstream) and the transitive `proxy-addr` 2.0.7 → 2.0.8 (a security fix). Everything else is development tooling.
14
-
15
- Nothing is breaking: `issue_labels` is a new allowlist entry, and every existing tool behaves as before. A redeploy is needed to pick up the new verb, since the installer merges the new `config.example.json` entry into the live config.
16
-
17
- ---
18
-
19
- ## ✨ Features
20
-
21
- ### `issue_labels`: add, remove, swap or set labels without touching the others ([#118](https://github.com/CLDMV/gh-broker/pull/118), closes [#117](https://github.com/CLDMV/gh-broker/issues/117))
22
-
23
- A custom verb (`mode: "write"`, `custom: true`) that runs on the write PAT (Issues: write) and works on issues and pull requests alike.
24
-
25
- - **`add` / `remove`** take one label or several and combine in one call. A label already present (`add`) or absent (`remove`) is a no-op, not an error.
26
- - **`swap: { prefix, to }`** removes every current label starting with `prefix` and adds `to`, e.g. `{ "prefix": "status: ", "to": "status: verified" }`. A `to` label that is already on the issue stays in place rather than being removed and re-added.
27
- - **`set`** replaces the whole set, only as an explicit choice. It cannot be combined with the other operations, and an empty `set` is refused so a call can never clear every label.
28
- - **Unknown labels are refused.** Every name on the add side is looked up first (case-insensitively, applying the repo's own spelling). A missing one fails the call with nothing written, unless `create_missing` is set, in which case the label is created (`color` defaults to `ededed`).
29
- - **Add before remove.** Writes run in a fixed order: create missing labels, one POST for the whole add side, then the DELETEs. A failure part-way never leaves an issue with fewer labels than it should have; a `swap` whose POST is rate-limited keeps its old status label.
30
- - **Partial failures are reported.** If any write fails, the error names what was applied and what was not, and keeps the HTTP status. A failure before the first write passes through unchanged, since nothing was written.
31
- - **Strict arguments.** The handler enforces its schema: an unknown key (a typo such as `remvoe`), a non-integer `issue_number`, a non-boolean `create_missing`, an empty list, a bad `color`, or more than 100 labels refuses the call before any request is made. Naming the same label on both the add and remove sides is refused too.
32
- - **Result.** The issue's resulting label set, plus what this call `added`, `removed` and `created`.
33
-
34
- Full parameter table and examples: [docs/TOOLS.md](https://github.com/CLDMV/gh-broker/blob/master/docs/TOOLS.md#issue_labels).
35
-
36
- ### `issue_write` warns that `labels` replaces the whole set ([#118](https://github.com/CLDMV/gh-broker/pull/118))
37
-
38
- The broker appends a warning to the advertised description of the proxied `issue_write` tool: `labels` replaces the issue's whole label set. When `issue_labels` is allowlisted, the warning points at it; otherwise it says to read the current labels first and pass the full set back. The tool's schema and its forwarded behavior are unchanged.
39
-
40
- ---
41
-
42
- ## 📚 Documentation
43
-
44
- - **NEW:** [docs/changelog/v0/v0.7.0.md](./v0.7.0.md), this changelog.
45
- - `docs/TOOLS.md` gains an `issue_labels` section, and its tool-category table lists the new verb and the `issue_write` description warning.
46
- - README: a feature section for label edits, the new `src/verbs/issue_labels.ts` in the layout, and `✨ What's New` promoted to v0.7.0.
47
-
48
- ---
49
-
50
- ## 🔧 Dependencies
51
-
52
- ### Runtime
53
-
54
- - `@modelcontextprotocol/sdk` `^1.32.0` → `^1.32.1` (locked 1.32.0 → 1.32.1) ([#114](https://github.com/CLDMV/gh-broker/pull/114)). The upstream release contains only documentation changes.
55
- - `proxy-addr` 2.0.7 → 2.0.8, transitive, lockfile only ([#113](https://github.com/CLDMV/gh-broker/pull/113)). Fixes CVE-2026-90711 (GHSA-jqcg-44mw-7w3h): IPv4 trust granted through IPv4-mapped IPv6 subnets with a short prefix. It arrives through the SDK's HTTP server dependencies; the broker serves MCP over a local Unix socket and does not use that server, but the patched version is now what gets installed.
56
-
57
- ### Development
58
-
59
- - `@cldmv/configs` `^1.2.4` → `^1.3.0` ([#115](https://github.com/CLDMV/gh-broker/pull/115)). Version 1.3.0 adds shared Prettier, ESLint and Vitest configs and makes the ESLint plugins it configures regular dependencies of the package, which adds 147 dev-only packages to the lockfile (`eslint`, the `@eslint/*` language plugins, `@cldmv/eslint-plugin-jsonv` and their dependencies). The broker only uses the package's `fix-headers.json`, which is unchanged.
60
- - `@cldmv/fix-headers` `^2.2.0` → `^2.2.1`, `@cldmv/vitest-runner` `^1.5.3` → `^1.5.4` and `@types/node` `^26.6.4` → `^26.6.5` ([#114](https://github.com/CLDMV/gh-broker/pull/114)). The two CLDMV packages ship nothing new; both releases only update their own development setup.
61
- - `source-map-js` 1.2.1 → 1.2.2, transitive, dev-only ([#113](https://github.com/CLDMV/gh-broker/pull/113)). Fixes CVE-2026-93749, a denial of service from malicious indexed source maps.
62
-
63
- `@octokit/auth-app`, `@octokit/core`, `esbuild`, `typescript`, `vitest` and `@vitest/coverage-v8` are unchanged.
64
-
65
- ---
66
-
67
- ## Upgrade notes
68
-
69
- - No breaking changes. Every existing verb and proxied tool behaves as in v0.6.1; the only visible change to an existing tool is the extra warning in `issue_write`'s description.
70
- - Re-run the installer to deploy: it merges the new `issue_labels` allowlist entry into the live config, rebuilds and restarts the service. MCP clients need to reconnect afterwards to see the new tool.
71
- - `issue_labels` uses the existing write PAT and needs no new token permissions (Issues: write is already part of the write token's scope).
@@ -1,82 +0,0 @@
1
- # Deploy architecture
2
-
3
- How gh-broker is installed, configured, and upgraded. The guiding goals are **portability** (copy the repo to another machine, run one installer, it works), a **clean upgrade path** (updates never lose operator-set data), and **platform-neutrality** (Linux today, with seams so macOS/Windows are additive later — not a rewrite).
4
-
5
- ## Config: one portable, token-free file, regenerated by merge
6
-
7
- There is a single config file. It is **portable** and **contains no secrets** — only the tool allowlist, owner names, and credential *references* (`"read": "cred:cldmv-read"`). Keeping tokens out of it is what makes it safe to copy, commit, and carry between machines; a file designed to be copied must never hold live credentials.
8
-
9
- On install *and* upgrade the deployed config is **regenerated by merging**, not overwritten:
10
-
11
- ```
12
- new_config = merge(example_defaults, current_config)
13
- ```
14
-
15
- - `example_defaults` — `config.example.json`, shipped with the code. Updated freely on upgrade; this is where new keys and allowlist changes arrive.
16
- - `current_config` — whatever is already deployed on this machine (absent on a first install).
17
-
18
- The merge is **field-level**, because the two halves have different owners:
19
-
20
- | Field | Source of truth | Why |
21
- |---|---|---|
22
- | `tools` (the allowlist) | **example** (repo) | It is a security control. A removal or tightening in an update must apply; a naive "current wins" would silently keep a tool the repo dropped. |
23
- | `owners`, `app`, `listen`, `upstream`, `defaultOwner` | **current** (deployed), falling back to example | Deployment data the operator set. Preserved across upgrades; new owners from the example are added, existing ones kept as-is. |
24
-
25
- So an upgrade brings new defaults and allowlist changes through the example, while everything the operator configured survives. Nothing is blindly clobbered.
26
-
27
- ## Secrets: separate, agent-unreadable, and the storage backend is the operator's choice
28
-
29
- Secrets never live in the config — the config carries only `cred:<name>` references. The tokens live in a separate store the agent user cannot read, behind a **provider seam** (`config.secrets.provider`) the installer sets from a prompt. Two providers ship:
30
-
31
- - **`systemd-creds`** (default, recommended on Linux): tokens are encrypted with a host/TPM-bound key as `.cred` files, delivered by a unit drop-in (`LoadCredentialEncrypted`) into a memory-only `$CREDENTIALS_DIRECTORY` at start. **Encrypted at rest** and resistant to backups/disk theft (the key isn't beside the ciphertext); not portable to another machine.
32
- - **`file`**: a JSON `{ "<name>": "<token>" }` map at `0640 root:gh-broker` inside the `0750` config dir. **Portable and platform-neutral**; at-rest protection is filesystem permissions only (plaintext on disk).
33
-
34
- The **agent-read boundary is identical either way** — it's filesystem permissions (the agent isn't in the `gh-broker` group), not encryption. The choice trades *at-rest protection* (systemd-creds) against *portability* (file); it does not weaken the agent boundary. This is a genuine trade because the daemon must auto-start, so any decryption key must be available without a human at boot — which only a host/TPM-bound key (systemd-creds) achieves without leaving the key beside the ciphertext.
35
-
36
- The installer moves tokens between backends without re-entry where it can: switching to `file` decrypts the existing host-bound `.cred` files (as root, same host) into the JSON; switching to `systemd-creds` encrypts the tokens into `.cred` files. It prompts only for tokens it can't find. Further providers (OS keychain, DPAPI, encrypted vault) slot into the same seam without touching the daemon's call sites.
37
-
38
- ## Paths: resolved, not hardcoded
39
-
40
- Neither the config nor the secrets path is hardcoded to `/etc`. Both resolve through the same order:
41
-
42
- 1. explicit `--config` flag / `GH_BROKER_CONFIG` (and `GH_BROKER_SECRETS`) — highest,
43
- 2. the platform's system config dir — Linux `/etc/gh-broker`, macOS `/Library/Application Support/gh-broker`, Windows `%ProgramData%\gh-broker`,
44
- 3. the packaged default.
45
-
46
- Only the Linux defaults are exercised today; the others are wired so a later platform port is a table entry, not surgery.
47
-
48
- ## Node runtime: a maintained symlink, no runtime resolution
49
-
50
- The service runs jailed (`ProtectHome`), so it cannot use a `/home` nvm, and there is no login shell at start. Rather than have the unit *resolve* node at runtime, the installer maintains a **stable symlink** — `/usr/local/bin/node` → the chosen node — and the unit simply runs it:
51
-
52
- ```
53
- ExecStart=/usr/local/bin/node /opt/gh-broker/dist/index.mjs --config <resolved>
54
- ```
55
-
56
- The symlink is the single indirection: the CLI (via `PATH`) and the service resolve the *same* link, so "CLI node == service node" holds by construction. Bumping node is re-pointing the symlink — which is exactly the node step the installer runs on setup and on update, so it is not a special case. This removes `launch.sh` (a runtime nvm resolver that duplicated the symlink) and keeps the hardened unit free of a `bash`/`nvm` startup dependency.
57
-
58
- ## Tooling: Node, with one optional shell bootstrap
59
-
60
- All install/deploy *logic* is Node (`deploy/install.mjs` + `deploy/lib/*.mjs`): config merge and validation (validation reuses the daemon's own loader via `node dist/index.mjs --check`), the interactive prompts, the privileged system operations (shelled out to `useradd` / `systemctl` / `rsync`), building `dist/`/`node_modules/` as the checkout's own owner via `sudo -u '#<uid>'` rather than leaving them root-owned, and the smoke test (a real MCP client over the socket). One language, testable with the repo's vitest suite.
61
-
62
- The only thing that can't be Node is bootstrapping node itself. So the entrypoint is **`sudo node deploy/install.mjs`** with node ≥20 as the one prerequisite — which runs on any platform node runs on. For zero-prerequisite convenience on Linux, an **optional** `deploy/install.sh` (~20 lines) sets up nvm + node + the symlink if node is absent, then `exec`s the Node installer. It is a convenience, not the real installer.
63
-
64
- ## The installer flow (install and upgrade are the same command)
65
-
66
- `sudo node deploy/install.mjs` is idempotent — first install and later upgrade are one command:
67
-
68
- 1. detect platform + check prerequisites (`socat`, `rsync`, `systemctl`, `sudo`),
69
- 2. **create or confirm the agent user** — hard stop if it can't (no silent warn-and-continue),
70
- 3. ensure node on a stable path + the `/usr/local/bin/node` symlink,
71
- 4. **regenerate the config** via `merge(example, current)`,
72
- 5. **choose the secret backend and store the PATs** — keep existing, migrate a legacy `systemd-creds` token, or take it from `--secrets-file` / env / a prompt (see the Secrets section),
73
- 6. validate the config with the real loader (`node dist/index.mjs --check`),
74
- 7. build + deploy (unit → `/usr/local/bin/node`, `dist/`, config, secrets),
75
- 8. **smoke test** — MCP `initialize` + one real read over the socket → ✅/❌.
76
-
77
- The same flow runs **fully unattended** via flags — `--agent-user`, `--secrets-provider`, `--secrets-file` / `GH_BROKER_SECRET_<NAME>`, `--create-agent-user`, `--non-interactive`; prompts are the default when flags are omitted. See [`../deploy/README.md`](../deploy/README.md) and `--help`.
78
-
79
- ## What is deliberately deferred
80
-
81
- - Non-Linux secret providers and path defaults (the seams exist; the implementations are additive).
82
- - `link_issue` (App-token-only) stays deferred — implemented but not advertised or wired; see [`../deploy/README.md`](../deploy/README.md).
package/src/appauth.ts DELETED
@@ -1,56 +0,0 @@
1
- /**
2
- *
3
- * @Project: @cldmv/gh-broker
4
- * @Filename: /src/appauth.ts
5
- * @Date: 2026-07-07T18:39:48-07:00 (1783474788)
6
- * @Author: Nate Corcoran <CLDMV>
7
- * @Email: <Shinrai@users.noreply.github.com>
8
- * -----
9
- * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
10
- * @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
11
- * -----
12
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
13
- *
14
- */
15
-
16
- import { Octokit } from "@octokit/core";
17
- import { createAppAuth } from "@octokit/auth-app";
18
- import type { Config, SecretStore } from "./types.ts";
19
-
20
- /**
21
- * Provides per-owner GitHub App–authenticated Octokit clients for the operations
22
- * that fine-grained PATs can't do (`link_issue` → addClosingIssueReference, which
23
- * is not in the fine-grained-PAT GraphQL schema — only App/classic tokens see it).
24
- *
25
- * `@octokit/auth-app` handles the JWT signing + installation-token minting/refresh
26
- * internally, so there's no hand-rolled token machinery here. The App private key
27
- * lives only in the broker (systemd-creds), never in the agent.
28
- */
29
- export class AppAuth {
30
- private readonly config: Config;
31
- private readonly appId: number;
32
- private readonly privateKey: string;
33
- private readonly cache = new Map<number, Octokit>();
34
-
35
- constructor(config: Config, secrets: SecretStore) {
36
- if (!config.app) throw new Error("AppAuth: no app config");
37
- this.config = config;
38
- this.appId = config.app.appId;
39
- this.privateKey = secrets.resolve(config.app.privateKey);
40
- }
41
-
42
- /** App-authed Octokit for this owner's installation, or null if none configured. */
43
- octokitFor(owner: string): Octokit | null {
44
- const installationId = this.config.owners[owner]?.installationId;
45
- if (installationId === undefined) return null;
46
- let ok = this.cache.get(installationId);
47
- if (!ok) {
48
- ok = new Octokit({
49
- authStrategy: createAppAuth,
50
- auth: { appId: this.appId, privateKey: this.privateKey, installationId }
51
- });
52
- this.cache.set(installationId, ok);
53
- }
54
- return ok;
55
- }
56
- }
package/src/audit.ts DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- *
3
- * @Project: @cldmv/gh-broker
4
- * @Filename: /src/audit.ts
5
- * @Date: 2026-07-07T00:24:51-07:00 (1783409091)
6
- * @Author: Nate Corcoran <CLDMV>
7
- * @Email: <Shinrai@users.noreply.github.com>
8
- * -----
9
- * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
10
- * @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
11
- * -----
12
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
13
- *
14
- */
15
-
16
- /**
17
- * Structured audit log. One JSON line per decision/call to stdout (journald
18
- * captures it). NEVER include token values — only tool, owner, mode, repo, result.
19
- */
20
-
21
- export interface AuditEntry {
22
- tool: string;
23
- owner?: string;
24
- mode?: "read" | "write";
25
- repo?: string;
26
- decision: "forward" | "custom" | "augment" | "deny" | "error";
27
- ok?: boolean;
28
- detail?: string;
29
- }
30
-
31
- export function audit(entry: AuditEntry): void {
32
- // eslint-disable-next-line no-console -- journald is the sink
33
- console.log(JSON.stringify({ ts: new Date().toISOString(), ...entry }));
34
- }
package/src/catalog.ts DELETED
@@ -1,165 +0,0 @@
1
- /**
2
- *
3
- * @Project: @cldmv/gh-broker
4
- * @Filename: /src/catalog.ts
5
- * @Date: 2026-07-07T00:24:51-07:00 (1783409091)
6
- * @Author: Nate Corcoran <CLDMV>
7
- * @Email: <Shinrai@users.noreply.github.com>
8
- * -----
9
- * @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
10
- * @Last modified time: 2026-10-16T17:17:02-07:00 (1792196222)
11
- * -----
12
- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
13
- *
14
- */
15
-
16
- import type { Tool } from "@modelcontextprotocol/sdk/types.js";
17
- import type { Config, SecretStore } from "./types.ts";
18
- import type { UpstreamPool } from "./upstream.ts";
19
- // DEFERRED: link_issue (addClosingIssueReference) is App-token-only and not wired in — see docs.
20
- // import { LINK_ISSUE_SCHEMA } from "./verbs/link_issue.ts";
21
- import { RESOLVE_REVIEW_THREAD_SCHEMA, REPLY_REVIEW_THREAD_SCHEMA } from "./verbs/review_thread.ts";
22
- import { REPORT_UNLABELED_SCHEMA } from "./verbs/report_unlabeled.ts";
23
- import { REPORT_SECURITY_ALERTS_SCHEMA } from "./verbs/report_security_alerts.ts";
24
- import { REPORT_RELEASE_PRS_SCHEMA } from "./verbs/report_release_prs.ts";
25
- import { ISSUE_LABELS_SCHEMA } from "./verbs/issue_labels.ts";
26
- import { REPORT_CI_STATUS_SCHEMA } from "./verbs/report_ci_status.ts";
27
-
28
- /**
29
- * Build the advertised tool list ONCE at startup. Proxied tools carry their real
30
- * schemas fetched from the hosted MCP (tool schemas are owner-independent, so any
31
- * valid read token works — we use the first configured owner's read PAT). Custom
32
- * verbs carry local schemas. Only allowlisted tools appear; nothing else is
33
- * advertised (and, per the router, nothing else is executable either).
34
- *
35
- * If the upstream fetch fails (network/token), we advertise minimal schemas rather
36
- * than fail startup — the daemon still serves and calls still route correctly.
37
- */
38
- export async function buildCatalog(config: Config, secrets: SecretStore, upstream: UpstreamPool): Promise<Tool[]> {
39
- const upstreamByName = new Map<string, Tool>();
40
- // Whether the upstream tool list was actually fetched. If it wasn't (network/token), we must NOT drop
41
- // allowlisted tools — we fall back to minimal schemas so the daemon still serves + routes. But if the
42
- // fetch DID succeed, a tool missing from it genuinely isn't served by this upstream (wrong toolset /
43
- // renamed), so advertising a minimal-schema stub only defers the failure to call time (the confusing
44
- // "could not determine repo owner" → "unknown tool" pair) — better to skip it and say why at startup.
45
- let upstreamFetched = false;
46
- const firstOwner = Object.values(config.owners)[0];
47
- if (firstOwner) {
48
- try {
49
- const token = secrets.resolve(firstOwner.read);
50
- // Never let a slow/unreachable upstream hang startup — fall back to minimal schemas.
51
- const timeoutMs = Number(process.env.GH_BROKER_CATALOG_TIMEOUT_MS ?? 10000);
52
- const tools = await Promise.race([
53
- upstream.listToolsFor(token),
54
- new Promise<never>((_, rej) => setTimeout(() => rej(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs).unref())
55
- ]);
56
- for (const t of tools) upstreamByName.set(t.name, t);
57
- upstreamFetched = true;
58
- } catch (err) {
59
- console.error(`catalog: could not fetch upstream tool schemas (${(err as Error).message}) — advertising minimal schemas`);
60
- }
61
- }
62
-
63
- const tools: Tool[] = [];
64
- for (const rule of config.tools) {
65
- if (rule.custom) {
66
- // DEFERRED: link_issue (App-token-only addClosingIssueReference) is not wired in — see docs.
67
- // Intentionally not advertised even if an allowlist entry is added; re-enable by restoring
68
- // this branch + the import above + the router dispatch case, then config.app + installationId.
69
- if (rule.name === "resolve_review_thread") {
70
- tools.push({
71
- name: "resolve_review_thread",
72
- description:
73
- "Resolve (or unresolve) a pull-request review thread — e.g. mark a Copilot review comment resolved once addressed. Takes the thread node id from pull_request_read get_review_comments. Runs on the write PAT (Pull requests: write).",
74
- inputSchema: RESOLVE_REVIEW_THREAD_SCHEMA
75
- });
76
- } else if (rule.name === "reply_review_thread") {
77
- tools.push({
78
- name: "reply_review_thread",
79
- description:
80
- "Post a reply inside a pull-request review thread — e.g. respond to a Copilot review comment. Takes the thread node id from pull_request_read get_review_comments. Runs on the write PAT (Pull requests: write).",
81
- inputSchema: REPLY_REVIEW_THREAD_SCHEMA
82
- });
83
- } else if (rule.name === "issue_labels") {
84
- tools.push({
85
- name: "issue_labels",
86
- description:
87
- 'Edit the labels on one issue or PR without touching any other label on it. add / remove take one label or several (combinable in one call); swap { prefix, to } removes every label starting with prefix and adds to (e.g. prefix "status: ", to "status: verified"); set replaces the whole set and only as an explicit choice. Labels already present (add) or absent (remove) are no-ops. A label name to add that does not exist in the repo refuses the call and changes nothing, unless create_missing is true. Returns the resulting label set plus what was added, removed and created. Runs on the write PAT (Issues: write).',
88
- inputSchema: ISSUE_LABELS_SCHEMA
89
- });
90
- } else if (rule.name === "report_unlabeled") {
91
- tools.push({
92
- name: "report_unlabeled",
93
- description:
94
- "Report issues and/or PRs with no labels, for one repo or every non-archived repo an owner has. Bounded by maxRepos/perRepoLimit/maxApiCalls; returns truncated:true (with a reason) instead of failing outright if a cap or GitHub's rate limit is hit mid-scan. Runs on the read PAT.",
95
- inputSchema: REPORT_UNLABELED_SCHEMA
96
- });
97
- } else if (rule.name === "report_security_alerts") {
98
- tools.push({
99
- name: "report_security_alerts",
100
- description:
101
- "Report every open Dependabot, code-scanning (CodeQL) and secret-scanning alert, for one repo or across every repo an owner has. Reads the org-level endpoints in a handful of calls and falls back to a per-repo sweep for any alert type the token can't read org-wide. Alert state tracks the default branch. Secret values are never returned. Bounded by maxRepos/alertLimit/maxApiCalls; returns truncated:true (with a reason) instead of failing outright if a cap or GitHub's rate limit is hit. Runs on the read PAT.",
102
- inputSchema: REPORT_SECURITY_ALERTS_SCHEMA
103
- });
104
- } else if (rule.name === "report_release_prs") {
105
- tools.push({
106
- name: "report_release_prs",
107
- description:
108
- "Report every open release PR (label `release`: next→master and hotfixes→master) for one repo or across every repo an owner has, and whether each one's per-version changelog file (docs/changelog[s]/v<major>/vX.Y.Z.md) is already on the release branch. Status per PR: ready, needs-changelog, or unknown-version (title has no `release: vX.Y.Z`). Use during mass updates to find which repos need a changelog. One search call plus two calls per release PR found; returns truncated:true with a reason if a cap or GitHub's rate limit is hit. Runs on the read PAT.",
109
- inputSchema: REPORT_RELEASE_PRS_SCHEMA
110
- });
111
- } else if (rule.name === "report_ci_status") {
112
- tools.push({
113
- name: "report_ci_status",
114
- description:
115
- "Report CI health for a PR, or every open PR in a repo, at a chosen verbosity: 0 counts only, 1 adds which run/job failed, 2 adds which step failed, 3 adds a tail of the failing job's log. Detail beyond tier 0 is only fetched for runs that are actually failing. Runs on the read PAT.",
116
- inputSchema: REPORT_CI_STATUS_SCHEMA
117
- });
118
- }
119
- continue;
120
- }
121
- const up = upstreamByName.get(rule.name);
122
- if (!up && upstreamFetched) {
123
- // Reachable upstream that doesn't expose this tool → it would 32602 at call time. Skip + warn.
124
- console.error(`catalog: allowlisted tool "${rule.name}" is NOT exposed by the upstream (${config.upstream}) — skipping it. Check the tool name or the upstream toolset (e.g. use /mcp/x/all for non-default toolsets like labels).`);
125
- continue;
126
- }
127
- const base = up ?? { name: rule.name, description: `(${rule.mode}) proxied to the GitHub MCP`, inputSchema: { type: "object" } };
128
- if (rule.augment && rule.name === "pull_request_read") {
129
- // Layer gh-broker's own extension params onto the real upstream schema — the
130
- // forwarded call still needs pull_request_read's actual params (method,
131
- // pullNumber, ...); this ADDS to that, it doesn't replace it. See router.ts's
132
- // dispatchAugmented for why gh-broker can extend a proxied tool's contract.
133
- const baseSchema = base.inputSchema as { properties?: Record<string, unknown>; [k: string]: unknown };
134
- tools.push({
135
- ...base,
136
- description: `${base.description} gh-broker extension: with method "get", pass "ciVerbosity" (0-3) to also compute CI status for the PR's current head — 0 counts only, 1 adds which run/job failed, 2 adds which step failed, 3 adds a tail of the failing job's log; "logLines" tunes tier 3's tail length.`,
137
- inputSchema: {
138
- ...baseSchema,
139
- type: "object",
140
- properties: {
141
- ...(baseSchema.properties ?? {}),
142
- ciVerbosity: { type: "number", enum: [0, 1, 2, 3], description: 'gh-broker extension: also compute CI status at this verbosity (method "get" only)' },
143
- logLines: { type: "number", description: "gh-broker extension: tier-3 log tail length" }
144
- }
145
- }
146
- });
147
- continue;
148
- }
149
- if (rule.name === "issue_write") {
150
- // issue_write's `labels` REPLACES the issue's whole label set — the trap behind
151
- // CLDMV/gh-broker#117. Say so where the agent reads the tool, and point at the verb
152
- // that edits labels one at a time when it is allowlisted.
153
- const pointer = config.tools.some((t) => t.name === "issue_labels" && t.custom)
154
- ? " To add, remove or swap individual labels, use issue_labels instead."
155
- : " Read the current labels first and pass the full set you want to keep.";
156
- tools.push({
157
- ...base,
158
- description: `${base.description ?? ""} WARNING (gh-broker): "labels" REPLACES the issue's whole label set — every label not listed is removed.${pointer}`.trim()
159
- });
160
- continue;
161
- }
162
- tools.push(base);
163
- }
164
- return tools;
165
- }