How the vvaharness engine is built
A stage-by-stage deep dive into the four-phase, eleven-stage pipeline that turns a source tree into verified, scored, and validated vulnerability findings — the orchestration, the data contracts, the transport layer, and the promises it keeps when pointed at hostile code.
Stages 10–11 are opt-in. A separate s1_autoexclude pre-pass runs before S1 under --auto-step1. Every stage boundary is a typed, checkpointed artifact.
The mental model
VVAH turns a source tree into a set of verified, scored, and optionally remediated-and-validated findings. Three ideas shape the whole design:
- Threat-model before you analyse. Stages 1–3 build a map of the attack surface — call graph, entry points, sinks, STRIDE threats — and use it to decompose the repo into ranked work units. Deep analysis (S4) only ever sees focused chunks, never "here is the whole repo, find bugs."
- Every stage boundary is a typed, checkpointed artifact. No raw dict crosses a boundary — each step consumes and emits a pydantic model, serialized to a SQLite state store as JSON, never pickle. The goal: "a $50 run that dies at step 3 stays debuggable," and --resume skips everything already done.
- Triage speed is the bottleneck, not discovery. The pipeline invests heavily in filtering — deterministic prefilter gates, adversarial per-finding verification, multi-axis dedup — so what reaches a human is small and confirmed. The headline metric is Mean Time to Adapt (MTTA): time from AI-discovered exploitability to a validated fix.
Discovery & Modeling
Map the attack surface and build a threat-aware work plan.
Deep Dive & Verification
Multi-lens findings, adversarially confirmed with CVSS.
Synthesis & Reporting
Deduplicate, chain, and score into Markdown + SARIF.
Remediation & Validation
Propose fixes and adversarially grade them before adoption.
The same flow, drawn as the Visa whitepaper presents it — inputs on the left, the per-stage skills the harness applies on the right, machine-readable artifacts on the outputs edge:
Module layout
The installed package is vvaharness/. The driver lives in orchestrator/, the analysis stages in pipeline/stages/, and everything the stages call — transport, config, redaction, contracts — sits alongside.
vvaharness/
cli.py console entry point — setup / doctor / estimate / gc / scan / remediate / validate
orchestrator/ pipeline driver: entry, scan, batch, preflight, checkpoints, store (SQLite), cmdb …
manifest.py run-level run_manifest.json — version, roles, config hash, target git SHA, timing
models.py pydantic contracts — ContextPackage, Finding, FinalReport, …
config/ loader (${ENV} expansion, local override, step1 overlays)
profiles/ default.yaml (all-CLI) · sdk.yaml (voting on) · full.yaml (multi-backend)
pipeline/stages/ s1_preprocess … s8_chain, s11_validate wrapper
remediation_agent/ Step 10 — proposes & applies a minimal fix, writes per-finding DTOs
validation/ Step 11 — Claude Agent SDK adversarial panel over the DTOs
backends/ transport: llm (dispatch) · sdk · agent_sdk · oai · claude_cli · localtools
report/ enrich (CVSS env, CMDB, MD→SARIF) · cvss · cwe · redact
injectors/ cve_feed · design_controls (optional context loaders)
util/ environment · tokens · metrics · errlog · prompts · json_extract · status
lang/ language hints — EXT_TO_LANG, LANG_HINTS, SPECIALIST_HINTS
Entry point & orchestration
cli.py is the installed vvaharness command. Before dispatching it checks the interpreter against Requires-Python from package metadata (never a hardcoded floor) and best-effort loads .env. A bare invocation prints help and exits — it deliberately does not default to scan, so a mistyped command never produces a junk manifest.
| Command | Purpose |
|---|---|
| setup / init | Readiness wizard: environment checks, profile recommendation, --write-env scaffolding, --install-agents. Read-only unless writing. |
| doctor | Read-only diagnostic — the same static checks, then a live connectivity probe against real endpoints. |
| estimate | Scope/cost preview with no API spend: walks a text-extension allowlist, approximates tokens as bytes // 4. |
| gc | Prunes the SQLite state store by age and count. Only ever touches state — never reports. |
| scan | The pipeline, wrapped in manifest.capture(...) so a run manifest is emitted for every real run. |
| remediate | Standalone Step 10 over a prior scan's findings. |
| validate / s11 | Standalone Step 11 over remediation DTOs; lazily imported (needs the Claude Agent SDK). |
The single-repo driver
scan_repo() runs the eleven stages. Each follows one shape: load checkpoint → skip if present under --resume → else run under status + token phase → save checkpoint → honor --stop-after. Key mechanics:
- State lives outside the scanned repo. run_id is a SHA-256 of the resolved path; checkpoints go to $VVAHARNESS_STATE_DIR (default ~/.vvaharness/state/). A hostile target can't pre-plant --resume state, and --resume is refused outright if the state dir resolves inside the repo.
- Stages 5+6+7 share one checkpoint — a 5-tuple under the s7 key — because the prefilter gates are near-zero-cost and re-run cheaply against the S4 checkpoint.
- Non-fatal degradation is deliberate. A failed threat model or auto-step1 warns and continues; an S10 preflight failure disables only S10.
- Artifacts land under <repo>/security-scan/ — timestamped report.md, report.sarif, and errors.jsonl. HEAD is pinned into report.git_sha so S10 refuses to patch a moved tree.
Batch mode
Batch is strictly sequential — the only parallelism lives inside the S4/S6 thread pools, never across repos. Cloning is hardened against git-option injection (reject - refs and ./.. dest names, -- separators), scrubs tokens from output, and binds each checkout to its source ref via a marker kept outside the workspace so a stale or pre-seeded directory is never silently reused.
Checkpoints & the run manifest
The store is WAL-mode SQLite with runs and checkpoints tables. The load-bearing invariant: payloads are JSON via pydantic, never pickle — a tampered checkpoint degrades to a step re-run, never CWE-502 RCE on resume. manifest.capture() writes run_manifest.json only when a scan actually ran, recording config_sha256 and config_local_sha256 (the effective config), the per-role model map, and the target git SHA — with argv scrubbed in two passes.
The pipeline, stage by stage
Data flows as in-memory pydantic objects through scan_repo, checkpointed at each boundary. Prompts are assembled from shared fragments in util/prompts.py plus per-stage system strings and, for S4, the language and specialist bodies from lang/hints.py.
Attack-surface map Agentic
repo → ContextPackage
An agentic pass (Read/Glob/Grep/Bash, jailed to the repo) emits language, modules, entry points, sinks, and a call graph — but never raw code. The important half is deterministic and treats model output as untrusted: _walk_repo builds the ground-truth inventory and drops any symlink whose target resolves outside the repo unconditionally; _dedup_configs collapses near-identical configs but promotes any that carries a new suspicious signal; _supplement_call_graph validates the model's graph against real source, dropping hallucinated names and qualifying nodes to file::name. Un-attributable paths are dropped, never guessed.
Threat model LLM · single-shot
ContextPackage → ThreatModel
Reasons over S1's mapped surface (not raw code) to produce assets, trust boundaries, and STRIDE threats — each with actor, impact, and likelihood. A deterministic evidence pass feeds it design docs, manifests, API artifacts, and a repo-kind baseline that seeds an OWASP/CWE checklist. Failure here is non-fatal; the scan continues without a threat model.
The work plan LLM · single-shot
ContextPackage → TaskManifest (ranked chunks)
Combines four chunk families, where the deterministic passes guarantee coverage even if the LLM output is empty — only ranking is lost. Risk chunks come from the manifest (hallucinated files dropped). Taint chunks BFS the call graph from entry point to sink (≤8 hops) and are ranked above the LLM's risk chunks. Catch-all chunks sweep every remaining source file. Specialist chunks are repo-wide lens passes, gated so a lens whose surface doesn't exist is dropped.
Per-chunk findings Agentic
chunks → Finding[] · ×N runs + vote
The system prompt is byte-identical across every call so the SDK prompt cache hits after the first; the per-chunk lens lives in the user prompt. Voting is off by default: the shipped model rejects an explicit temperature, so N identical samples would cost N× with zero filtering — runs/vote_threshold collapse to 1/1, and S5+S6 are the false-positive defense. The sdk.yaml profile sets a temperature to make real majority voting work. Code is redacted before egress (Luhn/BIN-gated PAN masking, then SSN/credential masking); read-only "neighbor context" lets the researcher rule out false positives without paying for the next chunk.
Deterministic gates Deterministic
Finding[] → kept · dropped
Drop test/mock/example paths (unless the finding is a committed-secret class), drop findings whose file isn't in the ground-truth inventory (hallucinations), drop below a confidence floor, and drop findings missing source/sink evidence when require_evidence is on. A cheap semantic pre-dedup fires only above a threshold, to avoid paying for N verifiers on one root cause.
Adversarial confirmation Agentic · per-finding
kept → verdict + CVSS per finding
One agentic session per finding (Read/Glob/Grep), prompted "assume the finding is WRONG until confirmed." The verifier reads the cited line, walks callers to an external entry point, tries to kill the finding, and probes any defense. It emits VERDICT: TRUE_POSITIVE|FALSE_POSITIVE plus a CVSS vector (scored even on false positives). The gate is confidence ≥ 7. An unparseable reply becomes VERIFY_ERROR — deliberately not laundered into FALSE_POSITIVE, so an undetermined result never understates risk.
Canonicalization Deterministic + semantic
verified → canonical Finding[]
Deterministic collapse (same file + class + overlapping line range → lower index wins) plus one optional semantic call: "two findings are the same when one engineering fix closes both." Collapsed findings are preserved as DupLocation entries — every call site needing the same fix is reported (surfaced as SARIF relatedLocations), not just the canonical. Between S7 and S8, findings gain environmental CVSS and OffensivePriority scores.
Chaining & final ranking LLM · single-shot
canonical → FinalReport
Does not find new bugs — it assesses what an attacker can do with the confirmed findings together, assigns true severity, finds exploit chains, and downranks chains blocked by controls. Severity is CVSS-framework-anchored (environmental band → base band → LLM label only when no vector exists). OffensivePriority is orthogonal — a sort tie-break, never folded into severity. Three degrade paths each emit a usable degraded=True report rather than crash.
CRITICAL HIGH MEDIUM LOW INFO
SARIF emission Deterministic
report.md → report.sarif
The Markdown report is the interchange format. report/enrich.py::md_to_sarif parses the enriched Markdown back into findings and emits SARIF 2.1.0. The Markdown is written through redact() first, so nothing secret-shaped reaches disk.
Data contracts
models.py defines every stage boundary. The invariant: no raw dict crosses a boundary, and every strict field populated from LLM JSON carries a field_validator(mode="before") that degrades one off-schema value to a safe default instead of killing the run.
| Model | Produced by | Key fields |
|---|---|---|
| CVE · Control · AppProfile | injectors / CMDB | injected context threaded through S1–S8 |
| ContextPackage | S1 | call_graph · entry_points · unsafe_sinks · modules · all_files · excluded |
| ThreatModel | S2 | assets · trust_boundaries · threats · open_questions |
| TaskManifest · Chunk | S3 | risk_rank · files · hypothesis · specialist |
| Finding | S4 → enriched thru S7 | location · vuln_class · cwe · confidence · votes · verdict + CVSS · env scores · duplicates |
| FinalReport | S8 | findings · chains · dropped · metrics · degraded |
Finding.canonical_key(line_bucket) — (file, line_start // bucket, vuln_class) — is the identity used for both S4 voting and S7 dedup; bucketing the line absorbs the small jitter between runs. Every model→prompt renderer neutralizes Markdown injection, since these strings mix operator and model-generated content.
The transport layer
Every model call goes through backends/llm.py, which resolves the per-role {id, via, …} config node and dispatches on via:. Two public calls exist — prompt() (single-shot, no tools) and agentic() (a tool-using session). Roles are swapped in config alone.
| via: | Module | Transport |
|---|---|---|
| sdk | backends/sdk.py | Anthropic Python SDK |
| openai | backends/oai.py | OpenAI-compatible Chat Completions |
| cli | backends/claude_cli.py | claude CLI subprocess |
sdk.py
Streaming + get_final_message() so large max_tokens never trips the HTTP timeout. Caches the system prompt, sends adaptive thinking, and can drop a rejected parameter and retry. Error bodies are redacted before raising — a gateway may reflect auth headers.
agent_sdk.py
The Claude Agent SDK backend for the mutating fix role, reached only by delegation from sdk.py. Deny-by-default gate: writes allowed only inside the repo root (handing the SDK the resolved path), reads always allowed, Bash always denied even if re-added.
oai.py
OpenAI function-calling loop with the heaviest secret scrubbing and active context-overflow recovery. No delegation path — a mutating role on openai raises and must move to cli.
claude_cli.py
Shells out to claude, pinned to an absolute path at import (CWE-426/427). Probes --help once to adapt to the installed CLI, prefers acceptEdits (never bypassPermissions by default), classifies transients structurally.
localtools.py
The sandboxed Read/Glob/Grep loop for the sdk and openai agentic paths. Every path is jailed by resolve-then-relative_to; output is size-capped and redacted. No Edit, Write, or Bash executor exists.
Remediation
remediation_agent/ proposes and (in fix mode) applies a minimal patch per verified finding, through the main dispatcher (role models.remediate). Standalone and in-pipeline paths adapt to the same per-finding loop. Flow: locate the newest report → select top-N by CVSS → policy pre-gate → snapshot files → model call (the agent edits the tree directly) → post-gate → write artifacts.
<repo>/security-remediation/<NN_slug>/ remediate_report.json the DTO — canonical hand-off contract evidence/triage.json the agent's RemediationVerdict + meta (redacted) evidence/summary.md human summary (redacted) evidence/diff.patch git diff, or a synthesized unified diff for non-git
The DTO carries status, the verbatim finding, the patch, and an empty validation block — remediation lays out the shape; Step 11 fills the scores. Policy gates are opt-in and fail-closed: a policy that fails to parse denies everything. The pre-gate can deny with no model spend; the post-gate inspects what actually changed on disk and reverts any edit to a forbidden path (CWE-22-hardened). Resume authenticates by content hash, closing the "existence-implies-done" gap for reordered state.
Doc/impl gap. remediation_playbook.yaml specifies forbid_patterns, candidates_per_finding, never_autofix_cwes, and allow_new_dependencies, but the current playbook.py reads only max_diff_lines and max_files_touched; the rest are reserved.
On via: sdk the Agent SDK gate denies Bash even if re-added. On the default via: cli route there is no permission gate — Bash is contained only by its absence from allowed_tools, so re-adding it grants a host shell. Run fix mode only against trusted repos.
Validation
validation/ grades each remediation DTO with an adversarial Claude Agent SDK panel. It ships its own harness abstraction; the only hard cross-links to the rest of the system are the shared checkpoint store and the DTO wire contract.
Discovery is deterministic and spends no model budget: glob the DTOs, skip malformed ones, select the validatable statuses. Per finding, the repo is staged into an ephemeral hardlinked copy (.git excluded) and the panel launches: security-architect and penetration-tester always, plus a conditional cross-repo-analyzer only when a fix spans 2+ repos. Per-CWE bypass hints from validator_hints.yaml are injected into the shared launch prompt.
Host-authoritative scoring
The load-bearing decision: the agents synthesize gate statuses into synthesized_gates.json, but the host recomputes the numeric verdict in-process with the canonical engine, keyed strictly by finding id. The agent's self-reported score is never trusted; a missing gate file fails closed to UNVERIFIABLE.
| Weighted gate | Weight | Notes |
|---|---|---|
| root_cause | 0.4300 | largest single lever |
| instance_coverage | 0.2467 | all instances of the class fixed |
| no_new_vulnerabilities CRITICAL | 0.1867 | skip/invalid → UNVERIFIABLE; partial/fail caps a Fixed down to Partially Fixed |
| security_best_practices | 0.1366 | defense-in-depth |
Multipliers are pass=1.0 / partial=0.5 / fail=0.0 / skip=0.0 (skips drop from both numerator and denominator). Below a 0.50 coverage floor the result is UNVERIFIABLE. Thresholds: ≥ 0.80 Fixed, ≥ 0.50 Partially Fixed, else Not Fixed. The verdict transitions the DTO status — Fixed → validated, Not/Partially → validation_failed (re-validatable), UNVERIFIABLE → needs_review.
Far stronger than remediation's: a deny-by-default policy denies Edit/NotebookEdit/Bash unconditionally and permits Write only to the two output files, enforced in three overlapping layers. The Bash entry in persona frontmatter is effectively dead. Validation requires an Anthropic model — a via: openai validate role is refused at step start with exit 2.
Reporting & enrichment
report/enrich.py owns the Markdown↔SARIF boundary and CVSS enrichment. The Markdown report is the interchange format; enrich.py parses it back with a line-oriented state machine, and an idempotency guard ensures re-enrichment never double-appends a score.
- CVSS base follows FIRST.org §7 — report/cvss.py is the strict library; enrich.py carries a second, more tolerant parser (a known duplication worth consolidating).
- VulContextSeverity folds CMDB context into the score: a non-externally-facing app downgrades Modified Attack Vector N → A, and CR/IR/AR come from the PCI/PAN/PII flags, under a fixed SAST temporal preset.
- CMDB join merges an application row with its parent when flags are blank, normalizes app ids, and warns on genuine post-normalization collisions.
- CWE mapping trusts a parsed CWE-NNN token, else falls back deterministically from the vuln class — never an LLM-emitted CWE blindly.
Cross-cutting concerns
config/
${ENV:-default} expansion, built-in defaults merged under user config (a partial config can't KeyError a stage), an optional git-ignored config.local.yaml whose overrides are logged, and per-scan step1 overlays. Network/UNC paths refused before any filesystem touch.
report/redact.py
Masks card/PII/credential material at three write boundaries — Markdown, SARIF, errlog — plus stderr. Cards are Luhn+IIN gated, SSNs area/group/serial gated. Strong credential keywords always mask; prose-ambiguous ones leave a plain word intact.
util/tokens.py
A process-wide singleton with per-phase buckets fed by every backend. The headline "prompt" total is fresh input + cache-write, with cache-read tracked separately so it never inflates the total. Budget caps live on the stages.
util/errlog.py
Non-fatal errors append to the per-scan errors.jsonl as redacted, chmod 0600 JSONL — a failed chunk is recorded as a coverage gap the report discloses, not swallowed into a clean result.
util/json_extract.py
Strips code fences, finds a balanced span, repairs invalid escapes, and holds a stray top-level list until a dict is found — the recovery layer that keeps a chatty model response from failing a stage.
agentdoc.py
setup --install-agents writes operating manuals telling AI coding agents to operate the released CLI, not develop or repair it. It never clobbers an existing file.
Security-relevant invariants
The whole point of a security scanner is that it can be pointed at hostile code. The design makes several promises worth stating in one place:
State lives outside the scanned repo — and is JSON, never pickle
A hostile target can neither pre-plant --resume state nor achieve CWE-502 RCE via a tampered checkpoint.
In-target config/.env is refused
Unless VVAHARNESS_ALLOW_CWD_CONFIG is set — defeats the "cd into the checkout, then scan" attack.
Off-repo symlinks are dropped unconditionally
Any symlink whose target resolves outside the repo is dropped during the file walk — a host-file-disclosure guard.
Bash is denied at every automated layer
No executor in localtools, NotImplementedError on openai, hard deny in the Agent SDK gate and validation policy. Granting a shell requires explicitly listing Bash on a via: cli role.
Secrets are never printed
Presence-only credential reporting, argv and error-body scrubbing, git-token scrubbing in batch output, redaction at every disk write.
Verification honesty
VERIFY_ERROR, GUARDRAIL_BLOCKED, and UNCONFIRMED are surfaced separately from confirmed false positives — a degraded run is visible, not laundered into "clean."
Validation scoring is host-authoritative & fail-closed
The agent's self-reported verdict is never trusted; missing evidence yields UNVERIFIABLE, not a pass.
git-option-injection is hardened
Reject - refs and ./.. dest names, use -- separators, run git with cwd= not -C throughout batch cloning.
Non-negotiable architectural security practices
The Visa whitepaper closes with twelve architectural and design practices it treats as non-negotiable for critical infrastructure — drawn from Visa's Mythos deployments and Project Glasswing, on one premise: frontier AI collapses discovery-to-exploit from weeks to minutes, so trust can never be assumed. They are the targets VVAH both enforces in its own internals (a scanner is pointed at hostile code) and hunts for in the code it scans. Each is quoted, then mapped to where it lives in this codebase.
1Secrets never live in code
AI can instantly scan repos for obfuscated keys; enforce pre-commit scanning and keep secrets in hardened managers, not source control.
In VVAHCredentials reported presence-only, git tokens scrubbed from batch output, secret-shaped values redacted at every write; _dedup_configs promotes a credential-carrying near-dup instead of collapsing it, and S5 keeps a committed-secret class even in test paths.
2Authorization is server-side, explicit, mandatory
Client-side checks and implicit trust are trivial for AI to abuse.
In VVAHThe S3 strategist targets authorization controls, S3 taint chunks trace every entry point to every sink, and S6's job is to walk callers to an external entry point and try to kill a finding on authz grounds before confirming it.
3"Internal" is not a security boundary
Treat all internal traffic as untrusted and verify it continuously.
In VVAHThe founding stance is that the scanned target is hostile; S2 models trust boundaries as first-class assets, and every layer treats model output as untrusted rather than assuming a safe zone.
4Tenant isolation is centrally enforced
Per-microservice isolation creates gaps; enforce boundaries through centralized, architecture-level controls.
In VVAHPolicy is one centrally evaluated RemediationGate (kill_switch → deny → deny_paths → allow → default), and validation scoring is host-authoritative — recomputed by one canonical engine keyed by finding id, never a per-agent self-report.
5No raw HTML or script rendering
Use modern auto-escaping frameworks; avoid ad-hoc HTML/script generation.
In VVAHEvery to_markdown / to_prompt_block renderer neutralizes Markdown injection (_demote_md_headings, _md_cell) because those strings mix operator and model content; XSS is itself a first-class vuln_class.
6Cryptography is either correct or not used
Homegrown crypto is trivial for semantic AI to break; use only validated standard libraries.
In VVAHcrypto is a built-in S4 specialist lens (gated off with no crypto surface), and internally the harness leans on standard primitives — SHA-256 identity keys, Luhn/IIN card gating — not bespoke schemes.
7Inputs are hostile until proven otherwise
Every input — user data, internal API calls, or AI-agent output — must be validated; treat AI-generated content as untrusted by default.
In VVAHThe spine of the pipeline: S1 validates the call graph against real source and drops hallucinated names, unattributable agent paths are dropped-never-guessed, checkpoints are JSON-via-pydantic (never pickle), and json_extract recovers every chatty reply rather than trusting it.
8Sensitive data is never logged
PII, secrets, and tokens must be scrubbed before anything reaches logging or observability.
In VVAHRedaction runs at four write boundaries (Markdown, SARIF, errlog, stderr status), the errlog is chmod 0600 JSONL, and redact_counts() is the thread-safe variant used inside the parallel S4/S6 loops.
9Security decisions fail closed
When authorization can't be verified or an error hits, default to deny; no permissive fallbacks.
In VVAHPolicy that fails to parse denies everything; validation with missing evidence fails closed to UNVERIFIABLE; an unparseable S6 reply becomes VERIFY_ERROR, never laundered into FALSE_POSITIVE; write-tool gates are deny-by-default.
10Security patterns are centralized, not copy-pasted
Concentrate critical security logic in centralized, heavily audited libraries — a single source of truth.
In VVAHEvery model call funnels through one dispatcher (backends/llm.py); standalone and in-pipeline remediation share the same process_findings loop; prompt fragments live once in util/prompts.py. (The one CVSS-parser duplication is flagged in-doc as worth consolidating.)
11AI agents are identities
Any agent that calls APIs or modifies systems is a first-class identity with scoped permissions and least privilege; assume attackers steal and chain agent credentials.
In VVAHEvery agentic session runs in a scoped deny-by-default sandbox: localtools has no Edit/Write/Bash, the Agent-SDK fix gate allows writes only to resolved in-repo paths and denies Bash unconditionally, and the SDK backend borrows the shared credential only when it is the sole backend.
12Design for absence
Default to removal over defense — eliminate unused features, dead code, redundant libraries; simplification is itself a control.
In VVAHAbsence over guardrails: there is no Bash executor to contain in the read-only loops (removal, not a check), S1 auto-exclude and S5 prefilter strip dead/generated/test paths before spend, and the openai backend has no mutation path — a mutating role there raises NotImplementedError.
References
- Visa whitepaper — Frontier AI: A New Era of Cyber ResilienceProject Glasswing, Visa Cybersecurity, June 2026. Source of the four-phase / eleven-stage diagram and the twelve non-negotiable practices.corporate.visa.com/…/project-glasswing.pdf
- Visa announcement — Visa Releases Its AI-Powered Cyber Defense System to Open Sourcecorporate.visa.com/…/visa-cybersecurity-mythos-project-glasswing.html
- Reference implementation — visa/visa-vulnerability-agentic-harnessgithub.com/visa/visa-vulnerability-agentic-harness
- Anthropic — Project Glasswinganthropic.com/project/glasswing