Skip to content

feat(selfhost): review-enrichment service scaffold — hono server, bearer auth, config loader + Railway deploy #1473

Description

@JSONbored

Context

Build the standalone review-enrichment/ Node/TS microservice (REES) that the engine seam calls. THIS issue ships the scaffold only: an HTTP server with health/ready + a /v1/enrich endpoint that authenticates, reads per-repo config, runs the orchestrator skeleton (no analyzers yet — returns an empty/partial:false brief), and deploys to Railway. Per-analyzer logic lands in separate issues behind this seam. Mirrors the engine's existing root Dockerfile/railway.json deploy pattern and the symmetric shared-secret bearer auth of src/auth/security.ts.

Requirements

  • Own Node/TS app under review-enrichment/ (NOT bundled into the engine or Worker). Runs NO LLM — purely deterministic + external-API analysis — so it cannot leak the AI subscription token and adds zero AI spend.
  • Symmetric shared-secret bearer auth: REES_SHARED_SECRET (operator-minted, e.g. createOpaqueToken('rees')) on BOTH engine and service env. The service verifies with a constant-time compare (node:crypto over sha256 bytes, byte-for-byte the pattern at src/auth/security.ts:84). Mismatch → 401 no body. Bind only on Railway's private network; /health//ready are unauthenticated liveness only.
  • Per-repo config read from GITTENSORY_REPO_CONFIG_DIR=/config/{owner}__{repo}/.gittensory.yml with a /config/.gittensory.yml global fallback — the SAME folder/global-fallback layout the branch already introduced for the other features:; the service reads the optional enrichment: block (analyzers list, license allow/deny, per-analyzer caps, timeoutMs).
  • The service NEVER throws into the engine: it always returns its best partial brief; 200 with partial:true when an analyzer is missing/degraded. Non-200 only for 401 (bad auth), 400 (malformed), 503 (overloaded).
  • HTTP contract: POST /v1/enrich with Authorization: Bearer, Idempotency-Key: <repo>#<pr>#<headSha>; request/response envelopes per the design (request carries diff+files so NO repo checkout is needed; response carries schemaVersion, analyzerStatus, partial, findings, promptSection, systemSuffix). The optional short-lived githubToken is held in-memory for the request only, NEVER logged or cached.

Deliverables

  • review-enrichment/package.json, src/server.ts (hono: /health, /ready, POST /v1/enrich), src/auth.ts (timingSafeEqual bearer mirror), src/config.ts (read GITTENSORY_REPO_CONFIG_DIR/{owner}__{repo}/.gittensory.yml + global fallback), src/brief.ts (orchestrator skeleton: Promise.allSettled over an empty analyzer set within budget.timeoutMs, assembles ReviewBrief JSON + analyzerStatus/partial), src/render.ts (ReviewBrief → public-safe promptSection, with the secret/value redaction discipline so a finding cites kind+line, never the value). Add a Redis per-headSha idempotency cache hook (the stack already runs Redis) keyed repo#pr#headSha.
  • review-enrichment/Dockerfile: multi-stage node:24-slim non-root, npm ci, bundle src/server.tsdist/server.mjs, EXPOSE 8080, HEALTHCHECK probing /ready, CMD ["node","dist/server.mjs"]. (Analyzer toolchain is baked in the per-analyzer issues that need it.)
  • review-enrichment/railway.json: DOCKERFILE builder with watchPatterns: ["review-enrichment/**"] (so engine-only pushes don't rebuild the service and vice-versa), healthcheckPath: "/ready", restartPolicyType: "ON_FAILURE", numReplicas: 1.
  • Deploy: as ghost@aethereal.dev create a NEW service in the existing gittensory-selfhost-test Railway project; set REES_SHARED_SECRET + REES_URL on both engine and service; push so Railway auto-deploys.

Acceptance criteria

  • /ready returns 200 in the deployed Railway service; /v1/enrich with a valid bearer returns a well-formed (empty-findings, partial:false) brief; a bad/missing bearer returns 401 with no body via constant-time compare.
  • The service depends on NO repo checkout (operates on the request diff+files) and never logs the githubToken or any secret value.
  • Unit tests for the auth bearer (match/mismatch, constant-time path), the config loader (per-repo + global fallback + missing-file), and the render redaction (a planted secret is never emitted verbatim). Service code is not measured by Codecov (only src/** is), but include the tests so the scaffold is regression-guarded.
  • npm audit --audit-level=moderate clean for the new package. (No src/** change in this issue, so no Codecov patch impact.)

Parent: #1029

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions