Integrations
Hyrax integrates with four external systems today: GitHub (App auth + webhooks + PR review), Linear (ticket sync), Anthropic (BYOK Anthropic-direct LLM path), and AWS Bedrock (Hyrax-paid Claude inference, the customer-plan default). Each integration's surface is one provider module under src/hyrax/integrations/providers/; cross-provider concerns (PR shape, ticket lifecycle) live one level up in src/hyrax/integrations/.
GitHub: App-based auth
Hyrax authenticates to GitHub as a GitHub App, not a personal access token. A tenant onboards their org by installing the App; Hyrax stores the installation ID; on every API call, Hyrax mints a short-lived installation token.
PER-API-CALL FLOW:
tenant_github_token ContextVar
← installation token (short-lived, ~1h)
← signed JWT (App private key, 10-min expiry)
← GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY
Storage: public.github_installations (cross-tenant — one tenant can have multiple installations). The token is fetched on demand, never cached past its TTL.
Local dev only: GH_TOKEN env var as a PAT fallback. Never used in deployed environments.
App config env vars:
GITHUB_APP_IDGITHUB_APP_SLUGGITHUB_APP_PRIVATE_KEYGITHUB_WEBHOOK_SECRET(note: no_APP_prefix — matches whatwebhooks.pyreads)
See GitHub App setup for the full setup procedure.
GitHub webhooks
Inbound webhooks land at one route. Verified against GITHUB_WEBHOOK_SECRET via HMAC-SHA256 on the raw body. The signature header is X-Hub-Signature-256.
Events handled:
| Event | Action |
|---|---|
pull_request.opened | Enqueue review job |
pull_request.synchronize | Update existing review (rolling checklist mode) or post new comments (scanner mode) |
pull_request.closed (hyrax/fix-* branch) | Find the matching observation_fix_attempts row by pr_url, stamp finished_at + outcome (merged / rejected), then drive the parent observation through apply_pr_merged_for_attempt (→ closed with close_reason='fixed') / apply_pr_rejected_for_attempt (→ triaged or terminal-closed once fix_attempts_count ≥ cap). #PI1.4, 2026-05-06 |
installation.created | Auto-provision a customer-plan tenant (slug = org login, schema + role + alembic), then register + auto-link the install |
installation.deleted | Unregister install + flip disabled_at on the customer tenant (audit history preserved; internal-plan tenants are not affected) |
A startup-time WARN fires if GITHUB_WEBHOOK_SECRET is missing. The single webhook route was audited clean in the 2026 webhook security review — no other routes accept webhook payloads.
Rotation grace window (CC6.1). The verifier accepts both the current and (when staged) the previous secret, sourced from AWSCURRENT + AWSPREVIOUS on the Secrets Manager secret (GITHUB_WEBHOOK_SECRET_AWS_SECRET_ID) in deployed envs, or GITHUB_WEBHOOK_SECRET + GITHUB_WEBHOOK_SECRET_PREVIOUS env vars in local dev. Rotation lands as aws secretsmanager put-secret-value … + a GitHub-UI update of the App's webhook secret; both old and new deliveries verify during the ~24h overlap.
PR review modes
review jobs run in one of two modes per the repo's configuration:
Scanner mode
Runs the audit pipeline with fast=True. Inline comments are posted via the GitHub Review API, anchored on (file_path, line). Each comment carries a Hyrax finding ID for traceability.
LLM mode
Pre-pass scanner produces hints; one Sonnet agent (src/hyrax/workflows/review/pr_review.py) takes the hints + diff + relevant repo context and emits up to 7 findings tiered as must_fix or consider. Output lands as one rolling checklist comment that updates in place across commits, rather than N inline comments — the rolling-checklist shape was chosen so PR conversation history doesn't fill up with stale Hyrax noise.
Mode selection is per-repo. Default is scanner mode; LLM mode is opt-in. Live per-mode cost: forecast(workflow="review", repo=...); the forecasting service splits scanner-vs-full client-side off cost_mid until per-mode forecasts ship server-side.
PR title convention
All Hyrax-created PRs (and issues) get a [Hyrax] prefix. Example: [Hyrax] fix: SQL injection in user lookup. Description bodies use real newlines, not \n escape sequences — a common shape regression when the PR creation path was rewritten.
PR push targets
| Repo | Push policy |
|---|---|
hyrax-app/hyrax (this one) | Direct push to develop is allowed |
The deploy substrate is infra/terraform/ driven by GitHub Actions in this repo.
Linear: ticket sync
integrations/providers/linear.py mirrors finding state into Linear cards. Sync is one-way (Hyrax → Linear). State mapping:
| Finding | Linear state |
|---|---|
open + ticket_url IS NULL | (no sync — ticket creation is opt-in) |
open + ticket_url IS NOT NULL | state_todo |
in_progress | state_in_progress |
closed + close_reason='fixed' | state_done |
closed + close_reason='expired' | state_done |
closed + close_reason='ignored' | state_cancelled (falls back to state_done) |
State updates require state UUIDs, not state names — Linear rejects name-based updates. The provider module carries the lookup helper.
Linear environment
LINEAR_API_KEY— per-tenant, stored intenant_secrets. Env fallback for local dev.- Linear projects are referenced by name in product docs (e.g.
INGESTfor ingest pipeline bugs); the API ID is resolved at sync time.
LLM providers — Hyrax-paid Bedrock (default) + operator-gated BYOK Anthropic-direct
Hyrax ships two inference vendors for Claude. Every tenant defaults to Hyrax-paid Bedrock: the inference call hits Hyrax's AWS account, AWS bills Hyrax, and AWS Cost & Usage Reports partition cost by tenant via the tenant_id cost-allocation tag stamped on every bedrock_request_metadata (see src/hyrax/llm/pydantic_ai/runner.py). BYOK Anthropic-direct is operator-gated via public.tenants.byok_enabled — flip the column true for select dev tenants (operator-shell tenant detail → BYOK access card) and they can configure an Anthropic API key; the auto-router then routes Anthropic-direct when a key is present. Tenants without the gate flipped pin Bedrock regardless of any stored key, and the secrets-write endpoint refuses Anthropic-key writes for them.
Submission-time routing (src/hyrax/jobs/submission.py::_maybe_default_to_bedrock_runner):
params.research_runner explicitly pinned? → use it (operator-only for non-defaults)
workflow is agent-loop (audit/improve/discover/review/task/benchmark)?
tenant byok_enabled=true AND anthropic_api_key in tenant_secrets?
→ leave default → pydantic-ai-sonnet (Anthropic-direct)
otherwise → pin pydantic-ai-bedrock-sonnet (Hyrax-paid Bedrock)
The Bedrock carve-out (_bedrock_runner_carve_out) lets the BYO-key gate skip when the resolved runner has provider="bedrock" — Bedrock auth is IAM-based (Fargate task role grants bedrock:InvokeModel on the inference profile ARN), so no Anthropic API key is required.
Every workflow has a Bedrock counterpart in DEFAULT_BEDROCK_RUNNER_BY_TYPE (src/hyrax/runners/catalog.py); the auto-route covers every customer-plan submission when byok_enabled=false. Agent-loop verbs (audit / scan / improve / discover / review / task / fix / benchmark) pin pydantic-ai-bedrock-sonnet; revalidate pins sdk-bedrock-haiku; learn / publish pin sdk-bedrock-sonnet; the meta_review synthesis pass pins sdk-bedrock-opus.
Anthropic-direct key resolution (BYOK path)
ANTHROPIC_API_KEY env (local dev fallback only)
← tenant_secrets.anthropic_api_key (deployed)
← Fernet-decrypted at job start
← InProcess: lives in worker ContextVar / Secret wrapper for job lifetime
← Fargate: written to AWS Secrets Manager scoped to the task ARN, injected by ECS task-def secrets block
The data plane never sees env vars carrying API keys in a way another tenant could observe. InProcess keeps the credential in worker-process memory under the per-process single-tenant invariant (one job per pod); Fargate scopes it to a per-task UID + namespace + IAM role.
Bedrock auth (Hyrax-paid path)
BedrockProvider() is constructed with no kwargs — boto3's default credential chain resolves AWS creds (env vars on local dev, IAM task role on Fargate). Per-tenant authorization is enforced at deploy time: the Fargate task role's bedrock:InvokeModel action is scoped to the inference profile ARN(s) listed in the bedrock_inference_profile_arns Terraform variable (see infra/terraform/envs/hydra-dev/). Today's runner ships us.anthropic.claude-sonnet-4-6 (the cross-region inference profile ID), Sonnet 4.6 only.
PostHog (frontend product analytics)
Decided 2026-05-07. PostHog is the product-analytics layer for the SPA — page views, click-stream, custom events, session replay. Frontend-only today (no posthog-python, no server-side events). Cloud-vs-self-host is a runtime config decision, not a code one.
Boot: apps/web/src/main.tsx calls initPostHog() from apps/web/src/integrations/posthog.ts before mounting <App />, so the first page-view is captured. Init is a no-op when VITE_POSTHOG_PROJECT_KEY is unset, which is the local-dev default.
Identify: apps/web/src/hooks/usePostHogIdentify.ts mounts inside ShellEntry and fires posthog.identify(user_id, { email, display_name, tier, shell }) once /api/me and /api/setup/status resolve. The hook also calls posthog.group('shell', bootShell) so analytics queries pivot per-shell (admin / tenant). Per-tenant grouping is deferred until /api/me exposes tenant_slug.
Env:
| Variable | Default | Purpose |
|---|---|---|
VITE_POSTHOG_PROJECT_KEY | unset | Project write key from the PostHog project. Empty = analytics off. |
VITE_POSTHOG_API_HOST | https://us.i.posthog.com | Cloud-US default. Flip to https://eu.i.posthog.com (cloud-EU) or your self-host URL when self-host lands. |
Privacy: session replay enabled with maskAllInputs: true and an extra mask selector for [data-secret], .secret-value, and input[type=password]. Mark any DOM node carrying tenant-secret display (admin BillingCard, secrets editor) with data-posthog-mask to extend the mask list.
CSP (when CloudFront ships an SPA-side response-headers policy — none today): allowlist the PostHog hosts you've configured:
script-src 'self' 'unsafe-inline'
connect-src 'self' https://us.i.posthog.com https://us-assets.i.posthog.com
worker-src 'self' blob:
For self-host, swap the cloud hosts for your self-host URL.
Not wired (deferred — separate decisions):
posthog-pythonserver-side events. Frontend autocapture covers product analytics; server-side adds value for billing-event correlation but doesn't ship today.- PostHog feature flags. Hyrax has a typed in-house resolver (
hyrax.config.feature_flags.flag()overREGISTERED_FLAGS) and isn't replacing it. - Cookie-consent banner. Privacy posture (GDPR scope, EU customer plan, DNT respect) is its own decision.
- Reverse-proxy for ad-blocker bypass. Only worth it if
posthog-jsblocking shows up in event-volume gaps post-launch.
Out-of-band Anthropic Console scrape
For cost reconciliation between Hyrax's llm_calls ledger and Anthropic's billing dashboard, an AppleScript+JS scrape pulls Console data into a flat file. Operator-only, not part of the runtime. See reference_anthropic_console_scrape for the org/workspace IDs, endpoint shapes, pagination, and caveats.
What's not integrated
- Slack. Notifications go through Linear; Hyrax has no direct Slack integration today. Alerting is "the operator subscribes to the Linear card."
- Generic webhooks (outbound). No public webhook surface. External systems read state via the REST API.
- Identity providers (SSO). Tenant-level — operators currently log in via tenant-issued credentials. Federated SSO is on the post-tenant-#2 list, not active.
Adding a new integration
The provider module pattern:
- Create
src/hyrax/integrations/providers/<name>.py. - Export a single class with the integration's surface (auth setup, list, get, sync).
- Resolve credentials via
secrets.get_secret(tenant_slug, "<name>_api_key")— never read env vars directly except as a documented local-dev fallback. - Cross-provider concerns (e.g. "what does a ticket mean") live one level up in
integrations/, not inside the provider.
Pre-existing tests are pattern-followable: tests/integrations/test_<name>_*.py.