Skip to main content

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_ID
  • GITHUB_APP_SLUG
  • GITHUB_APP_PRIVATE_KEY
  • GITHUB_WEBHOOK_SECRET (note: no _APP_ prefix — matches what webhooks.py reads)

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:

EventAction
pull_request.openedEnqueue review job
pull_request.synchronizeUpdate 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.createdAuto-provision a customer-plan tenant (slug = org login, schema + role + alembic), then register + auto-link the install
installation.deletedUnregister 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

RepoPush 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:

FindingLinear state
open + ticket_url IS NULL(no sync — ticket creation is opt-in)
open + ticket_url IS NOT NULLstate_todo
in_progressstate_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 in tenant_secrets. Env fallback for local dev.
  • Linear projects are referenced by name in product docs (e.g. INGEST for 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:

VariableDefaultPurpose
VITE_POSTHOG_PROJECT_KEYunsetProject write key from the PostHog project. Empty = analytics off.
VITE_POSTHOG_API_HOSThttps://us.i.posthog.comCloud-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-python server-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() over REGISTERED_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-js blocking 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:

  1. Create src/hyrax/integrations/providers/<name>.py.
  2. Export a single class with the integration's surface (auth setup, list, get, sync).
  3. Resolve credentials via secrets.get_secret(tenant_slug, "<name>_api_key") — never read env vars directly except as a documented local-dev fallback.
  4. 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.