Reference
Architecture
The system in one diagram
flowchart LR
subgraph ado["Azure DevOps (any onboarded repo)"]
hooks["Service hooks<br/>PR created / updated / commented"]
rest["ADO REST API<br/>(threads, replies, resolve)"]
end
subgraph argus["Argus"]
ingress["1 · Ingress<br/>validate · dedup · 202 fast"]
queue["2 · Queue<br/>per-PR sessions (FIFO)"]
worker["3 · Worker (ephemeral job)<br/>Claude Agent SDK harness<br/>over a read-only checkout"]
mcp["5 · ADO MCP server<br/>host-side credentials<br/>(the only mutation path)"]
state[("4 · State store<br/>Postgres")]
rules[("6 · Rules registry<br/>Postgres, published<br/>from the rules repo")]
end
hooks --> ingress --> queue --> worker
rules -- "routed rule packs" --> worker
worker <--> state
worker --> mcp --> rest
Figure 1 · A PR event becomes posted threads: ingress validates, the queue orders per PR, the worker reviews, and every mutation flows through the MCP server.
Review-turn sequence
sequenceDiagram participant ADO as Azure DevOps participant I as Ingress participant Q as Queue participant W as Worker participant M as ADO MCP server ADO->>I: service hook (PR event) I->>I: auth · self-filter · dedup I-->>ADO: 202 (fast, no model call) I->>Q: job (session = PR key) Q->>W: consume (per-PR FIFO) W->>W: rules + checkout + review W->>M: create_inline_thread / reply M->>ADO: post to PR thread
Figure 2 · One review turn — the ingress answers 202 before any model call, and the worker posts back through the MCP server.
Components
| # | Component | Package | Notes |
|---|---|---|---|
| 1 | Ingress | @argus/ingress |
Fastify webhook endpoint: Basic auth + shared-secret header, self-trigger drop, actionability checks against re-fetched ADO data, (subscription_id, notification_id) dedup, event→job mapping, fast 202 |
| 2 | Queue | @argus/queue |
Queue contract: MemoryQueue (in-process, per-PR FIFO sessions) and ServiceBusQueue (Azure Service Bus with sessions; Entra credential primary, connection string fallback) |
| 3 | Worker | @argus/worker |
Per-job pipeline: session → rules → read-only checkout → tier selection → ModelRunner (AgentSdkRunner or MockRunner) → validated findings → dedup + caps → ADO writes → audit |
| 4 | State store | @argus/db |
reviewer Postgres schema, idempotent migration runner, typed repos; tests run on pg-mem |
| 5 | ADO MCP server | @argus/mcp-ado |
The five-tool mutation surface, host-side credentials, per-PR confinement, mutation budgets with typed refusals |
| 6 | Rules registry | @argus/rules |
Registry-routed rule packs with default fallback and path-glob scoping, repo-rules discovery from the checkout, prompt assembly + prompt_sha, publish/drift tooling |
| — | Contracts | @argus/shared |
Every cross-package type: jobs, findings, queue, rules, AdoClient, runner, checkout, config, redacting logger |
| — | ADO client | @argus/ado |
RestAdoClient (ADO REST 7.1; Entra ID primary, PAT fallback; retry/backoff, typed errors) and FakeAdoClient (in-memory, for offline runs and evals) |
| — | Evals | @argus/evals |
Golden-PR harness: per-tenant corpus, deterministic scoring, baseline regression gate |
| — | Docs server | @argus/docs-server |
Serves this site (fastify + @fastify/static) from the same image, bin/dist/argus-docs.js |
Composition roots live in bin/ (argus-ingress.ts, argus-worker.ts,
argus-rules.ts, argus-evals.ts, argus-migrate.ts, argus-docs.ts) with
all infra/mock switches in bin/lib/wiring.ts. One container image runs every
component by overriding CMD. Each package has its own page — start at the
packages overview.
Event → job mapping
| ADO event | Condition | Job type |
|---|---|---|
| PR created | targets a protected branch | review.full |
| PR updated | head commit actually changed | review.incremental |
| PR commented on | reply in a thread Argus owns | thread.reply |
| PR commented on | comment @-mentions Argus | mention.answer |
| PR commented on | neither | dropped at ingress |
"Head commit changed" is evaluated at ingress — git.pullrequest.updated
also fires for votes and status churn, so the ingress re-fetches the PR and
compares its head SHA against last_reviewed_sha before queueing anything.
The MCP tool surface
The worker checks the PR merge commit out into an ephemeral read-only
workspace; repo reading needs no custom tools (the Agent SDK's built-in
Read/Grep/Glob operate on the checkout, and all diff text comes from an
allowlisted git diff). The custom surface is one small MCP server for ADO —
five tools, and the three mutating ones are the only path by which the
agent affects ADO:
| Tool | Purpose | Mutating |
|---|---|---|
get_pr_metadata |
Title, description, branches, author, status | No |
list_pr_threads |
Existing threads, status, authorship | No |
post_thread_reply |
Reply to an existing thread | Yes |
create_inline_thread |
New thread anchored to file + line range | Yes |
resolve_thread |
Mark a thread resolved | Yes |
Every tool executes host-side with the worker's credentials — the model never holds a token. Mutating tools are budget-limited per PR with typed refusals.
Keeping the surface this small is deliberate: no approve, no merge, no push tool exists. The agent cannot cross a boundary that has no tool.
Security model
Distilled from the PRD's security requirements — these are requirements, not suggestions:
| Requirement | Enforcement |
|---|---|
| Webhook authentication | ADO service hooks do not sign payloads, so the ingress requires Basic auth and a high-entropy shared-secret header (both constant-time compared, uniform 401), and treats the body as untrusted: IDs only, everything re-fetched from the ADO REST API before acting. |
| Credential isolation | The ADO credential is held by the worker, never placed in model context, a prompt, or a message. The agent emits a tool call; the host executes it with its own auth. The structured logger redacts any field whose key matches a credential pattern as a backstop, and config errors name variables, never values. |
| Workspace sandbox | Each job runs against an ephemeral, read-only checkout (credential-free clone URL, no writable remote); workspaces are never reused across PRs or tenants; Bash is allowlisted (git diff, git log); production egress is restricted to the inference endpoint, ADO, Postgres, and Service Bus. |
| Prompt-injection posture | The diff, title, description, and comments are attacker-controllable. The core prompt pins them as data, not instructions; output is schema-validated with closed enums (free text never drives a code path); the agent cannot set approval/status/merge state because no such tool exists; and the structured output carries an injection_suspected flag surfaced in logs and evals. |
| Loop prevention | Events authored by the Argus identity are dropped at ingress before any model call, plus hard caps: comments per PR per hour, findings per PR, and a per-thread reply depth with an explicit bow-out. |
Versioning end to end
ARGUS_VERSION / ARGUS_GIT_SHA are baked into the image at build and
surface everywhere through serviceVersion(): GET /health on the ingress
and this docs server, every log line, eval report headers, and the audit
rows in reviewer.event_log — so any comment Argus ever posted is traceable
to model + prompt_sha + build.
