Hosted deployment: Dispatcher and Workers on Vercel Sandbox
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 8/100
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
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:vercelandnode): 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, regionfra1), startsdockerd, runs our hardened container withdocker 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_unavailableafter 60 minutes without a claim.- Probe cache of 1 hour.
- The self-hosted default flips to
splitafter 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 (
opencodeis 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
dockerdand 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
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ExpidetGames/code-reviewer
-
area: app type: bug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
ExpidetGames/code-reviewer#308 ·
Maintainers usually reply within 1 day
-
type: docs
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
ExpidetGames/code-reviewer#94 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 25/100
ExpidetGames/code-reviewer#401 ·
Maintainers usually reply within 1 day
-
area: app type: chore
Difficulty 3/5 1-2 days Newbie friendliness 45/100
ExpidetGames/code-reviewer#393 ·
Maintainers usually reply within 1 day
-
area: runner type: bug
Difficulty 4/5 3-5 days Newbie friendliness 42/100
ExpidetGames/code-reviewer#390 ·
Maintainers usually reply within 1 day
All issues in ExpidetGames/code-reviewer
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Chocobozzz/PeerTube#7832 ·
Maintainers usually reply within 3 days
-
Pay to use?Open
Difficulty 1/5 Under an hour Newbie friendliness 66/100
darkreader/darkreader#16108 · 1 comment ·
Maintainers usually reply within 2 days
-
Add: Dak Nong TV SDOpencheck:passed streams:add
Difficulty 1/5 Under an hour Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
bug engine
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
frostney/GocciaScript#1746 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
agilepathway/label-checker#739 · 4 comments ·
Maintainers usually reply within 1 day