@cldmv/gh-broker 0.0.0-stage → 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 (104) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +279 -2
  3. package/config.example.json +294 -0
  4. package/config.sample.json +45 -0
  5. package/deploy/README.md +150 -0
  6. package/deploy/gh-broker.service +57 -0
  7. package/deploy/install.mjs +409 -0
  8. package/deploy/install.sh +29 -0
  9. package/deploy/lib/config-merge.mjs +214 -0
  10. package/deploy/lib/credentials.mjs +143 -0
  11. package/deploy/lib/install-args.mjs +144 -0
  12. package/deploy/lib/npm-setup.mjs +164 -0
  13. package/deploy/lib/prompt.mjs +98 -0
  14. package/deploy/lib/secret-store.mjs +132 -0
  15. package/deploy/lib/smoke.mjs +137 -0
  16. package/deploy/lib/system.mjs +246 -0
  17. package/dist/index.mjs +24406 -0
  18. package/dist/index.mjs.map +7 -0
  19. package/docs/TOOLS.md +538 -0
  20. package/docs/agent-user-model.md +89 -0
  21. package/docs/changelog/v0/v0.2.0.md +26 -0
  22. package/docs/changelog/v0/v0.2.1.md +23 -0
  23. package/docs/changelog/v0/v0.2.2.md +23 -0
  24. package/docs/changelog/v0/v0.2.3.md +33 -0
  25. package/docs/changelog/v0/v0.2.4.md +25 -0
  26. package/docs/changelog/v0/v0.2.5.md +29 -0
  27. package/docs/changelog/v0/v0.3.0.md +84 -0
  28. package/docs/changelog/v0/v0.3.1.md +30 -0
  29. package/docs/changelog/v0/v0.3.2.md +24 -0
  30. package/docs/changelog/v0/v0.3.3.md +28 -0
  31. package/docs/changelog/v0/v0.4.0.md +58 -0
  32. package/docs/changelog/v0/v0.4.1.md +33 -0
  33. package/docs/changelog/v0/v0.4.2.md +45 -0
  34. package/docs/changelog/v0/v0.5.0.md +39 -0
  35. package/docs/changelog/v0/v0.5.1.md +28 -0
  36. package/docs/changelog/v0/v0.6.0.md +52 -0
  37. package/docs/changelog/v0/v0.6.1.md +48 -0
  38. package/docs/changelog/v0/v0.7.0.md +71 -0
  39. package/docs/changelog/v0/v0.8.0.md +115 -0
  40. package/docs/deploy-architecture.md +84 -0
  41. package/package.json +71 -5
  42. package/src/appauth.ts +56 -0
  43. package/src/audit.ts +34 -0
  44. package/src/availability.ts +42 -0
  45. package/src/catalog.ts +182 -0
  46. package/src/config.ts +157 -0
  47. package/src/index.ts +107 -0
  48. package/src/paths.ts +60 -0
  49. package/src/router.ts +299 -0
  50. package/src/secrets.ts +167 -0
  51. package/src/server.ts +91 -0
  52. package/src/types.ts +128 -0
  53. package/src/upstream.ts +85 -0
  54. package/src/verbs/ghcr/archive.ts +314 -0
  55. package/src/verbs/ghcr/config.ts +142 -0
  56. package/src/verbs/ghcr/jobs.ts +141 -0
  57. package/src/verbs/ghcr/push.ts +192 -0
  58. package/src/verbs/ghcr/registry.ts +275 -0
  59. package/src/verbs/ghcr/tar.ts +203 -0
  60. package/src/verbs/ghcr_push.ts +264 -0
  61. package/src/verbs/issue_labels.ts +329 -0
  62. package/src/verbs/link_issue.ts +84 -0
  63. package/src/verbs/npm-registry.ts +168 -0
  64. package/src/verbs/npm-tarball.ts +76 -0
  65. package/src/verbs/npm_scaffold.ts +325 -0
  66. package/src/verbs/release-analysis.ts +212 -0
  67. package/src/verbs/repo-tier.ts +90 -0
  68. package/src/verbs/report_ci_status.ts +358 -0
  69. package/src/verbs/report_release_prs.ts +238 -0
  70. package/src/verbs/report_security_alerts.ts +442 -0
  71. package/src/verbs/report_unlabeled.ts +194 -0
  72. package/src/verbs/rest-client.ts +181 -0
  73. package/src/verbs/review_thread.ts +138 -0
  74. package/tests/appauth.test.vitest.mts +76 -0
  75. package/tests/audit.test.vitest.mts +48 -0
  76. package/tests/catalog.test.vitest.mts +234 -0
  77. package/tests/config-merge.test.vitest.mts +325 -0
  78. package/tests/config.test.vitest.mts +388 -0
  79. package/tests/credentials.test.vitest.mts +127 -0
  80. package/tests/ghcr-archive.test.vitest.mts +439 -0
  81. package/tests/ghcr-config.test.vitest.mts +96 -0
  82. package/tests/ghcr-push.test.vitest.mts +526 -0
  83. package/tests/ghcr-registry.test.vitest.mts +206 -0
  84. package/tests/helpers/ghcr-fixtures.mts +373 -0
  85. package/tests/install-args.test.vitest.mts +152 -0
  86. package/tests/issue-labels.test.vitest.mts +362 -0
  87. package/tests/npm-scaffold.test.vitest.mts +479 -0
  88. package/tests/npm-setup.test.vitest.mts +196 -0
  89. package/tests/paths.test.vitest.mts +57 -0
  90. package/tests/prompt.test.vitest.mts +128 -0
  91. package/tests/release-prs.test.vitest.mts +247 -0
  92. package/tests/rest-client.test.vitest.mts +115 -0
  93. package/tests/router.test.vitest.mts +505 -0
  94. package/tests/run-vitest.mjs +51 -0
  95. package/tests/secret-store.test.vitest.mts +167 -0
  96. package/tests/secrets.test.vitest.mts +208 -0
  97. package/tests/security-alerts.test.vitest.mts +338 -0
  98. package/tests/server.test.vitest.mts +200 -0
  99. package/tests/smoke.test.vitest.mts +177 -0
  100. package/tests/system.test.vitest.mts +355 -0
  101. package/tests/upstream.test.vitest.mts +91 -0
  102. package/tests/verb-helpers.test.vitest.mts +377 -0
  103. package/tests/verbs.test.vitest.mts +624 -0
  104. package/tsconfig.json +21 -0
@@ -0,0 +1,294 @@
1
+ {
2
+ "listen": "unix:/run/gh-broker/gh-broker.sock",
3
+ "upstream": "https://api.githubcopilot.com/mcp/x/all",
4
+ "tools": [
5
+ {
6
+ "name": "list_issues",
7
+ "mode": "read"
8
+ },
9
+ {
10
+ "name": "issue_read",
11
+ "mode": "read"
12
+ },
13
+ {
14
+ "name": "search_issues",
15
+ "mode": "read"
16
+ },
17
+ {
18
+ "name": "list_issue_types",
19
+ "mode": "read"
20
+ },
21
+ {
22
+ "name": "list_issue_fields",
23
+ "mode": "read"
24
+ },
25
+ {
26
+ "name": "list_label",
27
+ "mode": "read"
28
+ },
29
+ {
30
+ "name": "list_pull_requests",
31
+ "mode": "read"
32
+ },
33
+ {
34
+ "name": "pull_request_read",
35
+ "mode": "read",
36
+ "augment": true
37
+ },
38
+ {
39
+ "name": "search_pull_requests",
40
+ "mode": "read"
41
+ },
42
+ {
43
+ "name": "search_repositories",
44
+ "mode": "read"
45
+ },
46
+ {
47
+ "name": "search_code",
48
+ "mode": "read"
49
+ },
50
+ {
51
+ "name": "actions_list",
52
+ "mode": "read"
53
+ },
54
+ {
55
+ "name": "actions_get",
56
+ "mode": "read"
57
+ },
58
+ {
59
+ "name": "get_job_logs",
60
+ "mode": "read"
61
+ },
62
+ {
63
+ "name": "list_code_scanning_alerts",
64
+ "mode": "read"
65
+ },
66
+ {
67
+ "name": "get_code_scanning_alert",
68
+ "mode": "read"
69
+ },
70
+ {
71
+ "name": "list_dependabot_alerts",
72
+ "mode": "read"
73
+ },
74
+ {
75
+ "name": "get_dependabot_alert",
76
+ "mode": "read"
77
+ },
78
+ {
79
+ "name": "list_secret_scanning_alerts",
80
+ "mode": "read"
81
+ },
82
+ {
83
+ "name": "get_secret_scanning_alert",
84
+ "mode": "read"
85
+ },
86
+ {
87
+ "name": "get_file_contents",
88
+ "mode": "read"
89
+ },
90
+ {
91
+ "name": "get_commit",
92
+ "mode": "read"
93
+ },
94
+ {
95
+ "name": "list_commits",
96
+ "mode": "read"
97
+ },
98
+ {
99
+ "name": "list_branches",
100
+ "mode": "read"
101
+ },
102
+ {
103
+ "name": "list_tags",
104
+ "mode": "read"
105
+ },
106
+ {
107
+ "name": "get_repository_tree",
108
+ "mode": "read"
109
+ },
110
+ {
111
+ "name": "list_releases",
112
+ "mode": "read"
113
+ },
114
+ {
115
+ "name": "get_latest_release",
116
+ "mode": "read"
117
+ },
118
+ {
119
+ "name": "check_dependency_vulnerabilities",
120
+ "mode": "read"
121
+ },
122
+ {
123
+ "name": "get_code_quality_finding",
124
+ "mode": "read"
125
+ },
126
+ {
127
+ "name": "get_discussion",
128
+ "mode": "read"
129
+ },
130
+ {
131
+ "name": "get_discussion_comments",
132
+ "mode": "read"
133
+ },
134
+ {
135
+ "name": "get_global_security_advisory",
136
+ "mode": "read"
137
+ },
138
+ {
139
+ "name": "get_label",
140
+ "mode": "read"
141
+ },
142
+ {
143
+ "name": "get_me",
144
+ "mode": "read"
145
+ },
146
+ {
147
+ "name": "get_notification_details",
148
+ "mode": "read"
149
+ },
150
+ {
151
+ "name": "get_release_by_tag",
152
+ "mode": "read"
153
+ },
154
+ {
155
+ "name": "get_tag",
156
+ "mode": "read"
157
+ },
158
+ {
159
+ "name": "get_team_members",
160
+ "mode": "read"
161
+ },
162
+ {
163
+ "name": "get_teams",
164
+ "mode": "read"
165
+ },
166
+ {
167
+ "name": "list_discussion_categories",
168
+ "mode": "read"
169
+ },
170
+ {
171
+ "name": "list_discussions",
172
+ "mode": "read"
173
+ },
174
+ {
175
+ "name": "list_global_security_advisories",
176
+ "mode": "read"
177
+ },
178
+ {
179
+ "name": "list_notifications",
180
+ "mode": "read"
181
+ },
182
+ {
183
+ "name": "list_org_repository_security_advisories",
184
+ "mode": "read"
185
+ },
186
+ {
187
+ "name": "list_repository_collaborators",
188
+ "mode": "read"
189
+ },
190
+ {
191
+ "name": "list_repository_security_advisories",
192
+ "mode": "read"
193
+ },
194
+ {
195
+ "name": "projects_get",
196
+ "mode": "read"
197
+ },
198
+ {
199
+ "name": "projects_list",
200
+ "mode": "read"
201
+ },
202
+ {
203
+ "name": "search_commits",
204
+ "mode": "read"
205
+ },
206
+ {
207
+ "name": "search_orgs",
208
+ "mode": "read"
209
+ },
210
+ {
211
+ "name": "search_users",
212
+ "mode": "read"
213
+ },
214
+ {
215
+ "name": "semantic_issue_similarity_search",
216
+ "mode": "read"
217
+ },
218
+ {
219
+ "name": "issue_write",
220
+ "mode": "write"
221
+ },
222
+ {
223
+ "name": "add_issue_comment",
224
+ "mode": "write"
225
+ },
226
+ {
227
+ "name": "sub_issue_write",
228
+ "mode": "write"
229
+ },
230
+ {
231
+ "name": "create_pull_request",
232
+ "mode": "write"
233
+ },
234
+ {
235
+ "name": "update_pull_request",
236
+ "mode": "write"
237
+ },
238
+ {
239
+ "name": "issue_labels",
240
+ "mode": "write",
241
+ "custom": true
242
+ },
243
+ {
244
+ "name": "npm_scaffold",
245
+ "mode": "write",
246
+ "custom": true
247
+ },
248
+ {
249
+ "name": "resolve_review_thread",
250
+ "mode": "write",
251
+ "custom": true
252
+ },
253
+ {
254
+ "name": "reply_review_thread",
255
+ "mode": "write",
256
+ "custom": true
257
+ },
258
+ {
259
+ "name": "report_unlabeled",
260
+ "mode": "read",
261
+ "custom": true
262
+ },
263
+ {
264
+ "name": "report_security_alerts",
265
+ "mode": "read",
266
+ "custom": true
267
+ },
268
+ {
269
+ "name": "report_release_prs",
270
+ "mode": "read",
271
+ "custom": true
272
+ },
273
+ {
274
+ "name": "report_ci_status",
275
+ "mode": "read",
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
292
+ }
293
+ ]
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
+ }
@@ -0,0 +1,150 @@
1
+ # Deploying gh-broker
2
+
3
+ One interactive installer does first-install and upgrade. Design and rationale: [`../docs/deploy-architecture.md`](../docs/deploy-architecture.md). The broker runs as a dedicated `gh-broker` service user, separate from the unprivileged user the agent runs as — that two-user split is the security boundary; see [`../docs/agent-user-model.md`](../docs/agent-user-model.md).
4
+
5
+ ## 0. Host prerequisites
6
+
7
+ - **Node ≥ 20** — the only real prerequisite; the installer runs under it (`sudo node deploy/install.mjs`). If node isn't installed yet, the optional Linux bootstrap `sudo ./deploy/install.sh` sets up a system-wide nvm + node + the `/usr/local/bin/node` symlink, then hands off to the Node installer.
8
+ - **`socat`** — the agent's stdio↔socket bridge. **`rsync`** — used to sync the build (falls back to a plain copy if absent). **`systemd`** with `systemctl`. **`sudo`** — used internally to build as the checkout's own owner (see step 5 below), not just to invoke the installer itself. Install what's missing (`apt install socat rsync sudo` etc.).
9
+
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
+
12
+ ## 1. Mint the two fine-grained PATs
13
+
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
+
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
+
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
+
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
+
27
+ ## 2. Install (first install AND upgrade — same command)
28
+
29
+ ```sh
30
+ sudo node deploy/install.mjs [agent-user]
31
+ ```
32
+
33
+ `agent-user` defaults to `$SUDO_USER` (the account you ran `sudo` from). The installer walks the whole thing and is idempotent:
34
+
35
+ 1. checks prerequisites,
36
+ 2. **creates or confirms the agent user** (stops if it can't — no silent half-install),
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),
38
+ 4. points `/usr/local/bin/node` at the running node,
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,
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) → ✅/❌.
46
+
47
+ ### Unattended (single non-interactive command)
48
+
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:
50
+
51
+ ```sh
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 \
54
+ --secrets-provider systemd-creds --non-interactive
55
+ ```
56
+
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.
58
+
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).
60
+
61
+ ## Credential storage — your choice
62
+
63
+ The installer asks which backend holds the tokens at rest:
64
+
65
+ - **`systemd-creds`** (recommended, Linux): tokens are encrypted with a host/TPM-bound key (`.cred` files delivered via a unit drop-in into memory-only `$CREDENTIALS_DIRECTORY`). Encrypted at rest and resistant to backup/disk theft; not portable to another machine.
66
+ - **`file`**: tokens in a plaintext JSON file, `0640 root:gh-broker`. Portable and platform-neutral; at-rest protection is filesystem permissions only.
67
+
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).
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
+
103
+ ## 3. Point Claude at the broker
104
+
105
+ The installer prints this; add it to the agent user's `~/.claude.json` `mcpServers`:
106
+
107
+ ```json
108
+ "github": {
109
+ "type": "stdio",
110
+ "command": "socat",
111
+ "args": ["STDIO", "UNIX-CONNECT:/run/gh-broker/gh-broker.sock"]
112
+ }
113
+ ```
114
+
115
+ Re-login as the agent user first, so its `gh-broker-clients` membership is active.
116
+
117
+ ## Adding an org later
118
+
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.
138
+
139
+ ## Enabling `link_issue` (GitHub App) — DEFERRED
140
+
141
+ > **Status: deferred / not wired in.** `link_issue` is implemented in `src/verbs/link_issue.ts` but its catalog (advertise) and router (dispatch) hooks are **commented out**, and it has no entry in `config.example.json`'s allowlist — so it is neither advertised nor callable. To turn it on: uncomment the `link_issue` blocks in `src/catalog.ts` and `src/router.ts` (and their imports), add `{ "name": "link_issue", "mode": "write", "custom": true }` to the tools allowlist, and configure a GitHub App.
142
+
143
+ `link_issue` creates an explicit PR→issue closing reference via `addClosingIssueReference` — a mutation **not available to fine-grained PATs** (only App/classic tokens). It runs on a **GitHub App** installation token (`@octokit/auth-app`, from `config.app` = `{ appId, privateKey: "cred:gh-app-key" }` plus the owner's `installationId`). Store the App key like any other credential — as `gh-app-key` in the secrets file.
144
+
145
+ ## Verify no secret leaks
146
+
147
+ - The agent user cannot read `/etc/gh-broker/` — the dir is `0750 root:gh-broker`; the config (`0640`) and, whichever backend is used, the tokens (systemd-creds `.cred` files `0400 root`, or the `file` backend's `secrets.json` `0640 root:gh-broker`) are unreadable to a non-member.
148
+ - The config file holds **no tokens** — only `cred:` references; the tokens live in the separate secret store.
149
+ - The socket carries MCP frames only.
150
+ - `journalctl -u gh-broker` shows the audit log (tool, owner, mode, repo, result) — never tokens.
@@ -0,0 +1,57 @@
1
+ [Unit]
2
+ Description=CLDMV gh-broker — GitHub MCP credential-routing proxy
3
+ Documentation=https://github.com/CLDMV/gh-broker
4
+ After=network-online.target
5
+ Wants=network-online.target
6
+
7
+ [Service]
8
+ Type=simple
9
+ User=gh-broker
10
+ Group=gh-broker
11
+
12
+ # Secrets: the daemon resolves the config's cred: refs via the provider chosen at
13
+ # install — systemd-creds (a unit drop-in adds LoadCredentialEncrypted, delivering
14
+ # creds into $CREDENTIALS_DIRECTORY) or an agent-unreadable file (0640 root:gh-broker).
15
+ # See secrets.ts / docs/deploy-architecture.md.
16
+
17
+ # Socket dir /run/gh-broker (traversable), owned by the service user; the daemon
18
+ # creates the socket there and sets it 0660 group=gh-broker-clients so the agent
19
+ # user (a member of that group) can connect, while the token stays server-side.
20
+ RuntimeDirectory=gh-broker
21
+ RuntimeDirectoryMode=0755
22
+
23
+ # Node comes from the maintained /usr/local/bin/node symlink (the installer points
24
+ # it at the chosen node and re-points it on a bump), so no node version is pinned
25
+ # here and there is no nvm resolution at start. The symlink is the single source of
26
+ # truth, so CLI node == service node by construction. See docs/deploy-architecture.md.
27
+ ExecStart=/usr/local/bin/node /opt/gh-broker/dist/index.mjs --config /etc/gh-broker/config.json
28
+ Restart=on-failure
29
+ RestartSec=2
30
+
31
+ # ---- hardening (least privilege for a credential holder) ----
32
+ NoNewPrivileges=true
33
+ ProtectSystem=strict
34
+ ProtectHome=true
35
+ PrivateTmp=true
36
+ PrivateDevices=true
37
+ ProtectKernelTunables=true
38
+ ProtectKernelModules=true
39
+ ProtectControlGroups=true
40
+ RestrictNamespaces=true
41
+ RestrictSUIDSGID=true
42
+ RestrictRealtime=true
43
+ LockPersonality=true
44
+ # NOTE: MemoryDenyWriteExecute is intentionally OMITTED — V8's JIT requires
45
+ # writable→executable memory, and enabling it crashes Node at startup with
46
+ # "Fatal OOM ... during deserialization".
47
+ SystemCallArchitectures=native
48
+ SystemCallFilter=@system-service
49
+ SystemCallErrorNumber=EPERM
50
+ ReadWritePaths=/run/gh-broker
51
+ UMask=0077
52
+ # Optional egress lockdown once GitHub ranges are pinned:
53
+ # IPAddressDeny=any
54
+ # IPAddressAllow=<github api ranges>
55
+
56
+ [Install]
57
+ WantedBy=multi-user.target