github/

directory
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT, MIT

README

gh-broker

gh-broker is a small GitHub credential broker for coding agents. It gives an agent a broker secret, keeps the real GitHub credential server-side, and allows only the Git and GitHub API operations matched by scope.json.

The central invariant is strict: gh-broker must not provide an API, log path, error path, or helper that returns original GitHub credential material.

The shared install, setup, policy, approval, and release contract is in BrokerKit's unified broker contract.

Current Shape

  • Echo HTTP server
  • gh-broker --version
  • gh-broker setup client
  • gh-broker setup github-user enroll|rotate|revoke for protected local enrollment of expiring GitHub App user credentials
  • gh-broker setup systemd for Linux service file/config generation
  • gh-broker doctor github for local isolation, GitHub App, repository, and default-branch protection checks
  • Shared-secret authentication for one configured client id
  • Server-side named client secrets through GH_BROKER_SECRETS_FILE
  • Rule-based scope.json with GitHub classification delegated to the shared brokerkit policy engine
  • Git smart HTTP fetch and push route shape
  • Exhaustive typed Agent V1 operations generated from pinned GitHub REST and GraphQL definitions
  • Opaque broker-owned app-jwt, exact installation, user, and protected-file development credential providers
  • Exact installation-token narrowing and cache isolation by installation, repository ids, permissions, API host, and expiry behavior
  • Localhost bind by default for Tailnet-oriented deployment
  • Conservative receive-pack size cap and upstream GitHub timeouts
  • Structured audit logs without secrets, request bodies, diffs, or pack contents
  • Brokerkit-backed grants with a durable operator inbox and optional Telegram notifications
  • No credential API
  • No policy read/write API
  • Tests for auth, route shape, policy decisions, and receive-pack classification

Generated operation surface

The checked-in stage 2–3 inventory classifies all 1,196 operations in the pinned stable GitHub REST API and all 300 roots in the pinned full GraphQL introspection, including 16 deprecated mutation roots. It generates 1,436 canonical typed catalog operations, closed target/argument/result schemas, REST bindings, persisted GraphQL documents, policy metadata, GitHub App permission profiles, CLI metadata, MCP tool schemas, and capability docs.

Browse the generated reference in CAPABILITIES.md or query it locally:

gh-broker operations list --family pull_request
gh-broker operations describe repo.visibility.update

The MCP server exposes the exhaustive catalog through the paged github://operations resource. tools/list is separately filtered by the authenticated client's enabled operations, policy-visible operations, runtime capabilities, and the operator exposure profile. With no configured intersection it advertises zero execution tools; it never publishes the full catalog as 1,000+ tools by default.

Credential selection is immutable broker metadata. Callers choose an operation and target, never a credential kind, token scope, installation, or permission set. The generated operation catalog supplies the minimum GitHub App permission map used for installation-token minting.

Local Development

cp .env.example .env
cp scope.example.json scope.json
install -m 600 /dev/null github-token
# write a development-only fine-grained token to github-token
# edit GH_BROKER_SHARED_SECRET to a generated value with at least 32 bytes
# edit scope.json by hand
source .env
make check
make run

Install

Install the latest release globally:

curl -fsSL https://raw.githubusercontent.com/osolmaz/brokerkit/main/brokers/github/install.sh | sh

Pin a version from BrokerKit releases:

curl -fsSL https://raw.githubusercontent.com/osolmaz/brokerkit/main/brokers/github/install.sh | VERSION=<version> sh

Write a client config file from a broker secrets file:

gh-broker setup client \
  --client bob \
  --url http://127.0.0.1:8081 \
  --secret-file /etc/gh-broker/secrets \
  --home-dir /home/bob

The generated client.env contains only GH_BROKER_URL and GH_BROKER_SHARED_SECRET; it does not contain GitHub credentials.

Production Linux service setup with GitHub App credentials:

sudo gh-broker setup systemd \
  --github-app-id-file ./app-id \
  --github-app-private-key-file ./private-key.pem \
  --github-webhook-secret-file ./webhook-secret \
  --scope-file ./scope.json

To enable encrypted GitHub App user credentials, add the App's OAuth client files to setup:

sudo gh-broker setup systemd \
  --github-app-id-file ./app-id \
  --github-app-private-key-file ./private-key.pem \
  --github-app-client-id-file ./client-id \
  --github-app-client-secret-file ./client-secret \
  --github-user-id 1234 \
  --github-webhook-secret-file ./webhook-secret \
  --scope-file ./scope.json

Development setup with a token file:

sudo gh-broker setup systemd \
  --dev-token-fallback \
  --github-token-file ./github-token \
  --scope-file ./scope.json

This fallback is protected-file-only and non-production. doctor github returns an unsafe result if it is selected with GH_BROKER_ENVIRONMENT=production.

Enroll an expiring GitHub App user credential from a local operator shell while the broker is stopped. The state directory must already exist with the broker service user's ownership; setup preserves that owner across key and slot writes. The input is a mode 0600 JSON file and is deleted by the operator after a successful enrollment:

{
  "user_id": 1234,
  "login": "octocat",
  "access_token": "expiring-access-token",
  "refresh_token": "expiring-refresh-token",
  "access_expires_at": "2026-07-14T12:00:00Z",
  "refresh_expires_at": "2026-10-14T12:00:00Z"
}
chmod 600 user-credential.json app-client-id app-client-secret
gh-broker setup github-user enroll \
  --state-dir /var/lib/gh-broker \
  --credential-file ./user-credential.json \
  --github-app-client-id-file ./app-client-id \
  --github-app-client-secret-file ./app-client-secret
rm user-credential.json

Use the same command with rotate and a replacement credential file to rotate an enrollment. Revoke immediately with github-user revoke --user-id 1234 and the same state/client flags. These commands return only the action and immutable user id; there is no credential readback command or API route.

Add Telegram notifications by passing the bot token as a protected file:

sudo gh-broker setup systemd \
  --dev-token-fallback \
  --github-token-file ./github-token \
  --scope-file ./scope.json \
  --telegram-bot-token-file ./telegram-bot-token \
  --telegram-chat-id 123456789

Use --no-start to write files without enabling or starting the service. The setup command writes /etc/gh-broker/secrets, /etc/gh-broker/operator-secrets, and the optional Telegram token as protected files. It does not place broker client secrets, operator secrets, GitHub credentials, or Telegram credentials directly in the env file. The operator API uses a separate credential and listener at http://127.0.0.1:8082 by default. Use --operator-secret-file, --operator-bind-addr, and --operator-port to supply or change them.

Verify the installed deployment:

sudo gh-broker doctor github \
  --repo osolmaz/gh-broker \
  --agent-user bob \
  --service-user gh-broker

Use --json for machine-readable output. Exit code 0 means safe, 1 means unsafe, 2 means inconclusive, and 64 means the invocation or configuration is invalid. Doctor reads /etc/gh-broker/env by default; use --env-file for a nonstandard installation or --env-file '' to use only the current process environment.

When an env file is selected, it is authoritative; exported shell variables do not override the installed service configuration. Inline credential values are reported as inconclusive because doctor cannot prove which users have observed them. Protected credential files are required for a safe isolation verdict.

Health check:

curl http://localhost:8080/healthz

Fetch through the broker:

git -c http.extraHeader="Authorization: Bearer $GH_BROKER_SHARED_SECRET" \
  ls-remote http://localhost:8080/osolmaz/gh-broker.git

List repositories visible to the selected GitHub user credential:

gh-broker operation submit repo.list_for_authenticated_user \
  --target-json '{"kind":"user","name":"osolmaz"}' \
  --arguments-json '{}' \
  --wait

Read repository contents:

gh-broker operation submit repo.contents.read \
  --target-json '{"kind":"repo","owner":"osolmaz","name":"brokerkit"}' \
  --arguments-json '{"path":"README.md","ref":"main"}' \
  --wait

Open a pull request:

curl -X POST -H "Authorization: Bearer $GH_BROKER_SHARED_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"idempotency_key":"open-agent-pr","operation":"pull_request.create","target":{"kind":"repo","owner":"osolmaz","name":"gh-broker"},"arguments":{"title":"agent work","head":"bob/work","base":"main","body":"Ready for review."},"reason":"Open the reviewed feature branch"}' \
  http://localhost:8080/api/agent/v1/operations

Policy

scope.json is the authorization source of truth. A request is classified into:

client + operation + target + attrs -> allow | request | deny | no_match

Deny rules win over allow rules. A rule matches only when the same rule matches the client, operation, target, and operation-relevant attributes. If a rule has attrs, every operation in that rule must support every attr; split rules when operations need different attrs.

The default production-oriented workflow is:

  • allow fetch, repository listing, and content reads for scoped repositories
  • allow agents to push feature branches such as refs/heads/bob/*
  • allow agents to open pull requests into refs/heads/main
  • deny ref deletion and unsupported ref updates before forwarding
  • deny default-branch pushes unless a repository has an explicit direct-main allow rule
  • classify existing branch updates as git.push.force unless the broker can prove fast-forward
  • rely on GitHub rulesets or branch protections as the upstream enforcement layer after GitHub receives the pack

Example direct-main exception:

{
  "id": "direct-main-example",
  "effect": "allow",
  "clients": ["bob"],
  "operations": ["git.push.force"],
  "targets": [{"kind": "repo", "owner": "osolmaz", "name": "direct-main"}],
  "attrs": {"refs": ["refs/heads/main"]}
}

Broker Routes

GET  /healthz

POST /api/grants                         Git smart-HTTP protocol grant request
GET  /api/grants                         Git smart-HTTP protocol grants
GET  /api/grants/{id}                    Git smart-HTTP protocol grant

GET  /.well-known/brokerkit-agent
POST /api/agent/v1/operations
GET  /api/agent/v1/operations/{id}
GET  /api/agent/v1/operations/{id}/events
POST /api/agent/v1/operations/{id}/cancel
POST /api/agent/v1/sealed-payloads
POST /api/agent/v1/streams
GET  /api/agent/v1/streams/{id}

GET  /{owner}/{repo}.git/info/refs?service=git-upload-pack
POST /{owner}/{repo}.git/git-upload-pack

GET  /{owner}/{repo}.git/info/refs?service=git-receive-pack
POST /{owner}/{repo}.git/git-receive-pack

All discrete GitHub JSON operations use Agent V1. The /api/grants surface is reserved for approval requests created while handling Git smart-HTTP protocol traffic. Direct repository-list and contents proxy routes are not supported. Compatibility aliases are not part of the production surface.

Operator Inbox

The protected operator listener exposes Brokerkit's shared backend:

GET  /api/grants
GET  /api/grants/{id}
GET  /api/grants/events
POST /api/grants/{id}/approve
POST /api/grants/{id}/deny
POST /api/grants/{id}/cancel
POST /api/grants/{id}/revoke

Authenticate with the separate operator credential from /etc/gh-broker/operator-secrets. Agent credentials cannot use this listener. Telegram is an optional notification view over the same durable request, so a decision through either path closes the same state exactly once. A trusted web host keeps this credential server-side and exposes only its own authenticated browser session and bounded Brokerkit response fields.

Security Model

gh-broker should run behind Tailnet-only reachability, but Tailnet access is not authorization. Every broker endpoint still requires the configured shared secret.

Production should use GitHub App credentials:

GH_BROKER_GITHUB_APP_ID_FILE=/etc/gh-broker/github-app-id
GH_BROKER_GITHUB_APP_PRIVATE_KEY_FILE=/etc/gh-broker/github-app-private-key.pem
GH_BROKER_GITHUB_WEBHOOK_SECRET_FILE=/etc/gh-broker/github-webhook-secret

For repo-scoped requests, gh-broker resolves the GitHub App installation for the target repository with go-github and mints a short-lived token with the exact repository id and catalog-derived minimum permission map after broker policy allows the request. Cache keys include the installation id, sorted exact repository ids, exact permissions, API host, and refresh behavior. Broader credentials are never reused for narrower requests. App JWT transport is provided by ghinstallation; typed GitHub API pagination, errors, rate limits, installation resolution, token requests, and webhook parsing use go-github. Repository-list credentials are uncached and revoked immediately after use.

GH_BROKER_GITHUB_TOKEN_FILE remains available only as a protected-file local development fallback. Inline PAT configuration is rejected.

GitHub App webhooks are accepted at:

POST /webhooks/github

The webhook route requires X-Hub-Signature-256, X-GitHub-Event, and X-GitHub-Delivery. It verifies the payload with GH_BROKER_GITHUB_WEBHOOK_SECRET_FILE, accepts bodies up to 1 MiB, and logs only audit-safe metadata such as event, delivery id, action, installation id, and repository name. Installation suspension/deletion, repository selection changes, and github_app_authorization revocation invalidate affected cached credentials immediately. Invalid or unknown signed payloads fail closed.

User access and refresh credentials are stored only in BrokerKit's encrypted credential store. Access credentials refresh before expiry, rotating refresh credentials are persisted as one encrypted record, and enrollment, rotation, revocation, errors, plans, results, audit events, and operator surfaces never return credential material.

Deployment safety defaults:

  • GH_BROKER_BIND_ADDR defaults to 127.0.0.1.
  • GH_BROKER_STATE_DIR defaults to ./state.
  • GH_BROKER_GITHUB_HTTP_TIMEOUT defaults to 30 seconds.
  • GH_BROKER_GITHUB_STREAM_TIMEOUT defaults to 600 seconds for bounded uploads and downloads.
  • GH_BROKER_MAX_RECEIVE_PACK_BYTES defaults to 25 MiB.
  • GH_BROKER_TELEGRAM_BOT_TOKEN_FILE and GH_BROKER_TELEGRAM_CHAT_ID enable Telegram notifications for requestable grants. Telegram tokens are loaded only from protected files.
  • Audit logs record client, operation, owner, repo, method, path, outcome, status, reason, matched rule ids, GitHub installation id when one was minted for the request, and verified webhook event metadata.
  • Audit logs do not include tokens, cookies, request bodies, PR bodies, pack contents, diffs, raw upstream bodies, JWTs, installation tokens, or private keys, refresh tokens, PATs, or GitHub App client secrets.

Grants

Request rules do not execute directly. Discrete GitHub operations use Agent V1, which creates the durable approval request as part of the submitted operation. Git smart-HTTP protocol handling creates a pending grant through POST /api/grants; every such request must include a unique client_request_id, and retries must reuse that value. gh-broker sends the approval request to Telegram when Telegram is configured, but the durable operator inbox remains authoritative and works without Telegram. Approval creates a short-lived brokerkit grant that is evaluated by the same policy path as static rules. Deny rules still win over approved grants.

The editable Telegram message reference and its last delivered lifecycle state are stored with the grant. Once the reference is stored, status delivery resumes after a broker restart. A verified callback commits the decision and message reference in one transaction, including when the original send response was lost. Durable write failures leave the callback unanswered at the same Telegram update for retry. Ambiguous sends remain blocked for the two-minute claim lease; a later retry uses a fresh one-time decision token. Grant uses are reserved before GitHub forwarding; an ambiguous upstream result is retained for operator review instead of reopening the grant budget. Retained grants report status: retained and uses_remaining: 0.

License

MIT

Directories

Path Synopsis
cmd
gh-broker command
internal
appmanifest
Package appmanifest generates minimum GitHub App permission manifests.
Package appmanifest generates minimum GitHub App permission manifests.
approval
Package approval renders GitHub-specific operator approval details.
Package approval renders GitHub-specific operator approval details.
ghplan
Package ghplan owns immutable GitHub execution plans.
Package ghplan owns immutable GitHub execution plans.
githubauth
Package githubauth owns every GitHub credential used by gh-broker.
Package githubauth owns every GitHub credential used by gh-broker.
githubdoctor
Package githubdoctor verifies local isolation and GitHub-native enforcement.
Package githubdoctor verifies local isolation and GitHub-native enforcement.
githubsurface
Package githubsurface validates the complete generated GitHub operation surface.
Package githubsurface validates the complete generated GitHub operation surface.
graphqlmanifest
Package graphqlmanifest owns reviewed persisted GitHub GraphQL documents.
Package graphqlmanifest owns reviewed persisted GitHub GraphQL documents.
inventory
Package inventory owns exhaustive dispositions for pinned GitHub surfaces.
Package inventory owns exhaustive dispositions for pinned GitHub surfaces.
mcpcatalog
Package mcpcatalog filters typed GitHub MCP tools and pages discovery.
Package mcpcatalog filters typed GitHub MCP tools and pages discovery.
opbinding
Package opbinding loads immutable generated GitHub REST bindings.
Package opbinding loads immutable generated GitHub REST bindings.
opcatalog
Package opcatalog owns the generated GitHub capability vocabulary.
Package opcatalog owns the generated GitHub capability vocabulary.
operations
Package operations owns generated GitHub operation adapters.
Package operations owns generated GitHub operation adapters.
schemaregistry
Package schemaregistry owns closed generated GitHub operation schemas.
Package schemaregistry owns closed generated GitHub operation schemas.
targetregistry
Package targetregistry owns GitHub target kinds and safe policy fields.
Package targetregistry owns GitHub target kinds and safe policy fields.
upstream
Package upstream owns the pinned GitHub source snapshots used by generators.
Package upstream owns the pinned GitHub source snapshots used by generators.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL