Table of Contents generated with DocToc
security-issue-import (and the mail-touching paths in security-issue-sync, security-cve-allocate, security-issue-triage) scan an inbound <security-list> for security reports, read threads, and draft replies. The skills treat every supported source — Gmail, PonyMail, IMAP, a static mbox snapshot, the next one we plug in — the same way: through the abstract operations defined here. The adopting project's <project-config>/project.md → Mail sources section declares which backends are configured, what role each plays, and which (if any) are mandatory.
This file is the single source of truth for what a backend is expected to do and how the skills choose between configured backends. Backend-specific docs — tools/gmail/tool.md, tools/ponymail/tool.md, tools/mail-source/imap/README.md, and tools/mail-source/mbox/README.md — implement this contract.
A mail-source backend exposes some subset of these operations. Some subset matters — Ponymail is read-only, an mbox snapshot is read-only-and-offline, and a corporate IMAP may or may not allow drafts depending on policy. The skills check capability before dispatch; backends that don't support an op are skipped in the resolution chain for that op.
| Operation | What it does | Why the skills need it |
|---|---|---|
list_recent_threads(list, since) | Return threads on <list> newer than since (typically 14 / 30 / 90 days) | security-issue-import Step 1 — find candidate reports that have arrived since the last sweep |
read_thread(thread_id) | Return full message history of a thread by stable identifier | Steps 2–4 of -import; -sync reads tracker-linked threads for credit / CVE-reviewer / status signals |
list_drafts(thread_id) | Return draft replies already attached to a thread | Idempotency — never propose a fresh draft when one is already pending |
list_sent_since(thread_id, since) | Return outbound replies sent on a thread within a window | Detect “we already replied; the ball is in the reporter's court” |
create_draft(thread_id, body, …) | Compose an un-sent reply attached to the thread (subject inherits, In-Reply-To set per the threading rule) | Every reporter-facing reply the skills propose. Drafts only — never sends per the framework rule |
thread_url(thread_id) | Return a human-clickable URL to the thread (for tracker body fields, sync rollups, etc.) | The Security mailing list thread tracker field |
thread_id_kind (attribute) | The shape of identifiers this backend emits (e.g. Gmail UUID, PonyMail hash, RFC-5322 Message-ID) | Lets the skills tag stored IDs with their source so a future sync from a different backend doesn't dedupe across kinds |
A backend's capability set is its supported subset of the operations above. The capability matrix below summarises the in-tree adapters.
| Backend | list_recent_threads | read_thread | list_drafts | list_sent_since | create_draft | thread_url | Notes |
|---|---|---|---|---|---|---|---|
gmail | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | Full read + write; primary backend in the reference adopter |
ponymail | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | Read-only public/private archive viewer; auth via ASF LDAP |
imap | ✓ | ✓ | depends | ✓ | depends | ✓ | Stub adapter; create_draft / list_drafts depend on whether the IMAP server exposes the Drafts folder writably to the agent |
mbox | ✓ (offline) | ✓ | ✗ | ✗ | ✗ | ✗ (or file://) | Static archive snapshot; forensics / late triage only |
Backends added by adopters extend the matrix in their own adapter README. The skill never assumes a backend has an op without consulting the matrix first.
<project-config>/project.mdThe adopter declares the configured backends under a Mail sources section, one entry per backend:
## Mail sources | Backend | Role | Mandatory | Notes | |---|---|---|---| | `gmail` | primary | yes | Triager Gmail account is subscribed to `<security-list>` and `<private-list>` | | `ponymail` | fallback | no | Read-only archive backstop when Gmail history is incomplete |
primary — the default backend for every operation it supports. Exactly one backend may carry this role.preferred for <op> — overrides primary for one specific operation. Multiple backends may carry this role, each for a different op. Example: a project that runs IMAP for inbound mail but uses Gmail only for drafting would declare imap: preferred for read_thread, list_recent_threads and gmail: preferred for create_draft, list_drafts.fallback — try after primary / preferred for any op the primary doesn't support, in the order the backends are listed.optional — available for ad-hoc use but never in the resolution chain. Useful for adapter docs that are present but not wired in yet.yes means the skill refuses to run when the backend is unavailable (auth missing, MCP server down, archive directory not mounted). The skill surfaces a clear “mandatory backend <name> unavailable: <reason>; run aborted” and exits without proposing anything.
no means the skill continues with the remaining backends. If a specific op then has no backend to dispatch to, the skill skips that operation's proposal (e.g. “no draft backend available — Step 7 proposal omitted, please draft the receipt-of-confirmation reply by hand”) and keeps going.
For each operation <op> the skill needs to dispatch:
preferred for <op> and the matrix shows it supports <op> and it is available, use it. Stop.primary backend supports <op> and is available, use it. Stop.fallback backends in the declared order; use the first one that supports <op> and is available. Stop.<op>” note and continue.The resolution result is logged in every proposal recap so the user can see which backend served which op.
A new backend (tools/mail-source/<name>/) ships a README.md that:
thread_id.The contract is intentionally minimal — backends are expected to gracefully decline ops they don't support (NotSupported / explicit error) rather than fake-implement them. Faking causes silent data loss (a draft that goes nowhere, a thread read that returns empty); declining lets the resolution rule fall through.
create_draft is expected to honour it. No backend exposes a send op through this contract.tools/privacy-llm/) sits between the backend and any LLM consumer. The contract is at the wire-format level, not the content-policy level.