@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.
Files changed (107) hide show
  1. package/.configs/fix-headers.json +3 -0
  2. package/.configs/vitest.config.mjs +44 -0
  3. package/.github/dependabot.yml +111 -0
  4. package/.github/workflows/branch-retention.yml +44 -0
  5. package/.github/workflows/bundle-size.yml +52 -0
  6. package/.github/workflows/ci.yml +389 -0
  7. package/.github/workflows/cla.yml +66 -0
  8. package/.github/workflows/codeql.yml +87 -0
  9. package/.github/workflows/dependabot-auto-merge.yml +62 -0
  10. package/.github/workflows/dependabot-recreate.yml +67 -0
  11. package/.github/workflows/dependency-review.yml +46 -0
  12. package/.github/workflows/feature-pr.yml +75 -0
  13. package/.github/workflows/hotfix-redirector.yml +67 -0
  14. package/.github/workflows/hotfixes-release.yml +70 -0
  15. package/.github/workflows/labeler.yml +48 -0
  16. package/.github/workflows/master-commit-audit.yml +56 -0
  17. package/.github/workflows/member-auto-merge.yml +75 -0
  18. package/.github/workflows/next-release.yml +76 -0
  19. package/.github/workflows/next-reset.yml +50 -0
  20. package/.github/workflows/pr-notify.yml +44 -0
  21. package/.github/workflows/pr-title-normalizer.yml +52 -0
  22. package/.github/workflows/provenance.yml +51 -0
  23. package/.github/workflows/publish.yml +146 -0
  24. package/.github/workflows/release-merge.yml +104 -0
  25. package/.github/workflows/release-notify.yml +41 -0
  26. package/.github/workflows/scorecard.yml +64 -0
  27. package/.github/workflows/stale.yml +50 -0
  28. package/.github/workflows/tag-health.yml +70 -0
  29. package/.github/workflows/update-major-version-tags.yml +94 -0
  30. package/.github/workflows/v4-bootstrap.yml +115 -0
  31. package/.github/workflows/welcome.yml +42 -0
  32. package/LICENSE +202 -0
  33. package/README.md +185 -2
  34. package/config.example.json +281 -0
  35. package/deploy/README.md +92 -0
  36. package/deploy/gh-broker.service +57 -0
  37. package/deploy/install.mjs +344 -0
  38. package/deploy/install.sh +29 -0
  39. package/deploy/lib/config-merge.mjs +56 -0
  40. package/deploy/lib/prompt.mjs +98 -0
  41. package/deploy/lib/secret-store.mjs +114 -0
  42. package/deploy/lib/smoke.mjs +137 -0
  43. package/deploy/lib/system.mjs +222 -0
  44. package/dist/index.mjs +22970 -0
  45. package/dist/index.mjs.map +7 -0
  46. package/docs/TOOLS.md +347 -0
  47. package/docs/agent-user-model.md +89 -0
  48. package/docs/changelog/v0/v0.2.0.md +26 -0
  49. package/docs/changelog/v0/v0.2.1.md +23 -0
  50. package/docs/changelog/v0/v0.2.2.md +23 -0
  51. package/docs/changelog/v0/v0.2.3.md +33 -0
  52. package/docs/changelog/v0/v0.2.4.md +25 -0
  53. package/docs/changelog/v0/v0.2.5.md +29 -0
  54. package/docs/changelog/v0/v0.3.0.md +84 -0
  55. package/docs/changelog/v0/v0.3.1.md +30 -0
  56. package/docs/changelog/v0/v0.3.2.md +24 -0
  57. package/docs/changelog/v0/v0.3.3.md +28 -0
  58. package/docs/changelog/v0/v0.4.0.md +58 -0
  59. package/docs/changelog/v0/v0.4.1.md +33 -0
  60. package/docs/changelog/v0/v0.4.2.md +45 -0
  61. package/docs/changelog/v0/v0.5.0.md +39 -0
  62. package/docs/changelog/v0/v0.5.1.md +28 -0
  63. package/docs/changelog/v0/v0.6.0.md +52 -0
  64. package/docs/changelog/v0/v0.6.1.md +48 -0
  65. package/docs/changelog/v0/v0.7.0.md +71 -0
  66. package/docs/deploy-architecture.md +82 -0
  67. package/package.json +71 -5
  68. package/src/appauth.ts +56 -0
  69. package/src/audit.ts +34 -0
  70. package/src/catalog.ts +165 -0
  71. package/src/config.ts +130 -0
  72. package/src/index.ts +107 -0
  73. package/src/paths.ts +60 -0
  74. package/src/router.ts +258 -0
  75. package/src/secrets.ts +137 -0
  76. package/src/server.ts +91 -0
  77. package/src/types.ts +104 -0
  78. package/src/upstream.ts +85 -0
  79. package/src/verbs/issue_labels.ts +329 -0
  80. package/src/verbs/link_issue.ts +84 -0
  81. package/src/verbs/release-analysis.ts +210 -0
  82. package/src/verbs/repo-tier.ts +90 -0
  83. package/src/verbs/report_ci_status.ts +358 -0
  84. package/src/verbs/report_release_prs.ts +238 -0
  85. package/src/verbs/report_security_alerts.ts +442 -0
  86. package/src/verbs/report_unlabeled.ts +194 -0
  87. package/src/verbs/rest-client.ts +181 -0
  88. package/src/verbs/review_thread.ts +138 -0
  89. package/tests/appauth.test.vitest.mts +76 -0
  90. package/tests/audit.test.vitest.mts +48 -0
  91. package/tests/catalog.test.vitest.mts +180 -0
  92. package/tests/config-merge.test.vitest.mts +58 -0
  93. package/tests/config.test.vitest.mts +314 -0
  94. package/tests/issue-labels.test.vitest.mts +347 -0
  95. package/tests/paths.test.vitest.mts +52 -0
  96. package/tests/release-prs.test.vitest.mts +237 -0
  97. package/tests/rest-client.test.vitest.mts +115 -0
  98. package/tests/router.test.vitest.mts +399 -0
  99. package/tests/run-vitest.mjs +51 -0
  100. package/tests/secret-store.test.vitest.mts +106 -0
  101. package/tests/secrets.test.vitest.mts +155 -0
  102. package/tests/security-alerts.test.vitest.mts +328 -0
  103. package/tests/server.test.vitest.mts +20 -0
  104. package/tests/upstream.test.vitest.mts +91 -0
  105. package/tests/verb-helpers.test.vitest.mts +331 -0
  106. package/tests/verbs.test.vitest.mts +624 -0
  107. package/tsconfig.json +21 -0
package/README.md CHANGED
@@ -1,3 +1,186 @@
1
- # Temporary Holding Version
1
+ # @cldmv/gh-broker
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }
@@ -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