Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Hosted deployment: Dispatcher and Workers on Vercel Sandbox

Open
#316 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
8/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
docker, nextjs, postgresql, python, typescript
Domain
backend, cloud, devops

Research direction

This is an umbrella tracking issue, not a single task: pick one sub-issue from the MVP list and read its own text, starting with the gate spike #317 that the others depend on. The shared vocabulary and architecture are described here, and the code lives in the dispatcher/ project (TypeScript, Hono) and the Python Worker image. Done means the chosen sub-issue's acceptance points are merged, and the whole MVP is done when every listed sub-issue is merged and the end-to-end journeys pass on fakes.

Written by the indexing model from the issue text.

Description

area: ops type: feature

Tracking issue. Implements the idea of #130 (push runners) for a hosted deployment on Vercel, and is the umbrella for the sub-issues listed below.

Goal

Run Code Reviewer as a hosted deployment on Vercel: the App as a serverless project, a Dispatcher as a second Vercel project, and every review job executed by a short-lived Worker inside a fresh Vercel Sandbox VM. Self-hosted installations keep working unchanged and can move to the same Dispatcher and Worker split later.

Vocabulary

Used exactly like this in all sub-issues.

Term Meaning
App The Next.js app with the database. Receives GitHub webhooks, creates jobs, stores results, serves the UI. It never reviews.
Worker The isolated process tree that executes exactly one job. Variants: Diff-Worker (PR-Agent) and Dynamic-Worker (OpenCode engine). It knows only the App URL, the job id and a per-job credential.
Worker contract What the Worker may do with the per-job credential and nothing more: fetch job details, heartbeat, deliver the result. Identical no matter who started the Worker.
Dispatcher The middle layer. Learns that a job exists, claims it (with the dispatcher credential, today RUNNER_TOKEN), receives the per-job credential from the App and starts a Worker. It never reviews.
Trigger How the Dispatcher learns about a job: Poll-Trigger (asks the App) or Webhook-Trigger (the App calls it).
Dispatcher core Claim, per-job credential handling, concurrency cap, launch error handling. Platform-free.
Launcher How a Worker is started: Docker-Launcher (self-hosted: the Worker runs as its own process and user inside the already hardened container, no docker socket) or Sandbox-Launcher (a fresh VM from a golden snapshot that hosts Docker; the Worker runs there as our hardened container).

Other terms: the hardened container spec is one definition of the container flags (user, capabilities, no-new-privileges, read-only root, tmpfs, memory and pids limits, no volumes, devices or host namespaces). The golden snapshot is a snapshot of a VM with Docker installed and the Worker image pre-pulled; every job gets a fresh clone, and a snapshot never contains job data.

"Runner" is retired as a concept: today's Python runner process conflates Dispatcher (claim, cap) and Worker host (heartbeat, supervise, deliver). Existing identifiers (RUNNER_* variables, image and service names, /api/runners/v1) stay until a dedicated rename issue.

We build and test only Poll-Trigger with Docker-Launcher (self-hosting, today's behavior) and Webhook-Trigger with Sandbox-Launcher (the hosted deployment). Other combinations are configuration only, later.

Architecture summary

  • App (Next.js, Postgres): enqueues jobs, mints a per-job credential at claim, serves the Worker API (/api/workers/v1), sends a signed wake-up webhook after a job is enqueued, and runs a cron sweep (lease expiry, resends, give-up, cleanup).
  • Dispatcher (TypeScript on Hono, dispatcher/, two entries: vercel and node): verifies the signed webhook, claims exactly that job (the App enforces a pool cap atomically), and launches a Worker. It never sees the git token or the model key; those are minted by the App only when the Worker fetches the job.
  • Sandbox-Launcher: creates a VM from the golden snapshot (persistent:false, region fra1), starts dockerd, runs our hardened container with docker run (flags rendered from the one container spec, credential on stdin only), and destroys the VM after the job.
  • Worker (Python, same image and same startup as self-hosted): fetches its job, heartbeats and delivers the result with the per-job credential.
  • A Dynamic-Worker is advertised only if a probe of the real container hardening passed in a canary VM (fail closed). Diff jobs are unaffected.

Decisions

  • Image for the golden snapshot comes from Vercel Container Registry.
  • A separate Sandbox image target without the Dispatcher bundle.
  • dispatch_unavailable after 60 minutes without a claim.
  • Probe cache of 1 hour.
  • The self-hosted default flips to split after a few weeks without problems.
  • A failed sandbox launch counts as a failed attempt (no separate budget).
  • Region fra1; no failover region (snapshots are region-bound).
  • Vercel's firewall is not part of our security boundary. All hardening comes from our container, run through Docker inside the Sandbox VM. If it cannot be established, the Dynamic-Worker fails closed (opencode is not advertised).
  • VM sizes. Each vCPU includes 2 GB of memory, so the smallest Sandbox is 1 vCPU / 2 GB. Dynamic-Worker VM: 1 vCPU / 2 GB, with the Worker container limited to about 1.5 GB to leave room for dockerd and the OS. Diff-Worker VM: the smallest size Vercel allows. The spike measures real peak memory; if the Dynamic-Worker is OOM-killed at 1.5 GB, step up one size. VM size and container memory limit are configuration per Worker kind, not constants. (The 2 GB cap of the self-hosted Dynamic compose service was only a cap and was never measured.)

Milestones

  • MVP (staging on Vercel with Diff and Dynamic): the spike gates the Sandbox-Launcher; everything else can run in parallel on four tracks (App, Worker, Dispatcher, Ops). Critical path: spike, container spec and Sandbox-Launcher, probe, end-to-end journeys. Estimate: about 43 engineer-days, roughly five weeks wall-clock with parallel tracks.
  • Phase 2 (self-hosted split): Poll-Trigger and Docker-Launcher, then flipping the default after a soak. Estimate: about 6 engineer-days. No behavior change until the flip.
  • Later: remove the legacy loop, warm pool, opt-in container-per-job host, usage guard.

Tasks

MVP

Gate

  • #317 Spike: prove the hardened Worker container inside a Sandbox VM. Depends on: platform access only.

Contracts

  • #318 Contracts, generated types and fake App. Depends on: none.

App track

  • #319 Serverless runtime mode. Depends on: none.
  • #320 Worker API and per-job credential. Depends on: #318.
  • #321 Serverless-safe first-run setup. Depends on: #319.
  • #322 Claim by job id, pool cap, release, version. Depends on: #320.
  • #326 Webhook-Trigger (dispatch webhook out). Depends on: #318, #322.
  • #328 Cron sweep, lease expiry, dispatch_unavailable. Depends on: #326, #319.

Worker track

  • #323 Worker job host, Worker API client, worker --once. Depends on: #318.
  • #324 Image entry modes, Sandbox image target, probe. Depends on: #323.

Dispatcher track

  • #327 Dispatcher project, core, Webhook-Trigger, entry points. Depends on: #318, #322.
  • #325 Hardened container spec and renderer. Depends on: #318, #324 (informed by #317).
  • #329 Sandbox-Launcher and sandbox fake. Depends on: #327, #325, #317.

Ops track

  • #330 Golden snapshot pipeline. Depends on: #324, #317.
  • #331 Capability probe, fail-closed Dynamic-Worker, deploy canary. Depends on: #324, #329, #330.

Proof and docs

  • #332 End-to-end journeys on fakes. Depends on: #328, #329, #331.
  • #333 Documentation: deployment and migration guide (part 1). Depends on: #328, #321, #330, #331, #332.
Phase 2 (self-hosted split)
  • #334 Poll-Trigger, Docker-Launcher, RUNNER_MODE=split. Depends on: #324, #327.
  • #335 Flip the self-hosted default to split. Depends on: #334, #332, a soak of a few weeks.
Later
  • #336 Remove the legacy runner loop and legacy claim payload. Depends on: #335.
  • #337 Warm pool of fresh sandbox clones. Depends on: #329.
  • #338 Opt-in LocalDockerHost (container per job). Depends on: #325, #329.
  • #339 Usage guard and per-run usage recording. Depends on: #329 (coordinates with #272).

Related existing issues

  • #130 push runners: implemented by this tracking issue for a single configured target (webhook plus Dispatcher). Registering several targets in the admin portal stays in #130.
  • #82 egress gate: still open and still the hosted egress gate. The Sandbox deployment does not rely on Vercel's firewall, and the spike only records what the container can reach.
  • #202 Dynamic Review packaging and safe operation: the Dynamic-Worker image must keep its pins, user layout and fail-closed self-check; coordinate with the Sandbox image target.
  • #187 cross-instance Postgres tests: matter more with many serverless instances; close before the first production activation (not a blocker for staging).
  • #272 and #277 operator usage records and limits: related to the later usage guard.

Risks

Nested Docker in the VM (cgroup limits, nf_tables in the container namespace, seccomp, storage driver) is unverified until the spike; cold start is unmeasured (target at most 45 s from claim to the first Worker heartbeat); whether code in the container can reach the VM's host channel is decided by the spike; the platform operator can read job secrets by construction (the boundary protects against hostile pull request content and the model, not against the platform); a long platform outage can fail reviews after three failed launch attempts.

Done when

Every sub-issue of the MVP is merged, the end-to-end journeys on fakes are green, and the manual spike checklist has been repeated on the production golden snapshot before the first Dynamic activation.

Dominant language
TypeScript
Stars
0
Forks
1
Avg merge
1h 15m
Merged PRs (30d)
217

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ExpidetGames/code-reviewer

All issues in ExpidetGames/code-reviewer

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.