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.
Architecture
Section titled “Architecture”| Piece | Detail |
|---|---|
| Image | ghcr.io/seanb4t/octopus (web) + ghcr.io/seanb4t/octopus-migrate, multi-arch, digest-pinned from the fork stack tip |
| Web | One Deployment in octopus ns; in-process pg-boss review worker (ENABLE_REVIEW_WORKERS=true); dnsConfig ndots:1 (alpine/musl + fzymgc.house search-domain trap) |
| Schema | octopus-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 store | Dedicated qdrant-octopus (own Longhorn-encrypted PVC); code_chunks at 1024-dim (bge-m3, via EMBEDDING_DIM) |
| Database | Dedicated octopus DB on the main CNPG cluster (pg-boss queue + app data) |
| LLM/embeddings | OPENAI_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) |
| GitOps | ArgoCD Application octopus → argocd/app-configs/octopus/ |
Review flow
Section titled “Review flow”- GitHub sends a
pull_requestwebhook tooctopus-wh.fzymgc.net/api/github/webhook(HMAC-verified). - Octopus enqueues a pg-boss
process-reviewjob; the in-process worker retrieves relevantcode_chunks(dense + sparse RRF) from Qdrant. - The review model is called through agentgateway → OpenRouter
(
or-deepseek-v4-proby default). - Findings post as inline comments with severity. Findings at/above the org’s
checkFailureThreshold(defaultcritical) submit the review asREQUEST_CHANGES; otherwiseCOMMENT.
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/callbackwith “Redirect on update” enabled. Without it, GitHub never redirects back after an install, the signed-statecallback never fires, andorganizations.githubInstallationIdis 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
stateand cannot link). - One octopus org ↔ one GitHub installation (
githubInstallationIdis 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.
Model configuration
Section titled “Model configuration”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) | Category | Role |
|---|---|---|
or-deepseek-v4-pro | llm | org default review model |
or-minimax-m3, or-gemma-4-31b, or-gemma-4-31b-free | llm | selectable |
ollama/bge-m3 | embedding | org 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.
Fork state (seanb4t/octopus)
Section titled “Fork state (seanb4t/octopus)”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) | Adds | Cluster env |
|---|---|---|
master (tracks upstream) | — | — |
feat/configurable-embedding-dim | EMBEDDING_DIM (Qdrant vector size), QDRANT_API_KEY support, db:deploy script, migrator image stage, multi-arch GHCR CI | EMBEDDING_DIM=1024 |
feat/generic-oidc-auth | Better Auth genericOAuth provider (works with any OIDC IdP) + NEXT_PUBLIC_DISABLE_SOCIAL_LOGINS login-button gating | OIDC_DISCOVERY_URL, OIDC_PROVIDER_ID=authentik, OIDC_CLIENT_ID/SECRET (Vault octopus-oidc, written by tf/keycloak/octopus.tf) |
feat/configurable-embedding-batch | EMBEDDING_BATCH_ITEMS (indexer + embeddings-client batch size) | EMBEDDING_BATCH_ITEMS=64 |
feat/optional-review-branding | DISABLE_REVIEW_BRANDING (omit the “Reviewed by Octopus Review” footer) | DISABLE_REVIEW_BRANDING=true |
feat/file-recency-metadata | INDEX_FILE_RECENCY (+ optional INDEX_FILE_RECENCY_WINDOW) — per-file lastModifiedAt chunk payload via a bare blob-less history scan at index time | INDEX_FILE_RECENCY=true |
feat/doc-aware-analysis | Structured analysis-chunk selection (doc cap, path diversity, recency) replacing the blind scroll, + ## Documentation Accuracy analysis section + doc-skeptical prompt framing | — |
feat/recency-aware-review-context | Age 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(notdb:migrate). - Bug: duplicate-org creation — the
(app)/layout.tsxauto-createOrgForUsercan run twice around its own redirect (no “already has default org” guard). - Bug: stale
current_org_idcookie pointing at a deleted org hard-loops every page to/complete-profile(pages lack the layout’sorgs[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.
Embedding pipeline limits
Section titled “Embedding pipeline limits”Hard-won operating envelope — change any of these and indexing breaks again:
| Layer | Setting | Why |
|---|---|---|
| octopus batch size | EMBEDDING_BATCH_ITEMS=64 | upstream default 512 × ~24k-char chunks ≈ 40–60s/request on bge-m3 — exceeds any sane timeout |
| agentgateway buffer | maxBufferSize: 32Mi (Gateway-scoped policy) | AI processing buffers the full response; the 2 MiB default rejects large embed responses (“response was too large”) |
| route timeouts | llm-embeddings timeouts: {request: 300s, backendRequest: 120s} | the ~30s default tore connections down mid-response (ollama logs 200 + broken pipe) |
| gateway retry | llm-embeddings-retry policy (attempts: 4, backoff: 1s, codes: [503]) | octopus has no embed retry; one transient 503 aborts a whole repo index |
| encoding_format | must pass through to ollama | openai-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 ollama | OLLAMA_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) |
Operations
Section titled “Operations”Rotate the agentgateway virtual key
Section titled “Rotate the agentgateway virtual key”vault kv patch secret/fzymgc-house/cluster/octopus openai_api_key="<new VK>"kubectl -n octopus annotate externalsecret octopus-app force-sync="$(date +%s)" --overwritekubectl -n octopus rollout restart deploy/octopusUpdate the fork image
Section titled “Update the fork image”# 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 + mergeBackfill file-recency metadata
Section titled “Backfill file-recency metadata”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.
Add another repository / GitHub org
Section titled “Add another repository / GitHub org”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.
Rollback
Section titled “Rollback”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.Troubleshooting
Section titled “Troubleshooting”| Symptom | Check |
|---|---|
| Webhook deliveries 401 | Webhook secret mismatch (App vs Vault) — rotate both sides (see GitHub App section) |
| Repos don’t appear after install | Installed 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 idle | The 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/504s | See 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-profile | Stale 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 spam | Unconfigured pubby/ElasticSearch — cosmetic (hl-0we.17 tracks the NATS/ClickHouse replacement) |
| Review never posts | kubectl -n octopus logs deploy/octopus ([reviewer] lines); confirm the model alias exists in agentgateway and available_models has it with provider=openai |
| Image pull fails | ghcr-pull-secret ExternalSecret in octopus ns (private seanb4t image) |
ExternalSecret SecretSyncedError after a spec change | ArgoCD SSA spec.data lag — delete the ExternalSecret (ArgoCD self-heals it fresh); deleting only the target Secret is not enough |
References
Section titled “References”- Secrets:
docs/reference/secrets.md(secret/fzymgc-house/cluster/octopus,…/octopus-oidc) - Manifests:
argocd/app-configs/octopus/, Applicationargocd/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