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 14d80e6  feat(security): forwarder-routing policy for trackers with no 
direct reporter contact (#278)
14d80e6 is described below

commit 14d80e6e17326e4170b1cd8975649f7673effc81
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon May 25 16:17:10 2026 +0200

    feat(security): forwarder-routing policy for trackers with no direct 
reporter contact (#278)
    
    When a tracker has no direct way to reach the original reporter --
    ASF-security-relay reports, read-only GitHub Private Reporting, AI
    scan markdown imports, anonymous tips -- the skills now route
    reporter-facing communication through the forwarder (the security-
    team member or relay service that delivered the report). In that
    *via-forwarder mode*, only important lifecycle milestones are
    relayed. Regular workflow chatter and credit-acceptance confirmation
    messages are suppressed so the forwarder isn't pinged with
    low-signal updates that would burn their goodwill.
    
    - New `docs/security/forwarder-routing-policy.md`: single source of
      truth. Defines four ways via-forwarder mode is detected (ASF-relay
      sender, read-only GHSA, -from-md imports, explicit
      `<!-- apache-steward: routing-mode via-forwarder -->` marker).
      Milestones that DO relay: report-accepted-as-valid,
      report-assessed-as-invalid, advisory-sent, additional-information
      requests. Each milestone carries a short body template referencing
      the external identifier (GHSA ID, HackerOne URL) rather than
      re-stating the technical detail.
    - *CVE allocated* is intentionally handled OUTSIDE the policy:
      Vulnogram typically emits its own allocation notification, and the
      team owes the reporter (or forwarder) a single short notification
      here regardless of routing mode -- no recipient swap, no
      suppression.
    - Negative space is the *credit-acceptance confirmation* class:
      follow-up "please confirm we will credit you as X" chase-ups and
      the standalone bot/AI credit-clarification draft. The credit
      *question* itself (initial one-line ask folded into a milestone
      draft) is NOT suppressed -- the forwarder might know or might
      relay it. The distinction: a question is cheap and one-shot;
      a confirmation demands a reply the forwarder can't supply.
    - `security-issue-import` Step 7 ASF-relay branch: re-framed as the
      canonical via-forwarder receipt-of-confirmation. Folds the
      credit question in as a single best-effort line; no standalone
      credit-acceptance confirmation drafts.
    - `security-issue-sync` reporter-draft section: applies the policy
      to decide direct vs forwarder vs suppress, with a "skipped
      reporter draft" recap line for non-milestone events.
    - `security-issue-invalidate` Step 5d: re-framed as the *Report
      assessed as invalid* milestone; explicit direct vs forwarder
      recipient selection.
    - `security-cve-allocate` Step 4 #5: re-framed as out-of-scope per
      the policy. Same draft body in both modes; the credit *question*
      is folded in (allowed by the question-vs-confirmation
      distinction), the standalone re-confirmation is suppressed in
      via-forwarder mode.
    - `tools/vulnogram/bot-credits-policy.md`: defers to the new
      policy. The standalone bot/AI credit-clarification draft (a
      credit-acceptance confirmation by nature) is suppressed in
      via-forwarder mode; the bot detection itself still runs.
    - `docs/security/README.md` deep-doc index + `roles.md` *Shared
      conventions -> Keeping the reporter informed*: link to the
      policy.
    
    Generated-by: Claude Code (Opus 4.7)
---
 .claude/skills/security-cve-allocate/SKILL.md     |  25 ++-
 .claude/skills/security-issue-import/SKILL.md     |  48 +++--
 .claude/skills/security-issue-invalidate/SKILL.md |  28 ++-
 .claude/skills/security-issue-sync/SKILL.md       |  22 ++
 docs/security/README.md                           |   7 +
 docs/security/forwarder-routing-policy.md         | 237 ++++++++++++++++++++++
 docs/security/roles.md                            |  13 ++
 tools/vulnogram/bot-credits-policy.md             |  46 ++++-
 8 files changed, 389 insertions(+), 37 deletions(-)

diff --git a/.claude/skills/security-cve-allocate/SKILL.md 
b/.claude/skills/security-cve-allocate/SKILL.md
index 28c7c7f..f68e661 100644
--- a/.claude/skills/security-cve-allocate/SKILL.md
+++ b/.claude/skills/security-cve-allocate/SKILL.md
@@ -459,6 +459,22 @@ user to confirm. Numbered items:
    been allocated, one sentence that the advisory will be sent
    once the fix ships, the ASF CVE tool URL on its own line.
 
+   **This draft fires in both direct-reporter and via-forwarder
+   modes** and does **not** follow the
+   [forwarder-routing 
policy](../../../docs/security/forwarder-routing-policy.md)
+   suppress-on-non-milestone rule. CVE allocation is treated as
+   out-of-scope for that policy (see its
+   [*Events handled outside this 
policy*](../../../docs/security/forwarder-routing-policy.md#events-handled-outside-this-policy)
+   section): Vulnogram typically emits its own allocation email
+   when the CVE record is created, and even when it does not,
+   the team owes the reporter (or their forwarder) a single
+   short notification at this point regardless of routing mode.
+   The draft lands on whatever thread the tracker's
+   *Security mailing list thread* field resolves to — the
+   inbound reporter thread in direct-reporter mode, the relay
+   thread in via-forwarder mode — and the body is the same in
+   both cases.
+
    **Before drafting, check for an existing pending draft** on the
    inbound thread per the *Detecting drafts that already exist on a
    thread* section of
@@ -468,7 +484,14 @@ user to confirm. Numbered items:
    on the inbound `threadId` (catches thread-attached drafts that may
    pile up and hide from the global Drafts folder, regardless of
    backend). Re-ask the credit-preference question **only if it has
-   not yet been asked** on the thread — never ping twice.
+   not yet been asked** on the thread — never ping twice. The
+   initial credit *question* (one-line ask folded into this draft)
+   fires in both routing modes per the forwarder-routing policy's
+   [question-vs-confirmation 
distinction](../../../docs/security/forwarder-routing-policy.md#negative-space--do-not-relay);
+   what is suppressed in via-forwarder mode is *follow-up
+   credit-acceptance confirmation* messages, not the inclusion of
+   a one-line credit question in a milestone-class notification
+   like this one.
 
    **Never send.** Create a Gmail draft via the project's configured
    drafting backend per
diff --git a/.claude/skills/security-issue-import/SKILL.md 
b/.claude/skills/security-issue-import/SKILL.md
index a7b10f8..a237c56 100644
--- a/.claude/skills/security-issue-import/SKILL.md
+++ b/.claude/skills/security-issue-import/SKILL.md
@@ -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** — 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 (`*[bot]` 
suffix, known-bot l [...]
+| **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. |
@@ -1595,31 +1595,43 @@ For each confirmed `Report` / `ASF-security relay`:
    - **Class `ASF-security relay`** (the external reporter is
      unreachable to us directly; only the ASF forwarder can relay
      questions back to them through the original external channel —
-     GHSA, HackerOne, direct mail) — `toRecipients` is the
-     **personal `@apache.org` address of the ASF forwarder** (the
-     `From:` of the inbound relay message), not `[email protected]`
-     and not the unreachable external reporter. Body is **short**
-     per the "Brevity: emails state facts, not context" rule in
+     GHSA, HackerOne, direct mail). This is the canonical
+     **via-forwarder mode** per
+     
[`docs/security/forwarder-routing-policy.md`](../../../docs/security/forwarder-routing-policy.md);
+     the receipt-of-confirmation draft here is the first
+     forwarder-bound message in the lifecycle, and the rest of
+     the milestone drafts (CVE allocated, advisory sent,
+     invalidation, additional-information requests) follow the
+     same routing. `toRecipients` is the **personal
+     `@apache.org` address of the ASF forwarder** (the `From:` of
+     the inbound relay message), not `[email protected]` and
+     not the unreachable external reporter. Body is **short** per
+     the "Brevity: emails state facts, not context" rule in
      [`AGENTS.md`](../../../AGENTS.md):
 
      - one sentence acknowledging receipt, linking to the external
        reference (GHSA ID, HackerOne report URL);
-     - one sentence asking the forwarder to relay the
-       credit-preference question below through the original
-       channel;
-     - the credit-preference question itself (two or three lines,
-       adapted from the canned response — *"We will credit you in
-       the CVE record as <reporter-credited-as placeholder>. If
-       you would prefer a different credit line — full name,
-       handle, affiliation, or "anonymous" — please let us know
-       before the advisory goes out."*).
+     - one sentence asking the forwarder, **best-effort**, to
+       pass any preferred credit form back if the reporter has
+       one — folded in as a single line per the
+       forwarder-routing policy's
+       [question-vs-confirmation 
distinction](../../../docs/security/forwarder-routing-policy.md#negative-space--do-not-relay)
+       (initial credit *question* is allowed in milestone-class
+       drafts; what is suppressed is *follow-up
+       credit-acceptance confirmation* messages on subsequent
+       sync passes).
 
      Do **not** restate the vulnerability, the severity, or the
      Airflow handling process — the ASF security team already
-     knows all of that. See the
+     knows all of that. **Do not** include any of the negative-
+     space items from the forwarder-routing policy (regular
+     workflow status, standalone credit-acceptance confirmation
+     drafts, reviewer-comment relays). See
+     
[`docs/security/forwarder-routing-policy.md`](../../../docs/security/forwarder-routing-policy.md)
+     for the full milestone list + negative space and the
      "ASF-security-relay reports: a special case for drafting"
-     section in [`AGENTS.md`](../../../AGENTS.md) for the full
-     rationale.
+     section in [`AGENTS.md`](../../../AGENTS.md) for the
+     drafting-mechanics rationale.
 
    **Never send.** Always create a draft; the triager reviews in
    Gmail before sending.
diff --git a/.claude/skills/security-issue-invalidate/SKILL.md 
b/.claude/skills/security-issue-invalidate/SKILL.md
index 420f071..8869901 100644
--- a/.claude/skills/security-issue-invalidate/SKILL.md
+++ b/.claude/skills/security-issue-invalidate/SKILL.md
@@ -481,19 +481,31 @@ informational, not a blocker).
 Skip this entire substep when the import path detected in Step 2
 is *PR-imported*.
 
-For `security@`-imported trackers:
+For `security@`-imported trackers, the invalidation reply is one
+of the five [forwarder-routing-policy 
milestones](../../../docs/security/forwarder-routing-policy.md#milestones--do-relay)
+(*Report assessed as invalid*) — so the draft fires in both
+direct-reporter and via-forwarder modes; the policy only changes
+the **recipient** and the **body shape**.
 
 1. **Recipients:**
-   - `toRecipients`: `tracker.reporterEmail` (the `From:` of
-     the inbound root message). If the import was via the
-     ASF-security relay path (the `From:` is a `@apache.org`
-     forwarder, not the external reporter), reply to the
-     forwarder per the *ASF-security relay* convention in
-     [`security-issue-import` Step 7](../security-issue-import/SKILL.md).
+   - **Direct-reporter mode**: `toRecipients` is
+     `tracker.reporterEmail` (the `From:` of the inbound root
+     message). The reply lands on the inbound thread via thread
+     attachment.
+   - **Via-forwarder mode** (ASF-security relay or any other case
+     in the [policy's detection 
list](../../../docs/security/forwarder-routing-policy.md#when-does-via-forwarder-mode-apply)):
+     `toRecipients` is the **forwarder contact** (the
+     `@apache.org` forwarder address from the inbound `From:` for
+     ASF-relay, or the named contact from the explicit
+     no-direct-contact marker comment). The body follows the
+     *Report assessed as invalid* milestone-body shape in the
+     policy doc — short, references the external identifier (GHSA
+     ID, HackerOne URL) rather than restating the technical
+     detail.
    - `ccRecipients`: always includes `<security-list>`
      (`<security-list>` for the adopting project) —
      value comes from
-     
[`<project-config>/project.md`](../../../<project-config>/project.md#gmail-and-ponymail).
+     
[`<project-config>/project.md`](../../../<project-config>/project.md#mail-sources).
 2. **Subject:** `Re: <root subject>`. Never invent a fresh
    subject — the reply lands on the inbound thread via
    thread attachment (`replyToMessageId` for `claude_ai_mcp`,
diff --git a/.claude/skills/security-issue-sync/SKILL.md 
b/.claude/skills/security-issue-sync/SKILL.md
index 3efed88..e560155 100644
--- a/.claude/skills/security-issue-sync/SKILL.md
+++ b/.claude/skills/security-issue-sync/SKILL.md
@@ -1876,6 +1876,28 @@ will change and *why*. Group them by category:
   artifact link. See the "Brevity: emails state facts, not context"
   section of [`AGENTS.md`](../../../AGENTS.md).
 
+  **Apply the [forwarder-routing 
policy](../../../docs/security/forwarder-routing-policy.md)
+  to decide whether to propose the draft at all.** Run the detection
+  rules in the policy doc to determine the tracker's routing mode:
+
+  * **Direct-reporter mode** — proceed as written above; the draft
+    targets the reporter on the inbound thread.
+  * **Via-forwarder mode + event is on the [milestone 
list](../../../docs/security/forwarder-routing-policy.md#milestones--do-relay)**
+    (report accepted as valid, CVE allocated, advisory sent,
+    invalidation, or a specific *"we need additional information"*
+    question) — propose the draft to the **forwarder contact**, not
+    the reporter, using the short milestone-body shape from the
+    policy doc. Reference the external identifier (GHSA ID,
+    HackerOne URL, internal ticket number) rather than repeating
+    the technical detail of the report.
+  * **Via-forwarder mode + event is NOT on the milestone list**
+    (regular workflow status, credit-form questions, reviewer-
+    comment relays) — **suppress the draft entirely**. Record in
+    the proposal recap *"skipped reporter draft: `<event>` not on
+    the via-forwarder milestone list"* so the user can see why
+    no message was proposed. The forwarder is not pinged with
+    low-signal updates.
+
   **Never send.** Always create a draft. Prefer attaching it to the
   inbound mail thread (the default `claude_ai_mcp` backend resolves
   the latest message ID from the inbound `threadId` and passes it as
diff --git a/docs/security/README.md b/docs/security/README.md
index d514e72..537be7a 100644
--- a/docs/security/README.md
+++ b/docs/security/README.md
@@ -74,6 +74,13 @@ and reuse the skills verbatim.
   threat model for the security skill family: trust boundaries,
   adversary personas, STRIDE matrix per skill, mitigation cross-
   reference, residual risk, and the re-audit cadence.
+- [**`forwarder-routing-policy.md`**](forwarder-routing-policy.md) —
+  when a tracker has no direct reporter contact (ASF-relay,
+  read-only GHSA, anonymous tip), the skills route reporter-facing
+  communication through the forwarder. The policy defines when
+  that mode applies, the milestone list (events that **do** get
+  relayed), and the negative list (events that don't — including
+  credit-confirmation questions and regular workflow status).
 
 ## Adopter contract
 
diff --git a/docs/security/forwarder-routing-policy.md 
b/docs/security/forwarder-routing-policy.md
new file mode 100644
index 0000000..88d18c8
--- /dev/null
+++ b/docs/security/forwarder-routing-policy.md
@@ -0,0 +1,237 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [Forwarder-routing policy](#forwarder-routing-policy)
+  - [When does via-forwarder mode apply?](#when-does-via-forwarder-mode-apply)
+  - [Milestones — DO relay](#milestones--do-relay)
+  - [Events handled outside this policy](#events-handled-outside-this-policy)
+  - [Negative space — DO NOT relay](#negative-space--do-not-relay)
+  - [Implementation in the skills](#implementation-in-the-skills)
+  - [Worked examples](#worked-examples)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# Forwarder-routing policy
+
+When a security tracker has **no direct way to reach the original
+reporter** — there is no individual reporter email, GitHub Private
+Reporting access is read-only, or the report arrived through a
+forwarding service — the skills route reporter-facing communication
+through whoever delivered the report to us instead (the *forwarder*
+— typically the Apache Security team relaying via
+`[email protected]`, or an internal security-team member who
+opened the tracker on someone else's behalf).
+
+In that **"via-forwarder" mode**, only **important milestones** are
+relayed. Regular workflow chatter and credit-confirmation
+questions are **not** sent to the forwarder — they would burn the
+forwarder's goodwill with low-signal updates the forwarder has no
+useful reply to.
+
+This file is the single source of truth for *when* the
+via-forwarder mode applies and *what* gets relayed.
+
+## When does via-forwarder mode apply?
+
+The mode applies to a tracker when **any** of the following is true:
+
+1. **ASF-security relay.** The inbound report came from
+   `[email protected]` with the ASF forwarding preamble; the
+   original reporter is not addressable directly on the relayed
+   thread. The personal `@apache.org` address of the forwarding
+   security-team member is the **forwarder contact**. (See
+   [`tools/gmail/asf-relay.md`](../../tools/gmail/asf-relay.md) for
+   the detection mechanics.)
+2. **GitHub Private Reporting we cannot reply on.** A GHSA-style
+   private report we have read access to but can't post comments
+   on as a security team. Whoever made us aware of the GHSA
+   (typically the same Apache Security team member, or an internal
+   escalation thread) is the forwarder.
+3. **`security-issue-import-from-md`-imported tracker.** The
+   tracker came from a markdown file (AI scan / third-party scan
+   output) with no inbound reporter at all. There is no reporter to
+   relay to; treat the security-team member who ran the import as
+   the forwarder for any *"additional information"* questions.
+4. **Explicit no-direct-contact marker.** A security-team member
+   sets the marker comment
+
+   ```html
+   <!-- apache-steward: routing-mode via-forwarder -->
+   ```
+
+   on the tracker (one line in the body, or as the first line of a
+   pinned comment) and names the forwarder contact in the comment
+   body. This is the fall-through escape hatch for cases the
+   automatic detection above misses — an internal escalation, a
+   chat-only report, a printed letter, anything else where the
+   normal reporter address simply does not exist.
+
+The trackers opened by
+[`security-issue-import-from-pr`](../../.claude/skills/security-issue-import-from-pr/SKILL.md)
+are a **separate case** with its own *no outreach to the PR
+author* rule (see that skill's `Reporter credit policy for
+public-PR imports` section). The forwarder-routing policy does
+**not** apply there — the PR author is not someone we are
+deliberately relaying through, just someone whose public PR we
+imported. No reporter-facing drafts of any kind are proposed.
+
+## Milestones — DO relay
+
+Only these events warrant a draft to the forwarder. They map 1:1
+to the lifecycle decisions a reporter (or the team that relayed
+on their behalf) would actually want to hear about:
+
+| Milestone | Where it fires | Draft body summary |
+|---|---|---|
+| **Report accepted as valid** | After Step 5 lands a `valid` consensus + 
scope label applied | *"We received the report you forwarded; the team has 
confirmed it as valid. A CVE will be allocated next; we will write again when 
the advisory is sent. If you can pass this update back to the original 
reporter, please do; if you can also ask them to reply with their preferred 
credit form, that would help — otherwise we'll proceed with the credit line in 
the original report."* |
+| **Report assessed as invalid** | 
[`security-issue-invalidate`](../../.claude/skills/security-issue-invalidate/SKILL.md)
 — closing reply | *"The team has assessed the report you forwarded and 
concluded it is not a security vulnerability. Reasoning: \<one-paragraph 
summary\>. If you (or the original reporter) want to challenge the assessment, 
please reply with the additional context; otherwise this is our final 
disposition."* |
+| **Advisory sent** | 
[`security-issue-sync`](../../.claude/skills/security-issue-sync/SKILL.md) Step 
14 close-out — after the advisory archive URL is captured | *"The advisory for 
`CVE-YYYY-NNNNN` has been sent and is archived publicly at `<URL>`. This 
completes the lifecycle for the report you forwarded; thank you for the 
relay."* |
+| **Additional information requested** | Any skill that needs a specific 
clarification from the reporter (re-reproduction steps, attack-vector 
clarification, affected-version range) | *"We need additional information to 
assess the report you forwarded: `<specific question(s)>`. If you can relay 
this to the original reporter and pass back a reply, that would help us land a 
decision."* |
+
+The drafts go to the **forwarder contact**, not to the relay list
+address — same rule as the existing ASF-relay flow in
+[`tools/gmail/asf-relay.md`](../../tools/gmail/asf-relay.md). The
+body is short, references the external identifier (GHSA ID,
+HackerOne URL, internal ticket number) when one exists, and never
+re-states the technical detail of the report.
+
+## Events handled outside this policy
+
+Some lifecycle events generate reporter-facing notifications
+*outside* this policy's milestone / negative-space rules. They
+fire **the same way in both direct-reporter and via-forwarder
+modes** — no recipient swap, no body-shape swap, no
+suppression.
+
+* **CVE allocated**
+  
([`security-cve-allocate`](../../.claude/skills/security-cve-allocate/SKILL.md)
+  Step 4 #5). Vulnogram typically emits its own allocation
+  notification when the CVE record is created, and even when
+  it doesn't, the team owes the reporter (or their forwarder)
+  a single short notification at this point regardless of
+  routing mode. The notification lands on whatever thread the
+  tracker's *Security mailing list thread* field resolves to;
+  in via-forwarder mode that is the relay thread, so the same
+  draft reaches the forwarder without any policy-specific
+  re-routing. The credit-preference question is still
+  suppressed in via-forwarder mode (per the
+  [Negative space](#negative-space--do-not-relay) section
+  below) but the rest of the CVE-allocated notification still
+  fires.
+
+Future events that follow the same shape (independent of
+routing mode, not subject to milestone-only suppression) belong
+here. Add a bullet and a one-paragraph explanation; do **not**
+hide the event by silently omitting it from the milestone list.
+
+## Negative space — DO NOT relay
+
+These events would generate a draft in *direct-reporter* mode but
+are **suppressed** in via-forwarder mode:
+
+* **Regular workflow status** — `pr created` / `pr merged` /
+  `fix released` label transitions. The forwarder has no actionable
+  reason to learn about these intermediate states; the next
+  forwarder-bound message is at advisory-sent (the *Advisory sent*
+  milestone above).
+* **Credit-acceptance confirmations** — i.e. messages asking the
+  reporter to *confirm receipt and acceptance of the credit line
+  the team plans to use*. The standalone
+  [bot/AI credit-clarification 
draft](../../tools/vulnogram/bot-credits-policy.md)
+  belongs to this class (it asks *"is this AI/bot handle the
+  intended credit, or should we credit someone else?"* — a
+  confirmation prompt on a proposed credit). So do the
+  follow-up chase-ups *"please confirm we will credit you as
+  X"* that direct-reporter sync passes would otherwise generate
+  when the reporter has gone silent. The forwarder cannot
+  meaningfully accept a credit on behalf of the original
+  reporter, so the prompt bounces back and burns goodwill.
+
+  **The credit *question* itself is not suppressed.** Folding
+  a single short *"if the reporter has a preferred credit form,
+  please pass it back"* line into a milestone draft (the Step 7
+  receipt-of-confirmation, the *Report accepted as valid*
+  milestone, or the *CVE allocated* notification per the
+  [Events handled outside this policy](#events-handled-outside-this-policy)
+  section) is fine — the forwarder might know, or might be
+  able to relay the question through the original channel. The
+  distinction is:
+
+  * *Question* (allowed): a one-line ask included in a
+    milestone message the team is already sending. Cheap; the
+    forwarder either knows or can relay or drops it.
+  * *Confirmation* (suppressed): a message whose entire purpose
+    is to get the reporter to *accept* a credit line the team
+    has chosen. Demands a reply the forwarder can't supply, so
+    it becomes a chase-up loop.
+
+  The bot-credit detection still runs in via-forwarder mode and
+  still filters the auto-extracted credit before it lands in
+  the body field; it just does not generate its own
+  confirmation message.
+* **Reviewer-comment relays.** CVE reviewer feedback that lands on
+  `<security-list>` is handled by the security team internally; the
+  forwarder is not on that loop and does not need to be.
+* **Sync-rollup notifications.** The internal rollup comments
+  `security-issue-sync` posts on the tracker stay on the tracker —
+  they are for the security team, not for the forwarder.
+
+## Implementation in the skills
+
+Each skill that composes a reporter-facing draft checks the
+tracker's routing mode (using the detection rules in this doc) and
+applies one of three behaviours:
+
+1. **Direct-reporter mode** (the common case) — proceed exactly as
+   the skill's existing draft logic prescribes.
+2. **Via-forwarder mode + the event is on the milestone list** —
+   compose the draft to the forwarder contact instead of the
+   reporter, using the short milestone-body shape above. Reference
+   the external identifier rather than the technical detail.
+3. **Via-forwarder mode + the event is NOT on the milestone list**
+   — suppress the draft entirely. Record in the proposal recap
+   *"skipped draft: `<event>` not on the via-forwarder milestone
+   list"* so the user can see why no message was proposed.
+
+The detection runs once per skill invocation; subsequent dispatch
+through the skill is consistent for that run.
+
+## Worked examples
+
+**ASF-relayed GHSA report, advisory sent.** A report arrives via
+`[email protected]` carrying a GHSA reference; the import skill
+classifies it as `ASF-security relay`, drafts the Step 7 receipt to
+the forwarder (Arnout / the Apache Security team member who
+relayed it). Weeks later the fix ships and the advisory is
+archived on the users-list; `security-issue-sync` Step 14
+captures the URL and proposes an *Advisory sent* milestone draft
+to the same forwarder contact: *"The advisory for
+`CVE-2026-12345` has been sent and is archived publicly at
+`<URL>`. This completes the lifecycle for the report you
+forwarded; thank you for the relay."* No technical detail is
+restated. (The intermediate *CVE allocated* notification landed
+on the same thread per the
+[Events handled outside this policy](#events-handled-outside-this-policy)
+rule, so the forwarder already saw it.)
+
+**Bot-credit candidate in via-forwarder mode.** The relayed report
+names the discoverer as `Automated Scanner v3`. The bot-credit
+policy detects the match and would normally propose a
+clarification draft to the reporter. In via-forwarder mode the
+clarification draft is **suppressed**; the credit field stays at
+`_No response_` until the next milestone draft, where a single
+line *"if the original reporter has a preferred credit form,
+please pass it back"* is folded in.
+
+**Internal-escalation tracker.** An ASF PMC member forwards a
+private internal report to the security team verbally; the
+security team opens the tracker by hand and writes the
+`<!-- apache-steward: routing-mode via-forwarder -->` marker
+comment naming the PMC member as the forwarder contact. From that
+point on, every sync skill that would draft to a reporter routes
+to the named PMC member instead, and milestone-only suppression
+applies as if the tracker had come in via ASF-relay.
diff --git a/docs/security/roles.md b/docs/security/roles.md
index 2ac6100..44158d6 100644
--- a/docs/security/roles.md
+++ b/docs/security/roles.md
@@ -100,6 +100,19 @@ Reusable wording for the common cases lives in
 [`<project-config>/canned-responses.md`](<project-config>/canned-responses.md) 
— consult it before drafting a
 reply from scratch.
 
+**When there's no direct reporter contact** (ASF-relay reports,
+read-only GHSA, anonymous tips), the team communicates with the
+*forwarder* instead — the security-team member or relay service
+that delivered the report. In that **via-forwarder mode**, only
+the five lifecycle milestones (report accepted as valid, report
+invalidated, CVE allocated, advisory sent, additional information
+requested) are relayed. Regular workflow status (label flips,
+PR-opened, PR-merged) and credit-confirmation questions are
+**not** sent to the forwarder — they would burn the forwarder's
+goodwill with low-signal updates. See
+[`forwarder-routing-policy.md`](forwarder-routing-policy.md) for
+detection rules, the full milestone list, and the negative space.
+
 ### Recording status transitions on the tracker
 
 **Every status transition must also be recorded as a comment on the GitHub
diff --git a/tools/vulnogram/bot-credits-policy.md 
b/tools/vulnogram/bot-credits-policy.md
index f13c421..5cb9d90 100644
--- a/tools/vulnogram/bot-credits-policy.md
+++ b/tools/vulnogram/bot-credits-policy.md
@@ -120,13 +120,16 @@ extraction time:
    override to other trackers — overrides are per-tracker.
 
 4. **Draft a clarification reply to the reporter when an email
-   thread exists.** When the candidate would have been credited as
-   the *finder* (i.e. the reporter themselves is the bot-looking
-   name) **and** the tracker has an inbound `<security-list>` mail
-   thread to reply on, 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:
+   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
+   
[`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:
 
    * **Polite and short** — one or two short paragraphs; no
      accusations, no jargon.
@@ -139,9 +142,32 @@ extraction time:
    * **Sent only after explicit user confirmation** per the
      [framework's never-send-without-asking rule](../../AGENTS.md).
 
-   When the tracker has no inbound mail thread (e.g. a
-   `security-issue-import-from-pr` tracker), skip the draft step —
-   there is no reporter to ask.
+   **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
+   [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.
+
+   When the tracker has no inbound mail thread at all (e.g. a
+   `security-issue-import-from-pr` tracker — the
+   forwarder-routing policy explicitly does **not** apply
+   there), skip the draft step — there is no reporter to ask.
 
    A reusable template for the draft body lives in
    
[`<project-config>/canned-responses.md`](../../<project-config>/canned-responses.md);


Reply via email to