Argus0.1.5

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 ingressgit.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.

guardrail

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.