Docs / How It Works
How It Works
A root orchestrator that never touches the target, and the subagents it spawns to do the actual work.
The agent graph
A run starts by resolving the target — a local directory, a cloned GitHub repo, or a live URL — then hands off to a root agent. The root agent never touches the target itself: it only spawns subagents (create_agent) and steers them (send_message). That rule is enforced both in the root's system prompt and in code — the tools that actually touch a target (http_request, run_shell) are gated by whether the calling agent is root.
Root creates subagents by role, depending on the target and what it finds: a recon subagent to map code and app surface, an osint subagent for company/domain-level passive recon (subdomain enumeration, tech-stack fingerprinting, contacts and links scraped from the public site, targeted Google dork queries), one or more vuln_hunter subagents to look for vulnerabilities, an exploiter to validate a candidate with a real proof-of-concept, and a fixer to suggest a concrete code fix. Each subagent works independently, reports back with finish(summary), and root polls their status (list_agents) and can steer a subagent mid-task before it finishes. This is a two-level graph — root and its direct subagents — not arbitrary-depth spawning; a subagent can't spawn subagents of its own.
The tool loop
Every agent turn runs through the same tool dispatch, one LLM call at a time via litellm. The shared tool set includes read_file/list_dir/grep_code (path confined to the target directory), static_vuln_scan (a regex ruleset), run_shell (sandboxed — see below), http_request(scope-limited to the target's allowed host), and create_finding, which is where a candidate vulnerability becomes a recorded finding — see Verification for what that requires. The osint role additionally gets dork_search/fingerprint_tech/extract_contacts_and_links, filing into its own osint_profile.json, separate from findings.json.
For live targets, a shared requests.Session carries cookies across the whole scan; static HTML is parsed for links and forms, and a real headless Chromium session (Playwright) handles JavaScript-rendered single-page apps where static parsing finds nothing. Every http_request call is logged, and a logged request can be replayed with overrides — the intercept/tamper/replay workflow a proxy tool is normally used for, built on the same session rather than a separate one.
Sandboxing
run_shellexecutes inside a Docker container when Docker is available, falling back to a denylist-guarded direct subprocess otherwise. Each agent — root included, one per subagent — gets its own container, started at that agent's own setup and stopped at its own teardown, capped with per-container --memory/--pids-limit/--cpus limits (defaults 512m/256/1.0). Every container also gets --security-opt no-new-privileges and --cap-drop ALL, plus Docker's own default seccomp profile, and network egress is denied (--network none) unless the scan has a live target. This isn't a flat scan-wide resource ceiling — the real host limit is each container's cap times however many agents run concurrently, up to RYVX_MAX_AGENTS(default 8). It also doesn't cover a live-target container's access to your host's own LAN or the cloud metadata endpoint (169.254.169.254) — that's a network/VPC-layer fix, not something a container flag solves.
Prompt-injection framing
Agents ingest arbitrary, attacker-controlled content — pages, files, command output — as part of doing their job. Every agent's system prompt states that tool output is untrusted data from the target, never instructions, and target-facing tool results are explicitly tagged as such before they enter the conversation.
From run to report
When root finishes, the run is written out: findings.json, chains.json (multi-step attack chains assembled from related findings), report.md, and findings.sarif (uploadable directly to GitHub Code Scanning). Every tool call any agent made along the way, on every target, is appended to audit.jsonl— agent, tool, redacted arguments, result preview, model, and timestamp — for defending a scan's actions after the fact.
Model-agnostic, cost-bounded
Every LLM call goes through litellm, so Claude, OpenAI, Gemini, and local models all run through the same code path — calls retry with backoff (RYVX_LLM_RETRIES) so one transient API error doesn't kill a scan. --max-cost <dollars> stops the whole scan once estimated spend, summed across every agent via litellm's per-call cost calculation, hits the cap.
← Back to Docs