GCA · INTEGRATIONS

    Agentic Remediation

    How findings get fixed — from single-finding triage to autonomous multi-finding sweeps.

    Agentic Remediation

    How findings get fixed in Gadriel Code — from triaging a single finding at the terminal, through the MCP fix_finding round-trip an AI assistant runs mid-session, up to autonomous remediation campaigns that fix a whole class of findings repo-wide.

    Every workflow on this page shares one invariant: Gadriel proposes, the host applies. Gadriel never writes source code. It emits a diff + rationale; the developer (or the host AI agent) applies it, and Gadriel re-scans to verify. Every action is appended to the local audit log.

    Related: AI-coding integration · MCP tools reference · Tiered analysis model · False positives · Privacy boundary · CLI reference


    1. Three ways to remediate

    WorkflowDriverScaleLLMSource of truth
    Single-finding triagegadriel code fix <ID> (CLI)one findingoptionalterminal + audit log
    MCP fix round-tripfix_finding toolone findingyeshost agent + audit log
    Remediation campaignstart_remediation_campaign + campaign_advancerepo-wide classyes.security/campaigns/

    All three are grounded on the same deterministic findings in .security/findings.json (Tier 0 — see tiers.md), and all three route through the per-pillar reasoning runner when they call an LLM.


    2. Single-finding triage — gadriel code fix <FINDING_ID>

    The terminal path. Given a finding ID, it does one of three things depending on flags. See the fix command in the CLI reference.

    # accept the finding as real and get a confirmation + fix suggestion
    gadriel code fix CODE-W1-L2-021
    # mark it a false positive (records a durable waiver + Bayesian feedback)
    gadriel code fix CODE-W1-L2-021 --false-positive \
    --reason "zero-copy idiom, not an interface cast"
    # affirm it is a real risk (bumps the posterior toward "real")
    gadriel code fix CODE-W1-L2-021 --confirm-real

    The finding argument accepts <ID> or <ID>#<SCAN_SEQUENCE>; the #<seq> suffix disambiguates when the same rule fired at multiple sites. With no suffix and multiple matches, fix lists them and refuses to guess.

    2.1 The three dispositions

    Confirm real (no flag) — hand it to an agent. fix loads the finding, routes it to the right per-pillar agent via AgentDispatcher, retrieves the top-1 relevant skill, builds the prompt, and invokes the reasoning runner. It prints the verdict (confirmation, explanation, rationale, proposed fix, compliance controls, cost/latency) and appends audit-log entries: a confirmation event plus a fix_proposed event carrying the pre-fix before_hash. The fix is printed, not applied — the developer (or their editor agent) applies the suggested diff.

    --false-positive — waive it. Short-circuits the LLM entirely (zero LLM calls). It:

    • writes a durable [[waiver]] to .gadriel-waivers.toml, keyed by (rule_id, path, line), which segregates the finding on every future scan regardless of re-detection count (the --reason text is recorded verbatim);
    • updates the per-repo Bayesian posterior with a full-weight DismissNotReal signal (β += 1.0) — the CLI caller is a trusted human;
    • records a replay-buffer episode and transitions any still-proposed fix rows to rejected;
    • appends a confirmed_false_positive audit event (agent_runner: "human").

    See false-positives.md for how waivers and priors compound.

    --confirm-real — affirm it. Also LLM-free: bumps the posterior toward "real" (α += 1.0), records a lower-priority replay episode, and appends a confirmed_real audit event. It writes no waiver.

    When the live ClaudeRunner path is active (i.e. GADRIEL_USE_MOCK_RUNNER unset), the confirm-real path ships a ~30-line source excerpt around the finding to Anthropic. fix surfaces this before touching the network and requires explicit --send-source consent; without it, fix aborts. Consent may also be persisted via [tier2].source_consent in .gadriel.toml, and a config-load failure fails closed (consent stays whatever the flag said). Mock runs never reach the network and ignore the flag. See privacy.md.


    3. The MCP fix round-trip — fix_finding

    The in-session path an AI coding assistant runs. Instead of a human reading a report and relaying it, the assistant calls the fix_finding MCP tool, gets back a diff, applies it, and asks Gadriel to re-validate. This is the loop described in ai-coding-integration.md §3.

    assistant edits code
        │
        ▼
    validate_buffer / validate_file ──▶ findings?
        │
        └─ findings ──▶ fix_finding { finding_id, runner_id: "claude" }
                             │
                             ▼
                       FixProposal { diff, rationale, … }   ← Gadriel proposes
                             │
                       host agent APPLIES the diff          ← host applies (out of process)
                             │
                             ▼
                       validate_file (re-scan) ──▶ finding cleared?
                             │
                             └─▶ .security/audit-log.jsonl (local)
    

    fix_finding returns a FixProposal envelope ({ status, runner_id, finding_id, diff?, rationale, iterations_used, tool_calls }) when a runner_id is supplied. The host agent applies the returned diff — fix_finding itself writes no source. Re-validation is a fresh validate_file (or validate_buffer) call; a cleared finding closes the loop. Because validate_buffer runs on the unsaved buffer, a vulnerability can be corrected before the file is ever written — so it never enters git.

    For the exact params, response shapes, error envelopes, and the runner-level source-consent enforcement, see the fix_finding entry in the MCP tools reference.


    4. Autonomous remediation campaigns

    A single fix_finding call fixes one finding. To fix a whole class at repo-scale — "fix all 40 confirmed SQLi findings" — without the host agent hand-rolling a 40× loop, Gadriel exposes a stateful two-tool pair: start_remediation_campaign and campaign_advance. The host still applies every diff; Gadriel drives the ordering, verification, and progress accounting.

    4.1 Starting a campaign

    start_remediation_campaign loads .security/findings.json, filters it (severity / pillar / scan_type / rule_id / confidence_tier, plus an optional finding_ids allowlist), orders the worklist (highest effective_risk first, then attack-path presence, then shortest path; same-file findings stay adjacent), persists state to .security/campaigns/<campaign_id>.json, and takes a single-campaign-per-repo .lock. It returns { campaign_id, total, first_batch }.

    Only one campaign runs per repo at a time. A crashed campaign's lock becomes stale after an hour and is reclaimed automatically; an operator can also force past a live lock with force: true.

    4.2 The advance loop

    campaign_advance is the fix → apply → verify → next step the host calls repeatedly:

                           ┌──────────────────────────────────────────────┐
      campaign_advance ───▶│ pick next Pending item → mark InProgress      │
      (applied: absent)    │ propose diff via fix_finding (LLM)            │
                           │ return { status: "in_progress", proposed_fix }│
                           └──────────────────────────────────────────────┘
                                            │
                              host APPLIES the diff (out of process)
                                            │
                           ┌──────────────────────────────────────────────┐
      campaign_advance ───▶│ re-scan the touched file                      │
      (applied: true)      │  → verified / verified_partial / regressed    │
                           │ then advance to the next Pending item         │
                           └──────────────────────────────────────────────┘
                                            │
                                  … repeat until no Pending items …
                                            │
                           ┌──────────────────────────────────────────────┐
                           │ { status: "campaign_complete", summary }      │
                           └──────────────────────────────────────────────┘
    
    1. First call for an item (no applied signal) — reads the next pending item, marks it in_progress, calls fix_finding's propose logic, and returns a diff + rationale.
    2. The host applies the diff, then calls again with applied: true.
    3. Verification call — runs the re-scan on the touched file, records a terminal status, and returns the next item (or the completion summary).

    An explicit decline (applied: false) writes a mechanized waiver and moves the item to waived rather than leaving it stuck in_progress.

    4.3 Per-item terminal states

    StatusMeaning
    verifiedRe-scan confirmed the finding is gone (Secrets, or SAST on Python).
    verified_partialFix applied but the finding's scan type is outside instant-re-scan coverage (SCA / Container / API / non-Python SAST), or the re-scan itself failed. Never a silent synonym for verified.
    regressedThe finding is still present, or a new one was introduced — never waived, never dropped, always surfaced in the summary.
    waivedRunner could not fix it, or the host declined the diff → mechanized durable waiver with an auto-populated reason.
    skipped_no_agentNo agent/runner available (an unauthored pillar — only Security and Safety agents exist today; the other 6 fail closed — or a stub/offline runner).
    fixedHost reported applied: true but the finding could no longer be re-resolved to attempt verification (rare edge case).

    Verification is a line-based match (rule/pattern id + line_start), not a full source diff — good enough for an in-place fix at the flagged line, but a diff that shifts surrounding lines can produce a false regressed/verified read. The completion summary buckets items by status, lists every regressed finding explicitly, and — when any item is verified_partial — carries a verified_partial_note spelling out exactly what could not be deterministically re-verified (anti-overclaim discipline).

    4.4 Campaign state on disk

    State lives under .security/campaigns/:

    .security/campaigns/
    ├── .lock                       # single-campaign-per-repo lock (holder id + mtime)
    └── campaign-1719….json         # the campaign worklist + running cost
    

    The state file's JSON shape:

    {
    "campaign_id": "campaign-1719…",
    "items": [
    { "finding_id": "CODE-W1-AI-001", "status": "verified" },
    { "finding_id": "CODE-W1-AI-002", "status": "in_progress" }
    ],
    "cost_usd": 0.42
    }

    The only files a campaign writes are this state file, the campaigns directory, the .lock, and (on declines/failures) .gadriel-waivers.toml. Byte-for-byte, the repo tree outside .security/ is unchanged across a full campaign run — asserted by the module's own tests.


    5. Safety invariants

    The remediation surface is deliberately conservative. These hold across every workflow above:

    1. Gadriel never writes source. fix_finding and campaign_advance return diffs; the host agent applies them out of process and self-reports applied: true. No auto-commit, no auto-git add. A buggy or malicious diff can never be silently committed by Gadriel.
    2. Every action is audited. Confirmations, proposed fixes, dismissals, and campaign outcomes append to .security/audit-log.jsonl (and durable dispositions to .gadriel-waivers.toml). The audit log is local-only compliance evidence — see privacy.md.
    3. Verification never overclaims. Instant re-scan is deterministic only for Secrets + Python-SAST; everything else is verified_partial, and a regression is always surfaced, never waived or dropped.
    4. The gate stays deterministic. Remediation reads and clears Tier-0 findings; it cannot promote an AI/Tier-2 signal into the gate. Tier 2 is opt-in and default off. See tiers.md.
    5. Source leaves the machine only with consent. Live LLM fixes require explicit source-content consent (--send-source for the CLI; GADRIEL_MCP_SOURCE_CONSENT=true for the MCP server); the gate fails closed.

    5.1 Documented TODO — the per-fix Tier-2 second-opinion gate

    An optional per-fix Tier-2 second-opinion gate is planned: a corroboration step that would run before a campaign item's re-scan verification and narrow the verified_partial bucket further. It is not wired yet. campaign_advance today runs re-scan verification directly after applied: true with no corroboration step first. When it does land it inherits the never-gate invariant — a negative second opinion would flag and demote, never hard-fail the campaign or auto-revert an applied fix.