Arachne docs Home Open app

Learn

How it works

The architecture behind Arachne, the life of a fix from scan to pull request, the five verification gates, how usage is metered, and where the trust boundaries sit.

This guide follows a fix through the system. Every box and arrow below is something that exists in the deployed infrastructure or the code today; things that are not live yet are called out in Current limits.

System architecture#

Arachne runs on one hostname. CloudFront sends the application's route prefixes (/api/*, /auth/*, /v1/*, /mcp, /app, …) to API Gateway, and everything else (the public page, these docs, static files) to the Amplify-hosted static site. One container-image Lambda serves both the web API and the MCP server. A second Lambda, driven by EventBridge Scheduler, never runs a fix itself: it claims queued jobs and starts one CodeBuild build per job.

flowchart TB
  U["Browser or MCP client"] -- "HTTPS, Bearer token for /mcp" --> CF["CloudFront: one hostname"]
  CF -- "pages, docs, static" --> AMP["Amplify static site"]
  CF -- "/api /auth /v1 /mcp /app" --> GW["API Gateway"]
  GW --> API["API + MCP Lambda"]
  API --> IDS["Cognito, Secrets Manager"]
  API --> DDB[("DynamoDB: accounts, projects, findings, jobs, ledger, audit")]
  API -- "PR, approved only" --> GH["GitHub"]
  EB["Worker Lambda, EventBridge every minute"] -- "claim job" --> DDB
  EB -- "StartBuild" --> RUN
  subgraph CB["CodeBuild fix-job build"]
    RUN["Job runner"] --> SBX["Toolchain containers"]
    SBX --> PX["Egress allowlist proxy"]
  end
  RUN -- "result" --> DDB
  RUN -- "anonymous clone" --> GH
  PX -- "registries only" --> REG["Package registries"]

Everything shown is deployed today. Not shown: the S3 evidence store and audit anchor, which are designed but not provisioned (the hourly audit-anchor unit reports "skipped").

ComponentWhat it does
CloudFrontThe one public hostname. Overwrites x-viewer-ip with the real client address (used by per-IP rate limits) and adds a secret origin header the Lambda requires.
AmplifyStatic hosting for the public page, the signed-in app shell's files, and these docs.
API + MCP LambdaFastAPI: web API, browser sign-in (Cognito, PKCE), MCP token management, usage and the MCP server at /mcp.
DynamoDBAll state: accounts and organisations, projects, findings, the job queue, approvals, the usage ledger, the hash-chained audit log. Point-in-time recovery is on.
Worker LambdaScheduled units: tick every minute (reap expired leases, dispatch), usage reaper every 10 minutes, billing sweep hourly, deletion backstop every 30 minutes, audit anchor hourly.
CodeBuildOne build per fix job. Starts its own Docker daemon, an --internal sandbox network and an allowlisting egress proxy, then runs the pipeline as an unprivileged user.
Secrets ManagerRuntime secrets read by name (OIDC client secret, session key, origin header, account-identity salt, GitHub token). Secret values never sit in environment variables; only their names do.

From scan to pull request#

A scan is synchronous: the API clones the repository, reads its manifests and matches every declared dependency against the advisory database. A fix is asynchronous: submitting it reserves a use and queues a job; the worker hands the job to CodeBuild; the build writes the result back; a human approves; only then does the API push and open the PR.

sequenceDiagram
  autonumber
  actor You
  participant API as API / MCP Lambda
  participant DB as DynamoDB
  participant W as Worker Lambda
  participant CB as CodeBuild job
  participant GH as GitHub
  You->>API: Scan project (web) or repos_affected (MCP)
  API->>GH: shallow clone
  API->>DB: upsert findings
  API-->>You: findings (KEV, EPSS, fixed versions)
  You->>API: Fix this / fix_submit (idempotency key)
  API->>DB: entitlement check, reserve 1 use, enqueue job (queued)
  API-->>You: job_id
  loop every minute
    W->>DB: reap expired leases, claim job (running)
    W->>CB: StartBuild (TENANT_ID, JOB_ID, LEASE_OWNER only)
  end
  CB->>GH: anonymous clone (public repos)
  CB->>CB: resolve, patch, 5 gates in sandbox
  CB->>DB: complete (done + outcome), approval pending
  You->>API: read plan, gates, diff
  You->>API: Approve PR
  You->>API: Open PR / pr_open
  API->>DB: check verified + approved
  API->>GH: push remediation branch, open PR with evidence
  API->>DB: consume the reserved use
  API-->>You: PR URL

Tip

Job start latency is bounded by the one-minute scheduler tick plus CodeBuild provisioning. Poll with job_status (MCP) or watch the job in the web app rather than resubmitting; a resubmit with the same idempotency key returns the same job and never reserves a second use.

Job lifecycle#

A job has a status (where it is in the queue) and, once done, an outcome (what the pipeline concluded). Approvals are separate records attached to the job.

stateDiagram-v2
  direction TB
  [*] --> queued: submit (use reserved)
  queued --> running: worker claims, CodeBuild starts
  running --> queued: lease expired or infra error (retry with backoff)
  running --> dead: 3 attempts used up (use released)
  running --> done: pipeline finished
  state done {
    direction LR
    [*] --> verified
    [*] --> needs_approval
    [*] --> failed
    [*] --> unverifiable
    [*] --> no_fix_available
    [*] --> nothing_to_fix
  }
  done --> approval: verified or needs_approval
  state approval {
    direction LR
    [*] --> pending
    pending --> approved
    pending --> rejected
  }
  approval --> pr_opened: open_pr approved, PR opened (use consumed)
  approval --> rerun: major bump approved, new job queued (use moves over)
  approval --> released: rejected (use returned)
  rerun --> [*]
  pr_opened --> [*]
  released --> [*]
  dead --> [*]

The five verification gates#

After the patcher updates the manifest and lockfile, the job proves the change inside the sandbox. Each gate reports pass, fail, unverifiable, needs_approval, error or skipped; a gate that crashes becomes error, never an exception that hides the others.

flowchart TB
  P["Patched manifest + lockfile"] --> G1["1. clean_room_reinstall"]
  G1 -- "fail" --> SK["gates 3-5 recorded as skipped"]
  SK --> F
  G1 -- "pass" --> G2["2. build"]
  G2 --> G3["3. independent_rescan"]
  G3 --> G4["4. test_delta"]
  G4 --> G5["5. policy"]
  G5 --> D{"any gate fail, error or skipped?"}
  D -- "yes" --> F["outcome: failed"]
  D -- "no, but test baseline unverifiable" --> UV["outcome: unverifiable"]
  D -- "no" --> V["outcome: verified"]
GateWhat it proves
clean_room_reinstallDeletes the installed environment (venv, node_modules, …) and reinstalls from the lockfile through the egress proxy. If this fails, the later gates are recorded as skipped and the job fails.
buildThe project still builds with the new versions.
independent_rescanRe-checks the installed inventory (for example pip list, npm ls), not the manifest the fix edited, so a declaration that did not take effect is caught.
test_deltaRuns the tests before and after and compares them by name: lost or deleted tests, newly skipped tests, a lower count or modified test files fail it. A red or absent baseline makes it unverifiable (but tests that newly break still fail).
policyNo new vulnerable version anywhere in the installed tree, the package-age cooldown is respected, and the optional provenance requirement holds.

The result also carries a confidence label: high for a deterministic dependency-only change, medium when a person approved a major bump, low when anything else needed explaining (for example an unverifiable test baseline).

How usage is metered#

Usage is counted per person (not per organisation), per calendar month in UTC. The ledger reserves at submit, consumes when a PR opens, and releases in every other case. A reaper releases reservations that went stale.

flowchart TB
  S["fix_submit or Fix this"] --> E{"uses remaining?"}
  E -- "no" --> Q["refused: quota_exhausted with resets_at and options: request_more, wait, upgrade (payments not enabled yet)"]
  E -- "yes" --> R["reserve 1 use, queue job"]
  R --> J{"job outcome"}
  J -- "failed, unverifiable, no fix, dead" --> REL["use released"]
  J -- "verified or needs_approval" --> H["use held while a person decides"]
  H -- "rejected, or no decision within 7 days" --> REL
  H -- "PR opened" --> C["use consumed: 1 PR = 1 use"]

Note

The web API answers an exhausted allowance with HTTP 402. Over MCP the same body arrives as a tool result with isError: true, so your assistant can read it and explain the options:

{"error": "quota_exhausted", "resets_at": "2026-12-01T00:00:00+00:00", "options": ["upgrade", "request_more", "wait"]}

Trust boundaries#

Two rules shape the design: credentials never enter the sandbox that runs your project's build and tests, and every read or write is bound to exactly one tenant or person scope.

flowchart TB
  YOU["You and your MCP client"] -- "Bearer token, never forwarded" --> APIZ
  subgraph T2["Arachne control plane: holds the credentials"]
    APIZ["API + MCP: token, scope + role checks, tenant-scoped storage"]
    ORC["Fix-job orchestrator in CodeBuild"]
  end
  APIZ -- "tenant id + job id only" --> ORC
  ORC -- "work tree only" --> TC
  subgraph T3["Sandbox: no credentials, no route out"]
    TC["Toolchain containers: install, build, tests"]
  end
  TC -- "through the proxy" --> NET["Allowlisted package registries"]
  APIZ -- "reviewed file map, approved only" --> GHX["GitHub pull request"]

More detail in Security & data handling.

Current limits#

Not live yet

  • Fix jobs only clone public repositories. The CodeBuild build deliberately has no GitHub token and clones anonymously, so a private repository's fix job fails at checkout. Scanning a private repository works only where the API holds a token for it (today: the one allowlisted repository); anything else reports scan_failed.
  • Pull requests go to one allowlisted repository. The GitHub guard admits writes to a single configured repo (today the demo repo aadhi0612/arachne-demo), only on remediation/… branches.
  • No LLM fixer in production. The fixer model is configured as a mock, so live fixes are dependency-only; residual breakage in your own code is not patched.
  • Billing is off. The billing provider is none; "Upgrade" cannot take payment. Use "Request more uses" or wait for the reset.
  • Email and password sign-in only. Google sign-in is not configured.
  • Advisory feed sync is not scheduled. The database serves its last good data; a "data stale" banner appears when sources are older than 48 hours.
  • No S3 evidence store or audit anchor yet; very large job results that cannot be stored inline are dead-lettered rather than offloaded.