Aster reviews pull requests with AI. That means running static analysis, tests, and untrusted code from arbitrary repositories. You cannot do that on the box that holds your secrets, so the interesting part of Aster is not the model call. It is the isolation layer.
Here is the shape of the system.
The two halves
The backend is Rust. It receives the webhook, pulls the diff, runs a Semgrep gate first so that cheap static findings never reach a paid model, then asks the orchestrator to run anything that needs execution.
The orchestrator is a small Node service using dockerode. It does one job well: turn a request into a throwaway container, run the work, capture the output, and destroy the container.
// one request, one container, no shared state
const container = await docker.createContainer({
Image: SANDBOX_IMAGE,
Cmd: ["semgrep", "--config", "auto", "--json", "/work"],
HostConfig: {
AutoRemove: true,
NetworkMode: "none",
Memory: 512 * 1024 * 1024,
},
});Three flags carry most of the safety:
NetworkMode: "none"— untrusted code cannot phone home.Memorycap — a runaway analysis cannot take the host down.AutoRemove— no container outlives its request.
The request lifecycle
Put the pieces in motion and a single review looks like this:
Every container is born for one request and gone before the next. Nothing the untrusted code touches survives, and the box holding the secrets never runs it.
Why self-hosted
Managed sandboxes exist, but building the orchestrator directly bought three things: control over the container image, tunable concurrency (it holds 50+ concurrent runs on a single VPS), and cost that does not scale with a vendor’s per-invocation price.
What I would change next
The current design trusts the container boundary. The next iteration moves toward a microVM per run, which closes the kernel-sharing gap that Docker leaves open. That is the honest limitation of this version, and naming it is more useful than pretending it is not there.