Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
/.idea/
# Personal agent state and local planning artifacts
.superpowers/
.Codex/
AGENTS.md
**/settings.local.json
**/scheduled_tasks.lock
.apache-steward/
docs/superpowers/
/.vscode/
*.iml
logs
Expand Down
6 changes: 6 additions & 0 deletions docs/mode-economics.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ and the large multi-step security skills go much higher —
`security-issue-sync` ~36K. This overhead applies before any
project-specific content is read.

Security draft pre-flight also loads the shared CC-resolution rule from
`tools/mail-source/contract.md`: approximately 600 additional tokens once
per run, estimated from the rule's prose and configuration identifiers.
It reuses already-loaded project/organization configuration and adds no
mail or tracker calls, so the per-mode ranges below remain unchanged.

### Model classes

Skills are written against a capability contract, not a vendor.
Expand Down
19 changes: 19 additions & 0 deletions docs/setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Setup skill family](#setup-skill-family)
- [Install & first runs](#install--first-runs)
- [Try these first](#try-these-first)
- [Security mailing-list configuration](#security-mailing-list-configuration)
- [Skills](#skills)
- [Deep documentation](#deep-documentation)
- [Typical lifecycle](#typical-lifecycle)
Expand Down Expand Up @@ -104,6 +105,24 @@ Apply 1-3? [y/N]
drift none
```

## Security mailing-list configuration

Set `security_list` in your project manifest to your project's private
security mailing list. If it is missing, blank, or still a template TODO,
security workflows warn and resolve draft CC from
`security_inbox.foundation_security_address` through the normal
project/organization/default configuration chain. For ASF adopters the
organization supplies `security@apache.org`; other adopters use their own
organization's configured address. If neither address is configured,
drafting is blocked until configuration is corrected.

The selected address and fallback are shown before draft confirmation.
This does not configure subscriptions or redirect list searches to the
organization's mailbox. Configure mail reads separately; read-only work
can continue only when its own prerequisites are met. See the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution)
for the complete rule.

## Skills

| Skill | Purpose |
Expand Down
9 changes: 9 additions & 0 deletions skills/security-cve-allocate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ allocation skill itself does not hard-gate on it).

## Step 0 — Pre-flight check

**Security draft recipients.** Run the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution)
before mail probes or draft proposals. Keep `security_cc` and `cc_fallback`
in the observed-state bag; a missing address blocks drafting, while
read-only work remains subject to its own prerequisites.

Before touching the tracker, verify:

1. **`gh` is authenticated** —
Expand Down Expand Up @@ -637,6 +643,9 @@ user to confirm. Numbered items:
a one-line credit question in a milestone-class notification
like this one.

Include the pre-flight's resolved `security_cc` in CC; block the draft
if no security CC resolves.

**Never send.** Create a Gmail draft via the project's configured
drafting backend per
[`tools/gmail/draft-backends.md`](../../tools/gmail/draft-backends.md#how-the-skills-pick-a-backend).
Expand Down
12 changes: 9 additions & 3 deletions skills/security-issue-import/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ in `docs/prerequisites.md` for the overall setup.

## Step 0 — Pre-flight check

**Security draft recipients.** Run the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution)
before mail probes or draft proposals. Keep `security_cc` and `cc_fallback`
in the observed-state bag; a missing address blocks drafting, while
read-only work remains subject to its own prerequisites.

Before touching any candidate thread, verify:

1. **Mail-source backends from `<project-config>/project.md →
Expand Down Expand Up @@ -1884,9 +1890,9 @@ For each confirmed `Report` or forwarder-relayed candidate:

Never fabricate a new subject — subject is always
`Re: <root subject>`, even when the recipient changes.
`ccRecipients` always includes the adopting project's `security_list`
(see
[`<project-config>/project.md`](../../<project-config>/project.md#gmail-and-ponymail)).
`ccRecipients` includes `security_cc` from the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution).
If no address resolves, block draft creation.

**Two variants depending on how the candidate was classified:**

Expand Down
17 changes: 11 additions & 6 deletions skills/security-issue-invalidate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ for the Gmail draft path.

## Step 0 — Pre-flight check

**Security draft recipients.** Run the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution)
before mail probes or draft proposals. Keep `security_cc` and `cc_fallback`
in the observed-state bag; a missing address blocks drafting, while
read-only work remains subject to its own prerequisites.

Before any work, verify:

1. **`gh` is authenticated and has access.** Run
Expand Down Expand Up @@ -668,10 +674,9 @@ the **recipient** and the **body shape**.
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#mail-sources).
- `ccRecipients`: includes `security_cc` from the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution).
If no address resolves, block draft creation.
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`,
Expand Down Expand Up @@ -891,13 +896,13 @@ Use the backend chosen in Step 5d:
on `<tracker.threadId>` with `messageFormat: MINIMAL`, take
the chronologically-last message's `id`, and call
`mcp__claude_ai_Gmail__create_draft` with `to=<reporterEmail>`,
`cc=<security-list>`, `subject='Re: <root subject>'`,
`cc=security_cc`, `subject='Re: <root subject>'`,
`body=<file>`, and `replyToMessageId=<that message id>`. The
draft lands attached to the inbound thread.
- **`oauth_curl`:** call the `oauth_curl drafts:create` script
per [`draft-backends.md`](../../tools/gmail/draft-backends.md)
with `threadId=<tracker.threadId>`, `to=<reporterEmail>`,
`cc=<security-list>`, `subject='Re: <root subject>'`,
`cc=security_cc`, `subject='Re: <root subject>'`,
`body=<file>`. The draft lands attached to the inbound thread.

Capture the returned `draftId`. Update the rollup entry's
Expand Down
10 changes: 9 additions & 1 deletion skills/security-issue-sync/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,12 @@ in `docs/prerequisites.md` for the overall setup.

## Step 0 — Pre-flight check

**Security draft recipients.** Run the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution)
before mail probes or draft proposals. Keep `security_cc` and `cc_fallback`
in the observed-state bag; a missing address blocks drafting, while
read-only work remains subject to its own prerequisites.

Before reading any tracker state, verify:

1. **Mail-source backends per
Expand Down Expand Up @@ -435,7 +441,9 @@ Produce a single, compact summary for the user with three sections:

A bullet list of the facts gathered in Step 1 — current labels, milestone,
assignees, linked PRs, mailing-thread status, and the process step the issue is
currently at. Keep it tight.
currently at. Include `security_cc` and `cc_fallback` from pre-flight
when a draft is proposed, and surface any missing-configuration warning.
Keep it tight.

### 2b. Proposed changes

Expand Down
4 changes: 3 additions & 1 deletion skills/security-issue-sync/apply-and-push.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ before moving on to the next item. Use:
`updateProjectV2ItemFieldValue`). Re-fetch the option IDs via the
introspection query in the same reference if a write mutation
starts returning `not found`.
- **Gmail draft:** create via the project's configured drafting
- **Gmail draft:** include `security_cc` from the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution)
in every draft's CC; block creation if unresolved. Create via the project's configured drafting
backend per [`tools/gmail/draft-backends.md`](../../tools/gmail/draft-backends.md#how-the-skills-pick-a-backend).
The **preferred** backend is `oauth_curl` — it preserves URLs in
the body verbatim. The `claude_ai_mcp` backend is **discouraged**
Expand Down
7 changes: 4 additions & 3 deletions skills/security-issue-sync/gather.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,9 +573,10 @@ draft on the notification thread closes the loop:

- **To:** the reviewer's address (the `From:`
of the original notification).
- **Cc:** `<security-list>` (so the security team thread
carries the round-trip), plus any broader security list the
original notification CC'd.
- **Cc:** `security_cc` from the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution),
plus any broader security list the original notification CC'd.
Block the draft if no security CC resolves.
- **Subject:** `Re: <original notification subject>`
(typically `Re: Comment added on <CVE-ID>`).
- **Body shape:** one paragraph acknowledging what was
Expand Down
5 changes: 3 additions & 2 deletions skills/security-issue-sync/github-advisory.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ Create a **draft** email to the org's advisory-admin security team
(`<foundation-security-list>`) with
`oauth-draft-create` — never send directly (SKILL Golden rule 1; and the
Gmail MCP mangles the `security/advisories/GHSA-…` URLs into redirects, so
use oauth-draft). **Always CC the project `<security-list>`** so the
project security team stays looped in on what was requested. Reply on the
use oauth-draft). **Include the resolved `security_cc`** per the shared
[security draft CC resolution](../../tools/mail-source/contract.md#security-draft-cc-resolution);
block the draft if no address resolves. Reply on the
originating `<security-list>` thread when the report was relayed there
(`--thread-id <id>`); otherwise it is a **new** message (omit `--thread-id`)
with a self-describing subject. Do **not** post a `<tracker>` comment as the
Expand Down
4 changes: 3 additions & 1 deletion tools/gmail/asf-relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ Placeholder convention:
`@apache.org` address; use that, not the `security@apache.org`
list alias, so the conversation stays with the individual who
already knows the report.
- **`Cc:`** — `<security-list>` as always.
- **`Cc:`**: the resolved `security_cc` per the shared
[security draft CC resolution](../mail-source/contract.md#security-draft-cc-resolution).
Block the draft if no address resolves.
- **Body** — short, per the *"Brevity: emails state facts, not
context"* rule in [`../../AGENTS.md`](../../AGENTS.md). The ASF
security team knows the handling process; do **not** restate the
Expand Down
105 changes: 105 additions & 0 deletions tools/gmail/oauth-draft/tests/test_security_cc_recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Exercise the documented security draft command, without Gmail or credentials.

Regression coverage: https://github.com/apache/magpie/issues/181
"""

from __future__ import annotations

import email
import email.policy
import os
import subprocess
from pathlib import Path

import pytest

from oauth_draft.create_draft import build_mime, parse_args


def run_recipe(tmp_path, recipient):
repo = Path(__file__).resolve().parents[4]
doc = (repo / "tools/gmail/operations.md").read_text()
section = doc.split("### Create draft — `oauth_curl` backend", 1)[1]
recipe = section.split("```bash\n", 1)[1].split("```", 1)[0]
recipe = recipe.replace("<framework>", ".").replace("<gmail-threadId>", "thread-181")
recipe = recipe.replace("<root subject>", "Report")
capture = tmp_path / "argv"
# Shell function replaces only the external command; argument expansion and
# every guard in the actual documentation still execute in Bash.
script = 'uv() { printf "%s\\0" "$@" > "$CC_CAPTURE"; }\n' + recipe
env = os.environ.copy()
env.pop("security_cc", None)
env["CC_CAPTURE"] = str(capture)
if recipient is not None:
env["security_cc"] = recipient
result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True)
return result, capture


@pytest.mark.parametrize("recipient", [None, "", " "])
def test_unresolved_security_cc_stops_before_backend(tmp_path, recipient):
result, capture = run_recipe(tmp_path, recipient)
assert result.returncode != 0
assert not capture.exists()


@pytest.mark.parametrize(
"recipient", ["security@project.example", "security@apache.org", "security@acme.example"]
)
def test_resolved_security_cc_reaches_cli_and_mime(tmp_path, recipient):
result, capture = run_recipe(tmp_path, recipient)
assert result.returncode == 0, result.stderr
argv = capture.read_bytes().decode().split("\0")[:-1]
args = parse_args(argv[argv.index("oauth-draft-create") + 1 :])
assert args.cc == [recipient]
raw = build_mime("triager@example.com", args.to, args.cc, [], args.subject, "Body", None, None)
msg = email.message_from_bytes(raw, policy=email.policy.default)
assert str(msg["Cc"]) == recipient


@pytest.mark.parametrize("recipient", [None, "", " ", "security@project.example", "security@apache.org"])
def test_mcp_recipe_validates_and_passes_resolved_cc(recipient):
repo = Path(__file__).resolve().parents[4]
doc = (repo / "tools/gmail/operations.md").read_text()
section = doc.split("### Create draft — `claude_ai_mcp` backend", 1)[1]
recipe = section.split("```text\n", 1)[1].split("```", 1)[0]
# Ellipsis denotes optional additional recipients in the catalogue.
recipe = recipe.replace(", ...", "")
calls = []

def record_read(**kwargs):
calls.append(("read", kwargs))

def record_draft(**kwargs):
calls.append(("create_draft", kwargs))

namespace = {
"security_cc": recipient,
"mcp__claude_ai_Gmail__get_thread": record_read,
"mcp__claude_ai_Gmail__create_draft": record_draft,
}
if recipient is None or not recipient.strip():
with pytest.raises(ValueError, match="security CC"):
exec(recipe, namespace)
assert calls == []
else:
exec(recipe, namespace)
assert [name for name, _ in calls] == ["read", "create_draft"]
assert calls[-1][1]["cc"] == [recipient]
assert "htmlBody" not in calls[-1][1]
Loading