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").
| Component | What it does |
|---|---|
| CloudFront | The 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. |
| Amplify | Static hosting for the public page, the signed-in app shell's files, and these docs. |
| API + MCP Lambda | FastAPI: web API, browser sign-in (Cognito, PKCE), MCP token management, usage and the MCP server at /mcp. |
| DynamoDB | All 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 Lambda | Scheduled units: tick every minute (reap expired leases, dispatch), usage reaper every 10 minutes, billing sweep hourly, deletion backstop every 30 minutes, audit anchor hourly. |
| CodeBuild | One 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 Manager | Runtime 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 URLTip
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 --> [*]- verified: every gate passed. An
open_prapproval is created and waits for a person. - needs_approval: the safe version is a major-version bump. Nothing was patched; a
major_bumpapproval waits. Approving it queues a re-run that may take the major, and the held use moves to the re-run. - failed (a gate failed, errored or was skipped), unverifiable (the baseline tests were red or absent), no_fix_available, nothing_to_fix: no PR is possible and the use is returned.
- dead: the job could not be run after 3 attempts (infrastructure failure, not a verdict on your code). The use is returned.
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"]| Gate | What it proves |
|---|---|
clean_room_reinstall | Deletes 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. |
build | The project still builds with the new versions. |
independent_rescan | Re-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_delta | Runs 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). |
policy | No 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"]- No token passthrough. Your bearer token authenticates you to Arachne and nothing else. Calls to GitHub use Arachne's own credentials, limited in code to an allowlisted repository and branch prefix.
- The sandbox gets no secrets. CodeBuild receives only the tenant id, job id and lease owner. The toolchain containers sit on an internal network whose only way out is the allowlisting proxy, which also refuses link-local and loopback addresses (the cloud metadata endpoints).
- Tenant isolation. Every storage call carries one scope (a tenant, a person, pre-auth or a named system purpose) and the adapters refuse calls outside it. An id from another organisation is answered as
not_found.
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 onremediation/…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.