@cldmv/gh-broker 0.8.0 → 0.10.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.
- package/README.md +48 -13
- package/config.example.json +86 -3
- package/config.sample.json +1 -1
- package/deploy/README.md +58 -18
- package/deploy/cli.mjs +38 -0
- package/deploy/gh-broker.service +9 -2
- package/deploy/install.mjs +100 -41
- package/deploy/lib/agent-cli.mjs +166 -0
- package/deploy/lib/agents.mjs +128 -0
- package/deploy/lib/cli.mjs +72 -0
- package/deploy/lib/config-merge.mjs +29 -8
- package/deploy/lib/install-args.mjs +3 -2
- package/deploy/lib/mcp-config.mjs +211 -0
- package/deploy/lib/otp-cli.mjs +136 -0
- package/deploy/lib/otp.mjs +325 -0
- package/deploy/lib/secret-store.mjs +1 -0
- package/deploy/lib/system.mjs +24 -12
- package/deploy/real-deps.mjs +102 -0
- package/dist/index.mjs +5171 -1686
- package/dist/index.mjs.map +4 -4
- package/package.json +14 -3
- package/docs/TOOLS.md +0 -538
- package/docs/agent-user-model.md +0 -89
- package/docs/changelog/v0/v0.2.0.md +0 -26
- package/docs/changelog/v0/v0.2.1.md +0 -23
- package/docs/changelog/v0/v0.2.2.md +0 -23
- package/docs/changelog/v0/v0.2.3.md +0 -33
- package/docs/changelog/v0/v0.2.4.md +0 -25
- package/docs/changelog/v0/v0.2.5.md +0 -29
- package/docs/changelog/v0/v0.3.0.md +0 -84
- package/docs/changelog/v0/v0.3.1.md +0 -30
- package/docs/changelog/v0/v0.3.2.md +0 -24
- package/docs/changelog/v0/v0.3.3.md +0 -28
- package/docs/changelog/v0/v0.4.0.md +0 -58
- package/docs/changelog/v0/v0.4.1.md +0 -33
- package/docs/changelog/v0/v0.4.2.md +0 -45
- package/docs/changelog/v0/v0.5.0.md +0 -39
- package/docs/changelog/v0/v0.5.1.md +0 -28
- package/docs/changelog/v0/v0.6.0.md +0 -52
- package/docs/changelog/v0/v0.6.1.md +0 -48
- package/docs/changelog/v0/v0.7.0.md +0 -71
- package/docs/changelog/v0/v0.8.0.md +0 -115
- package/docs/deploy-architecture.md +0 -84
- package/src/appauth.ts +0 -56
- package/src/audit.ts +0 -34
- package/src/availability.ts +0 -42
- package/src/catalog.ts +0 -182
- package/src/config.ts +0 -157
- package/src/index.ts +0 -107
- package/src/paths.ts +0 -60
- package/src/router.ts +0 -299
- package/src/secrets.ts +0 -167
- package/src/server.ts +0 -91
- package/src/types.ts +0 -128
- package/src/upstream.ts +0 -85
- package/src/verbs/ghcr/archive.ts +0 -314
- package/src/verbs/ghcr/config.ts +0 -142
- package/src/verbs/ghcr/jobs.ts +0 -141
- package/src/verbs/ghcr/push.ts +0 -192
- package/src/verbs/ghcr/registry.ts +0 -275
- package/src/verbs/ghcr/tar.ts +0 -203
- package/src/verbs/ghcr_push.ts +0 -264
- package/src/verbs/issue_labels.ts +0 -329
- package/src/verbs/link_issue.ts +0 -84
- package/src/verbs/npm-registry.ts +0 -168
- package/src/verbs/npm-tarball.ts +0 -76
- package/src/verbs/npm_scaffold.ts +0 -325
- package/src/verbs/release-analysis.ts +0 -212
- package/src/verbs/repo-tier.ts +0 -90
- package/src/verbs/report_ci_status.ts +0 -358
- package/src/verbs/report_release_prs.ts +0 -238
- package/src/verbs/report_security_alerts.ts +0 -442
- package/src/verbs/report_unlabeled.ts +0 -194
- package/src/verbs/rest-client.ts +0 -181
- package/src/verbs/review_thread.ts +0 -138
- package/tests/appauth.test.vitest.mts +0 -76
- package/tests/audit.test.vitest.mts +0 -48
- package/tests/catalog.test.vitest.mts +0 -234
- package/tests/config-merge.test.vitest.mts +0 -325
- package/tests/config.test.vitest.mts +0 -388
- package/tests/credentials.test.vitest.mts +0 -127
- package/tests/ghcr-archive.test.vitest.mts +0 -439
- package/tests/ghcr-config.test.vitest.mts +0 -96
- package/tests/ghcr-push.test.vitest.mts +0 -526
- package/tests/ghcr-registry.test.vitest.mts +0 -206
- package/tests/helpers/ghcr-fixtures.mts +0 -373
- package/tests/install-args.test.vitest.mts +0 -152
- package/tests/issue-labels.test.vitest.mts +0 -362
- package/tests/npm-scaffold.test.vitest.mts +0 -479
- package/tests/npm-setup.test.vitest.mts +0 -196
- package/tests/paths.test.vitest.mts +0 -57
- package/tests/prompt.test.vitest.mts +0 -128
- package/tests/release-prs.test.vitest.mts +0 -247
- package/tests/rest-client.test.vitest.mts +0 -115
- package/tests/router.test.vitest.mts +0 -505
- package/tests/run-vitest.mjs +0 -51
- package/tests/secret-store.test.vitest.mts +0 -167
- package/tests/secrets.test.vitest.mts +0 -208
- package/tests/security-alerts.test.vitest.mts +0 -338
- package/tests/server.test.vitest.mts +0 -200
- package/tests/smoke.test.vitest.mts +0 -177
- package/tests/system.test.vitest.mts +0 -355
- package/tests/upstream.test.vitest.mts +0 -91
- package/tests/verb-helpers.test.vitest.mts +0 -377
- package/tests/verbs.test.vitest.mts +0 -624
- package/tsconfig.json +0 -21
package/README.md
CHANGED
|
@@ -19,19 +19,19 @@ It runs as a dedicated `gh-broker` system user, holds a read-scoped and a write-
|
|
|
19
19
|
|
|
20
20
|
## ✨ What's New
|
|
21
21
|
|
|
22
|
-
### Latest: v0.
|
|
22
|
+
### Latest: v0.10.0 (November 2026)
|
|
23
23
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- **
|
|
27
|
-
- [View full v0.
|
|
24
|
+
- **OTP gate for risky write tools.** Gated tools need a current code from an authenticator app, so the agent has to ask you. The first good code opens a five-minute window for that agent, and bad codes lock it out, then hard-lock it until `sudo gh-broker otp unlock <user>`.
|
|
25
|
+
- **One socket per agent and the `gh-broker` command.** Each agent user gets its own socket, and `gh-broker agent add|remove|list|sync` writes its MCP config. `gh-broker otp status|list|unlock|rotate` manages the gate, and `gh-broker install` runs the installer.
|
|
26
|
+
- **Eleven new write tools and a deny list.** These include merging, branch updates, repo create and fork, `delete_branch`, `dismiss_alert` and `minimize_comment`, with the risky ones gated by default and the Copilot coding-agent tools disabled. A built-in deny list refuses file writes, repo deletion and repo settings at config load. Breaking: the socket path, the `bin` and `npm_scaffold`'s `npm_otp`; see the upgrade notes.
|
|
27
|
+
- [View full v0.10.0 Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.10.0.md)
|
|
28
28
|
|
|
29
29
|
### Recent Releases
|
|
30
30
|
|
|
31
|
+
- **v0.9.0** (November 2026) — `report_alert_status`, `report_release_changes` and `report_dependents` for the release and fleet skills, `ciVerbosity` on `list_pull_requests`, and install from npm ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.9.0.md))
|
|
32
|
+
- **v0.8.0** (November 2026) — `npm_scaffold` creates an npm package and its trusted publisher, `ghcr_push` pushes images without an agent-held token, and optional credentials hide their tools ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.8.0.md))
|
|
31
33
|
- **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
34
|
- **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
35
|
|
|
36
36
|
📚 For the complete version history, see the [docs/changelog/](https://github.com/CLDMV/gh-broker/tree/master/docs/changelog/) folder.
|
|
37
37
|
|
|
@@ -51,7 +51,7 @@ A tool not listed in `config.tools` is neither advertised nor executable. Adding
|
|
|
51
51
|
|
|
52
52
|
### 📊 **CI Visibility Without Per-Call Log Fetches**
|
|
53
53
|
|
|
54
|
-
`report_ci_status` answers "does this PR have failing checks" at whatever detail level you ask for — counts only by default, escalating to which job failed, which step failed, and a tail of the failing job's actual log — paying for that detail only on runs that are genuinely failing. The same status can ride inline on a normal `pull_request_read` via an opt-in `ciVerbosity` parameter.
|
|
54
|
+
`report_ci_status` answers "does this PR have failing checks" at whatever detail level you ask for — counts only by default, escalating to which job failed, which step failed, and a tail of the failing job's actual log — paying for that detail only on runs that are genuinely failing. The same status can ride inline on a normal `pull_request_read` or `list_pull_requests` via an opt-in `ciVerbosity` parameter.
|
|
55
55
|
|
|
56
56
|
```json
|
|
57
57
|
{ "owner": "cldmv", "repo": "droidsock", "pull": 35, "verbosity": 2 }
|
|
@@ -91,12 +91,30 @@ npm only accepts a trusted publisher on a package that already exists, so every
|
|
|
91
91
|
|
|
92
92
|
🛡️ **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_security_alerts)**
|
|
93
93
|
|
|
94
|
+
### 🩹 **Where Each Alert's Fix Stands**
|
|
95
|
+
|
|
96
|
+
`report_alert_status` puts every open Dependabot alert in one bucket — `released-still-open`, `fixed-on-next`, `open-pr`, `closed-unmerged` or `no-pr` — by reading the lockfile nearest the alert's manifest on the default branch, `next` and `hotfixes`, and the repo's PR history. An alert that reads open because its fix is waiting on `next` stops looking like work to do.
|
|
97
|
+
|
|
98
|
+
🩹 **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_alert_status)**
|
|
99
|
+
|
|
94
100
|
### 🚢 **Release PR Discovery**
|
|
95
101
|
|
|
96
102
|
`report_release_prs` finds every open release PR across an owner's repos and says which ones still lack their per-version changelog file — the list an agent needs during a mass update.
|
|
97
103
|
|
|
98
104
|
🚢 **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_release_prs)**
|
|
99
105
|
|
|
106
|
+
### 📝 **Changelog Source Material**
|
|
107
|
+
|
|
108
|
+
`report_release_changes` returns the PRs one release actually ships — already-released and reverted PRs dropped — grouped by changelog section at the detail level you ask for, with the release's `package.json` and dependency deltas and an optional skeleton in the per-version template.
|
|
109
|
+
|
|
110
|
+
📝 **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_release_changes)**
|
|
111
|
+
|
|
112
|
+
### 📦 **Package Dependents**
|
|
113
|
+
|
|
114
|
+
`report_dependents` finds every repo an owner has that declares an npm package, with the declared range, the lockfile-resolved version and the repo's flow tier — where a fleet-wide version bump starts. It reads `dev` branches too, so dev-trunk repos are included, and skips a `dev` whose version has fallen behind the default branch.
|
|
115
|
+
|
|
116
|
+
📦 **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_dependents)**
|
|
117
|
+
|
|
100
118
|
### 🐳 **Container Image Pushes Without a Registry Token**
|
|
101
119
|
|
|
102
120
|
`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.
|
|
@@ -115,7 +133,7 @@ The broker presents its **own** MCP contract to the agent — a proxied tool's s
|
|
|
115
133
|
|
|
116
134
|
### 📦 **One-Command Install & Upgrade**
|
|
117
135
|
|
|
118
|
-
`sudo node deploy/install.mjs` handles first install and every upgrade: builds under the checkout's own owner (no `chown`, ever), merges shipped config defaults over the deployed config, lets you choose how PATs are stored at rest, and runs a post-deploy smoke test.
|
|
136
|
+
`sudo gh-broker install` (or `sudo node deploy/install.mjs` from a checkout) handles first install and every upgrade: builds under the checkout's own owner (no `chown`, ever), merges shipped config defaults over the deployed config, lets you choose how PATs are stored at rest, and runs a post-deploy smoke test.
|
|
119
137
|
|
|
120
138
|
📦 **Full prerequisites, PAT scopes, and flags: [deploy/README.md](deploy/README.md)**
|
|
121
139
|
|
|
@@ -132,7 +150,16 @@ The broker presents its **own** MCP contract to the agent — a proxied tool's s
|
|
|
132
150
|
|
|
133
151
|
### Install
|
|
134
152
|
|
|
135
|
-
gh-broker is
|
|
153
|
+
gh-broker is a system service, not a library. The installer handles both the first install and every upgrade, and you can run it from npm or from a checkout.
|
|
154
|
+
|
|
155
|
+
From npm ([`@cldmv/gh-broker`](https://www.npmjs.com/package/@cldmv/gh-broker)), with the prebuilt bundle, as a global install (upgrade with the same command, then `gh-broker install` again):
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
sudo npm install -g --prefix /usr/local --ignore-scripts @cldmv/gh-broker
|
|
159
|
+
sudo gh-broker install <agent-user>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
From a checkout:
|
|
136
163
|
|
|
137
164
|
```sh
|
|
138
165
|
git clone https://github.com/CLDMV/gh-broker.git
|
|
@@ -140,13 +167,13 @@ cd gh-broker
|
|
|
140
167
|
sudo node deploy/install.mjs <agent-user>
|
|
141
168
|
```
|
|
142
169
|
|
|
143
|
-
The installer creates the service user
|
|
170
|
+
Both take the same flags. The installer creates the service user, registers each agent user (its own group, socket and MCP config; see `gh-broker agent`), builds as the checkout's owner when there's no bundle yet, 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).
|
|
144
171
|
|
|
145
172
|
---
|
|
146
173
|
|
|
147
174
|
## 🚀 Quick Start
|
|
148
175
|
|
|
149
|
-
|
|
176
|
+
The installer writes each agent's MCP config for its own socket ([deploy/README.md § 3](deploy/README.md#3-agents-one-socket-and-one-mcp-config-each)). 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:
|
|
150
177
|
|
|
151
178
|
```json
|
|
152
179
|
// tools/call
|
|
@@ -175,10 +202,15 @@ src/
|
|
|
175
202
|
catalog.ts build the advertised tool list (real upstream schemas + custom + augmented)
|
|
176
203
|
server.ts Unix-socket MCP server toward the agent (StdioServerTransport)
|
|
177
204
|
audit.ts structured audit log (never tokens)
|
|
205
|
+
denylist.ts tools refused at config load, gated or not (file writes, repo deletion, repo configuration)
|
|
178
206
|
verbs/
|
|
179
207
|
report_unlabeled.ts issues/PRs with no labels, one repo or org-wide
|
|
180
208
|
report_security_alerts.ts open Dependabot/CodeQL/secret alerts, one repo or org-wide
|
|
181
209
|
report_release_prs.ts open release PRs + whether each has its changelog file
|
|
210
|
+
report_dependents.ts repos that declare an npm package, with range, resolved version and flow tier
|
|
211
|
+
report_alert_status.ts where each open Dependabot alert's fix stands (lockfiles + PRs)
|
|
212
|
+
report_release_changes.ts the PRs a release ships, grouped, with net package/dependency deltas
|
|
213
|
+
lockfile.ts npm / pnpm / yarn lockfile readers shared by the report verbs
|
|
182
214
|
report_ci_status.ts tiered CI status for a PR or every open PR in a repo
|
|
183
215
|
rest-client.ts shared rate-limit-floor + call-budget REST helpers
|
|
184
216
|
issue_labels.ts add/remove/swap/set labels on an issue or PR (write PAT)
|
|
@@ -186,6 +218,9 @@ src/
|
|
|
186
218
|
npm-registry.ts the npm registry requests npm_scaffold makes
|
|
187
219
|
npm-tarball.ts minimal npm tarball builder for the placeholder
|
|
188
220
|
review_thread.ts resolve/reply PR review threads (write PAT)
|
|
221
|
+
minimize_comment.ts hide or show a comment (write PAT)
|
|
222
|
+
delete_branch.ts delete a branch, refusing flow branches and open-PR heads/bases (write PAT, gated)
|
|
223
|
+
dismiss_alert.ts dismiss or reopen a code scanning, Dependabot or secret scanning alert (write PAT, gated)
|
|
189
224
|
ghcr_push.ts ghcr_push / ghcr_push_status / ghcr_tags (GHCR classic PAT)
|
|
190
225
|
ghcr/ config policy, tar + image-archive reader, OCI registry client, push, job queue
|
|
191
226
|
link_issue.ts addClosingIssueReference (App token) — DEFERRED, not wired in
|
|
@@ -208,7 +243,7 @@ config.sample.json a full config with placeholder values, for reference only
|
|
|
208
243
|
|
|
209
244
|
## 📚 Documentation
|
|
210
245
|
|
|
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`
|
|
246
|
+
- **[Tools Reference](docs/TOOLS.md)** — every custom and augmented tool: `report_unlabeled`, `report_security_alerts`, `report_alert_status`, `report_release_prs`, `report_release_changes`, `report_dependents`, `report_ci_status`, the `pull_request_read` and `list_pull_requests` `ciVerbosity` extensions, `issue_labels`, `npm_scaffold`, `resolve_review_thread`/`reply_review_thread`, `minimize_comment`, `delete_branch`, `dismiss_alert`, `ghcr_push`/`ghcr_push_status`/`ghcr_tags`, `otp_status`, the OTP gate and the deny list, and how to add a new one
|
|
212
247
|
- **[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
|
|
213
248
|
- **[Deploy Architecture](docs/deploy-architecture.md)** — config merge, secret store backends, node handling, the installer's full flow
|
|
214
249
|
- **[Install Guide](deploy/README.md)** — prerequisites, PAT scopes, install/upgrade command, flags
|
package/config.example.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"listen": "
|
|
2
|
+
"listen": "agents:/run/gh-broker/agents",
|
|
3
3
|
"upstream": "https://api.githubcopilot.com/mcp/x/all",
|
|
4
4
|
"tools": [
|
|
5
5
|
{
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"name": "list_pull_requests",
|
|
31
|
-
"mode": "read"
|
|
31
|
+
"mode": "read",
|
|
32
|
+
"augment": true
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
35
|
"name": "pull_request_read",
|
|
@@ -255,6 +256,65 @@
|
|
|
255
256
|
"mode": "write",
|
|
256
257
|
"custom": true
|
|
257
258
|
},
|
|
259
|
+
{
|
|
260
|
+
"name": "update_pull_request_branch",
|
|
261
|
+
"mode": "write"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "request_copilot_review",
|
|
265
|
+
"mode": "write"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "minimize_comment",
|
|
269
|
+
"mode": "write",
|
|
270
|
+
"custom": true
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "create_repository",
|
|
274
|
+
"mode": "write"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "fork_repository",
|
|
278
|
+
"mode": "write"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "merge_pull_request",
|
|
282
|
+
"mode": "write"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"name": "pull_request_review_write",
|
|
286
|
+
"mode": "write"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "actions_run_trigger",
|
|
290
|
+
"mode": "write"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "label_write",
|
|
294
|
+
"mode": "write"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"name": "delete_branch",
|
|
298
|
+
"mode": "write",
|
|
299
|
+
"custom": true
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"name": "dismiss_alert",
|
|
303
|
+
"mode": "write",
|
|
304
|
+
"custom": true
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"name": "create_pull_request_with_copilot",
|
|
308
|
+
"mode": "write"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "assign_copilot_to_issue",
|
|
312
|
+
"mode": "write"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "assign_copilot_to_issue_with_intent",
|
|
316
|
+
"mode": "write"
|
|
317
|
+
},
|
|
258
318
|
{
|
|
259
319
|
"name": "report_unlabeled",
|
|
260
320
|
"mode": "read",
|
|
@@ -265,11 +325,26 @@
|
|
|
265
325
|
"mode": "read",
|
|
266
326
|
"custom": true
|
|
267
327
|
},
|
|
328
|
+
{
|
|
329
|
+
"name": "report_dependents",
|
|
330
|
+
"mode": "read",
|
|
331
|
+
"custom": true
|
|
332
|
+
},
|
|
268
333
|
{
|
|
269
334
|
"name": "report_release_prs",
|
|
270
335
|
"mode": "read",
|
|
271
336
|
"custom": true
|
|
272
337
|
},
|
|
338
|
+
{
|
|
339
|
+
"name": "report_release_changes",
|
|
340
|
+
"mode": "read",
|
|
341
|
+
"custom": true
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"name": "report_alert_status",
|
|
345
|
+
"mode": "read",
|
|
346
|
+
"custom": true
|
|
347
|
+
},
|
|
273
348
|
{
|
|
274
349
|
"name": "report_ci_status",
|
|
275
350
|
"mode": "read",
|
|
@@ -289,6 +364,14 @@
|
|
|
289
364
|
"name": "ghcr_tags",
|
|
290
365
|
"mode": "read",
|
|
291
366
|
"custom": true
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"name": "otp_status",
|
|
370
|
+
"mode": "read",
|
|
371
|
+
"custom": true
|
|
292
372
|
}
|
|
293
|
-
]
|
|
373
|
+
],
|
|
374
|
+
"otp": {
|
|
375
|
+
"seed": "cred:otp-seed"
|
|
376
|
+
}
|
|
294
377
|
}
|
package/config.sample.json
CHANGED
package/deploy/README.md
CHANGED
|
@@ -4,7 +4,7 @@ One interactive installer does first-install and upgrade. Design and rationale:
|
|
|
4
4
|
|
|
5
5
|
## 0. Host prerequisites
|
|
6
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.
|
|
7
|
+
- **Node ≥ 20** — the only real prerequisite; the installer runs under it (`sudo gh-broker install`, or `sudo node deploy/install.mjs` from a checkout). 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
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
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.
|
|
@@ -14,9 +14,9 @@ The installer maintains `/usr/local/bin/node` as the single node indirection (CL
|
|
|
14
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
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).
|
|
17
|
+
**`<org>-write`** — Issues: R/W, Pull requests: R/W, Contents: R/W, Metadata: Read (auto). For the tools added in #146, also: Administration: R/W (`create_repository`, `fork_repository`), Actions: R/W (`actions_run_trigger`), Workflows: R/W (`merge_pull_request` and `update_pull_request_branch` on a PR that changes `.github/workflows/`), Code scanning alerts: R/W, Dependabot alerts: R/W and Secret scanning alerts: R/W (`dismiss_alert`). The Copilot coding-agent tools ship disabled and need nothing extra beyond Issues and Pull requests: R/W, plus Copilot coding agent on the account. The installer ships every tool on each run, so a scope left off doesn't hide its tools: the calls are advertised and GitHub refuses them. Administration: R/W is broad: on its own it also lets the token delete repos and change their settings. The broker never offers those, but the token itself can do them, so treat the write PAT accordingly.
|
|
18
18
|
|
|
19
|
-
No organization
|
|
19
|
+
No organization 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
21
|
### GHCR pushes: one classic PAT (`<org>-packages`)
|
|
22
22
|
|
|
@@ -27,22 +27,25 @@ The installer also creates the drop directory from `ghcr.dropDir` (default `/srv
|
|
|
27
27
|
## 2. Install (first install AND upgrade — same command)
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
|
-
sudo
|
|
30
|
+
sudo npm install -g --prefix /usr/local --ignore-scripts @cldmv/gh-broker # install, or upgrade with @latest
|
|
31
|
+
sudo gh-broker install [agent-user]
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
`
|
|
34
|
+
`--prefix /usr/local` puts the package in `/usr/local/lib/node_modules/@cldmv/gh-broker` and the command at `/usr/local/bin/gh-broker`, independent of the Node version and on `sudo`'s PATH. `--ignore-scripts` because a global install runs as root on the box that holds the tokens. The package ships a prebuilt `dist/`, so there is nothing to build. A copy installed under an nvm version folder breaks when Node is upgraded; `gh-broker install` warns if it finds one. One-offs still work: `sudo npx @cldmv/gh-broker install [agent-user]`, and from a checkout `sudo node deploy/install.mjs [agent-user]`.
|
|
35
|
+
|
|
36
|
+
`gh-broker install` takes the same flags either way. `agent-user` registers that user as an agent (see [Agents](#3-agents-one-socket-and-one-mcp-config-each)); on an upgrade with agents already registered it is optional and adds nobody. A first install with no agent asks for one (`--non-interactive` needs `--agent-user`); it never takes `$SUDO_USER`, the admin running sudo. The installer walks the whole thing and is idempotent:
|
|
34
37
|
|
|
35
38
|
1. checks prerequisites,
|
|
36
|
-
2. **
|
|
37
|
-
3. creates the `gh-broker` service user
|
|
39
|
+
2. **registers the agents**: members of the old shared `gh-broker-clients` group become registered agents (the group itself is left alone), the user named on the command line is added (and created if missing), and each one gets its own group `gh-broker-agent-<user>`,
|
|
40
|
+
3. creates the `gh-broker` service user and, when the config has a `ghcr` block, the GHCR drop directory (owned by the agent named on the command line, else the first registered one),
|
|
38
41
|
4. points `/usr/local/bin/node` at the running node,
|
|
39
42
|
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,
|
|
43
|
+
6. regenerates the config (replacing an old `unix:` `listen` with `agents:/run/gh-broker/agents`) 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
44
|
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),
|
|
45
|
+
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); when no OTP seed is stored it also generates one and shows it once (see [OTP gate](#otp-gate-gh-broker-otp)),
|
|
43
46
|
9. validates the config with the real loader (`--check`),
|
|
44
|
-
10. installs the unit, deploys `dist
|
|
45
|
-
11. runs a **smoke test** (MCP handshake + a real read
|
|
47
|
+
10. installs the unit, deploys `dist/` to `/opt/gh-broker`, restarts, writes every agent's MCP config (`--no-mcp-config` prints the entry instead), and
|
|
48
|
+
11. runs a **smoke test** over each agent's own socket (MCP handshake + a real read) → ✅/❌.
|
|
46
49
|
|
|
47
50
|
### Unattended (single non-interactive command)
|
|
48
51
|
|
|
@@ -58,6 +61,28 @@ Re-running on an existing install needs no `--owner`: the deployed owners are ke
|
|
|
58
61
|
|
|
59
62
|
**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
63
|
|
|
64
|
+
## OTP gate (`gh-broker otp`)
|
|
65
|
+
|
|
66
|
+
Tools marked `"otp": true` in `/etc/gh-broker/config.json` (an upgrade keeps each tool's `otp` and `enabled` settings) only run when the call carries a current code from an authenticator app, so a code is something only you can supply. The seed behind those codes is generated by the installer and stored next to the PATs, where the agent can't read it.
|
|
67
|
+
|
|
68
|
+
**Setup.** On a first install, and on any later run that finds no stored `otp-seed`, the installer generates a seed, prints it, then stores it in the chosen backend (an encrypted `otp-seed.cred` delivered by the unit drop-in under `systemd-creds`, or the `otp-seed` key in `secrets.json` under `file`). It is printed once, before it is stored, so a run that fails later can't leave a stored seed nobody saw: a QR code of the `otpauth://` URI, the base32 key as text, and the URI itself. If a run fails and the next one prints a different key, use the newer one. Scan the QR code with an authenticator app, or enter the key by hand. `--non-interactive` does the same, so capture the output of an unattended run. An existing seed is never overwritten by the installer. To supply your own, pass `--secret otp-seed=<base32>`, `--secrets-file` or env `GH_BROKER_SECRET_OTP_SEED`; it is then used as is and not printed.
|
|
69
|
+
|
|
70
|
+
**The `gh-broker` command.** The global npm install puts `gh-broker` on root's PATH (`/usr/local/bin/gh-broker`). It needs root, because the state directory is `0700 gh-broker`:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
sudo gh-broker otp status [<user>] [--json] # per agent: bad codes so far, short lockouts in the last 24 h, hard lock, state file health
|
|
74
|
+
sudo gh-broker otp list [--json] # who is locked out now, and the command that clears each
|
|
75
|
+
sudo gh-broker otp unlock <user> # clear one agent's lockout and hard lock
|
|
76
|
+
sudo gh-broker otp unlock --all # clear everyone
|
|
77
|
+
sudo gh-broker otp rotate # new seed: stored, shown once, service restarted
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`unlock <user>` drops that user's entry from the state file and leaves the others alone, rewriting the file atomically as `0600 gh-broker:gh-broker`. Every write from the command also counts each code up to the next 30-second step as spent, so a write that races the daemon can't hand a code back that the daemon just accepted. After an unlock the current code and the one after it are refused (they don't count as bad codes); the command says how many seconds to wait for a fresh one, up to a minute. If the broker writes the state file while the command runs (a lockout, an accepted code), the command changes nothing and asks you to run it again, so it can't undo what the broker just recorded. Each unlock is logged to the journal as `gh-broker`, next to the daemon's audit log, with the sudo user who ran it. If the state file is corrupt the broker treats every user as hard-locked; `status` and `list` report that, `unlock <user>` refuses (clearing one user would not fix the file), and `unlock --all` rewrites it clean. `rotate` replaces the stored seed and restarts the service, which closes every open window; remove the old entry from your authenticator.
|
|
81
|
+
|
|
82
|
+
**State file.** The lock counters live in `/var/lib/gh-broker/otp-state.json` (`otp.statePath` to change it): `version`, a global `lastAcceptedStep`, and a `users` object with `failures`, `lockedUntil`, `lockouts` and `hardLock` per agent user. A user with no entry is all zero. The unit sets `StateDirectory=gh-broker` with `StateDirectoryMode=0700`, which makes the directory writable under `ProtectSystem=strict` and owned by the service user. It survives restarts, so a hard lock does too. The broker re-reads it whenever it changes, so `unlock` takes effect on the next call with no restart.
|
|
83
|
+
|
|
84
|
+
**Config.** `config.example.json` ships `"otp": { "seed": "cred:otp-seed" }`. An upgrade adds it to a deployed config that lacks it; an `otp` block you already have (`maxFailures`, `lockoutSeconds`, `maxLockouts`, `windowSeconds`, `statePath`) wins field by field, and a `--config` file may hold one too.
|
|
85
|
+
|
|
61
86
|
## Credential storage — your choice
|
|
62
87
|
|
|
63
88
|
The installer asks which backend holds the tokens at rest:
|
|
@@ -100,23 +125,38 @@ Flags that build a single GHCR owner do so for one owner per run; use `--config`
|
|
|
100
125
|
|
|
101
126
|
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
127
|
|
|
103
|
-
## 3.
|
|
128
|
+
## 3. Agents: one socket and one MCP config each
|
|
129
|
+
|
|
130
|
+
The broker knows which agent is calling from the socket the call arrives on: there is one Unix socket per agent user, `/run/gh-broker/agents/<user>.sock`, reachable only by that user. An agent is registered when the group `gh-broker-agent-<user>` exists with exactly `gh-broker` and `<user>` as members (user names are lowercase, up to 16 characters: `^[a-z_][a-z0-9_-]{0,15}$`). There is no shared socket, so a locked-out agent can't keep using the broker through another door.
|
|
104
131
|
|
|
105
|
-
|
|
132
|
+
```sh
|
|
133
|
+
sudo gh-broker agent add <user> # group, MCP config, restart, smoke test over the user's socket
|
|
134
|
+
sudo gh-broker agent remove <user> # empty the group, restart (closes the socket), then the MCP entry (backup first) and the user's lock state
|
|
135
|
+
sudo gh-broker agent list # socket path, whether it exists, and whether the MCP config points at it (ok / missing / points elsewhere)
|
|
136
|
+
sudo gh-broker agent sync [<user>] # re-apply the MCP config for one agent or all
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`agent remove` empties the agent's group rather than deleting it. Processes the user already has running keep that group's id, so deleting it would let a later `agent add` reuse the id and give those processes the new agent's socket. An empty group isn't an agent; `agent add` for the same user fills it again.
|
|
140
|
+
|
|
141
|
+
**The MCP config.** The broker writes the entry into the top-level `mcpServers` of `<home>/.claude.json`, using `sg` so the new group is effective without a re-login:
|
|
106
142
|
|
|
107
143
|
```json
|
|
108
144
|
"github": {
|
|
109
145
|
"type": "stdio",
|
|
110
|
-
"command": "
|
|
111
|
-
"args": ["
|
|
146
|
+
"command": "sg",
|
|
147
|
+
"args": ["gh-broker-agent-<user>", "-c", "exec socat STDIO UNIX-CONNECT:/run/gh-broker/agents/<user>.sock"]
|
|
112
148
|
}
|
|
113
149
|
```
|
|
114
150
|
|
|
115
|
-
|
|
151
|
+
If an existing entry already mentions `/run/gh-broker/` (the old shared socket or the new one) that entry is updated in place and keeps its name; otherwise the entry is added as `github`, or `gh-broker` when that name is taken. Nothing else in the file changes. The file is parsed, edited, written to a temp file in the same directory and renamed, keeping its owner and mode, and `~/.claude.json.gh-broker.bak` keeps the previous copy (overwritten each time). A missing file is created `0600`; a file that doesn't parse is left alone and the entry is printed to add by hand. The home directory belongs to the agent user and the command runs as root, so it reads the file without following a symlink and only touches a plain file the user owns with a single link; anything else (a symlink, a hard link, someone else's file) is left alone with a warning, and the temp file is created with a random name that can't be pre-planted. Running Claude sessions for that user must be restarted to pick the change up, and `agent sync <user>` puts the entry back if a running session overwrote the file. `--no-mcp-config` (on `install`, `agent add` and `agent sync`) skips the write and prints the entry.
|
|
152
|
+
|
|
153
|
+
**Upgrading from the shared socket.** The first `gh-broker install` after the change registers every member of `gh-broker-clients` (other than `gh-broker`) as an agent, writes the new `listen` (`agents:/run/gh-broker/agents`), rewrites each agent's MCP config to its own socket and smoke-tests every socket. `gh-broker-clients` is left in place; nothing depends on it any more. Agents added later need a service restart, which `agent add` does.
|
|
154
|
+
|
|
155
|
+
**Versions.** The service runs from `/opt/gh-broker`, which `gh-broker install` deploys. `gh-broker otp status` and `gh-broker agent list` warn when the installed package and the deployed copy differ; run `sudo gh-broker install` again.
|
|
116
156
|
|
|
117
157
|
## Adding an org later
|
|
118
158
|
|
|
119
|
-
Re-run
|
|
159
|
+
Re-run `sudo gh-broker install` 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
160
|
|
|
121
161
|
## npm integration (`npm_scaffold`)
|
|
122
162
|
|
|
@@ -147,4 +187,4 @@ Without the `npm` block nothing changes: the installer says "not enabled", and t
|
|
|
147
187
|
- 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
188
|
- The config file holds **no tokens** — only `cred:` references; the tokens live in the separate secret store.
|
|
149
189
|
- The socket carries MCP frames only.
|
|
150
|
-
- `journalctl -
|
|
190
|
+
- `journalctl -t gh-broker` shows the audit log (tool, user, owner, mode, repo, result, OTP outcome, lockouts and unlocks) — never tokens, codes or the seed.
|
package/deploy/cli.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @Project: @cldmv/gh-broker
|
|
5
|
+
* @Filename: /deploy/cli.mjs
|
|
6
|
+
* @Date: 2026-11-01T20:43:03-08:00 (1793594583)
|
|
7
|
+
* @Author: Nate Corcoran <CLDMV>
|
|
8
|
+
* @Email: <Shinrai@users.noreply.github.com>
|
|
9
|
+
* -----
|
|
10
|
+
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
|
|
11
|
+
* @Last modified time: 2026-11-01T20:43:03-08:00 (1793594583)
|
|
12
|
+
* -----
|
|
13
|
+
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `gh-broker` command (the package `bin`): `gh-broker install`, `gh-broker otp ...` and
|
|
19
|
+
* `gh-broker agent ...`. `install` hands over to the installer (deploy/install.mjs); the rest is
|
|
20
|
+
* lib/cli.mjs with the real effects from real-deps.mjs.
|
|
21
|
+
*/
|
|
22
|
+
import { realpathSync } from "node:fs";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { runCli } from "./lib/cli.mjs";
|
|
25
|
+
import { realDeps } from "./real-deps.mjs";
|
|
26
|
+
|
|
27
|
+
/** Run the CLI with the real effects and set the exit code. `install` is the installer. */
|
|
28
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
29
|
+
if (argv[0] === "install") {
|
|
30
|
+
process.argv.splice(2, 1); // the installer reads its flags from argv[2..]
|
|
31
|
+
await import("./install.mjs");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
process.exitCode = await runCli(argv, realDeps());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// npm links the bin as a symlink, so compare real paths.
|
|
38
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|
package/deploy/gh-broker.service
CHANGED
|
@@ -15,8 +15,9 @@ Group=gh-broker
|
|
|
15
15
|
# See secrets.ts / docs/deploy-architecture.md.
|
|
16
16
|
|
|
17
17
|
# Socket dir /run/gh-broker (traversable), owned by the service user; the daemon
|
|
18
|
-
# creates
|
|
19
|
-
# user
|
|
18
|
+
# creates one socket per agent under /run/gh-broker/agents, each 0660 with group
|
|
19
|
+
# gh-broker-agent-<user>, so only that agent can connect and the broker knows who
|
|
20
|
+
# is calling. The token stays server-side.
|
|
20
21
|
RuntimeDirectory=gh-broker
|
|
21
22
|
RuntimeDirectoryMode=0755
|
|
22
23
|
|
|
@@ -24,6 +25,8 @@ RuntimeDirectoryMode=0755
|
|
|
24
25
|
# it at the chosen node and re-points it on a bump), so no node version is pinned
|
|
25
26
|
# here and there is no nvm resolution at start. The symlink is the single source of
|
|
26
27
|
# truth, so CLI node == service node by construction. See docs/deploy-architecture.md.
|
|
28
|
+
# The audit log (stdout) and `gh-broker otp unlock` both log as gh-broker: journalctl -t gh-broker
|
|
29
|
+
SyslogIdentifier=gh-broker
|
|
27
30
|
ExecStart=/usr/local/bin/node /opt/gh-broker/dist/index.mjs --config /etc/gh-broker/config.json
|
|
28
31
|
Restart=on-failure
|
|
29
32
|
RestartSec=2
|
|
@@ -48,6 +51,10 @@ SystemCallArchitectures=native
|
|
|
48
51
|
SystemCallFilter=@system-service
|
|
49
52
|
SystemCallErrorNumber=EPERM
|
|
50
53
|
ReadWritePaths=/run/gh-broker
|
|
54
|
+
# OTP gate state (lock counters, hard lock): /var/lib/gh-broker, created 0700 gh-broker:gh-broker.
|
|
55
|
+
# StateDirectory= makes it writable under ProtectSystem=strict; `sudo gh-broker otp unlock` edits it.
|
|
56
|
+
StateDirectory=gh-broker
|
|
57
|
+
StateDirectoryMode=0700
|
|
51
58
|
UMask=0077
|
|
52
59
|
# Optional egress lockdown once GitHub ranges are pinned:
|
|
53
60
|
# IPAddressDeny=any
|