@cldmv/gh-broker 0.7.0 → 0.8.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 (90) hide show
  1. package/README.md +134 -40
  2. package/config.example.json +21 -8
  3. package/config.sample.json +45 -0
  4. package/deploy/README.md +73 -15
  5. package/deploy/install.mjs +166 -101
  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 +1638 -202
  14. package/dist/index.mjs.map +4 -4
  15. package/docs/TOOLS.md +192 -1
  16. package/docs/changelog/v0/v0.8.0.md +115 -0
  17. package/docs/deploy-architecture.md +13 -11
  18. package/package.json +7 -7
  19. package/src/availability.ts +42 -0
  20. package/src/catalog.ts +18 -1
  21. package/src/config.ts +29 -2
  22. package/src/router.ts +43 -2
  23. package/src/secrets.ts +34 -4
  24. package/src/types.ts +25 -1
  25. package/src/verbs/ghcr/archive.ts +314 -0
  26. package/src/verbs/ghcr/config.ts +142 -0
  27. package/src/verbs/ghcr/jobs.ts +141 -0
  28. package/src/verbs/ghcr/push.ts +192 -0
  29. package/src/verbs/ghcr/registry.ts +275 -0
  30. package/src/verbs/ghcr/tar.ts +203 -0
  31. package/src/verbs/ghcr_push.ts +264 -0
  32. package/src/verbs/npm-registry.ts +168 -0
  33. package/src/verbs/npm-tarball.ts +76 -0
  34. package/src/verbs/npm_scaffold.ts +325 -0
  35. package/src/verbs/release-analysis.ts +3 -1
  36. package/tests/catalog.test.vitest.mts +55 -1
  37. package/tests/config-merge.test.vitest.mts +275 -8
  38. package/tests/config.test.vitest.mts +75 -1
  39. package/tests/credentials.test.vitest.mts +127 -0
  40. package/tests/ghcr-archive.test.vitest.mts +439 -0
  41. package/tests/ghcr-config.test.vitest.mts +96 -0
  42. package/tests/ghcr-push.test.vitest.mts +526 -0
  43. package/tests/ghcr-registry.test.vitest.mts +206 -0
  44. package/tests/helpers/ghcr-fixtures.mts +373 -0
  45. package/tests/install-args.test.vitest.mts +152 -0
  46. package/tests/issue-labels.test.vitest.mts +15 -0
  47. package/tests/npm-scaffold.test.vitest.mts +479 -0
  48. package/tests/npm-setup.test.vitest.mts +196 -0
  49. package/tests/paths.test.vitest.mts +5 -0
  50. package/tests/prompt.test.vitest.mts +128 -0
  51. package/tests/release-prs.test.vitest.mts +10 -0
  52. package/tests/router.test.vitest.mts +108 -2
  53. package/tests/secret-store.test.vitest.mts +64 -3
  54. package/tests/secrets.test.vitest.mts +53 -0
  55. package/tests/security-alerts.test.vitest.mts +10 -0
  56. package/tests/server.test.vitest.mts +187 -7
  57. package/tests/smoke.test.vitest.mts +177 -0
  58. package/tests/system.test.vitest.mts +355 -0
  59. package/tests/verb-helpers.test.vitest.mts +46 -0
  60. package/.configs/fix-headers.json +0 -3
  61. package/.configs/vitest.config.mjs +0 -44
  62. package/.github/dependabot.yml +0 -111
  63. package/.github/workflows/branch-retention.yml +0 -44
  64. package/.github/workflows/bundle-size.yml +0 -52
  65. package/.github/workflows/ci.yml +0 -389
  66. package/.github/workflows/cla.yml +0 -66
  67. package/.github/workflows/codeql.yml +0 -87
  68. package/.github/workflows/dependabot-auto-merge.yml +0 -62
  69. package/.github/workflows/dependabot-recreate.yml +0 -67
  70. package/.github/workflows/dependency-review.yml +0 -46
  71. package/.github/workflows/feature-pr.yml +0 -75
  72. package/.github/workflows/hotfix-redirector.yml +0 -67
  73. package/.github/workflows/hotfixes-release.yml +0 -70
  74. package/.github/workflows/labeler.yml +0 -48
  75. package/.github/workflows/master-commit-audit.yml +0 -56
  76. package/.github/workflows/member-auto-merge.yml +0 -75
  77. package/.github/workflows/next-release.yml +0 -76
  78. package/.github/workflows/next-reset.yml +0 -50
  79. package/.github/workflows/pr-notify.yml +0 -44
  80. package/.github/workflows/pr-title-normalizer.yml +0 -52
  81. package/.github/workflows/provenance.yml +0 -51
  82. package/.github/workflows/publish.yml +0 -146
  83. package/.github/workflows/release-merge.yml +0 -104
  84. package/.github/workflows/release-notify.yml +0 -41
  85. package/.github/workflows/scorecard.yml +0 -64
  86. package/.github/workflows/stale.yml +0 -50
  87. package/.github/workflows/tag-health.yml +0 -70
  88. package/.github/workflows/update-major-version-tags.yml +0 -94
  89. package/.github/workflows/v4-bootstrap.yml +0 -115
  90. package/.github/workflows/welcome.yml +0 -42
package/README.md CHANGED
@@ -1,23 +1,39 @@
1
1
  # @cldmv/gh-broker
2
2
 
3
- A local **credential-routing MCP proxy** for GitHub. It lets an autonomous agent read and write GitHub issues/PRs across multiple orgs **without ever possessing a token** — the agent gets to *act*, never to *hold* the credential.
3
+ **@cldmv/gh-broker** is a local **credential-routing MCP proxy** for GitHub. It lets an autonomous agent read and write issues, pull requests and more across several orgs **without ever holding a token**: the agent gets to *act*, never to *possess* the credential.
4
4
 
5
- ## Why
5
+ The hosted GitHub MCP authenticates with one token per server, so several orgs means several servers, and the token sits in a config file the agent can read. A classic PAT spans orgs but is coarse; a GitHub App acts as a bot and can't drive the MCP; `gh` and raw PATs are readable by the agent (`gh auth token`). gh-broker covers what none of them do together: the user's own identity, least privilege, several orgs, one server, and a secret the agent cannot exfiltrate.
6
6
 
7
- The hosted GitHub MCP authenticates with one token per server, so multi-org means many servers (tool-list bloat) and the token sits in an agent-readable config file. A classic PAT spans orgs but is coarse; a GitHub App gives bot attribution and can't drive the MCP. `gh` and raw PATs are readable by the agent (`gh auth token`, `cat ~/.claude.json`). None satisfy: **user identity + least-privilege + multi-org + one server + the agent cannot exfiltrate the secret.**
7
+ It runs as a dedicated `gh-broker` system user, holds a read-scoped and a write-scoped fine-grained PAT per org in an agent-unreadable secret store (`systemd-creds` or a `0640` file), and picks the right one per call by repo owner and read/write mode. The toolset is config-driven and default-deny: mostly proxied to GitHub's hosted MCP, plus custom verbs for what the hosted MCP has no tool for, and augmented tools that extend a proxied tool's result. The agent reaches it over a permissioned Unix socket that carries MCP frames and never credential material.
8
8
 
9
- ## How
9
+ > _The agent gets to act, never to hold the credential._
10
10
 
11
- A small daemon (this package) runs as a dedicated **`gh-broker` system user** and:
11
+ [![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] [![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] [![npm last update]][npm_last_update_url] [![Coverage]][coverage_url]
12
12
 
13
- - holds per-org **read-scoped** and **write-scoped** fine-grained PATs, read from an agent-unreadable secret store — `systemd-creds` (encrypted at rest, default) or a plaintext file, chosen at install — via `cred:` references in a token-free config, into memory only;
14
- - routes the right PAT by **repo owner + read/write mode** on each call;
15
- - exposes a **config-driven, default-deny** toolset — mostly **proxied** to GitHub's hosted MCP (which already does labels, type, issue fields, dedup), plus **custom verbs** for gaps the hosted MCP has no tool for, and **augmented** tools that extend a proxied tool's response in place. See [Key Features](#-key-features) and [docs/TOOLS.md](docs/TOOLS.md) for the full set;
16
- - talks to the agent over a **Unix socket** (bytes only; no token crosses).
13
+ > [!NOTE]
14
+ > **Status: 0.x, in production.** Running under systemd and brokering live GitHub reads and writes. Linux and systemd only for now ([#9](https://github.com/CLDMV/gh-broker/issues/9)). `link_issue` (App-token only) is implemented but not wired in, see [deploy/README.md](deploy/README.md#enabling-link_issue-github-app--deferred).
17
15
 
18
- The agent (uid 1003, rootless docker, no passwordless sudo — verified) cannot read another user's mode-0400 secrets, so the boundary is real. The **agent-as-separate-user** model — the users, the bridge group, and the permissions that make the boundary real, as a reproducible example setup — is documented in [`docs/agent-user-model.md`](docs/agent-user-model.md).
16
+ [![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
19
17
 
20
- Full design + threat model: `~/.claude/designs/github-mcp-broker.md`.
18
+ ---
19
+
20
+ ## ✨ What's New
21
+
22
+ ### Latest: v0.8.0 (October 2026)
23
+
24
+ - **`npm_scaffold`: new npm packages without a manual first publish.** The new write verb publishes a broker-built `0.0.0-stage` placeholder and registers the repo's publish workflow as its trusted publisher, so CI publishes the first real release through OIDC. The npm token has no 2FA bypass: each write takes a one-time password from the user, so it's two calls, and the verb refuses any package that already exists.
25
+ - **`ghcr_push`: container image pushes without a registry token.** Push a `docker save` archive from a drop directory to an allowlisted `ghcr.io/<owner>/<image>` as a background job, follow it with `ghcr_push_status` and check it with `ghcr_tags`. Release tags are never overwritten, and the classic PAT stays with the broker.
26
+ - **Optional credentials.** The npm and GHCR tokens can be skipped at install; their tools stay hidden until the token is stored. The installer also takes tokens via `--secret <cred-name>=<token>`.
27
+ - [View full v0.8.0 Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.8.0.md)
28
+
29
+ ### Recent Releases
30
+
31
+ - **v0.7.0** (October 2026) — `issue_labels` adds, removes or swaps single labels without replacing the set, and `issue_write` warns that its `labels` does ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.7.0.md))
32
+ - **v0.6.1** (October 2026) — MCP SDK 1.32.0 (same-origin redirects only) and a refreshed vitest toolchain; no broker source changes ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.6.1.md))
33
+ - **v0.6.0** (October 2026) — Compact per-repo roll-up, filters and flow tier for `report_security_alerts`, and release-readiness analysis for `report_release_prs` ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.6.0.md))
34
+ - **v0.5.1** (October 2026) — The installer keeps the existing secrets backend instead of prompting on every upgrade ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.5.1.md))
35
+
36
+ 📚 For the complete version history, see the [docs/changelog/](https://github.com/CLDMV/gh-broker/tree/master/docs/changelog/) folder.
21
37
 
22
38
  ---
23
39
 
@@ -59,6 +75,16 @@ The hosted MCP's `issue_write` takes `labels` as a full replacement, so changing
59
75
 
60
76
  🏷️ **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#issue_labels)**
61
77
 
78
+ ### 📦 **New npm Packages Without a Manual First Publish**
79
+
80
+ npm only accepts a trusted publisher on a package that already exists, so every new package used to need a hand-run first publish and a form on npmjs.com. `npm_scaffold` does both: it publishes a broker-built placeholder (`0.0.0-stage`, no code) and registers the repo's publish workflow as the trusted publisher, after which CI publishes every release through OIDC. The broker's npm token has no 2FA bypass, so every npm write needs a one-time password from the user: one call publishes, a second call with the next code adds the trusted publisher. The verb refuses any package that already exists.
81
+
82
+ ```json
83
+ { "owner": "cldmv", "repo": "new-thing", "package": "@cldmv/new-thing", "otp": "123456" }
84
+ ```
85
+
86
+ 📦 **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#npm_scaffold)**
87
+
62
88
  ### 🛡️ **Org-Wide Security Alerts**
63
89
 
64
90
  `report_security_alerts` lists every open Dependabot, CodeQL and secret-scanning alert — for one repo or across everything an owner has — using the org-level endpoints where the token can read them and a per-repo sweep where it can't. Secret values are never returned.
@@ -71,6 +97,16 @@ The hosted MCP's `issue_write` takes `labels` as a full replacement, so changing
71
97
 
72
98
  🚢 **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_release_prs)**
73
99
 
100
+ ### 🐳 **Container Image Pushes Without a Registry Token**
101
+
102
+ `ghcr_push` pushes a `docker save` archive to `ghcr.io/<owner>/<image>:<tag>` with a classic `write:packages` PAT the agent never sees. The image must be on an allowlist, the archive must sit in a dedicated drop directory (symlinks, hard links and paths outside it are refused), and an existing release tag is never overwritten. The push runs as a background job, uploads only the blobs the registry doesn't already have, and is tracked with `ghcr_push_status`; `ghcr_tags` checks the result.
103
+
104
+ ```json
105
+ { "owner": "cldmv", "image": "hoot", "tag": "1.4.0", "archive": "hoot-1.4.0.tar" }
106
+ ```
107
+
108
+ 🐳 **Full parameter reference and threat model: [docs/TOOLS.md](docs/TOOLS.md#ghcr_push)**
109
+
74
110
  ### 🔧 **Extensible By Design**
75
111
 
76
112
  The broker presents its **own** MCP contract to the agent — a proxied tool's schema and response are not required to match the hosted MCP's exactly. Adding a custom verb or extending a proxied tool with new parameters follows a documented pattern, not a one-off hack.
@@ -85,29 +121,32 @@ The broker presents its **own** MCP contract to the agent — a proxied tool's s
85
121
 
86
122
  ---
87
123
 
88
- ## ✨ What's New
124
+ ## 📦 Installation
89
125
 
90
- ### Latest: v0.7.0 (October 2026)
126
+ ### Requirements
91
127
 
92
- - **`issue_labels`: edit labels without replacing the set.** The new write verb adds, removes or swaps individual labels on an issue or PR (`swap { prefix: "status: ", to: "status: verified" }` moves a status in one call), so the priority, area and type labels next to it are never dropped the way they are by `issue_write`'s whole-set `labels`. Unknown label names are refused unless `create_missing` is set, and the call returns the resulting set.
93
- - **`issue_write` warns about the replacement.** Its advertised description now says that `labels` replaces the whole set and points at `issue_labels`.
94
- - **Dependencies.** MCP SDK 1.32.1 and a patched `proxy-addr` (CVE-2026-90711) at runtime; `@cldmv/configs` 1.3.0 and other dev-tooling bumps.
95
- - [View full v0.7.0 Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.7.0.md)
128
+ - **Linux with systemd.** The broker runs as a hardened systemd service and listens on a Unix socket.
129
+ - **Node.js 22.12.0 or later** (`engines.node` is `>=22.12.0`). The installer points `/usr/local/bin/node` at the node it runs under.
130
+ - **The agent runs as its own Linux user**, without passwordless sudo, so it can't read the broker's secrets. The model behind that is in [docs/agent-user-model.md](docs/agent-user-model.md).
131
+ - **Two fine-grained PATs per org** (read and write). The scopes are listed in [deploy/README.md](deploy/README.md).
96
132
 
97
- ### Recent Releases
133
+ ### Install
98
134
 
99
- - **v0.6.1** (October 2026) — MCP SDK 1.32.0 (same-origin redirects only) and a refreshed vitest toolchain; no broker source changes ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.6.1.md))
100
- - **v0.6.0** (October 2026) — Compact per-repo roll-up, filters and flow tier for `report_security_alerts`, and release-readiness analysis for `report_release_prs` ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.6.0.md))
101
- - **v0.5.1** (October 2026) — The installer keeps the existing secrets backend instead of prompting on every upgrade ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.5.1.md))
102
- - **v0.5.0** (October 2026) — New `report_security_alerts` and `report_release_prs` verbs, and a permission 403 is no longer mistaken for a rate limit ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.5.0.md))
135
+ gh-broker is published on npm as [`@cldmv/gh-broker`](https://www.npmjs.com/package/@cldmv/gh-broker), but it is a system service, not a library or a global CLI. Install it from a checkout with the installer, which handles both the first install and every upgrade:
103
136
 
104
- 📚 For the complete version history, see the [docs/changelog/](https://github.com/CLDMV/gh-broker/tree/master/docs/changelog/) folder.
137
+ ```sh
138
+ git clone https://github.com/CLDMV/gh-broker.git
139
+ cd gh-broker
140
+ sudo node deploy/install.mjs <agent-user>
141
+ ```
142
+
143
+ The installer creates the service user and the socket group, builds as the checkout's owner, merges the shipped defaults with the deployed config, stores the PATs in the backend you pick, installs the unit and runs a smoke test. Prerequisites, PAT scopes, unattended installs and every flag: [deploy/README.md](deploy/README.md).
105
144
 
106
145
  ---
107
146
 
108
- ## Calling a tool
147
+ ## 🚀 Quick Start
109
148
 
110
- The agent's MCP client sees an ordinary `tools/call`; the broker resolves the credential, routes it, and (for custom/augmented tools) does the extra work in between:
149
+ Point the agent's MCP client at the socket ([deploy/README.md § 3](deploy/README.md#3-point-claude-at-the-broker)). From then on the client sees an ordinary `tools/call`, and the broker resolves the credential, routes it, and (for custom/augmented tools) does the extra work in between:
111
150
 
112
151
  ```json
113
152
  // tools/call
@@ -123,7 +162,7 @@ No token ever appears in the request, the response, or anywhere the agent can re
123
162
 
124
163
  ---
125
164
 
126
- ## Layout
165
+ ## 📁 Layout
127
166
 
128
167
  ```
129
168
  src/
@@ -143,12 +182,17 @@ src/
143
182
  report_ci_status.ts tiered CI status for a PR or every open PR in a repo
144
183
  rest-client.ts shared rate-limit-floor + call-budget REST helpers
145
184
  issue_labels.ts add/remove/swap/set labels on an issue or PR (write PAT)
185
+ npm_scaffold.ts create an npm package + its trusted publisher (npm token + user OTP)
186
+ npm-registry.ts the npm registry requests npm_scaffold makes
187
+ npm-tarball.ts minimal npm tarball builder for the placeholder
146
188
  review_thread.ts resolve/reply PR review threads (write PAT)
189
+ ghcr_push.ts ghcr_push / ghcr_push_status / ghcr_tags (GHCR classic PAT)
190
+ ghcr/ config policy, tar + image-archive reader, OCI registry client, push, job queue
147
191
  link_issue.ts addClosingIssueReference (App token) — DEFERRED, not wired in
148
192
  deploy/
149
193
  install.mjs interactive installer — first install AND upgrade (sudo node)
150
194
  install.sh optional Linux bootstrap — set up node, then hand off to install.mjs
151
- lib/ installer pieces (config merge, secret store, system ops, smoke test)
195
+ lib/ installer pieces (config merge, secret store, system ops, smoke test, npm token setup)
152
196
  gh-broker.service systemd unit (hardened)
153
197
  README.md prerequisites + PAT scopes + install/upgrade/wiring
154
198
  docs/
@@ -156,31 +200,81 @@ docs/
156
200
  deploy-architecture.md config merge, secret store, node handling, installer flow
157
201
  agent-user-model.md the agent-as-separate-user isolation model (example setup)
158
202
  changelog/ per-version release notes
159
- config.example.json shipped defaults (token-free); merged into the deployed config
203
+ config.example.json shipped generic defaults (socket, upstream, tools allowlist)
204
+ config.sample.json a full config with placeholder values, for reference only
160
205
  ```
161
206
 
207
+ ---
208
+
162
209
  ## 📚 Documentation
163
210
 
164
- - **[Tools Reference](docs/TOOLS.md)** — every custom and augmented tool: `report_unlabeled`, `report_security_alerts`, `report_release_prs`, `report_ci_status`, the `pull_request_read` `ciVerbosity` extension, `issue_labels`, `resolve_review_thread`/`reply_review_thread`, and how to add a new one
211
+ - **[Tools Reference](docs/TOOLS.md)** — every custom and augmented tool: `report_unlabeled`, `report_security_alerts`, `report_release_prs`, `report_ci_status`, the `pull_request_read` `ciVerbosity` extension, `issue_labels`, `npm_scaffold`, `resolve_review_thread`/`reply_review_thread`, `ghcr_push`/`ghcr_push_status`/`ghcr_tags`, and how to add a new one
165
212
  - **[Agent-as-Separate-User Model](docs/agent-user-model.md)** — the isolation model behind "use ≠ possession": users, groups, filesystem permissions, verified against a real host
166
213
  - **[Deploy Architecture](docs/deploy-architecture.md)** — config merge, secret store backends, node handling, the installer's full flow
167
214
  - **[Install Guide](deploy/README.md)** — prerequisites, PAT scopes, install/upgrade command, flags
168
215
  - **[Changelog](docs/changelog/)** — per-version release notes
169
216
 
170
- ## Status
217
+ [![CodeFactor]][codefactor_url] [![OpenSSF Scorecard]][ossf_scorecard_url] [![npms.io score]][npms_url] [![npm unpacked size]][npm_size_url] [![Repo size]][repo_size_url]
171
218
 
172
- **Deployed and in production** — running under systemd, brokering live GitHub reads and writes across the configured org. The Unix-socket MCP server, the default-deny allowlist, owner/mode→PAT routing, and every custom/augmented tool in [docs/TOOLS.md](docs/TOOLS.md) are exercised in normal use — `report_ci_status` and the `pull_request_read` augmentation have both been verified against real failing CI data, not just mocked tests. `link_issue` (App-token-only) is implemented but deferred — see [`deploy/README.md`](deploy/README.md#enabling-link_issue-github-app--deferred). Install/upgrade with `sudo node deploy/install.mjs` (see [`deploy/README.md`](deploy/README.md)).
219
+ ---
173
220
 
174
- ## Install / build
221
+ ## 🤝 Contributing
175
222
 
176
- ```sh
177
- npm ci
178
- npm run typecheck
179
- npm run build
180
- ```
223
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/CLDMV/gh-broker/issues). Pull requests target the `next` branch; releases ship from `next` to `master`. Run `npm run build:ci` and `npm run ci:coverage` before opening one; coverage is held at 100%.
181
224
 
182
- ## License
225
+ [![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
183
226
 
184
- Apache-2.0 © Shinrai / CLDMV
227
+ ---
228
+
229
+ ## 🔗 Links
230
+
231
+ - **npm**: [@cldmv/gh-broker](https://www.npmjs.com/package/@cldmv/gh-broker)
232
+ - **GitHub**: [CLDMV/gh-broker](https://github.com/CLDMV/gh-broker)
233
+ - **Issues**: [GitHub Issues](https://github.com/CLDMV/gh-broker/issues)
234
+ - **Changelog**: [docs/changelog/](https://github.com/CLDMV/gh-broker/tree/master/docs/changelog/)
235
+ - **Releases**: [GitHub Releases](https://github.com/CLDMV/gh-broker/releases)
236
+
237
+ ---
185
238
 
186
- Published on npm as [`@cldmv/gh-broker`](https://www.npmjs.com/package/@cldmv/gh-broker). It's a systemd daemon, not a typical CLI tool — install it with [`deploy/install.mjs`](deploy/README.md), not `npm install -g`.
239
+ ## 📄 License
240
+
241
+ [![GitHub license]][github_license_url] [![npm license]][npm_license_url]
242
+
243
+ Apache-2.0 © Shinrai / CLDMV. See [LICENSE](https://github.com/CLDMV/gh-broker/blob/master/LICENSE) for the full text.
244
+
245
+ <!-- Badge definitions -->
246
+ <!-- [github release]: https://img.shields.io/github/v/release/CLDMV/gh-broker?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
247
+ <!-- [github_release_url]: https://github.com/CLDMV/gh-broker/releases -->
248
+ <!-- [release date]: https://img.shields.io/github/release-date/CLDMV/gh-broker?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
249
+ <!-- [release_date_url]: https://github.com/CLDMV/gh-broker/releases -->
250
+
251
+ [npm version]: https://img.shields.io/npm/v/%40cldmv%2Fgh-broker.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
252
+ [npm_version_url]: https://www.npmjs.com/package/@cldmv/gh-broker
253
+ [npm downloads]: https://img.shields.io/npm/dm/%40cldmv%2Fgh-broker.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
254
+ [npm_downloads_url]: https://www.npmjs.com/package/@cldmv/gh-broker
255
+ [github downloads]: https://img.shields.io/github/downloads/CLDMV/gh-broker/total?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
256
+ [github_downloads_url]: https://github.com/CLDMV/gh-broker/releases
257
+ [last commit]: https://img.shields.io/github/last-commit/CLDMV/gh-broker?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
258
+ [last_commit_url]: https://github.com/CLDMV/gh-broker/commits
259
+ [npm last update]: https://img.shields.io/npm/last-update/%40cldmv%2Fgh-broker?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
260
+ [npm_last_update_url]: https://www.npmjs.com/package/@cldmv/gh-broker
261
+ [coverage]: https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FCLDMV%2Fgh-broker%2Fbadges%2Fcoverage.json&style=for-the-badge&logo=vitest&logoColor=white
262
+ [coverage_url]: https://github.com/CLDMV/gh-broker/blob/badges/coverage.json
263
+ [contributors]: https://img.shields.io/github/contributors/CLDMV/gh-broker.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
264
+ [contributors_url]: https://github.com/CLDMV/gh-broker/graphs/contributors
265
+ [sponsor shinrai]: https://img.shields.io/github/sponsors/shinrai?style=for-the-badge&logo=githubsponsors&logoColor=white&labelColor=EA4AAA&label=Sponsor
266
+ [sponsor_url]: https://github.com/sponsors/shinrai
267
+ [codefactor]: https://img.shields.io/codefactor/grade/github/CLDMV/gh-broker?style=for-the-badge&logo=codefactor&logoColor=white&labelColor=F44A6A
268
+ [codefactor_url]: https://www.codefactor.io/repository/github/cldmv/gh-broker
269
+ [openssf scorecard]: https://img.shields.io/ossf-scorecard/github.com/CLDMV/gh-broker?style=for-the-badge&label=OpenSSF%20Scorecard
270
+ [ossf_scorecard_url]: https://scorecard.dev/viewer/?uri=github.com/CLDMV/gh-broker
271
+ [npms.io score]: https://img.shields.io/npms-io/final-score/%40cldmv%2Fgh-broker?style=for-the-badge&logo=npms&logoColor=white&labelColor=0B5D57
272
+ [npms_url]: https://npms.io/search?q=%40cldmv%2Fgh-broker
273
+ [npm unpacked size]: https://img.shields.io/npm/unpacked-size/%40cldmv%2Fgh-broker.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
274
+ [npm_size_url]: https://www.npmjs.com/package/@cldmv/gh-broker
275
+ [repo size]: https://img.shields.io/github/repo-size/CLDMV/gh-broker?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
276
+ [repo_size_url]: https://github.com/CLDMV/gh-broker
277
+ [npm license]: https://img.shields.io/npm/l/%40cldmv%2Fgh-broker.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
278
+ [npm_license_url]: https://www.npmjs.com/package/@cldmv/gh-broker
279
+ [github license]: https://img.shields.io/github/license/CLDMV/gh-broker.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
280
+ [github_license_url]: https://github.com/CLDMV/gh-broker/blob/master/LICENSE
@@ -240,6 +240,11 @@
240
240
  "mode": "write",
241
241
  "custom": true
242
242
  },
243
+ {
244
+ "name": "npm_scaffold",
245
+ "mode": "write",
246
+ "custom": true
247
+ },
243
248
  {
244
249
  "name": "resolve_review_thread",
245
250
  "mode": "write",
@@ -269,13 +274,21 @@
269
274
  "name": "report_ci_status",
270
275
  "mode": "read",
271
276
  "custom": true
277
+ },
278
+ {
279
+ "name": "ghcr_push",
280
+ "mode": "write",
281
+ "custom": true
282
+ },
283
+ {
284
+ "name": "ghcr_push_status",
285
+ "mode": "read",
286
+ "custom": true
287
+ },
288
+ {
289
+ "name": "ghcr_tags",
290
+ "mode": "read",
291
+ "custom": true
272
292
  }
273
- ],
274
- "owners": {
275
- "cldmv": {
276
- "read": "cred:cldmv-read",
277
- "write": "cred:cldmv-write"
278
- }
279
- },
280
- "defaultOwner": "cldmv"
293
+ ]
281
294
  }
@@ -0,0 +1,45 @@
1
+ {
2
+ "listen": "unix:/run/gh-broker/gh-broker.sock",
3
+ "upstream": "https://api.githubcopilot.com/mcp/x/all",
4
+ "defaultOwner": "your-org",
5
+ "owners": {
6
+ "your-org": {
7
+ "read": "cred:your-org-read",
8
+ "write": "cred:your-org-write",
9
+ "installationId": 12345678
10
+ }
11
+ },
12
+ "app": {
13
+ "appId": 123456,
14
+ "privateKey": "cred:gh-app-key"
15
+ },
16
+ "secrets": {
17
+ "provider": "file",
18
+ "path": "/etc/gh-broker/secrets.json"
19
+ },
20
+ "npm": {
21
+ "token": "cred:npm-token",
22
+ "scopes": [
23
+ "@your-org"
24
+ ],
25
+ "registry": "https://registry.npmjs.org",
26
+ "tokenOrgs": [
27
+ "your-org"
28
+ ]
29
+ },
30
+ "ghcr": {
31
+ "dropDir": "/srv/gh-broker/ghcr-drop",
32
+ "protectReleaseTags": true,
33
+ "maxArchiveBytes": 17179869184,
34
+ "owners": {
35
+ "your-org": {
36
+ "username": "your-login",
37
+ "credential": "cred:your-org-packages",
38
+ "images": [
39
+ "your-image",
40
+ "your-image-worker-*"
41
+ ]
42
+ }
43
+ }
44
+ }
45
+ }
package/deploy/README.md CHANGED
@@ -9,15 +9,21 @@ One interactive installer does first-install and upgrade. Design and rationale:
9
9
 
10
10
  The installer maintains `/usr/local/bin/node` as the single node indirection (CLI and service resolve the same link). Bumping node is re-pointing that symlink — which the installer does on every run.
11
11
 
12
- ## 1. Mint the two CLDMV fine-grained PATs
12
+ ## 1. Mint the two fine-grained PATs
13
13
 
14
- GitHub → Settings → Developer settings → **Fine-grained tokens** → Generate new token, **twice**. Resource owner: **CLDMV**. Repository access: **All repositories**.
14
+ For each GitHub owner (user or org) the broker serves, `<org>`: GitHub → Settings → Developer settings → **Fine-grained tokens** → Generate new token, **twice**. Resource owner: `<org>`. Repository access: **All repositories**.
15
15
 
16
- **`cldmv-read`** — Issues: Read, Pull requests: Read, Contents: Read, Metadata: Read (auto).
17
- **`cldmv-write`** — Issues: R/W, Pull requests: R/W, Contents: R/W, Metadata: Read (auto).
16
+ **`<org>-read`** — Issues: Read, Pull requests: Read, Contents: Read, Metadata: Read (auto).
17
+ **`<org>-write`** — Issues: R/W, Pull requests: R/W, Contents: R/W, Metadata: Read (auto).
18
18
 
19
19
  No organization or admin permissions. If the org requires approval for fine-grained PATs, approve them in Org → Settings → Personal access tokens. The two-PAT split (read vs write) is a deliberate least-privilege control — see the design doc.
20
20
 
21
+ ### GHCR pushes: one classic PAT (`<org>-packages`)
22
+
23
+ `ghcr_push` needs a third credential, because the GitHub Container Registry does not accept fine-grained PATs. Create a **classic** token with **`write:packages` only** (it implies `read:packages`; do not add `delete:packages`, and add `repo` only if pushes to a private-repo-linked package are refused without it), and set an expiry: [create the token](https://github.com/settings/tokens/new?scopes=write:packages&description=gh-broker%20ghcr). Enable it with `--ghcr-owner <org> --ghcr-username <login> --ghcr-image <name>` (or a `ghcr` block in a `--config` file, see [Configuring the install](#configuring-the-install)). The installer then prompts for the token as `<org>-packages`; it is an **optional credential**, so you can press Enter to skip it (an unattended run skips it with a note), and the broker then starts normally with the `ghcr_*` tools hidden until the token is stored. Supply it on a later run with `--secret <org>-packages=<token>`, `--secrets-file` or env `GH_BROKER_SECRET_<ORG>_PACKAGES`. `--ghcr-username` (`ghcr.owners.<org>.username`) must be the GitHub login that owns the token.
24
+
25
+ The installer also creates the drop directory from `ghcr.dropDir` (default `/srv/gh-broker/ghcr-drop`) as `2750 <agent-user>:gh-broker`: the agent writes archives there, the setgid bit gives each file the broker's group, and nobody else can look in. Archives must be group-readable (`chmod g+r`); the broker can only read them, so the agent deletes them after a push. See [`../docs/TOOLS.md`](../docs/TOOLS.md#ghcr_push).
26
+
21
27
  ## 2. Install (first install AND upgrade — same command)
22
28
 
23
29
  ```sh
@@ -28,26 +34,27 @@ sudo node deploy/install.mjs [agent-user]
28
34
 
29
35
  1. checks prerequisites,
30
36
  2. **creates or confirms the agent user** (stops if it can't — no silent half-install),
31
- 3. creates the `gh-broker` service user + `gh-broker-clients` bridge group, adds the agent user to it,
37
+ 3. creates the `gh-broker` service user + `gh-broker-clients` bridge group, adds the agent user to it (and, when the config has a `ghcr` block, creates the GHCR drop directory),
32
38
  4. points `/usr/local/bin/node` at the running node,
33
39
  5. builds `dist/` if it isn't already built, or rebuilds it if a prior run left it (or `node_modules/`) owned by someone other than the checkout's owner — the build itself runs via `sudo -u '#<uid>'` as whoever owns `deploy/install.mjs`, not as root, so the installer never leaves root-owned files behind in your checkout,
34
- 6. regenerates the config by **merging** `config.example.json` with whatever is deployed (your data is preserved; new keys/allowlist changes from the repo arrive),
35
- 7. **asks how to store the credentials** — `systemd-creds` (encrypted at rest, host-bound; recommended) or a plaintext `file` (`0640 root:gh-broker`; portable) — then stores the tokens in the chosen backend: it keeps existing creds, decrypts a legacy `systemd-creds` token when you switch to `file`, and **prompts only for what it can't find** (input hidden),
36
- 8. validates the config with the real loader (`--check`),
37
- 9. installs the unit, deploys `dist/`, restarts, and
38
- 10. runs a **smoke test** (MCP handshake + a real read over the socket) → ✅/❌.
40
+ 6. regenerates the config by layering `config.example.json`, whatever is deployed, your `--config` file and your flags (your data is preserved; allowlist changes from the repo arrive; see [Configuring the install](#configuring-the-install)); a first install with no owner asks for one,
41
+ 7. sets up the optional **npm integration** when asked (offered on a first install, or `--npm`): an npm web login it waits on, then the token it creates for `npm_scaffold` (see [npm integration](#npm-integration-npm_scaffold)),
42
+ 8. **asks how to store the credentials** — `systemd-creds` (encrypted at rest, host-bound; recommended) or a plaintext `file` (`0640 root:gh-broker`; portable) — then stores the tokens in the chosen backend: it keeps existing creds, decrypts a legacy `systemd-creds` token when you switch to `file`, and **prompts only for what it can't find** (input hidden),
43
+ 9. validates the config with the real loader (`--check`),
44
+ 10. installs the unit, deploys `dist/`, restarts, and
45
+ 11. runs a **smoke test** (MCP handshake + a real read over the socket) → ✅/❌.
39
46
 
40
47
  ### Unattended (single non-interactive command)
41
48
 
42
- Pass flags plus `--non-interactive` (`-y`) to skip every prompt: `--agent-user <name>`, `--secrets-provider systemd-creds|file`, `--create-agent-user`, and supply tokens via `--secrets-file <json>` (`{ "<cred-name>": "<token>" }`) or env `GH_BROKER_SECRET_<NAME>` (NAME upper-cased, non-alnum → `_`). Example:
49
+ Pass flags plus `--non-interactive` (`-y`) to skip every prompt: `--agent-user <name>`, `--secrets-provider systemd-creds|file`, `--create-agent-user`, and supply tokens via `--secrets-file <json>` (`{ "<cred-name>": "<token>" }`), env `GH_BROKER_SECRET_<NAME>` (NAME upper-cased, non-alnum → `_`), or `--secret <cred-name>=<token>` (repeatable; a name the config doesn't use is refused, so a typo can't silently do nothing; it is visible in the process list and shell history, so prefer the file or env). Precedence is `--secret`, then the file, then env, then the interactive prompt; a token from any of the first three replaces a stored one. Example:
43
50
 
44
51
  ```sh
45
- sudo GH_BROKER_SECRET_CLDMV_READ=… GH_BROKER_SECRET_CLDMV_WRITE=… \
46
- /usr/local/bin/node deploy/install.mjs --agent-user repo \
52
+ sudo GH_BROKER_SECRET_ACME_READ=… GH_BROKER_SECRET_ACME_WRITE=… \
53
+ /usr/local/bin/node deploy/install.mjs --agent-user repo --owner acme \
47
54
  --secrets-provider systemd-creds --non-interactive
48
55
  ```
49
56
 
50
- Migrating an existing `systemd-creds` install needs **no tokens** — `--secrets-provider systemd-creds --non-interactive` keeps the existing `.cred` files (nothing to prompt). `--help` lists every flag.
57
+ Re-running on an existing install needs no `--owner`: the deployed owners are kept. Migrating an existing `systemd-creds` install needs **no tokens** — `--secrets-provider systemd-creds --non-interactive` keeps the existing `.cred` files (nothing to prompt). `--help` lists every flag.
51
58
 
52
59
  **Upgrade** is just re-running it: config merged (edits kept), your previously-chosen storage backend pre-selected, existing tokens kept. You can **switch backends** on any run — pick `file` and it decrypts the existing `systemd-creds` tokens into the file (no re-entry); pick `systemd-creds` and it encrypts the tokens into `.cred` files. `launch.sh` is no longer used — remove it after a successful run: `sudo rm -f /opt/gh-broker/launch.sh` (and `sudo rm -rf /etc/gh-broker/creds` only if you've moved fully to the `file` backend).
53
60
 
@@ -60,6 +67,39 @@ The installer asks which backend holds the tokens at rest:
60
67
 
61
68
  Either way the **agent user cannot read the tokens** (that's filesystem perms, not encryption) and the **config stays token-free**. The choice only affects at-rest protection vs portability. See [`../docs/deploy-architecture.md`](../docs/deploy-architecture.md).
62
69
 
70
+ ## Configuring the install
71
+
72
+ `config.example.json` ships only the generic defaults: `listen`, `upstream` and the `tools` allowlist. Everything specific to your deployment (owners, GHCR, App, npm, secrets) is supplied at install time. `config.sample.json` shows a complete config with placeholder values; nothing loads it.
73
+
74
+ Settings are layered, lowest precedence first:
75
+
76
+ 1. `config.example.json` (shipped defaults),
77
+ 2. the config already deployed on the host,
78
+ 3. `--config <file>`,
79
+ 4. command-line flags.
80
+
81
+ A scalar is replaced by a higher layer. `owners` and `ghcr.owners` are merged per owner, so adding an owner never drops the deployed ones. `tools` always comes from the example: the allowlist is a security control, so a `--config` file that contains `tools` is refused. The `--config` file is JSON in the deployed config's shape, holds only `cred:` references (a value shaped like a token is refused), and the merged result is validated by the real loader before anything is deployed. An upgrade run with no config flags keeps the deployed owners, `ghcr` and everything else as they are.
82
+
83
+ | Flag | Sets |
84
+ |---|---|
85
+ | `--config <file>` | JSON config file merged on top of the deployed config |
86
+ | `--owner <org>` (repeatable) | `owners.<org>` = `cred:<org>-read` / `cred:<org>-write` |
87
+ | `--installation-id <org>=<id>` (repeatable) | `owners.<org>.installationId` |
88
+ | `--default-owner <org>` | `defaultOwner` (a first install with a single owner defaults to it) |
89
+ | `--listen <unix:path>` | `listen` |
90
+ | `--upstream <url>` | `upstream` |
91
+ | `--app-id <n>`, `--app-private-key <cred-name>` | `app.appId`, `app.privateKey` (`cred:gh-app-key` unless named) |
92
+ | `--secrets-provider <p>`, `--secrets-path <path>` | `secrets.provider`, `secrets.path` (path is for the `file` provider) |
93
+ | `--npm`, `--npm-scope`, `--npm-org`, `--npm-registry <url>`, `--npm-token-days` | the `npm` block, see [npm integration](#npm-integration-npm_scaffold) |
94
+ | `--ghcr-owner <org>`, `--ghcr-username <login>`, `--ghcr-image <glob>` (repeatable) | `ghcr.owners.<org>`; the credential is `cred:<org>-packages` |
95
+ | `--ghcr-drop-dir <path>` | `ghcr.dropDir` (default `/srv/gh-broker/ghcr-drop`) |
96
+ | `--ghcr-max-archive-bytes <n>` | `ghcr.maxArchiveBytes` |
97
+ | `--protect-release-tags`, `--no-protect-release-tags` | `ghcr.protectReleaseTags` |
98
+
99
+ Flags that build a single GHCR owner do so for one owner per run; use `--config` for several, or for a credential name other than the derived one.
100
+
101
+ With no owner anywhere (a first install with no flags and no `--config`), an interactive run asks for the GitHub owner name and then for its two tokens; `--non-interactive` fails and names `--owner` / `--config`.
102
+
63
103
  ## 3. Point Claude at the broker
64
104
 
65
105
  The installer prints this; add it to the agent user's `~/.claude.json` `mcpServers`:
@@ -76,7 +116,25 @@ Re-login as the agent user first, so its `gh-broker-clients` membership is activ
76
116
 
77
117
  ## Adding an org later
78
118
 
79
- Add an `owners` entry to `config.example.json` (name + `cred:<org>-read` / `cred:<org>-write` refs), then re-run the installer — the merge brings the new owner through, and it prompts for the new credentials. No code change.
119
+ Re-run the installer with `--owner <org>` (repeatable). The deployed owners stay, the new one is added with `cred:<org>-read` / `cred:<org>-write`, and the installer prompts for its two tokens. No code change.
120
+
121
+ ## npm integration (`npm_scaffold`)
122
+
123
+ Optional. It lets `npm_scaffold` create new npm packages and their trusted publishers. The broker holds an npm granular access token; the installer creates it for you.
124
+
125
+ **Enable or rotate:** re-run the installer with `--npm` (a first interactive install also offers it). The installer then:
126
+
127
+ 1. asks which scopes to allow (`--npm-scope @your-org`, repeatable; defaults to the existing setting, else `@<first owner>`),
128
+ and, with `--npm-org your-org` (repeatable), also grants the token org read-write on those orgs; the choice is saved as `npm.tokenOrgs` so a later rotation creates the same kind of token,
129
+ 2. starts an npm web login and shows the login link, then **waits** until you approve it in a browser (the same flow as `npm login --auth-type=web`),
130
+ 3. asks for your npm password and a one-time password (both hidden; the password is used for this one request and never stored), and creates a granular token: those scopes read-write, `--npm-token-days` lifetime (default 90), **no 2FA bypass**,
131
+ 4. revokes the login session, stores the token as `npm-token` in the chosen credential backend, and adds `"npm": { "token": "cred:npm-token", "scopes": [...] }` to the config.
132
+
133
+ Without 2FA bypass the token can't publish or change trust settings on its own: every npm write `npm_scaffold` makes carries its own one-time password from you (one for the publish, one for the trusted publisher). Running `--npm` again creates a fresh token and replaces the stored one. The installer does not revoke the old token; it says so, and the old one stays valid until it expires or you revoke it in your npm account's Access Tokens settings.
134
+
135
+ **Unattended:** the web login needs a browser, so `--non-interactive --npm` takes an existing token instead, via `--secrets-file` (`{ "npm-token": "npm_…" }`) or env `GH_BROKER_SECRET_NPM_TOKEN`.
136
+
137
+ Without the `npm` block nothing changes: the installer says "not enabled", and the broker does not expose `npm_scaffold` at all (startup logs why). The token is an **optional credential**: if it is missing (skipped at the prompt, or an unattended run without it), the installer carries on and the broker starts normally with `npm_scaffold` hidden. Only the owners' PATs are required.
80
138
 
81
139
  ## Enabling `link_issue` (GitHub App) — DEFERRED
82
140