@cldmv/gh-broker 0.0.0-stage → 0.7.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/.configs/fix-headers.json +3 -0
- package/.configs/vitest.config.mjs +44 -0
- package/.github/dependabot.yml +111 -0
- package/.github/workflows/branch-retention.yml +44 -0
- package/.github/workflows/bundle-size.yml +52 -0
- package/.github/workflows/ci.yml +389 -0
- package/.github/workflows/cla.yml +66 -0
- package/.github/workflows/codeql.yml +87 -0
- package/.github/workflows/dependabot-auto-merge.yml +62 -0
- package/.github/workflows/dependabot-recreate.yml +67 -0
- package/.github/workflows/dependency-review.yml +46 -0
- package/.github/workflows/feature-pr.yml +75 -0
- package/.github/workflows/hotfix-redirector.yml +67 -0
- package/.github/workflows/hotfixes-release.yml +70 -0
- package/.github/workflows/labeler.yml +48 -0
- package/.github/workflows/master-commit-audit.yml +56 -0
- package/.github/workflows/member-auto-merge.yml +75 -0
- package/.github/workflows/next-release.yml +76 -0
- package/.github/workflows/next-reset.yml +50 -0
- package/.github/workflows/pr-notify.yml +44 -0
- package/.github/workflows/pr-title-normalizer.yml +52 -0
- package/.github/workflows/provenance.yml +51 -0
- package/.github/workflows/publish.yml +146 -0
- package/.github/workflows/release-merge.yml +104 -0
- package/.github/workflows/release-notify.yml +41 -0
- package/.github/workflows/scorecard.yml +64 -0
- package/.github/workflows/stale.yml +50 -0
- package/.github/workflows/tag-health.yml +70 -0
- package/.github/workflows/update-major-version-tags.yml +94 -0
- package/.github/workflows/v4-bootstrap.yml +115 -0
- package/.github/workflows/welcome.yml +42 -0
- package/LICENSE +202 -0
- package/README.md +185 -2
- package/config.example.json +281 -0
- package/deploy/README.md +92 -0
- package/deploy/gh-broker.service +57 -0
- package/deploy/install.mjs +344 -0
- package/deploy/install.sh +29 -0
- package/deploy/lib/config-merge.mjs +56 -0
- package/deploy/lib/prompt.mjs +98 -0
- package/deploy/lib/secret-store.mjs +114 -0
- package/deploy/lib/smoke.mjs +137 -0
- package/deploy/lib/system.mjs +222 -0
- package/dist/index.mjs +22970 -0
- package/dist/index.mjs.map +7 -0
- package/docs/TOOLS.md +347 -0
- package/docs/agent-user-model.md +89 -0
- package/docs/changelog/v0/v0.2.0.md +26 -0
- package/docs/changelog/v0/v0.2.1.md +23 -0
- package/docs/changelog/v0/v0.2.2.md +23 -0
- package/docs/changelog/v0/v0.2.3.md +33 -0
- package/docs/changelog/v0/v0.2.4.md +25 -0
- package/docs/changelog/v0/v0.2.5.md +29 -0
- package/docs/changelog/v0/v0.3.0.md +84 -0
- package/docs/changelog/v0/v0.3.1.md +30 -0
- package/docs/changelog/v0/v0.3.2.md +24 -0
- package/docs/changelog/v0/v0.3.3.md +28 -0
- package/docs/changelog/v0/v0.4.0.md +58 -0
- package/docs/changelog/v0/v0.4.1.md +33 -0
- package/docs/changelog/v0/v0.4.2.md +45 -0
- package/docs/changelog/v0/v0.5.0.md +39 -0
- package/docs/changelog/v0/v0.5.1.md +28 -0
- package/docs/changelog/v0/v0.6.0.md +52 -0
- package/docs/changelog/v0/v0.6.1.md +48 -0
- package/docs/changelog/v0/v0.7.0.md +71 -0
- package/docs/deploy-architecture.md +82 -0
- package/package.json +71 -5
- package/src/appauth.ts +56 -0
- package/src/audit.ts +34 -0
- package/src/catalog.ts +165 -0
- package/src/config.ts +130 -0
- package/src/index.ts +107 -0
- package/src/paths.ts +60 -0
- package/src/router.ts +258 -0
- package/src/secrets.ts +137 -0
- package/src/server.ts +91 -0
- package/src/types.ts +104 -0
- package/src/upstream.ts +85 -0
- package/src/verbs/issue_labels.ts +329 -0
- package/src/verbs/link_issue.ts +84 -0
- package/src/verbs/release-analysis.ts +210 -0
- package/src/verbs/repo-tier.ts +90 -0
- package/src/verbs/report_ci_status.ts +358 -0
- package/src/verbs/report_release_prs.ts +238 -0
- package/src/verbs/report_security_alerts.ts +442 -0
- package/src/verbs/report_unlabeled.ts +194 -0
- package/src/verbs/rest-client.ts +181 -0
- package/src/verbs/review_thread.ts +138 -0
- package/tests/appauth.test.vitest.mts +76 -0
- package/tests/audit.test.vitest.mts +48 -0
- package/tests/catalog.test.vitest.mts +180 -0
- package/tests/config-merge.test.vitest.mts +58 -0
- package/tests/config.test.vitest.mts +314 -0
- package/tests/issue-labels.test.vitest.mts +347 -0
- package/tests/paths.test.vitest.mts +52 -0
- package/tests/release-prs.test.vitest.mts +237 -0
- package/tests/rest-client.test.vitest.mts +115 -0
- package/tests/router.test.vitest.mts +399 -0
- package/tests/run-vitest.mjs +51 -0
- package/tests/secret-store.test.vitest.mts +106 -0
- package/tests/secrets.test.vitest.mts +155 -0
- package/tests/security-alerts.test.vitest.mts +328 -0
- package/tests/server.test.vitest.mts +20 -0
- package/tests/upstream.test.vitest.mts +91 -0
- package/tests/verb-helpers.test.vitest.mts +331 -0
- package/tests/verbs.test.vitest.mts +624 -0
- package/tsconfig.json +21 -0
package/README.md
CHANGED
|
@@ -1,3 +1,186 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cldmv/gh-broker
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
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.**
|
|
8
|
+
|
|
9
|
+
## How
|
|
10
|
+
|
|
11
|
+
A small daemon (this package) runs as a dedicated **`gh-broker` system user** and:
|
|
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).
|
|
17
|
+
|
|
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).
|
|
19
|
+
|
|
20
|
+
Full design + threat model: `~/.claude/designs/github-mcp-broker.md`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🚀 Key Features
|
|
25
|
+
|
|
26
|
+
### 🔒 **Use ≠ Possession**
|
|
27
|
+
|
|
28
|
+
The agent can trigger a GitHub read/write; it can never hold the token that performs it. A separate system user owns the secret store and the daemon; the agent reaches it only over a permissioned Unix socket carrying MCP frames, never credential material.
|
|
29
|
+
|
|
30
|
+
🔒 **For the full isolation model — users, groups, filesystem permissions, verified against this host — see [docs/agent-user-model.md](docs/agent-user-model.md)**
|
|
31
|
+
|
|
32
|
+
### 🎯 **Default-Deny, Config-Driven Toolset**
|
|
33
|
+
|
|
34
|
+
A tool not listed in `config.tools` is neither advertised nor executable. Adding an org is a config entry plus two credentials — no new server, no new agent-visible tool-list bloat. Each entry is `mode: "read"` or `"write"`, optionally `custom` (handled entirely locally) or `augment` (proxied, then enriched) — see below.
|
|
35
|
+
|
|
36
|
+
### 📊 **CI Visibility Without Per-Call Log Fetches**
|
|
37
|
+
|
|
38
|
+
`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.
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "owner": "cldmv", "repo": "droidsock", "pull": 35, "verbosity": 2 }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
📊 **For every tier, the classification rules, and real verified examples, see [docs/TOOLS.md](docs/TOOLS.md#report_ci_status)**
|
|
45
|
+
|
|
46
|
+
### 🏷️ **Cross-Repo Label Auditing**
|
|
47
|
+
|
|
48
|
+
`report_unlabeled` finds every issue/PR with no labels — in one repo, or every repo an owner has — bounded by fan-out safety caps and a rate-limit floor that reads the token's own real limit rather than assuming one.
|
|
49
|
+
|
|
50
|
+
🏷️ **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_unlabeled)**
|
|
51
|
+
|
|
52
|
+
### 🏷️ **Label Edits That Leave Other Labels Alone**
|
|
53
|
+
|
|
54
|
+
The hosted MCP's `issue_write` takes `labels` as a full replacement, so changing one status label that way drops every other label on the issue. `issue_labels` edits labels one at a time instead: `add` and `remove` (one or several, combinable), `swap` (drop every `status: ` label and add `status: verified` in one call), and an explicit `set`. A misspelled label is refused rather than created, and the call returns the resulting set.
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "owner": "cldmv", "repo": "gh-broker", "issue_number": 117, "swap": { "prefix": "status: ", "to": "status: implemented" } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
🏷️ **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#issue_labels)**
|
|
61
|
+
|
|
62
|
+
### 🛡️ **Org-Wide Security Alerts**
|
|
63
|
+
|
|
64
|
+
`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.
|
|
65
|
+
|
|
66
|
+
🛡️ **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_security_alerts)**
|
|
67
|
+
|
|
68
|
+
### 🚢 **Release PR Discovery**
|
|
69
|
+
|
|
70
|
+
`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.
|
|
71
|
+
|
|
72
|
+
🚢 **Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md#report_release_prs)**
|
|
73
|
+
|
|
74
|
+
### 🔧 **Extensible By Design**
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
🔧 **How to add one: [docs/TOOLS.md § Extending this](docs/TOOLS.md#extending-this-adding-a-new-custom-verb-or-augmented-tool)**
|
|
79
|
+
|
|
80
|
+
### 📦 **One-Command Install & Upgrade**
|
|
81
|
+
|
|
82
|
+
`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.
|
|
83
|
+
|
|
84
|
+
📦 **Full prerequisites, PAT scopes, and flags: [deploy/README.md](deploy/README.md)**
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## ✨ What's New
|
|
89
|
+
|
|
90
|
+
### Latest: v0.7.0 (October 2026)
|
|
91
|
+
|
|
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)
|
|
96
|
+
|
|
97
|
+
### Recent Releases
|
|
98
|
+
|
|
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))
|
|
103
|
+
|
|
104
|
+
📚 For the complete version history, see the [docs/changelog/](https://github.com/CLDMV/gh-broker/tree/master/docs/changelog/) folder.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Calling a tool
|
|
109
|
+
|
|
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:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
// tools/call
|
|
114
|
+
{ "name": "report_ci_status", "arguments": { "owner": "cldmv", "repo": "droidsock", "pull": 35, "verbosity": 1 } }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
// result
|
|
119
|
+
{ "pull": 35, "summary": { "passed": 6, "failed": 1, "pending": 0 }, "failures": [{ "workflow": "🧪 CI Tests & Build", "job": "Build & Test (22.12)", "runId": 34787050719, "runUrl": "https://github.com/CLDMV/droidsock/actions/runs/34787050719", "conclusion": "failure" }] }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
No token ever appears in the request, the response, or anywhere the agent can read. See [docs/TOOLS.md](docs/TOOLS.md) for every custom and augmented tool, with full parameter tables and real verified examples.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Layout
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
src/
|
|
130
|
+
index.ts entry — resolve paths, load config + secrets, start server
|
|
131
|
+
paths.ts platform-neutral config/secrets path resolution
|
|
132
|
+
config.ts load + validate the token-free config
|
|
133
|
+
secrets.ts resolve cred refs from the secrets file (provider seam)
|
|
134
|
+
router.ts allowlist (default-deny) + owner/mode → PAT + dispatch (custom/augment/forward)
|
|
135
|
+
upstream.ts pool of MCP clients to the hosted GitHub MCP (per-token, Bearer)
|
|
136
|
+
catalog.ts build the advertised tool list (real upstream schemas + custom + augmented)
|
|
137
|
+
server.ts Unix-socket MCP server toward the agent (StdioServerTransport)
|
|
138
|
+
audit.ts structured audit log (never tokens)
|
|
139
|
+
verbs/
|
|
140
|
+
report_unlabeled.ts issues/PRs with no labels, one repo or org-wide
|
|
141
|
+
report_security_alerts.ts open Dependabot/CodeQL/secret alerts, one repo or org-wide
|
|
142
|
+
report_release_prs.ts open release PRs + whether each has its changelog file
|
|
143
|
+
report_ci_status.ts tiered CI status for a PR or every open PR in a repo
|
|
144
|
+
rest-client.ts shared rate-limit-floor + call-budget REST helpers
|
|
145
|
+
issue_labels.ts add/remove/swap/set labels on an issue or PR (write PAT)
|
|
146
|
+
review_thread.ts resolve/reply PR review threads (write PAT)
|
|
147
|
+
link_issue.ts addClosingIssueReference (App token) — DEFERRED, not wired in
|
|
148
|
+
deploy/
|
|
149
|
+
install.mjs interactive installer — first install AND upgrade (sudo node)
|
|
150
|
+
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)
|
|
152
|
+
gh-broker.service systemd unit (hardened)
|
|
153
|
+
README.md prerequisites + PAT scopes + install/upgrade/wiring
|
|
154
|
+
docs/
|
|
155
|
+
TOOLS.md reference for every custom/augmented tool, with real examples
|
|
156
|
+
deploy-architecture.md config merge, secret store, node handling, installer flow
|
|
157
|
+
agent-user-model.md the agent-as-separate-user isolation model (example setup)
|
|
158
|
+
changelog/ per-version release notes
|
|
159
|
+
config.example.json shipped defaults (token-free); merged into the deployed config
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 📚 Documentation
|
|
163
|
+
|
|
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
|
|
165
|
+
- **[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
|
+
- **[Deploy Architecture](docs/deploy-architecture.md)** — config merge, secret store backends, node handling, the installer's full flow
|
|
167
|
+
- **[Install Guide](deploy/README.md)** — prerequisites, PAT scopes, install/upgrade command, flags
|
|
168
|
+
- **[Changelog](docs/changelog/)** — per-version release notes
|
|
169
|
+
|
|
170
|
+
## Status
|
|
171
|
+
|
|
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)).
|
|
173
|
+
|
|
174
|
+
## Install / build
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
npm ci
|
|
178
|
+
npm run typecheck
|
|
179
|
+
npm run build
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
Apache-2.0 © Shinrai / CLDMV
|
|
185
|
+
|
|
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`.
|
|
@@ -0,0 +1,281 @@
|
|
|
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": "resolve_review_thread",
|
|
245
|
+
"mode": "write",
|
|
246
|
+
"custom": true
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "reply_review_thread",
|
|
250
|
+
"mode": "write",
|
|
251
|
+
"custom": true
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"name": "report_unlabeled",
|
|
255
|
+
"mode": "read",
|
|
256
|
+
"custom": true
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "report_security_alerts",
|
|
260
|
+
"mode": "read",
|
|
261
|
+
"custom": true
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "report_release_prs",
|
|
265
|
+
"mode": "read",
|
|
266
|
+
"custom": true
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"name": "report_ci_status",
|
|
270
|
+
"mode": "read",
|
|
271
|
+
"custom": true
|
|
272
|
+
}
|
|
273
|
+
],
|
|
274
|
+
"owners": {
|
|
275
|
+
"cldmv": {
|
|
276
|
+
"read": "cred:cldmv-read",
|
|
277
|
+
"write": "cred:cldmv-write"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"defaultOwner": "cldmv"
|
|
281
|
+
}
|
package/deploy/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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 CLDMV fine-grained PATs
|
|
13
|
+
|
|
14
|
+
GitHub → Settings → Developer settings → **Fine-grained tokens** → Generate new token, **twice**. Resource owner: **CLDMV**. Repository access: **All repositories**.
|
|
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).
|
|
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
|
+
## 2. Install (first install AND upgrade — same command)
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
sudo node deploy/install.mjs [agent-user]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`agent-user` defaults to `$SUDO_USER` (the account you ran `sudo` from). The installer walks the whole thing and is idempotent:
|
|
28
|
+
|
|
29
|
+
1. checks prerequisites,
|
|
30
|
+
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,
|
|
32
|
+
4. points `/usr/local/bin/node` at the running node,
|
|
33
|
+
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) → ✅/❌.
|
|
39
|
+
|
|
40
|
+
### Unattended (single non-interactive command)
|
|
41
|
+
|
|
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:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
sudo GH_BROKER_SECRET_CLDMV_READ=… GH_BROKER_SECRET_CLDMV_WRITE=… \
|
|
46
|
+
/usr/local/bin/node deploy/install.mjs --agent-user repo \
|
|
47
|
+
--secrets-provider systemd-creds --non-interactive
|
|
48
|
+
```
|
|
49
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
**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
|
+
|
|
54
|
+
## Credential storage — your choice
|
|
55
|
+
|
|
56
|
+
The installer asks which backend holds the tokens at rest:
|
|
57
|
+
|
|
58
|
+
- **`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.
|
|
59
|
+
- **`file`**: tokens in a plaintext JSON file, `0640 root:gh-broker`. Portable and platform-neutral; at-rest protection is filesystem permissions only.
|
|
60
|
+
|
|
61
|
+
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
|
+
|
|
63
|
+
## 3. Point Claude at the broker
|
|
64
|
+
|
|
65
|
+
The installer prints this; add it to the agent user's `~/.claude.json` `mcpServers`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
"github": {
|
|
69
|
+
"type": "stdio",
|
|
70
|
+
"command": "socat",
|
|
71
|
+
"args": ["STDIO", "UNIX-CONNECT:/run/gh-broker/gh-broker.sock"]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Re-login as the agent user first, so its `gh-broker-clients` membership is active.
|
|
76
|
+
|
|
77
|
+
## Adding an org later
|
|
78
|
+
|
|
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.
|
|
80
|
+
|
|
81
|
+
## Enabling `link_issue` (GitHub App) — DEFERRED
|
|
82
|
+
|
|
83
|
+
> **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.
|
|
84
|
+
|
|
85
|
+
`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.
|
|
86
|
+
|
|
87
|
+
## Verify no secret leaks
|
|
88
|
+
|
|
89
|
+
- 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.
|
|
90
|
+
- The config file holds **no tokens** — only `cred:` references; the tokens live in the separate secret store.
|
|
91
|
+
- The socket carries MCP frames only.
|
|
92
|
+
- `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
|