This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new eb90d72  feat(security): credit obvious bots as CVE type:"tool", not 
skipped finders (#290)
eb90d72 is described below

commit eb90d72eef7cf6d09ab4e8cd7402a50ec229a29c
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon May 25 21:53:54 2026 +0200

    feat(security): credit obvious bots as CVE type:"tool", not skipped finders 
(#290)
    
    CVE 5.x's `credits[]` schema has a dedicated `type: "tool"` value
    exactly for automation that surfaced a vulnerability -- scanners,
    AI agents, GitHub bots. Until this commit, the framework's bot
    detection treated all such candidates the same way: skip the
    credit row entirely. That under-credits real tool contributions
    to the CVE record.
    
    This change splits the rule by which credit field is involved:
    
    - **Finder side (`Reporter credited as`)**: detected bot/AI ->
      included in the field; the CVE JSON generator emits the row
      with `type: "tool"` instead of the default `type: "finder"`.
      Scanners and agents get the credit they deserve, just under
      the right schema category.
    - **Remediation-developer side (`Remediation developer`)**:
      detected bot -> still skipped. There is no remediation-side
      equivalent of `type: "tool"`; a Dependabot dep-bump is the
      automation doing what humans configured it to do and does not
      warrant a credit row.
    
    The clarification Gmail draft for direct-reporter mode is
    reframed accordingly: instead of "is `<bot>` really how you want
    to be credited?", it now asks "we've credited `<bot>` as a tool;
    if a human was behind it who should also be credited as finder,
    please reply with their name". The tool credit is the floor; the
    human finder credit is an additive ask.
    
    Detection rules (`[bot]` suffix, known-bot/automation-name list,
    `*-bot` / `*-ai` / `*-agent` / `*-gpt` / `*scanner*` / `*automat*`
    patterns, noreply/relay senders) are unchanged. The single source
    of truth for `who counts as a bot` stays in
    `tools/vulnogram/bot-credits-policy.md`; the same rule is now also
    implemented in `generate_cve_json.is_bot_credit()` so the
    generator can route per-row.
    
    Generator change:
    
    - `tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py`:
      new `is_bot_credit(name)` predicate (mirrors the policy doc's
      three handle-shaped rules: `[bot]` suffix, known-name whole-word
      match, regex pattern set). New `TOOL_CREDIT_TYPE = "tool"`
      constant. `build_credits()` runs `is_bot_credit()` on every line
      of *Reporter credited as* and emits `type: "tool"` when it
      matches, falling back to `type: "finder"` otherwise. The
      remediation-developer side is unchanged -- it still emits
      `type: "remediation developer"` for every row, and the
      upstream-skill skip rule keeps bots out of that field.
    - Module docstring's `credits[]` summary updated to name the
      new tool routing.
    
    Tests:
    
    - New `TestIsBotCredit` (16 cases): GitHub `[bot]` suffix
      variants, known-name list (case-insensitive, free-form string),
      pattern handles (`*-bot`, `securitybot`, `scan-ai`, etc.),
      human-name false-positive guards (`Alice Smith`, `Jane Doe`,
      `Joe Bot` -- space breaks the word boundary so the pattern
      doesn't fire on the human name), empty / whitespace.
    - New `TestBuildCreditsBotTypeAssignment` (6 cases): plain human
      -> finder, GitHub bot suffix -> tool, known bot name -> tool,
      pattern handle -> tool, mixed-row field -> per-row type
      assignment, remediation-developer side unaffected by the new
      routing (an explicit regression test for the asymmetric scope
      decision).
    - 229 total tests pass (210 existing + 19 new).
    
    Policy doc + skills:
    
    - `tools/vulnogram/bot-credits-policy.md`: rewritten *Why*,
      *Default behaviour*, *Where the rule applies / does NOT apply*,
      and *Worked examples* to reflect the asymmetric finder/
      remediation routing. Detection rules unchanged. New worked
      example showing a mixed-credit field through the generator
      (Alice Smith -> finder; Dependabot -> tool).
    - `security-issue-import` SKILL.md: ASF-relay credit extraction
      + Reporter-credited-as field rows updated -- bot detection now
      produces a `credited as tool: <handle>` proposal entry and
      keeps the handle in the field. The relay/noreply-sender
      suppression for routing artefacts is preserved.
    - `security-issue-import-from-md` SKILL.md: reporter-line
      extraction routed through tool-credit instead of skip.
    - `security-issue-sync` SKILL.md: reporter-credit mining from
      email replies routed through tool-credit; the clarification
      draft framing updated to ask about an additional human finder
      rather than replacing the bot credit. The remediation-side
      PR-author-append rule (Step 2b) is unchanged.
    - `security-issue-deduplicate` SKILL.md: explicit per-side
      behaviour -- finder-side rows from either tracker propagate as
      tool credits; remediation-side rows still skip.
    
    Generated-by: Claude Code (Opus 4.7)
---
 .claude/skills/security-issue-deduplicate/SKILL.md |  40 ++-
 .../skills/security-issue-import-from-md/SKILL.md  |   2 +-
 .claude/skills/security-issue-import/SKILL.md      |   4 +-
 .claude/skills/security-issue-sync/SKILL.md        |  20 +-
 tools/vulnogram/bot-credits-policy.md              | 347 ++++++++++++++-------
 .../src/generate_cve_json/cve_json.py              | 111 ++++++-
 .../tests/test_generate_cve_json.py                | 135 +++++++-
 7 files changed, 508 insertions(+), 151 deletions(-)

diff --git a/.claude/skills/security-issue-deduplicate/SKILL.md 
b/.claude/skills/security-issue-deduplicate/SKILL.md
index 5163d0f..6f77510 100644
--- a/.claude/skills/security-issue-deduplicate/SKILL.md
+++ b/.claude/skills/security-issue-deduplicate/SKILL.md
@@ -304,20 +304,34 @@ confirmed, or the placeholder form when unconfirmed; the 
merge
 does not silently re-synthesize credits)
 
 **Apply the [bot/AI credit 
policy](../../../tools/vulnogram/bot-credits-policy.md)
-when consolidating.** If either tracker carries a credit line that
-matches the bot detection rule (`*[bot]` suffix, known-bot list,
+when consolidating.** If either tracker carries a credit line on
+the **finder side** (*Reporter credited as*) that matches the bot
+detection rule (`*[bot]` suffix, known-bot list,
 `*-bot`/`*-ai`/`*-agent`/`*-gpt` / `*scanner*` / `*automat*`
-suffix patterns, automation-name list), **do not** propagate that
-line into the kept tracker's *Reporter credited as* field. Instead,
-surface in the proposal *"skipped credit (during merge): `<line>`
-(matches bot policy — `<rule>`)"* with the source tracker number.
-If the drop tracker has an inbound reporter thread to reply on,
-also propose the policy's *clarification-reply* Gmail draft asking
-whether the bot/AI handle is the intended credit. The user can
-override per the policy doc. Manual credits that a human
-security-team member typed in (visible in the issue timeline) are
-always preserved verbatim — the filter only fires on credit lines
-that were auto-extracted upstream.
+suffix patterns, automation-name list), propagate the line into
+the kept tracker's *Reporter credited as* field unchanged — the
+CVE JSON generator emits it with `type: "tool"` per the policy's
+finder-side rule. Surface in the proposal *"credited as tool
+(during merge): `<line>` (matches bot policy — `<rule>`)"* with
+the source tracker number so the user can see which rows are
+being routed as tools. If the drop tracker has an inbound
+reporter thread to reply on, also propose the policy's
+*clarification-reply* Gmail draft asking whether a human behind
+the bot/AI handle should be **additionally** credited as finder.
+The user can override per the policy doc.
+
+For the **remediation-developer side**, the dedup still applies
+the original *skip* rule: a bot-matching line in either tracker's
+*Remediation developer* field is dropped from the merge result
+(no `type: "tool"` mapping exists for remediation-developer
+credits — see the policy doc). Surface *"skipped credit
+(during merge): `<line>` (matches bot policy — `<rule>`)"* for
+remediation-side rows.
+
+Manual credits that a human security-team member typed in
+(visible in the issue timeline) are always preserved verbatim
+on both sides — the filter only fires on credit lines that were
+auto-extracted upstream.
 
 ```markdown
 ### PR with the fix
diff --git a/.claude/skills/security-issue-import-from-md/SKILL.md 
b/.claude/skills/security-issue-import-from-md/SKILL.md
index 4ca0d7c..f249cfd 100644
--- a/.claude/skills/security-issue-import-from-md/SKILL.md
+++ b/.claude/skills/security-issue-import-from-md/SKILL.md
@@ -360,7 +360,7 @@ the role → concrete-name mapping comes from
 | `**Repository:**` + `**Branch:**` | `Affected versions` | Literal text 
*"`<owner>/<repo>` @ `<branch>` — versions to be confirmed during triage."* The 
release-train mapping happens at allocation. |
 | (auto) | `Security mailing list thread` | `N/A — imported from markdown file 
<basename>; no security@ thread.` |
 | (auto) | `Public advisory URL` | `_No response_`. |
-| (auto) | `Reporter credited as` | `_No response_`. The credit decision 
happens at triage; if the file is AI-generated, there is typically no human 
finder to credit. If the markdown carries a `**Reporter:**` / `**Finder:**` / 
`**Discovered by:**` metadata line naming a specific handle, **apply the 
[bot/AI credit policy](../../../tools/vulnogram/bot-credits-policy.md)** before 
lifting it into the field — when the policy fires (e.g. the markdown was 
generated by an LLM scan and names the  [...]
+| (auto) | `Reporter credited as` | `_No response_`. The credit decision 
happens at triage; if the file is AI-generated, there is typically no human 
finder to credit. If the markdown carries a `**Reporter:**` / `**Finder:**` / 
`**Discovered by:**` metadata line naming a specific handle, **apply the 
[bot/AI credit policy](../../../tools/vulnogram/bot-credits-policy.md)** before 
lifting it into the field — when the policy fires (e.g. the markdown was 
generated by an LLM scan and names the  [...]
 | `## Location` URL (when it points at a `<upstream>` PR) | `PR with the fix` 
| The URL. Otherwise `_No response_` — the location commonly references a 
vulnerable file, not a fix. |
 | (auto) | `Remediation developer` | `_No response_`. |
 | `**Category:**` | `CWE` | Literal value (free text); the actual CWE 
assignment happens at triage / allocation. |
diff --git a/.claude/skills/security-issue-import/SKILL.md 
b/.claude/skills/security-issue-import/SKILL.md
index a237c56..149a65e 100644
--- a/.claude/skills/security-issue-import/SKILL.md
+++ b/.claude/skills/security-issue-import/SKILL.md
@@ -995,7 +995,7 @@ Decide the candidate's class from the root message:
 | Class | How to spot it | How to handle |
 |---|---|---|
 | **Report**: a reporter describes a vulnerability | The body has a 
description, a PoC / reproduction steps, an impact claim. Sender is an external 
address (not `@apache.org`, not on the security-team roster in 
[`AGENTS.md`](../../../AGENTS.md)). | Proceed to Step 4. |
-| **ASF-security relay**: `[email protected]` forwarded a report from a 
reporter via the Foundation channel | Sender is `[email protected]`. The body 
almost always starts with the ASF forwarding preamble — *"Dear PMC, The 
security vulnerability report has been received by the Apache Security Team and 
is being passed to you for action …"* — and contains the original report 
underneath (often after a `====GHSA-…` separator when the report came in via 
GitHub Security Advisory). The pream [...]
+| **ASF-security relay**: `[email protected]` forwarded a report from a 
reporter via the Foundation channel | Sender is `[email protected]`. The body 
almost always starts with the ASF forwarding preamble — *"Dear PMC, The 
security vulnerability report has been received by the Apache Security Team and 
is being passed to you for action …"* — and contains the original report 
underneath (often after a `====GHSA-…` separator when the report came in via 
GitHub Security Advisory). The pream [...]
 | **Report (disposition converged)**: a `Report` where the inbound thread has 
a team-member substantive technical disposition AND the reporter has 
acknowledged it | Same body shape as `Report`, but the thread has a team-member 
reply with one of: option-1/option-2 framing, *"we agree, opening fix PR"* 
disposition, a docs-clarification acknowledgement; AND the reporter has replied 
confirming the disposition; AND no further reporter follow-up is needed. 
Detected at Step 3 by reading the thr [...]
 | **CVE-tool bookkeeping**: an automated or human status-change notification 
on the ASF CVE tool | Sender is `[email protected]` (or one of the 
security-team members acting on behalf of the CVE tool). Subject matches one 
of: `"CVE-YYYY-NNNNN reserved for airflow"`, `"Comment added on 
CVE-YYYY-NNNNN"`, `"CVE-YYYY-NNNNN is now READY"`, `"CVE-YYYY-NNNNN is now 
PUBLIC"`, `"CVE-YYYY-NNNNN is now PUBLISHED"`, `"CVE-YYYY-NNNNN REJECTED"`, or 
a verbatim `"<state-change>"` line in the body poin [...]
 | **Automated scanner dump**: SAST/DAST tool output, CodeQL/Dependabot alert 
paste, a string of "issues" with no human PoC | Body is machine-generated, 
contains multiple unrelated findings, no explanation of Security Model 
violation | Surface as a candidate with class `automated-scanner` and **do 
not** propose auto-import. In Step 5 the skill proposes a Gmail draft from the 
*"Automated scanning results"* canned response in 
[`canned-responses.md`](../../../<project-config>/canned-response [...]
@@ -1067,7 +1067,7 @@ here.
 | **Affected versions** | Extract `Airflow <version>` / `>= X, < Y` / `<Y` 
phrases from the body. If the reporter gave only a single version they tested 
on (e.g. `3.1.5`), record that verbatim; the triager can widen the range later. 
Leave `_No response_` if no version is mentioned. |
 | **Security mailing list thread** | **Keep the private thread handle, and — 
if possible — also link the PonyMail archive entry.** The full URL-construction 
recipe (search URL template, month-token format, user-pastes-back flow, 
Gmail-threadId fallback) lives in 
[`tools/gmail/ponymail-archive.md`](../../../tools/gmail/ponymail-archive.md#use-case--security-issue-import);
 the adopting project's private-search URL template is declared in 
[`<project-config>/project.md`](../../../<project-co [...]
 | **Public advisory URL** | `_No response_`. Populated at Step 14 by 
`security-issue-sync` once the advisory is archived. |
-| **Reporter credited as** | The reporter's full display name from the email 
`From:` header (e.g. `Alice Example` from `"Alice Example" 
<[email protected]>`). This is a **placeholder** — in direct-reporter mode, the 
receipt-of-confirmation reply in Step 7 asks the reporter to confirm their 
preferred credit form. **Apply the [bot/AI credit 
policy](../../../tools/vulnogram/bot-credits-policy.md) before populating** — 
if the `From:`-header name or address matches the bot detection rule (`*[ [...]
+| **Reporter credited as** | The reporter's full display name from the email 
`From:` header (e.g. `Alice Example` from `"Alice Example" 
<[email protected]>`). This is a **placeholder** — in direct-reporter mode, the 
receipt-of-confirmation reply in Step 7 asks the reporter to confirm their 
preferred credit form. **Apply the [bot/AI credit 
policy](../../../tools/vulnogram/bot-credits-policy.md) before populating** — 
if the `From:`-header name or address matches the bot detection rule (`*[ [...]
 | **PR with the fix** | `_No response_`. |
 | **Remediation developer** | `_No response_`. Auto-populated by the 
`security-issue-sync` skill from the linked PR's author the first time *PR with 
the fix* is set; manual edits are preserved on subsequent syncs. The 
auto-populate step applies the same [bot/AI credit 
policy](../../../tools/vulnogram/bot-credits-policy.md). |
 | **CWE** | `_No response_`. The security team scores CWE independently; a 
reporter-supplied CWE is informational only (per the *"Reporter-supplied CVSS 
scores are informational only"* rule in [`AGENTS.md`](../../../AGENTS.md)). Do 
**not** copy a CWE from the reporter's body into this field. |
diff --git a/.claude/skills/security-issue-sync/SKILL.md 
b/.claude/skills/security-issue-sync/SKILL.md
index e560155..571bf98 100644
--- a/.claude/skills/security-issue-sync/SKILL.md
+++ b/.claude/skills/security-issue-sync/SKILL.md
@@ -640,14 +640,18 @@ Process for finding the real reporter and the original 
thread:
    **Apply the [bot/AI credit 
policy](../../../tools/vulnogram/bot-credits-policy.md)
    to the extracted credit string** before proposing the update. If the
    credit handle matches the bot detection rule (`*[bot]` suffix,
-   known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` suffix patterns,
-   `noreply`/`security-alerts@` service sender), do **not** propose
-   landing the credit. Instead, surface in Step 2 *"skipped credit:
-   `<handle>` (matches bot policy — `<which rule fired>`)"* **and
-   propose a Gmail draft on the reporter's thread** per the policy's
-   *clarification-reply* step, asking whether the AI/bot handle is
-   the intended credit or whether there's a human behind it to credit
-   instead. The user can override the skip per the policy doc.
+   known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` suffix patterns),
+   propose landing the credit anyway — the CVE JSON generator will
+   emit it with `type: "tool"` per the policy's finder-side rule.
+   Surface in Step 2 *"credited as tool: `<handle>` (matches bot
+   policy — `<which rule fired>`)"* **and propose a Gmail draft on
+   the reporter's thread** per the policy's *clarification-reply*
+   step, asking whether a human behind the bot/AI handle should be
+   **additionally** credited as finder (the tool credit stands
+   regardless of the reply). The user can override the routing per
+   the policy doc. Service-sender addresses (noreply / relays) are
+   still suppressed from the field — they are routing artefacts, not
+   identities.
 
    If the reporter has been *asked* the credit question but has not yet
    responded, do not propose a change — leave the placeholder in place
diff --git a/tools/vulnogram/bot-credits-policy.md 
b/tools/vulnogram/bot-credits-policy.md
index 5cb9d90..fb3ea80 100644
--- a/tools/vulnogram/bot-credits-policy.md
+++ b/tools/vulnogram/bot-credits-policy.md
@@ -6,6 +6,8 @@
   - [Why](#why)
   - [Detection — when does the rule fire?](#detection--when-does-the-rule-fire)
   - [Default behaviour — what the skills do when the rule 
fires](#default-behaviour--what-the-skills-do-when-the-rule-fires)
+    - [Finder side (*Reporter credited as*)](#finder-side-reporter-credited-as)
+    - [Remediation-developer side (*Remediation 
developer*)](#remediation-developer-side-remediation-developer)
   - [Where the rule applies](#where-the-rule-applies)
   - [Where the rule does NOT apply](#where-the-rule-does-not-apply)
   - [Worked examples](#worked-examples)
@@ -17,34 +19,52 @@
 
 # CVE-credit policy for bot / AI accounts
 
-The CVE record's `credits[]` array records *people* who discovered a
-vulnerability (`type: "finder"`) or shipped the fix
-(`type: "remediation developer"`). When an obvious bot or AI account
-appears as the candidate for either role, the default is to **not**
-credit them. The skills that populate the *Reporter credited as* and
-*Remediation developer* tracker body fields enforce this rule at the
-point of extraction, and ask the user before adding the credit when
-the rule matches.
-
-This file is the single source of truth for *who counts as a bot* and
-*how the skills behave when one is detected*. Skills reference it
-instead of duplicating the heuristic.
+The CVE 5.x `credits[]` schema distinguishes between people who
+discovered a vulnerability (`type: "finder"`), automation that
+discovered it (`type: "tool"`), and people who shipped the fix
+(`type: "remediation developer"`). When an obvious bot or AI
+account appears as a candidate credit, the framework routes it
+asymmetrically:
+
+* **On the finder side** (*Reporter credited as* body field), the
+  bot is credited with `type: "tool"`. Scanners, AI agents, and
+  automation that surface a real vulnerability deserve the credit
+  — just under the schema's tool category, not as a human finder.
+* **On the remediation-developer side** (*Remediation developer*
+  body field), the bot is **not** credited. A dependency-bump
+  PR from Dependabot is the automation doing what humans
+  configured it to do, not a credit-worthy remediation effort.
+
+This file is the single source of truth for *who counts as a bot*
+and *how the skills + the CVE JSON generator behave when one is
+detected*. Both reference it instead of duplicating the heuristic.
 
 ## Why
 
 * Bot accounts (Dependabot, Renovate, GHSA Probot, GitHub Actions,
   Snyk, Mend, …) act on behalf of an organisation or a piece of
-  automation — they are not the *person* who found the bug or fixed
-  it. Naming them as `finder` or `remediation developer` in a public
-  CVE record misrepresents authorship and pollutes the CNA-feed
-  with handles that are not actionable as contributor credit.
-* AI / LLM agents that opened a PR or filed a report are tools the
-  human used. The human who drove the agent — if there is one
-  identifiable in the thread — is the candidate for credit. The
-  agent itself never is.
+  automation. Naming them as `finder` in a public CVE record is
+  inaccurate — they are not human researchers. Naming them as
+  `tool` is accurate and is what CVE 5.x's
+  [credit-type 
enum](https://cveproject.github.io/cve-schema/schema/CVE_Record_Format.json)
+  was designed for: the same axis as `finder`, just for automation.
+* AI / LLM agents that opened a PR or filed a report fit the same
+  shape — they are tools the human used. If the human behind the
+  agent is identifiable in the thread, they get the human
+  `finder` credit *in addition to* the tool credit; the agent
+  itself is the tool row.
 * Forwarding services (`[email protected]`,
   `security-alerts@<scanner>.com`, …) sit between the actual
-  reporter and us; their address is a relay, not an identity.
+  reporter and us; their address is a relay, not an identity. They
+  are not credited at all — the actual reporter (or the scanner
+  whose alerts the relay forwards) is what the skills extract.
+* On the remediation-developer side the asymmetry is intentional:
+  a CVE record's remediation credit speaks to *who fixed it*, and
+  a Dependabot dep-bump (or any other "automation did the
+  mechanical change") does not warrant credit. There is no
+  remediation-side equivalent to `type: "tool"` — the cleanest
+  expression of "no human deserves credit here" is to omit the
+  row entirely.
 
 ## Detection — when does the rule fire?
 
@@ -85,84 +105,101 @@ public CVE record).
 
 ## Default behaviour — what the skills do when the rule fires
 
-Each skill that extracts a credit candidate applies these rules at
-extraction time:
+The action depends on which credit field is involved.
 
-1. **Skip silently in the data flow.** Do not write the bot's name
-   into the *Reporter credited as* or *Remediation developer* body
-   field. Leave the field at its current value (typically
-   `_No response_` for a fresh tracker, or whatever the prior
-   resolved value was for an existing tracker).
-2. **Surface the skip in the user-facing proposal.** Include a
-   one-line entry in the proposal's "skipped" section of the shape:
+### Finder side (*Reporter credited as*)
 
-   ```text
-   skipped credit: <handle> (matches bot policy — ends with [bot])
-                                                  ^^^^^ which rule matched
-   skipped credit: <handle> (matches bot policy — in known-bot list)
-   skipped credit: <handle> (matches bot policy — *-bot suffix pattern)
-   skipped credit: <email>  (matches bot policy — noreply service sender)
-   ```
+The skills that extract a reporter-credit candidate (see
+[Where the rule applies](#where-the-rule-applies) below) treat a
+detected bot as a **tool credit**:
 
-   The reason — *which* rule fired — is mandatory; it lets the user
-   judge whether the skip is correct.
-3. **Honour an explicit override.** The user may override the skip
-   for a specific tracker with any of these phrasings (or obvious
-   variants):
+1. **Include the bot in the *Reporter credited as* field**, on
+   its own line, exactly as detected. The
+   [CVE JSON generator](generate-cve-json/) reads the field on its
+   next regeneration, runs the same detection rule on every line,
+   and emits the bot row with `type: "tool"` (instead of the
+   default `type: "finder"`). The generator is the single point
+   where `finder`-vs-`tool` is decided — skills do not need to
+   annotate the field.
+2. **Surface the routing in the user-facing proposal.** Include a
+   one-line entry of the shape:
 
-   * *"include `<handle>` anyway"*
-   * *"credit `<handle>` as finder"*
-   * *"credit `<handle>` as remediation developer"*
-   * *"yes, add `<handle>`"*
+   ```text
+   credited as tool: <handle> (matches bot policy — ends with [bot])
+                                                    ^^^^^ which rule matched
+   credited as tool: <handle> (matches bot policy — in known-bot list)
+   credited as tool: <handle> (matches bot policy — *-bot suffix pattern)
+   credited as tool: <handle> (matches bot policy — *scanner* pattern)
+   ```
 
-   When the user confirms the override, set the appropriate body
-   field exactly as the user dictated. Do not auto-extend the
-   override to other trackers — overrides are per-tracker.
+   The reason — *which* rule fired — is mandatory; it lets the
+   user judge whether the routing is correct.
+3. **Honour an explicit override.** The user may override the
+   tool routing for a specific tracker with any of these
+   phrasings (or obvious variants):
+
+   * *"credit `<handle>` as finder"* — the credit lands in the
+     field; the user is explicitly telling the generator the
+     handle is a human researcher despite matching the
+     heuristic. Today the generator still re-classifies on
+     pattern match, so an override of this shape requires
+     editing the row to a form that does not match (e.g.
+     `Alice (Dependabot Security Research)` → drop the
+     `Dependabot` word, or move the affiliation outside the
+     credit string).
+   * *"omit `<handle>` from credits"* — drop the row entirely
+     (the tool is not credit-worthy in this specific case).
+   * *"yes, credit `<handle>` as tool"* — explicit confirmation
+     of the default; no action needed beyond proceeding.
+
+   Overrides are per-tracker; do not auto-extend.
 
 4. **Draft a clarification reply to the reporter when an email
    thread exists *and* the tracker is in direct-reporter mode.**
-   When the candidate would have been credited as the *finder*
-   (i.e. the reporter themselves is the bot-looking name), the
-   tracker has an inbound `<security-list>` mail thread to reply
-   on, **and** the tracker's routing mode (per
+   When the bot/AI handle is the only candidate the skill found
+   for the *finder* role, the tracker has an inbound
+   `<security-list>` mail thread, **and** the tracker's routing
+   mode (per
    
[`docs/security/forwarder-routing-policy.md`](../../docs/security/forwarder-routing-policy.md))
    is *direct-reporter*, propose a **Gmail draft** (not a sent
-   message) on the same thread asking whether the bot/AI handle
-   is the intended credit or whether there's a human behind it
-   who should be credited instead. The draft should be:
+   message) on the same thread. The draft asks whether a human
+   behind the bot/AI should be **additionally** credited as
+   `finder` — the bot stays in the field as a tool credit
+   regardless; the question is whether to *also* add a human
+   finder row. The draft should be:
 
    * **Polite and short** — one or two short paragraphs; no
      accusations, no jargon.
-   * **Specific** — name the handle that was detected and which
-     rule fired (so the reporter sees the same reasoning the
-     security team did).
-   * **Actionable** — offer two clear paths: *"credit `<handle>`
-     as-is"* or *"credit `<human-name>` instead — please reply
-     with the preferred attribution"*.
+   * **Specific** — name the handle that was credited as tool
+     and the rule that fired (so the reporter sees the same
+     reasoning the security team did).
+   * **Actionable** — offer one clear path: *"if a human was
+     behind `<handle>` who should also be credited as finder,
+     please reply with their name; otherwise the tool credit
+     stands as-is"*.
    * **Sent only after explicit user confirmation** per the
      [framework's never-send-without-asking rule](../../AGENTS.md).
 
    **In via-forwarder mode the standalone clarification draft
-   is suppressed.** It is a *credit-acceptance confirmation*
-   message (asking the reporter to confirm the AI/bot handle is
-   the intended credit, or to accept a different one) — and
-   credit-acceptance confirmations are on the
+   is suppressed.** Even with the new tool-credit default it
+   remains a *credit-acceptance confirmation* message (asking
+   the reporter to confirm or extend the credit attribution),
+   and credit-acceptance confirmations are on the
    [forwarder-routing-policy negative 
list](../../docs/security/forwarder-routing-policy.md#negative-space--do-not-relay).
-   The forwarder cannot meaningfully accept a credit on behalf
-   of the original reporter, so the message becomes a chase-up
-   loop. The bot-credit detection still runs and still keeps
-   the bot/AI handle out of the credit field; what is suppressed
-   is the dedicated message to confirm the alternative.
-
-   The credit *question* itself (initial ask, *"if the reporter
-   has a preferred credit form, please pass it back"*) is **not**
-   suppressed — it is folded as a single line into whatever
-   milestone draft the via-forwarder lifecycle next produces (the
-   Step 7 receipt-of-confirmation, the *Report accepted as
-   valid* milestone, the *CVE allocated* notification). The
-   credit field stays at `_No response_` (or whatever the
-   original report's `Credit:` line yielded after bot filtering)
-   until a meaningful answer comes back.
+   The forwarder cannot meaningfully accept or extend a credit
+   on behalf of the original reporter. The bot-credit detection
+   still runs, the tool row still lands in the field, and the
+   generator still emits `type: "tool"` for it; what is
+   suppressed is the dedicated message asking for a human
+   addition.
+
+   The credit *question* itself (initial ask, *"if a human was
+   behind the tool, please pass back their preferred
+   attribution"*) is **not** suppressed in via-forwarder mode —
+   it is folded as a single line into whatever milestone draft
+   the lifecycle next produces (the Step 7 receipt-of-
+   confirmation, the *Report accepted as valid* milestone, the
+   *CVE allocated* notification).
 
    When the tracker has no inbound mail thread at all (e.g. a
    `security-issue-import-from-pr` tracker — the
@@ -175,30 +212,71 @@ extraction time:
    inline and propose adding it to the canned-responses file as a
    follow-up.
 
+### Remediation-developer side (*Remediation developer*)
+
+The skills that extract a remediation-developer candidate (see
+[Where the rule applies](#where-the-rule-applies) below) **skip**
+the bot — there is no remediation-side equivalent of `type:
+"tool"`, and a dependency-bump from automation does not warrant
+a credit row. Behaviour:
+
+1. **Skip silently in the data flow.** Do not write the bot's
+   name into the *Remediation developer* body field. Leave the
+   field at its current value (typically `_No response_` for a
+   fresh tracker, or whatever the prior resolved value was for
+   an existing tracker).
+2. **Surface the skip in the user-facing proposal.** Include a
+   one-line entry of the shape:
+
+   ```text
+   skipped credit: <handle> (matches bot policy — ends with [bot])
+   skipped credit: <handle> (matches bot policy — in known-bot list)
+   skipped credit: <handle> (matches bot policy — *-bot suffix pattern)
+   ```
+3. **Honour an explicit override** with the same per-tracker
+   phrasings as the finder side (e.g. *"credit `<handle>` as
+   remediation developer"*).
+4. **No clarification draft.** The remediation-developer field is
+   reconciled by the skills from PR-author signals, not from
+   reporter input — there is no thread to ask. If a human
+   committer was the *real* remediation developer (the bot only
+   pushed mechanical formatting), the user adds them with an
+   explicit override.
+
 ## Where the rule applies
 
 This policy fires at every site where the suite *auto-extracts* a
 credit candidate without explicit user instruction:
 
-| Skill | Extraction site |
-|---|---|
-| 
[`security-issue-import`](../../.claude/skills/security-issue-import/SKILL.md) 
| Reporter name from email `From:` header; ASF-relay `Credit:` line |
-| 
[`security-issue-import-from-pr`](../../.claude/skills/security-issue-import-from-pr/SKILL.md)
 | PR author → *Remediation developer* |
-| 
[`security-issue-import-from-md`](../../.claude/skills/security-issue-import-from-md/SKILL.md)
 | Reporter / finder name from markdown metadata |
-| [`security-issue-sync`](../../.claude/skills/security-issue-sync/SKILL.md) | 
Reporter credit mined from email replies; PR author auto-append to *Remediation 
developer* |
-| 
[`security-issue-deduplicate`](../../.claude/skills/security-issue-deduplicate/SKILL.md)
 | Credit consolidation from two trackers |
+| Skill | Extraction site | Field | Action |
+|---|---|---|---|
+| 
[`security-issue-import`](../../.claude/skills/security-issue-import/SKILL.md) 
| Reporter name from email `From:` header; ASF-relay `Credit:` line | *Reporter 
credited as* | Include → tool |
+| 
[`security-issue-import-from-pr`](../../.claude/skills/security-issue-import-from-pr/SKILL.md)
 | PR author | *Remediation developer* | Skip |
+| 
[`security-issue-import-from-md`](../../.claude/skills/security-issue-import-from-md/SKILL.md)
 | Reporter / finder name from markdown metadata | *Reporter credited as* | 
Include → tool |
+| [`security-issue-sync`](../../.claude/skills/security-issue-sync/SKILL.md) | 
Reporter credit mined from email replies | *Reporter credited as* | Include → 
tool |
+| [`security-issue-sync`](../../.claude/skills/security-issue-sync/SKILL.md) | 
PR author auto-append | *Remediation developer* | Skip |
+| 
[`security-issue-deduplicate`](../../.claude/skills/security-issue-deduplicate/SKILL.md)
 | Credit consolidation from two trackers | both | Per-row by field type |
 
 The rule does **not** fire when the user *explicitly* types a name
 into a credit field, or when a tracker already carries a credit that
 a human security-team member set — those are explicit decisions and
-the skill respects them.
+the skill respects them. (On the finder side, the generator will
+still re-classify the row as `tool` if the name matches the policy;
+to force a human `finder` row, the user must edit the row to a form
+that does not match the heuristic — see the override note in
+[Default behaviour → Finder side](#finder-side-reporter-credited-as)
+above.)
 
 ## Where the rule does NOT apply
 
-* [`generate-cve-json`](generate-cve-json/SKILL.md) stays neutral.
-  Whatever is in the tracker's credit fields is what lands in the
-  CVE JSON. The filter is upstream, at the skills, so that an
-  intentional human override survives JSON regeneration.
+* [`generate-cve-json`](generate-cve-json/) **applies the bot
+  detection on the finder side** to decide between `type: "finder"`
+  and `type: "tool"`. It does **not** filter rows out (every
+  non-empty line in *Reporter credited as* becomes a credit entry)
+  and it does **not** apply the detection on the remediation-
+  developer side — those decisions live in the skills upstream so
+  an intentional human override on the remediation side survives
+  JSON regeneration.
 * The *PR with the fix* body field on a tracker still records the
   PR even when its author is a bot — the field captures the
   artifact, not the author. Only *Remediation developer* is
@@ -207,41 +285,70 @@ the skill respects them.
 ## Worked examples
 
 **`security-issue-import-from-pr` import of a Dependabot PR.** The
-PR's author is `dependabot[bot]`. The skill skips the
-*Remediation developer* assignment and surfaces:
-`skipped credit: dependabot[bot] (matches bot policy — ends with
-[bot])`. The user can override with *"credit dependabot[bot] as
-remediation developer"* if a real human at Dependabot HQ is owed the
-credit (unusual but allowed).
+PR's author is `dependabot[bot]`. The skill **skips** the
+*Remediation developer* assignment (remediation side) and
+surfaces: `skipped credit: dependabot[bot] (matches bot policy —
+ends with [bot])`. The user can override with *"credit
+dependabot[bot] as remediation developer"* if a real human at
+Dependabot HQ is owed the credit (unusual but allowed). No
+finder-side action — the PR-import path does not extract a
+reporter credit.
 
 **`security-issue-sync` mining a reporter email reply.** The
 reporter wrote *"please credit me as claude-bot, I used Claude
-Code"*. The skill skips the credit, surfaces `skipped credit:
-claude-bot (matches bot policy — *-bot suffix pattern)`, **and
-proposes a Gmail draft** on the original report thread asking
-*"the credit you suggested (`claude-bot`) reads like an AI/agent
-handle — would you prefer we credit you under your name (e.g. the
-one on the original report) instead, or is `claude-bot` the
-attribution you want?"* The user reviews the draft and approves
-the send. If the reporter replies with a human name, sync picks
-it up on the next pass.
+Code"*. The skill **includes** `claude-bot` in *Reporter credited
+as*, surfaces `credited as tool: claude-bot (matches bot policy —
+*-bot suffix pattern)`, **and proposes a Gmail draft** on the
+original report thread asking *"we've credited `claude-bot` as a
+tool (CVE 5.x `type: tool`); if a human was behind it who should
+also be credited as finder, please reply with their name —
+otherwise the tool credit stands as-is"*. The user reviews the
+draft and approves the send. If the reporter replies with a human
+name, sync picks it up on the next pass and the generator emits
+a second credit row of `type: "finder"` alongside the tool row.
 
 **`security-issue-import` from a relay address.** The `From:`
-header is `[email protected]`. The skill skips
-*Reporter credited as*, surfaces `skipped credit:
[email protected] (matches bot policy — noreply
-service sender)`, and prompts the user for the actual reporter
-identity (typically findable inside the email body).
+header is `[email protected]`. The relay
+address is a routing artefact, not a credit candidate — the skill
+extracts the actual reporter from the email body and credits
+that. The relay sender never lands in *Reporter credited as* at
+all (the noreply-service rule prevents the From-header heuristic
+from picking it up). If the email body contains
+*"automated scan by SecurityScanner-7"*, that string lands in the
+field and the generator emits it with `type: "tool"`.
 
 **`security-issue-import` of an ASF-relay report with an
 automation credit line.** The forwarded body ends with *"This
 vulnerability was discovered and reported by Automated Security
 Scanner v3 (run by ACME Sec Team)"*. The skill matches both the
 `automated` known-name and the `*scanner*` contains-pattern,
-skips *Reporter credited as*, surfaces `skipped credit:
-"Automated Security Scanner v3" (matches bot policy — known
-automation name + *scanner* pattern)`, and routes the
-credit-preference question to `@raboof` / Arnout via the
-ASF-relay credit-preference flow. The user can override with
-*"credit ACME Sec Team as finder"* if the human team behind the
-scanner is owed the credit.
+**includes** the string in *Reporter credited as*, surfaces
+`credited as tool: "Automated Security Scanner v3" (matches bot
+policy — known automation name + *scanner* pattern)`, and folds
+the *"is there a human at ACME Sec Team who should also be
+credited as finder?"* question into the next milestone draft
+routed to `@raboof` / Arnout via the ASF-relay credit-preference
+flow. The user can override with *"add ACME Sec Team as finder"*
+to land a second human credit row alongside the tool row.
+
+**`generate-cve-json` regeneration with a mixed-credit field.**
+The *Reporter credited as* field on tracker #NNN currently
+reads:
+
+```text
+Alice Smith, ACME Security Research
+Dependabot
+```
+
+The generator emits two `credits[]` entries:
+
+```json
+[
+  {"lang": "en", "type": "finder", "value": "Alice Smith, ACME Security 
Research"},
+  {"lang": "en", "type": "tool",   "value": "Dependabot"}
+]
+```
+
+No skill action is needed at regeneration time — the field text
+is the source of truth; the type assignment is computed per row
+from `is_bot_credit()` in the generator.
diff --git 
a/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py 
b/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py
index 21b45f5..3bf04c2 100644
--- a/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py
+++ b/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py
@@ -37,11 +37,15 @@ record. In particular, the script produces:
 * ``problemTypes[].descriptions[]`` with ``cweId``, ``description`` and
   ``type`` = *"CWE"*;
 * ``credits[]`` entries from the *Reporter credited as* field
-  (``type: "finder"``) and the *Remediation developer* field
-  (``type: "remediation developer"``) — both newline-separated, with
-  the ``Full Name, Affiliation`` pattern preserved as one credit; the
-  ``--remediation-developer`` CLI flag still works as an additional /
-  override mechanism on top of the body field;
+  (``type: "finder"`` by default; ``type: "tool"`` when the credit
+  matches the bot/AI policy in
+  ``tools/vulnogram/bot-credits-policy.md`` — Dependabot, Renovate,
+  Snyk, scanners, ``*-bot`` / ``*[bot]`` handles, etc.) and the
+  *Remediation developer* field (``type: "remediation developer"``)
+  — both newline-separated, with the ``Full Name, Affiliation``
+  pattern preserved as one credit; the ``--remediation-developer``
+  CLI flag still works as an additional / override mechanism on top
+  of the body field;
 * ``references[]`` with automatic ``tags`` (``patch`` for
   ``github.com/.../pull/...`` URLs, ``vendor-advisory`` for
   ``lists.apache.org``/``security.apache.org``). URLs from the
@@ -248,9 +252,103 @@ TRACKER_FILTER_TOKEN: str = ""
 
 # CVE 5.x convention values that are not project-specific.
 DEFAULT_CREDIT_TYPE = "finder"
+TOOL_CREDIT_TYPE = "tool"
 DEFAULT_LANG = "en"
 DEFAULT_DISCOVERY = "UNKNOWN"
 
+# Bot / AI / automation detection for the *Reporter credited as*
+# field. When a credit row matches any of these rules, the CVE 5.x
+# ``credits[]`` entry is emitted with ``type: "tool"`` instead of
+# ``type: "finder"``. The matching rules mirror
+# ``tools/vulnogram/bot-credits-policy.md`` (single source of truth);
+# update both together. Detection is intentionally broad — false
+# positives are cheap (the user can edit the JSON afterwards or set
+# the credit name to a clearer human-style string), false negatives
+# put a bot handle into the ``finder`` credit class in a public CVE
+# record, which is what this policy exists to prevent.
+#
+# Known bot / automation names matched as a case-insensitive
+# whole-word against the credit string (e.g. ``"Dependabot"``,
+# ``"discovered by Automated Scanner v3"``).
+BOT_CREDIT_KNOWN_NAMES: tuple[str, ...] = (
+    "dependabot",
+    "renovate",
+    "snyk-bot",
+    "snyk",
+    "copilot",
+    "ghsa-probot",
+    "github-actions",
+    "mend-bot",
+    "mend",
+    "whitesource",
+    "sonatype-lift",
+    "lift-bot",
+    "codecov",
+    "mergify",
+    "mergifyio",
+    "allcontributors",
+    "fossabot",
+    "imgbotapp",
+    "pre-commit-ci",
+    "claude",
+    "chatgpt",
+    "gpt-bot",
+    "gpt",
+    "anthropic",
+    "openai",
+    "automated",
+    "automation",
+    "scanner",
+    "auto-scanner",
+    "vulnerability-scanner",
+    "security-scanner",
+    "sast",
+    "dast",
+)
+
+# Handle-shaped patterns matched as case-insensitive regexes against
+# the credit string. ``\b`` anchors keep them from firing on
+# unrelated human names (``Joe Bot`` does not match ``*-bot`` because
+# the space breaks the word boundary, but ``joe-bot`` does).
+BOT_CREDIT_PATTERNS: tuple[re.Pattern[str], ...] = (
+    re.compile(r"\b[\w]*-bot\b", re.IGNORECASE),
+    re.compile(r"\b[\w]+bot\b", re.IGNORECASE),
+    re.compile(r"\b[\w]*-ai\b", re.IGNORECASE),
+    re.compile(r"\b[\w]*-agent\b", re.IGNORECASE),
+    re.compile(r"\b[\w]*-gpt\b", re.IGNORECASE),
+    re.compile(r"\b[\w]*scanner[\w]*\b", re.IGNORECASE),
+    re.compile(r"\b[\w]*automat[\w]*\b", re.IGNORECASE),
+)
+
+
+def is_bot_credit(name: str) -> bool:
+    """Return True if *name* matches the bot/AI credit policy.
+
+    Matches any of three rules:
+
+    1. Literal GitHub ``[bot]`` suffix — handle ends in ``[bot]``
+       (e.g. ``dependabot[bot]``).
+    2. Known-bot / automation-name list — case-insensitive whole-word
+       occurrence of any of ``BOT_CREDIT_KNOWN_NAMES`` in the credit
+       string.
+    3. Suffix / contains-pattern handles — any of
+       ``BOT_CREDIT_PATTERNS`` matches.
+
+    See ``tools/vulnogram/bot-credits-policy.md`` for the canonical
+    rule set and rationale.
+    """
+    cleaned = name.strip()
+    if not cleaned:
+        return False
+    if cleaned.endswith("[bot]"):
+        return True
+    lowered = cleaned.lower()
+    for known in BOT_CREDIT_KNOWN_NAMES:
+        if re.search(rf"\b{re.escape(known)}\b", lowered):
+            return True
+    return any(pattern.search(cleaned) for pattern in BOT_CREDIT_PATTERNS)
+
+
 # Populate at import time. If the config is not present, defer the
 # error to first actual use so test harnesses can call
 # `_set_config_path()` before exercising the module.
@@ -864,7 +962,8 @@ def build_credits(
 ) -> list[dict]:
     credits: list[dict] = []
     for name in parse_credits_from_field(credited_as_value):
-        credits.append({"lang": DEFAULT_LANG, "type": DEFAULT_CREDIT_TYPE, 
"value": name})
+        credit_type = TOOL_CREDIT_TYPE if is_bot_credit(name) else 
DEFAULT_CREDIT_TYPE
+        credits.append({"lang": DEFAULT_LANG, "type": credit_type, "value": 
name})
     for name in remediation_developers:
         cleaned = name.strip()
         if cleaned:
diff --git a/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py 
b/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py
index 148054d..2615577 100644
--- a/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py
+++ b/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py
@@ -58,7 +58,7 @@ from generate_cve_json import (
     resolve_title,
     wrap_cve_record,
 )
-from generate_cve_json.cve_json import normalise_severity, to_html
+from generate_cve_json.cve_json import is_bot_credit, normalise_severity, 
to_html
 
 DEFAULT_AFFECTED_ARGS: dict[str, Any] = {
     "vendor": "Apache Software Foundation",
@@ -140,6 +140,139 @@ class TestParseCreditsFromField:
         assert parse_credits_from_field("") == []
 
 
+# ---------------------------------------------------------------------------
+# Bot / AI credit detection (drives `type: "tool"` in build_credits)
+# ---------------------------------------------------------------------------
+
+
+class TestIsBotCredit:
+    @pytest.mark.parametrize(
+        "name",
+        [
+            "dependabot[bot]",
+            "github-actions[bot]",
+            "renovate[bot]",
+            "copilot[bot]",
+            "ghsa-probot[bot]",
+        ],
+    )
+    def test_github_bot_suffix_matches(self, name: str):
+        assert is_bot_credit(name) is True
+
+    @pytest.mark.parametrize(
+        "name",
+        [
+            "Dependabot",
+            "DEPENDABOT",
+            "Snyk",
+            "Renovate",
+            "github-actions",
+            "Claude",
+            "ChatGPT",
+            "Mend",
+            "Whitesource",
+        ],
+    )
+    def test_known_name_list_matches_case_insensitively(self, name: str):
+        assert is_bot_credit(name) is True
+
+    @pytest.mark.parametrize(
+        "name",
+        [
+            "discovered by Automated Scanner v3",
+            "reported via security-scanner",
+            "found by Renovate during dependency sweep",
+        ],
+    )
+    def test_known_name_matches_inside_free_form_string(self, name: str):
+        assert is_bot_credit(name) is True
+
+    @pytest.mark.parametrize(
+        "name",
+        [
+            "claude-bot",
+            "release-bot",
+            "securitybot",
+            "scan-ai",
+            "triage-agent",
+            "secaudit-gpt",
+            "securityscanner-7",
+            "automated-triage",
+            "automaton",
+        ],
+    )
+    def test_pattern_handles_match(self, name: str):
+        assert is_bot_credit(name) is True
+
+    @pytest.mark.parametrize(
+        "name",
+        [
+            "Alice Smith",
+            "Bob Jones (Acme Corp)",
+            "Jane Doe, Acme Security",
+            "Joe Bot",  # space breaks word boundary on *bot/-bot patterns
+            "Botev Martinov",  # word boundary stops the bot suffix at "Botev"
+            "Renovation Engineering Inc",  # 'renovate' substring, not whole 
word
+            "AI Research Lab",  # uppercase free-standing; no *-ai handle shape
+        ],
+    )
+    def test_human_names_do_not_match(self, name: str):
+        assert is_bot_credit(name) is False
+
+    def test_empty_string_does_not_match(self):
+        assert is_bot_credit("") is False
+
+    def test_whitespace_only_does_not_match(self):
+        assert is_bot_credit("   ") is False
+
+
+class TestBuildCreditsBotTypeAssignment:
+    def test_plain_human_credit_gets_finder_type(self):
+        credits = build_credits("Alice Smith", remediation_developers=[])
+        assert credits == [{"lang": "en", "type": "finder", "value": "Alice 
Smith"}]
+
+    def test_github_bot_suffix_credit_gets_tool_type(self):
+        credits = build_credits("dependabot[bot]", remediation_developers=[])
+        assert credits == [{"lang": "en", "type": "tool", "value": 
"dependabot[bot]"}]
+
+    def test_known_bot_name_credit_gets_tool_type(self):
+        credits = build_credits("Dependabot", remediation_developers=[])
+        assert credits == [{"lang": "en", "type": "tool", "value": 
"Dependabot"}]
+
+    def test_pattern_handle_credit_gets_tool_type(self):
+        credits = build_credits("claude-bot", remediation_developers=[])
+        assert credits == [{"lang": "en", "type": "tool", "value": 
"claude-bot"}]
+
+    def test_mixed_credits_get_per_row_types(self):
+        credits = build_credits(
+            "Alice Smith\nDependabot\nBob Jones (Acme Corp)",
+            remediation_developers=[],
+        )
+        assert credits == [
+            {"lang": "en", "type": "finder", "value": "Alice Smith"},
+            {"lang": "en", "type": "tool", "value": "Dependabot"},
+            {"lang": "en", "type": "finder", "value": "Bob Jones (Acme Corp)"},
+        ]
+
+    def test_remediation_developer_type_unaffected_by_bot_policy(self):
+        """Remediation-developer side is intentionally NOT routed through 
tool."""
+        credits = build_credits(
+            "Alice Smith",
+            remediation_developers=["dependabot[bot]"],
+        )
+        # Bot lands as remediation developer with its original type — the
+        # finder-side tool routing does not extend here. Skipping bots on
+        # the remediation side is a separate (upstream-skill) concern.
+        assert credits == [
+            {"lang": "en", "type": "finder", "value": "Alice Smith"},
+            {
+                "lang": "en",
+                "type": "remediation developer",
+                "value": "dependabot[bot]",
+            },
+        ]
+
+
 # ---------------------------------------------------------------------------
 # URL list parsing
 # ---------------------------------------------------------------------------

Reply via email to