Skip to content

Octopus (Self-Hosted PR Review)

Octopus is a self-hosted AI code-review tool. It receives GitHub pull-request webhooks, indexes the repository into a vector store, reviews diffs with an LLM, and posts inline severity-rated comments.

This cluster runs a fork (seanb4t/octopus) carrying a stack of four env-gated patches (see Fork state). LLM and embedding traffic is routed through the cluster LLM gateway (agentgateway), so private-repo content never leaves the cluster’s governed egress and inherits the OpenRouter ZDR guarantee where configured.

See ADRs: hl-6nm (split model routing), hl-78b (fork-and-contribute), hl-msh (dedicated statefuls), hl-8g4 (two-hostname split). Epic: hl-0we.

PieceDetail
Imageghcr.io/seanb4t/octopus (web) + ghcr.io/seanb4t/octopus-migrate, multi-arch, digest-pinned from the fork stack tip
WebOne Deployment in octopus ns; in-process pg-boss review worker (ENABLE_REVIEW_WORKERS=true); dnsConfig ndots:1 (alpine/musl + fzymgc.house search-domain trap)
Schemaoctopus-migrate initContainer runs prisma db push --accept-data-loss — octopus is schema-sync, not migration-based (the lone shipped migration cannot migrate deploy from scratch)
Vector storeDedicated qdrant-octopus (own Longhorn-encrypted PVC); code_chunks at 1024-dim (bge-m3, via EMBEDDING_DIM)
DatabaseDedicated octopus DB on the main CNPG cluster (pg-boss queue + app data)
LLM/embeddingsOPENAI_BASE_URL=https://llm-gw.fzymgc.house/v1 (agentgateway Gateway-API listener); internal CA trusted via NODE_EXTRA_CA_CERTS=/etc/ssl/fzymgc/fullchain.crt (mounted from the fzymgc-ica1-ca configmap — node needs the root; ca.crt alone fails UNABLE_TO_GET_ISSUER_CERT)
Auth (UI)Keycloak SSO (Better Auth genericOAuth; provider id intentionally stays authentik for backward compatibility with existing linked accounts — see tf/keycloak/octopus.tf) + magic-link; built-in social logins hidden (NEXT_PUBLIC_DISABLE_SOCIAL_LOGINS, baked). ADMIN_EMAILS gates platform admin and must match the Keycloak-surfaced email
Webhook (public)octopus-wh.fzymgc.net/api/github/webhook via the Cloudflare tunnel webhook_services entry; HMAC-verified (github_webhook_secret)
Admin UI (internal)octopus.fzymgc.house (router-hosts, LAN/Tailscale only)
GitOpsArgoCD Application octopusargocd/app-configs/octopus/
  1. GitHub sends a pull_request webhook to octopus-wh.fzymgc.net/api/github/webhook (HMAC-verified).
  2. Octopus enqueues a pg-boss process-review job; the in-process worker retrieves relevant code_chunks (dense + sparse RRF) from Qdrant.
  3. The review model is called through agentgateway → OpenRouter (or-deepseek-v4-pro by default).
  4. Findings post as inline comments with severity. Findings at/above the org’s checkFailureThreshold (default critical) submit the review as REQUEST_CHANGES; otherwise COMMENT.

Repo indexing (Create Index / Sync) embeds in batches of EMBEDDING_BATCH_ITEMS (64 here — see Embedding pipeline limits) through agentgateway → the seattle ollama box (bge-m3).

GitHub App (octopus-fzymgc, app id 3985029)

Section titled “GitHub App (octopus-fzymgc, app id 3985029)”
  • Setup URL must be set to https://octopus.fzymgc.house/api/github/callback with “Redirect on update” enabled. Without it, GitHub never redirects back after an install, the signed-state callback never fires, and organizations.githubInstallationId is never written — repos will not sync.
  • Always connect installations from octopus (“Connect GitHub” per org), never from GitHub’s settings page (that path has no state and cannot link).
  • One octopus org ↔ one GitHub installation (githubInstallationId is unique). A user may own up to 3 octopus orgs; create one per GitHub account/org to be reviewed.
  • The webhook secret cannot be read back from GitHub. To rotate: PATCH /app/hook/config (app JWT) + vault kv patch … github_webhook_secret=… with the same value, then force the ESO refresh. Webhook 401s = the two sides disagree.

Models live in the Octopus DB (available_models + org defaults), not GitOps. All are registered with provider=openai — required so octopus’s ai-router uses the OpenAI client (→ agentgateway) instead of falling back to Anthropic (unregistered, non-gpt-prefixed model IDs default to the Anthropic client and fail).

Model (agentgateway alias)CategoryRole
or-deepseek-v4-prollmorg default review model
or-minimax-m3, or-gemma-4-31b, or-gemma-4-31b-freellmselectable
ollama/bge-m3embeddingorg default embed model (1024-dim)

Avoid model IDs containing codex (octopus routes them to the OpenAI Responses API, which agentgateway does not serve). Do not set an org Anthropic key.

The fork is a thin staging ground for upstreamable patches (ADR hl-78b). Branches are stacked — each is one upstream-PR-sized feature — and the tip builds the cluster image:

Branch (each stacked on the previous)AddsCluster env
master (tracks upstream)
feat/configurable-embedding-dimEMBEDDING_DIM (Qdrant vector size), QDRANT_API_KEY support, db:deploy script, migrator image stage, multi-arch GHCR CIEMBEDDING_DIM=1024
feat/generic-oidc-authBetter Auth genericOAuth provider (works with any OIDC IdP) + NEXT_PUBLIC_DISABLE_SOCIAL_LOGINS login-button gatingOIDC_DISCOVERY_URL, OIDC_PROVIDER_ID=authentik, OIDC_CLIENT_ID/SECRET (Vault octopus-oidc, written by tf/keycloak/octopus.tf)
feat/configurable-embedding-batchEMBEDDING_BATCH_ITEMS (indexer + embeddings-client batch size)EMBEDDING_BATCH_ITEMS=64
feat/optional-review-brandingDISABLE_REVIEW_BRANDING (omit the “Reviewed by Octopus Review” footer)DISABLE_REVIEW_BRANDING=true
feat/file-recency-metadataINDEX_FILE_RECENCY (+ optional INDEX_FILE_RECENCY_WINDOW) — per-file lastModifiedAt chunk payload via a bare blob-less history scan at index timeINDEX_FILE_RECENCY=true
feat/doc-aware-analysisStructured analysis-chunk selection (doc cap, path diversity, recency) replacing the blind scroll, + ## Documentation Accuracy analysis section + doc-skeptical prompt framing
feat/recency-aware-review-contextAge annotations + stale-doc demotion in review context (both the webhook and local-review paths)
feat/configurable-max-orgs (tip)MAX_OWNED_ORGS_PER_USER env-gate for the per-user owned-org cap (default 3; <= 0 = unlimited)MAX_OWNED_ORGS_PER_USER=0

Image build: the fork’s .github/workflows/build-image.yaml triggers on every stack branch + master, and bakes build-time (NEXT_PUBLIC_*) values: GITHUB_APP_SLUG=octopus-fzymgc, OIDC_PROVIDER_ID=authentik, OIDC_PROVIDER_NAME=Authentik, DISABLE_SOCIAL_LOGINS=true. The cluster Deployment pins web + migrate digests from the tip branch’s build.

Adding fork work: branch from the current tip, add the new branch to the CI trigger list, push (build runs), then repoint the Deployment digests in a cluster PR.

Rebasing on upstream: rebase the stack bottom-up (embedding-dim onto master, then each branch onto its rebased parent), force-push the whole stack, rebuild from the tip, repoint digests.

Upstream contribution plan (hl-0we.4, P4): each stack branch is one upstream PR. Additionally file as issues/docs PRs:

  • Self-hosting docs: GitHub App Setup URL + Redirect on update are required for installation linking; schema init is prisma db push (not db:migrate).
  • Bug: duplicate-org creation — the (app)/layout.tsx auto-createOrgForUser can run twice around its own redirect (no “already has default org” guard).
  • Bug: stale current_org_id cookie pointing at a deleted org hard-loops every page to /complete-profile (pages lack the layout’s orgs[0] fallback).
  • Self-host polish: no-op the pubby realtime SaaS (api.pubby.dev) and ElasticSearch sync-log writers when unconfigured (currently constant unhandled-rejection noise; cosmetic — indexing/reviews work without them).

Cluster-specific, never upstreamed: hl-0we.17 (P3) — replace pubby with NATS and ES logging with ClickHouse on a separate private-fork track.

Hard-won operating envelope — change any of these and indexing breaks again:

LayerSettingWhy
octopus batch sizeEMBEDDING_BATCH_ITEMS=64upstream default 512 × ~24k-char chunks ≈ 40–60s/request on bge-m3 — exceeds any sane timeout
agentgateway buffermaxBufferSize: 32Mi (Gateway-scoped policy)AI processing buffers the full response; the 2 MiB default rejects large embed responses (“response was too large”)
route timeoutsllm-embeddings timeouts: {request: 300s, backendRequest: 120s}the ~30s default tore connections down mid-response (ollama logs 200 + broken pipe)
gateway retryllm-embeddings-retry policy (attempts: 4, backoff: 1s, codes: [503])octopus has no embed retry; one transient 503 aborts a whole repo index
encoding_formatmust pass through to ollamaopenai-node silently requests base64; stripping the param makes the SDK mis-decode float arrays 4:1 (1024 → 256 dims, Qdrant rejects). ollama ≥0.21 supports base64. Do not re-add a drop-encoding_format transformation
seattle ollamaOLLAMA_NUM_PARALLEL=4, OLLAMA_HOST=0.0.0.0:11434 (launchd plist)default parallelism 1 serializes overlapping requests → queued latency → timeouts. Restart with launchctl unload/load, never brew services restart — brew regenerates the plist and silently drops the env (including OLLAMA_HOST, taking ollama off-network)
Terminal window
vault kv patch secret/fzymgc-house/cluster/octopus openai_api_key="<new VK>"
kubectl -n octopus annotate externalsecret octopus-app force-sync="$(date +%s)" --overwrite
kubectl -n octopus rollout restart deploy/octopus
Terminal window
# after the fork tip builds:
docker buildx imagetools inspect ghcr.io/seanb4t/octopus:<sha> | grep Digest
# repoint both digests in argocd/app-configs/octopus/deployment.yaml, PR + merge

lastModifiedAt is stamped on chunks at index time (gated by INDEX_FILE_RECENCY). Repos indexed before the recency rollout read as “age unknown” (no age annotations, no stale-doc demotion) until re-indexed: Cancel + re-index from the repo page. The selection/prompt improvements (doc-aware context, ## Documentation Accuracy) apply regardless; only age-awareness needs the re-index.

Same-account repos: adjust the installation’s repository selection (GitHub side), then Sync in octopus. New GitHub account/org: create a new octopus org (org switcher → Create Organization) → Connect GitHub from that org → install → it links + syncs.

Terminal window
kubectl -n octopus scale deploy/octopus --replicas=0
# data is isolated to the octopus DB + qdrant-octopus PVC — no blast radius into
# engram/agent-memory. Uninstall the GitHub App to stop webhooks entirely.
SymptomCheck
Webhook deliveries 401Webhook secret mismatch (App vs Vault) — rotate both sides (see GitHub App section)
Repos don’t appear after installInstalled from GitHub’s settings page (no state) or Setup URL missing — organizations.githubInstallationId null; reconnect via octopus’s Connect GitHub
Index stuck “Indexing…” forever, pod idleThe fire-and-forget index task died without updating status (its progress logs go to the unconfigured pubby/ES, not stdout) — check Failed to index repo in pod logs, then Cancel + re-index
Embed 503/504sSee Embedding pipeline limits; watch agentgateway_requests_total{backend="agentgateway/ollama"} on the gateway metrics port (:15020/metrics)
Qdrant “expected dim 1024, got 256”encoding_format is being stripped en route to ollama (see limits table)
Redirect loop to /complete-profileStale current_org_id browser cookie pointing at a deleted org — clear the cookie (do not submit the form: it creates a duplicate org)
Failed to trigger event / sync log to ES log spamUnconfigured pubby/ElasticSearch — cosmetic (hl-0we.17 tracks the NATS/ClickHouse replacement)
Review never postskubectl -n octopus logs deploy/octopus ([reviewer] lines); confirm the model alias exists in agentgateway and available_models has it with provider=openai
Image pull failsghcr-pull-secret ExternalSecret in octopus ns (private seanb4t image)
ExternalSecret SecretSyncedError after a spec changeArgoCD SSA spec.data lag — delete the ExternalSecret (ArgoCD self-heals it fresh); deleting only the target Secret is not enough
  • Secrets: docs/reference/secrets.md (secret/fzymgc-house/cluster/octopus, …/octopus-oidc)
  • Manifests: argocd/app-configs/octopus/, Application argocd/cluster-app/templates/octopus.yaml
  • Keycloak OIDC app: tf/keycloak/octopus.tf
  • Spec/plan: docs/engineering/specs/2026-06-06-octopus-self-host-design.md, docs/engineering/plans/2026-06-06-octopus-self-host.md