Argus. Quickstart API

Self-hosted · LangGraph · Sandboxed

Argus.

The hundred-eyed reviewer for your pull requests. It clones the head commit, runs your real checks inside a sandbox, reads the surrounding code through tools, and publishes one honest review: findings anchored to file and line, a verdict, and a GitHub check run.

  • Node ≥ 22.12
  • Postgres + Redis
  • Providers OpenAI · OpenRouter · nan.builders · openai-compatible
  • Container docker compose
01

What it does

A review is not a diff summary. It is a claim about code that someone has to verify, so every finding carries the evidence that produced it.

Ground truth first

Your own test, lint and typecheck scripts run in a sandbox. A failing script is stronger evidence than an opinion.

Tools, not guessing

The model reads files, searches the repository and asks for more commands, bounded by a token, time and tool-call budget.

Evidence required

Critical, high and security findings must quote the line the model read, or the submission is rejected. A claim nobody can check is not published.

It remembers the PR

A re-review is a delta: it reports what is new and names what earlier runs found and this one did not raise again.

Self-hosted, not a hosted bot. You own the data, the model spend and the review policy. Every decision the agent makes is recorded and streamed.
02

Quickstart

Local development runs the three processes directly and keeps Postgres and Redis in containers. Five commands, then a review.

# 1 — dependencies
npm install

# 2 — your settings: AUTH_SECRET is the only one with no default
cp .env.example .env

# 3 — infrastructure only (Postgres + Redis)
npm run dev:infra

# 4 — schema, then the three processes
npm run db:generate && npm run db:migrate
npm run dev            # api :4000 · worker · web :3000

Open http://localhost:3000 and sign in. The API creates the first account from BOOTSTRAP_ADMIN_EMAIL / BOOTSTRAP_ADMIN_PASSWORD the first time it boots with an empty users table.

Order matters. Migrate before you boot. Without a schema the API fails with “table does not exist” — that is the missing step, not a broken install.

Optional: demo data

npm run db:seed      # seeded repositories, runs and findings + admin@example.com / change-me-please

The seed adopts any account that already owns the admin email instead of failing, and never overwrites a password — reseeding cannot invalidate a real account.

03

Review pipeline

One review, step by step. Every step is idempotent and streams events, so a crashed worker can be resumed.

  1. Triggerwebhook (verified, deduplicated by delivery id), API call, CLI, or retry.
  2. Enqueuea ReviewRun row with an idempotency key from (repository, pull request, head sha, trigger). Three webhooks from one push produce one review.
  3. Locka Redis lock on review:<pullRequestId> stops two workers reviewing the same pull request at once.
  4. Prepareshallow clone of the head commit into a workspace keyed by the run, plus PR metadata, commits and file list from GitHub.
  5. Classifychanged files are bucketed (source, test, docs, generated, …) and the plan decides which checks are worth running.
  6. Run checksplanned scripts execute in the sandbox; output is parsed into findings with real file and line anchors.
  7. Agent loopthe model reads files, searches the repository, requests commands and drafts findings, bounded by budget and permissions.
  8. Validateeach draft is schema-checked, deduplicated, calibrated against REVIEW_MIN_PUBLISH_CONFIDENCE and compared with the previous run. A model critique may only soften or discard — never strengthen.
  9. Finalizea verdict (passed / neutral / failed), a narrative and counts.
  10. Publishsummary comment found and updated in place via an embedded marker, plus a check run with annotations — or a pending approval when the gate is on.
Severity What it means for the check run
critical Blocks by default. The check fails and the finding is published inline.
high Blocks by default, same as critical. Both are configurable per repository.
medium Published, does not block unless you add it to the blocking set.
low · info Notes. Kept and shown, never blocking.
04

Architecture

An npm-workspaces monorepo. The API, the worker and the CLI share one container: same dependency set, different entrypoint.

Packages

PackageResponsibility
@acr/configEnvironment schema, typed configuration, logger, workspace paths
@acr/sharedDomain types, ports, finding validation, budgets, prompt security
@acr/databasePrisma schema, review store, queries, the seeder
@acr/githubGitHub App auth, read and publish clients, webhook verification
@acr/sandboxDocker and process command runners, workspace confinement
@acr/queueBullMQ client, job schemas, Redis locks, worker pool
@acr/aiLangGraph review graph, provider abstraction, tools, prompts
@acr/pipelineThe review use-case, publishing, container wiring

Apps

AppResponsibility
apps/apiFastify HTTP API, sessions, SSE event stream, webhook receiver
apps/workerBullMQ consumers, review reconciliation, health endpoint on :9100
apps/webNext.js dashboard: repositories, pull requests, runs, live events
apps/cliOne-shot review from the terminal, no dashboard needed
05

Configuration

Every variable is validated in packages/config/src/env.ts. Startup fails with the list of what is wrong instead of booting half-configured, and .env.example documents all of them.

VariableDefaultEffect
AUTH_SECRETSession signing key. Required, ≥ 32 chars
DATABASE_URLPostgres connection string
REDIS_URLOptional. Without it the system runs inline: in-process locks, in-memory events
SANDBOX_MODEdockerdocker | process | off
ALLOW_PROCESS_SANDBOXfalseMust be true for process mode: an explicit opt-in to the weaker sandbox
SANDBOX_NETWORKnonenone blocks package installs during checks
REVIEW_COMMAND_EXECUTIONinlineinline runs checks in the reviewing process; queued dispatches them as worker jobs (needs Redis, and compose sets queued)
LLM_PROVIDERopenai-compatibleopenai | openrouter | nan-builders | openai-compatible. The named ones carry a default base URL
LLM_BASE_URLExplicit endpoint; overrides the provider preset. Required for openai-compatible
LLM_MODELgpt-4o-miniModel id. OpenRouter uses vendor/model, nan.builders uses ids like glm5.3-flash
REVIEW_MAX_TOKENS200000Per-review token budget. Hitting it stops the run, it never silently truncates
REVIEW_MAX_DURATION_MS900000Per-review wall clock
REVIEW_MAX_TOOL_CALLS60Agent loop cap
REVIEW_MIN_PUBLISH_CONFIDENCE0.6Below it, findings are kept and shown but not published
REVIEW_REQUIRE_APPROVAL_FOR_PUBLISHfalseAdds the human gate before anything reaches GitHub
API_PUBLIC_URLhttp://localhost:4000Used for the dashboard link inside published comments

Provider presets

All four speak OpenAI Chat Completions, so tool calls and JSON mode behave identically. An explicit base URL always wins; openai-compatible has no default and needs one.

# OpenRouter
LLM_PROVIDER=openrouter
LLM_API_KEY=sk-or-...
LLM_MODEL=openai/gpt-4o
LLM_HTTP_REFERER=https://your-app.example   # optional app attribution
LLM_APP_TITLE=Argus                        # optional app attribution

# nan.builders
LLM_PROVIDER=nan-builders
LLM_API_KEY=sk-...
LLM_MODEL=glm5.3-flash

# Any other gateway, local or hosted
LLM_PROVIDER=openai-compatible
LLM_BASE_URL=https://gateway.internal/v1
LLM_MODEL=my-model

Per-repository policy

Stored in the database and editable in the dashboard or via PATCH /repositories/:id/settings. It can narrow or widen the deployment defaults inside the bounds the environment allows: which checks run, deep review, inline findings, summary comment, check run, ignored paths, minimum confidence, blocking severities, the approval gate and the agent's permission set.

06

Connecting GitHub

A GitHub App is the full path: webhooks and installation tokens. A personal access token is the quick path: reviews without webhooks.

GitHub App (recommended)

WhereWhat to set
github.com/settings/apps/newHomepage URL, and a Webhook URL pointing at your deployment
PermissionsContents: read · Issues: read & write · Pull requests: read & write · Checks: read & write · Metadata: read
EventsPull request
Values into .envGITHUB_APP_ID · GITHUB_APP_SLUG · GITHUB_PRIVATE_KEY_PATH · GITHUB_WEBHOOK_SECRET
Add repositories with Sync, not Add. “Add repository” records the repo with no installation, so the platform looks for GITHUB_TOKEN and fails without one. Sync imports the installation's list and keeps the installation id, which is what mints tokens.

Personal access token

A classic token with the repo scope, or a fine-grained token with Contents: read, Issues: read & write, Pull requests: read & write, Checks: read & write. Put it in GITHUB_TOKEN. It cannot receive webhooks, so reviews are triggered from the dashboard, the CLI or the API.

Local webhook testing

# expose the API (port 4000, not the web) and point the App's Webhook URL at:
#   https://<your-tunnel>/github/webhooks
cloudflared tunnel --url http://localhost:4000   # or ngrok http 4000

Saving the Webhook URL makes GitHub send a ping. A bad signature answers 401, a repeated delivery answers duplicate — that is the idempotency working, not an error. Redeliver from the App’s Advanced tab to see it.

07

Using it

Three doors to the same pipeline: the dashboard, the CLI, and HTTP.

Dashboard

  • Repositories — every repository the platform can review, with its counts, plus Add and Sync.
  • Repository detail — Trigger a review by number, the review policy, and access management.
  • Pull requests — review history per PR, Sync from GitHub, Review now.
  • Reviews — status, plan, usage, cost, node trace, findings, check runs, live events, approvals, SARIF export.

CLI

npm run review -- --repo owner/name --pr 42            # full review, no dashboard, no Redis
npm run review -- --repo owner/name --pr 42 --no-checks  # fastest: AI pipeline only
npm run review -- --repo owner/name --pr 42 --publish    # write the comment and check run
FlagEffect
--repo, --repositoryRepository as owner/name (required)
--pr, --numberPull request number (required)
--publishPost the summary, inline findings and check run. Off by default: a local trial must not surprise the author
--no-checksSkip test/lint/typecheck execution
--model <name>Override LLM_MODEL for this run
--freshForce a new run for the same head instead of reusing it
--json · --quietMachine-readable result · only the final result

The first run reads the repository and the pull request straight from GitHub and records both, so a PR shows up in the dashboard without waiting for a webhook. Exit code is non-zero when the run fails, so it works as a CI step.

08

HTTP API

Everything the dashboard uses, in plain HTTP with a session cookie. Routes are not prefixed: the API is either served on its own host or routed by your ingress.

MethodPathNotes
GET/healthNever fails, never rate limited: version, uptime, dependency probes
GET/ready200 only when the database answers; also lists configuration issues
POST/auth/loginRate limited, sets the acr_session cookie
GET/auth/meCurrent user, or 401
GET POST/repositoriesList what you can see; add a repository (admin)
POST/repositories/syncImport the App installation's repository list
PATCH/repositories/:id/settingsReview policy for one repository
GET PUT DELETE/repositories/:id/accessGrant, replace or revoke a user's access
GET/pull-requestsFilterable by repository, author, state
POST/pull-requests/:id/reviewTrigger a review; returns the run id immediately. Needs triage
POST/reviewsCreate a run by repository + number. Same triage rule as above
GET/reviews/:idStatus, plan, usage, cost, node trace
GET/reviews/:id/findingsPaginated, filterable by severity and category
POST/reviews/:id/publish · /rejectDecide the pending approval and publish the stored snapshot, or cancel
POST/reviews/:id/retryNew run for the same head with a fresh idempotency key. Needs triage
GET/reviews/:id/eventsServer-Sent Events: replay from Last-Event-ID, then tail live
POST/github/webhooksSignature-verified, delivery-deduplicated

Errors are one envelope — { code, message, details, requestId } — with codes mirroring the status: validation_error, unauthorized, forbidden, not_found, conflict, internal_error. Every response carries x-request-id, which is also the key in the logs.

09

Sandbox, permissions and cost

The model never decides what is safe. Budgets, permissions and publishing are computed from configuration, and the agent operates inside those bounds.

Sandbox modes

ModeWhat it gives you
dockerDefault. Container with CPU, memory and PID limits, a writable per-run workspace, --network none by default, --cap-drop ALL, no-new-privileges and a command allow-list
processRuns on the host. Requires ALLOW_PROCESS_SANDBOX=true, an explicit opt-in, and is meant for environments that cannot run containers
offNo command execution at all. The review reasons from the diff and the files it can read

Workspace confinement is enforced with real paths, so a symlink cannot escape the run's directory, and the command allow-list is derived from the repository manifest — the model can request a script, not invent a command.

Prompt injection

Pull request text is untrusted input from strangers. It is wrapped in a per-request random boundary, injection signals are detected and surfaced, and the instruction hints field is the repository owner's channel, not the author's.

Cost

  • Every run records tokens in, tokens out and an estimated cost, visible per run and per repository.
  • Budget limits (tokens, wall clock, tool calls) stop a run instead of letting it drift; the run records that it hit the limit.
  • A single surviving finding skips the adversarial critique — a deliberate cost decision, not an omission.
10

Deployment

docker-compose.yml defines postgres, redis, migrate, api, worker and web. Four commands and the stack is up.

docker compose build
docker compose up -d
docker compose logs -f api worker
# health:  GET /health (never fails)   ·   GET /ready (gates on the database)
  • One backend image with api and worker targets: same dependency set, different entrypoint.
  • The migrate service runs prisma migrate deploy and gates the API with service_completed_successfully, so nothing serves traffic on an old schema.
  • api and worker share the workspaces volume — that is what makes REVIEW_COMMAND_EXECUTION=queued work: the worker opens the exact path the API created.
  • Scale by adding workers; BullMQ plus the per-pull-request lock makes that safe.
  • The worker image carries the Docker CLI. Mount /var/run/docker.sock only if you accept that a container can then control the host daemon; it is commented out for that reason.

Before you call it production

  • Set NODE_ENV=production explicitly. Compose substitutes it from your .env, so a container run inherits development — and the production invariant checks only run when it is production.
  • Mount the App private key and point GITHUB_PRIVATE_KEY_PATH at the in-container path, or pass GITHUB_PRIVATE_KEY inline.
  • Set COOKIE_SECURE=true behind TLS, and keep real secrets in the environment rather than a committed file. The API already honors X-Forwarded-* through trustProxy.
  • Back up Postgres: the volumes hold every run, finding and approval. Redis needs its AOF, already enabled in compose, so accepted reviews survive a restart.
11

Testing

One command runs everything that is expected to pass.

npm run verify        # typecheck + typecheck:tests + lint + all test projects
npm run test:unit     # no database needed
npm run test:integration
npm run test:e2e
npm run test:e2e:web  # Playwright against the dashboard
Don’t run the suite while the stack is up. The e2e suite drives a review in-process against the same Redis the containers use, so a live worker can hold the pull-request lock and the test’s review comes back skipped / already_running. Stop the app containers first, or give the tests their own Redis.

The database-backed suites read the seeded fixtures, so run npm run db:seed once after a fresh volume. A review is a real HTTP and database flow in e2e, not a mocked one.

12

Troubleshooting

Real failures, and what they actually mean.

SymptomCause and fix
table does not existMigrations never ran. npm run db:migrate before booting.
Environment variable not found: DATABASE_URLThe Prisma CLI reads the .env from its working directory. The root scripts run it from the repo root with an explicit schema for exactly this reason; run them from the root.
Cannot find module '@acr/…' on buildStale *.tsbuildinfo next to a missing dist: tsc -b believes the projects are current and emits nothing. Delete the tsbuildinfo, or use the package’s clean script, which now removes both.
Repository has no GitHub App installationThe row came from Add repository (no installation) and GITHUB_TOKEN is empty. Sync the repository, or set a token.
api exits with pino-pretty errorLOG_PRETTY=true in an image without dev dependencies. The logger falls back to JSON and warns; set LOG_PRETTY=false.
Web build fails on /_global-errornext build ran with NODE_ENV=development. The image sets production for the build step; if you build by hand, do the same.
Review stuck in QUEUEDIts job is not in the queue. The worker reconciles on start: it re-dispatches QUEUED runs past the threshold that have no live job, so restart the worker once.
db:seed fails on user emailOlder builds upserted the admin by a fixed id and collided with the bootstrap account. Current code adopts the id the email already has.
401 on the webhookSignature mismatch: GITHUB_WEBHOOK_SECRET must be exactly the App’s secret. Also point the tunnel at the API (4000), not the web (3000).
13

Known limits

What this does not do yet, stated plainly.

  • A review is a model’s reading of your code. Evidence is required for blocking findings and the checks give ground truth, but a finding is a claim to verify, not a verdict you delegate.
  • A re-review reports a delta: it never claims a previous finding was fixed, only that this run did not raise it again.
  • Checks are npm-shaped. A repository whose scripts live elsewhere gets the AI review without the sandboxed checks.
  • SARIF export is generated by the dashboard from a run’s findings; uploading it to code scanning is your pipeline’s job, not automatic.
  • TLS, ingress, secret management and database backups are deployment concerns this repository does not ship.