Vulnerability Remediation Workflow

The vulnerability remediation workflow is Foundry's primary use case. It replaces a linear shell script pipeline with an event-driven chain of task blocks that branches based on the state of the codebase.

The Two Paths

When a vulnerability is detected, the chain branches based on whether the main branch still contains the vulnerability:

flowchart TD
    A[vulnerability_detected] --> B([Audit Release Tag])
    B --> C[release_tag_audited]
    C --> D([Audit Main Branch])
    D --> E[main_branch_audited]
    E --> F{dirty?}
    F -->|"dirty=true (vulnerability present on main)"| G([Remediate Vulnerability])
    G --> H[remediation_completed]
    H --> I([Commit and Push])
    I --> J[project_changes_committed]
    I --> K[project_changes_pushed]
    K --> L([Install Locally])
    L --> M[local_install_completed]
    F -->|"dirty=false (main already fixed, no release cut yet)"| N([Cut Release])
    N --> O[release_completed]
    O --> P([Watch Pipeline])
    P --> Q[release_pipeline_completed]
    Q --> R([Install Locally])
    R --> S[local_install_completed]

Dirty path — the vulnerability exists on main. Foundry remediates it (e.g., dependency update), commits, pushes, and reinstalls locally.

Clean path — main is already fixed (perhaps by a developer), but no release has been cut. Foundry tags a patch release, watches the CI pipeline, and reinstalls locally.

If the release tag is not vulnerable at all, the chain stops after release_tag_auditedAudit Main Branch self-filters and emits nothing.

Self-Filtering

The engine routes events by type only — it cannot inspect payloads. When both Remediate Vulnerability and Cut Release sink on main_branch_audited, both blocks receive every main_branch_audited event. Each block checks the dirty flag in the payload and returns an empty result if the condition doesn't match. This ensures only one path fires.

Running the Workflow

Full run (default)

All blocks execute and emit. The complete chain runs to local_install_completed:

foundry emit vulnerability_detected \
  --project my-tool \
  --payload '{"cve": "CVE-2026-1234", "vulnerable": true, "dirty": true}'

Then inspect the trace:

foundry trace <event_id>
vulnerability_detected (evt_...) project=my-tool
  → Audit Release Tag: ok — Release tag audited: CVE-2026-1234 vulnerable=true
    release_tag_audited (evt_...) project=my-tool
      → Audit Main Branch: ok — Main branch audited: CVE-2026-1234 dirty=true
        main_branch_audited (evt_...) project=my-tool
          → Remediate Vulnerability: ok — Remediated CVE-2026-1234
            remediation_completed (evt_...) project=my-tool
              → Commit and Push: ok — Committed and pushed fix for CVE-2026-1234
                project_changes_committed (evt_...) project=my-tool
                project_changes_pushed (evt_...) project=my-tool
                  → Install Locally: ok — Installed locally
                    local_install_completed (evt_...) project=my-tool
          → Cut Release: ok — Skipped: main branch is dirty

Notice that Cut Release still appears in the trace — it received the event but self-filtered and returned an empty result.

Audit only

Observers run and emit, mutators run but suppress downstream events:

foundry emit vulnerability_detected \
  --project my-tool \
  --throttle audit_only \
  --payload '{"cve": "CVE-2026-1234", "vulnerable": true, "dirty": true}'

This tells you what would happen without actually remediating, committing, or releasing. The audit blocks produce their findings, but the chain stops at main_branch_audited.

Dry run

Only observers execute. Mutators are skipped entirely:

foundry emit vulnerability_detected \
  --project my-tool \
  --throttle dry_run \
  --payload '{"cve": "CVE-2026-1234", "vulnerable": true, "dirty": true}'

The chain produces vulnerability_detectedrelease_tag_auditedmain_branch_audited and stops. No remediation, no commits, no releases.

Clean Path Example

When the main branch is already fixed but no release has been cut:

foundry emit vulnerability_detected \
  --project my-tool \
  --payload '{"cve": "CVE-2026-5678", "vulnerable": true, "dirty": false}'

The trace shows the release path instead:

vulnerability_detected (evt_...) project=my-tool
  → Audit Release Tag: ok — ...
    release_tag_audited (evt_...) project=my-tool
      → Audit Main Branch: ok — Main branch audited: CVE-2026-5678 dirty=false
        main_branch_audited (evt_...) project=my-tool
          → Remediate Vulnerability: ok — Skipped: main branch is clean
          → Cut Release: ok — Cut patch release for CVE-2026-5678
            release_completed (evt_...) project=my-tool
              → Watch Pipeline: ok — Release pipeline completed successfully
                release_pipeline_completed (evt_...) project=my-tool
                  → Install Locally: ok — Installed locally
                    local_install_completed (evt_...) project=my-tool

Not Vulnerable

If the release tag has no vulnerability, the chain stops early:

foundry emit vulnerability_detected \
  --project my-tool \
  --payload '{"cve": "CVE-2026-9999", "vulnerable": false}'

Only two events: vulnerability_detectedrelease_tag_audited. The Audit Main Branch block sees vulnerable=false and emits nothing.

Payload Fields

FieldUsed ByValuesDefault
cveAll blocksCVE identifier string"unknown"
vulnerableAudit Release Tag, Audit Main Branchtrue / falsetrue
dirtyAudit Main Branch, Remediate, Cut Releasetrue / falsetrue

Real Implementations

All blocks in the vulnerability remediation workflow perform real work:

  • Audit Release Tag — shells out to the stack-appropriate tool (cargo audit --json, npm audit --json, pip-audit --format=json, mix deps.audit) via the ScannerGateway abstraction
  • Audit Main Branch — checks out the main branch and runs the same scan
  • Remediate Vulnerability — invokes the configured AI agent (e.g. via the claude CLI) to apply a fix
  • Commit and Push — runs git add, git commit, and optionally git push
  • Cut Release — creates and pushes a git tag using the AI agent
  • Watch Pipeline — polls the GitHub Actions API until the release workflow completes or times out
  • Install Locally — runs the project's configured install command (e.g. cargo install --path .) or Homebrew formula after a successful release