28 apps told the user to paste an `hk-` key — input placeholders, settings help text, READMEs, the error thrown when the field is empty — and five of them validated that paste against /^hk-/ before sending it as a bearer. Nothing mints that prefix any more: cloud accepts pk- and sk-, IAM mints sk-. So the guard admitted a credential that authenticates nothing, and the panel failed later at the gateway with nothing on screen to explain it. The three leak detectors keep hk- and gain sk-. Salesforce's source scan and the two Zendesk tests exist to catch a key committed to source or echoed back through a request; retired keys are still in circulation and leak exactly as badly, while a detector that only knew hk- would have gone blind to every key minted from here on. `sk-` used to be the example of the foreign key the guard screened out — an OpenAI key. It cannot mean that now, and shape alone cannot tell one vendor's sk- from another's, so those comments say what the check really proves and point at the /v1/models call that proves the rest. The three tests that pinned "sk- is rejected" now pin "hk- is rejected", which is the invariant that survived. Verified: 2029 tests pass across the 28 packages; typecheck clean on 25 of them (hubspot, jupyter and teams fail identically at HEAD on JSX and ESM type conflicts in files this does not touch); salesforce validate-source green. Co-authored-by: Hanzo Dev <[email protected]>
6.4 KiB
@hanzo/github
Hanzo AI on GitHub. A GitHub App (webhooks + Octokit) that reviews pull
requests, triages new issues, and answers @hanzo mentions in issue and PR
comments — across open source and enterprise repos.
Built on the shared Hanzo foundation:
@hanzo/ai— the headless model client. Every model call goes throughcreateAiClient(...).chat.completions.create(...)againsthttps://api.hanzo.ai(/v1/...). This package never speaks raw HTTP to the gateway.@hanzo/iam— Hanzo identity. Used to validate an inbound Hanzo user token when a request is owner-scoped.
What it does
| Event | Action | Behaviour |
|---|---|---|
pull_request |
opened, synchronize, reopened |
Fetch the PR's unified diff, window it to the model budget (honest truncation note on large PRs), review it, and post a PR review (event: COMMENT). |
issues |
opened |
Summarize the issue, assign a severity, suggest labels from the repo's real labels only, draft a first response, comment, and apply the labels. |
issue_comment |
created containing @hanzo <question> |
Answer the question in context (issue/PR body + comment thread) and reply. A bare @hanzo summarizes the thread. |
Bot-authored comments and comments without @hanzo are ignored, so the app
never talks to itself.
Architecture
The cores are pure — total functions over their inputs, no network, no
Octokit, no @hanzo/ai. That is what makes the whole pipeline unit-testable
without a live gateway or a GitHub App:
webhook.ts verify X-Hub-Signature-256 HMAC (constant time) + route (event,payload) → intent
review.ts diff → window/truncate → prompt → review comment (pure)
triage.ts issue → prompt ; parse severity/labels/response (pure)
mention.ts issue/PR + thread + question → prompt → answer (pure)
The impure edges compose them:
config.ts the only env boundary → typed Config
hanzo.ts thin over @hanzo/ai — the ONE path to the model
github-api.ts thin Octokit .request() wrappers (getPRDiff, createReview, comment, addLabels, …)
app.ts Octokit App + webhook handlers → routeEvent → dispatch
server.ts HTTP server (health + graceful shutdown) around Octokit's node middleware
Create the GitHub App
GitHub → Settings → Developer settings → GitHub Apps → New GitHub App (or an org's settings for an org-owned app).
-
Webhook
- URL:
https://<your-host>/v1/github/webhooks(served byhanzoai/ingress). - Secret: a strong random string. This is
GITHUB_WEBHOOK_SECRET; the app verifiesX-Hub-Signature-256against it (HMAC-SHA256, constant time).
- URL:
-
Repository permissions
Permission Access Why Pull requests Read & write Read the diff, post reviews/comments. Issues Read & write Comment on issues, apply labels. Contents Read-only Repo metadata for context. Metadata Read-only Mandatory. -
Subscribe to events:
Pull request,Issues,Issue comment. -
Private key: generate one on the app page and download the
.pem. This isGITHUB_PRIVATE_KEY. The app mints a JWT from it and exchanges it for a per-installation token for every repo it operates on (Octokit handles this). -
App ID: shown at the top of the app page →
GITHUB_APP_ID. -
Install the app on the repos or the whole org you want covered.
Environment
Never commit secrets — env only (store in KMS, sync to k8s via KMSSecret).
| Var | Required | Default | Notes |
|---|---|---|---|
GITHUB_APP_ID |
yes | — | Numeric app id. |
GITHUB_PRIVATE_KEY |
yes | — | PEM. \n-escaped one-liners are un-escaped automatically. |
GITHUB_WEBHOOK_SECRET |
yes | — | Verifies X-Hub-Signature-256. |
HANZO_API_KEY |
no | '' |
sk-… gateway bearer. Empty ⇒ anonymous/limited models. |
HANZO_API_BASE_URL |
no | https://api.hanzo.ai |
Gateway. |
HANZO_MODEL |
no | zen5 |
Review/triage model (Zen, qwen3+). |
HANZO_MAX_DIFF_CHARS |
no | 120000 |
Diff budget before windowing. |
HANZO_IAM_SERVER_URL |
no | https://hanzo.id |
Token issuer (owner-scoping). |
HANZO_IAM_CLIENT_ID |
no | hanzo-github |
Audience for inbound Hanzo tokens. |
PORT |
no | 3000 |
Webhook server port. |
Run
pnpm --filter @hanzo/github build
pnpm --filter @hanzo/github start # node dist/server.js
GET /health→ readiness probe ({ status: "ok", model }).POST /v1/github/webhooks→ the webhook sink. Octokit's node middleware verifies the signature and dispatches into the registered handlers.
Embed it instead of running the server:
import { createApp, loadConfig } from '@hanzo/github';
const app = createApp(loadConfig()); // Octokit App with handlers wired
Deploy over hanzoai/ingress
Run as a stateless Deployment behind hanzoai/ingress (never nginx/caddy):
- Image
ghcr.io/hanzoai/github:<tag>(--platform linux/amd64), built by CI. - Route the app's webhook host to the Service; path
/v1/github/webhooks. - Secrets (
GITHUB_*,HANZO_API_KEY) viaKMSSecretfromkms.hanzo.ai— never in the manifest. livenessProbe/readinessProbe→GET /health. The server drains onSIGTERMfor zero-downtime rollouts.- Scale horizontally: handlers are stateless; GitHub retries deliveries, and the installation token is fetched per request.
GitHub Marketplace
To list publicly on the GitHub Marketplace: make the app public, complete the
Marketplace listing (name, logo, description, pricing plan — a free plan is
fine), and submit for review. The same webhook + permission model above applies;
Marketplace adds marketplace_purchase events for plan changes, which can be
handled later without touching the review/triage cores. Enterprises can install
the private app directly on their org without a Marketplace listing.
Test
pnpm --filter @hanzo/github test # vitest
pnpm --filter @hanzo/github typecheck # tsc --noEmit
The pure cores are covered exhaustively: HMAC signature verification, event
routing, diff windowing/truncation, review/triage/mention prompt assembly, label
parsing, @hanzo/ai request shaping (mocked), and the full route → dispatch →
post path (fake Octokit).