Skip to content

Architecture & modules

This page is the map for reading the code: how a scan flows from a target to a kept test, and where each piece lives. If you want to extend Mylonite, pair this with Plugin authoring.

Two validation layers

Everything in Mylonite answers one of two questions:

  • Layer 1 — "did the attack land?" (is one attempt a finding) → an effect probe first, when the target declares one (final on true/false), then a deterministic predicate, then an LLM judge if inconclusive. See Weakness classes.
  • Layer 2 — "is the finding worth a committed test that gates CI?" (is it kept) → the differential oracle. See The validation engine.

The flow

target ──> scan (Layer 1) ──> generate ──> validate (Layer 2) ──> gate ──> PR / CI
            findings           pytest file    kept?               workflows

Module map

Scanning & attacks — mylonite.scan

  • engine.py — ScanEngine: the single-shot orchestrator (customise → invoke → judge, with the scan-time flakiness filter and the LLM-call budget).
  • seeds.py / predicates.py — the bundled attack seeds (W1–W4) and the deterministic success predicates.
  • judge.py — the Layer-1 success ladder (effect probe first, then predicate, then LLM judge).
  • seed_synth.py — descriptor-driven seed synthesis: builds attack seeds for the channels a target's introspected tool surface actually supports (a direct-content tool, a poisoned tool description, …), so a target that doesn't match the bundled kitchen-sink's plant/recall shape still gets a runnable probe instead of a skip.
  • control_shim.py — the BoundaryControl subclasses (W1–W4) and ControlServerShim that synthesize a boundary-guarded build of any target (a server-layer control needs control_env); the control-efficacy check.
  • tool_roles.py — heuristics that classify a tool surface (store / retrieve / sink).
  • artefacts.py — the terminal trust panel.

The validation engine — mylonite.plugins._reference.reference_validator

DifferentialValidator implements the oracle legs: build · differential · flakiness · metamorphic (gating, incl. the evasion encodings) · mutation score (report-only), plus the custom-target legs stability · effect · consensus. The honesty invariant (plant + effect probe bypass the control) lives here and in control_shim.py.

Targets — mylonite.plugins._mcp & _reference

  • _mcp/stdio_adapter.py — MCPStdioAdapter (drives any stdio MCP server) + the bundled family adapters (filesystem/fetch/github) + the AttackSession (stateful, multi-turn) used to plant and drive across turns.
  • _mcp/remote_adapter.py — MCPRemoteAdapter: connects to a remote MCP server over SSE / streamable-HTTP (transport: sse|http + url in the target.yaml), a first-class transport alongside stdio.
  • _mcp/target_file.py — the target.yaml model + auto-wiring.
  • _mcp/target_registry.py — the bundled family TargetSpecs.
  • _reference/reference_target_adapter.py — the in-process reference app builds (reference:vulnerable / reference:guarded), the ground-truth differential.

Outputs — mylonite.report & mylonite.gate

  • report/sarif.py, report/bundle.py, report/severity.py — the SARIF / JSON renderers + shared severity rule.
  • report/render.py — the terminal renderers for a validation report and the control-ablation matrix (kept out of cli.py, which stays a thin composition root). Target-file scaffolding lives in plugins/_mcp/scaffold.py for the same reason.
  • gate/orchestrator.py — the scan→generate→validate→PR sequence + exit codes.
  • gate/mitigation.py + gate/recommend.py — the PR body and the target-specific, evidence-anchored recommendation (a fenced code sketch, never a diff).
  • gate/localize.py + gate/annotate.py — pin a finding to its locus and post inline PR check-run annotations.
  • gate/pr.py / gate/workflows.py — the git/gh PR flow and CI workflow templates.

Compliance — mylonite.taxonomy

The bundled OWASP-LLM / OWASP-ASI / MITRE ATLAS / NIST data and the mapper that stamps every finding. See Standards mapping.

The five extension contracts — mylonite.contracts

Public API from day one (versioned Protocol/ABCs + JSON schemas + entry-point loading):

Contract Role Reference impl
AttackModule generate payloads for a target PromptInjectionAttackModule, …
TargetAdapter speak to a target (sync/async; optional SupportsAttackSession) MCPStdioAdapter, InProcessReferenceAdapter
TestGenerator emit a regression test ReferencePytestGenerator
Validator the differential oracle DifferentialValidator
ComplianceMapper tag with OWASP/ATLAS/NIST ReferenceComplianceMapper

Plugins are discovered via setuptools entry-point groups (mylonite.attack_modules, mylonite.validators, …). Treat any change to a contract as an API change — see Plugin authoring. Validator's ValidationOutcome.metric field is a per-stage numeric whose meaning depends on stage (reproducibility, discrimination strength, robustness, …) — see Validation for what each stage's number means.

Constraints worth knowing

  • All LLM access flows through LiteLLM — no provider SDKs imported directly, and no default credential: the model defaults to Claude Haiku 4.5, but nothing runs until you supply that provider's key or point it at a self-hosted one. This is what makes the model roles possible. Every model call routes through one chokepoint — the transport wrapper that owns call-budget counting and the active policy, whose public name is mylonite.scan.llm. A direct litellm.completion/acompletion call anywhere else is a bypass and fails the build (tests/test_llm_chokepoint_boundary.py).
  • The reference app is ground truth. The bundled mcp_kitchen_sink vulnerable/guarded pair is intentionally (un)guarded; the differential is proven against it.
  • Scope discipline. Only the AI attack surface — no general SAST/DAST, no non-AI test generation.
  • Output and paths are funneled, not ad hoc. Everything printed, persisted, pushed, or published goes through mylonite._cli_io.echo/_redaction.redact*; every path sourced from target.yaml resolves through mylonite._paths.resolve_contained before it reaches open() or an argv — no direct typer.echo calls or unchecked paths remain in cli.py.
  • A decision is not a fault. The adapter's invoke() collapses exceptions into a skipped attempt, which is right for a fault (a crashed subprocess, a protocol error) and wrong for a decision (the LLM budget is gone, stop the run). The re-raise tuple in plugins/_mcp/_session_adapter.py is the control-flow allowlist: an exception that means "stop", not "this seed failed", must be listed there. It isn't decoration — omitting BudgetExceededError from it is what let one budget exhaustion produce three different exit codes depending on which layer noticed first.
  • Side effects gate on the flag that requests them. gate writes only to its own output directory unless --open-pr (branch/commit/push/PR) or --workflows (.github/workflows/ templates) is passed. Both default to off. Evidence is written to disk before any git command runs, so a git failure costs no findings.