-
-
Notifications
You must be signed in to change notification settings - Fork 16
feat: add AI attribution governance (forbid known AI tool signatures) #456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0c9a4e2
feat: add AI attribution governance policy (forbid/require/ignore)
shenxianpeng 3f7b6c6
fix: resolve P0/P1 issues in AI attribution feature
shenxianpeng 0b708c3
refactor: remove ai_trailer_style config and AiTrailerStyleValidator
shenxianpeng 777e4b4
docs: add AI attribution documentation
shenxianpeng 34e90ca
docs: bump AI attribution feature version to 2.11.0
shenxianpeng 7f0a856
docs: fix Bot Branch Types version from 2.9.1 to 2.10.0
shenxianpeng 877e5fd
chore: improve forbid suggestion message to clarify project policy
shenxianpeng dba74da
chore: trim forbid suggestion message
shenxianpeng f272e49
refactor: split AI signatures into data and logic modules
shenxianpeng 48d9a17
fix: prevent false positives for human names in AI detection
shenxianpeng File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| """AI tool signature detection logic. | ||
|
|
||
| This module provides the public API for detecting AI tool signatures in commit | ||
| messages. The signature data (tool definitions and patterns) lives in | ||
| :mod:`commit_check.ai_signatures_data`. | ||
|
|
||
| Typical usage:: | ||
|
|
||
| from commit_check.ai_signatures import detect_ai_signatures | ||
|
|
||
| result = detect_ai_signatures( | ||
| "feat: init\\n\\nCo-authored-by: Claude <noreply@anthropic.com>" | ||
| ) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
|
|
||
| from commit_check.ai_signatures_data import ALL_KNOWN_TOOLS as _ALL_KNOWN_TOOLS | ||
|
|
||
| # Re-export for convenience — consumers can import everything from | ||
| # commit_check.ai_signatures without knowing about the data/logic split. | ||
| ALL_KNOWN_TOOLS = _ALL_KNOWN_TOOLS | ||
|
|
||
|
|
||
| #: Flat list of all compiled patterns for bulk scanning. | ||
| #: Each tuple is ``(regex, tool_name, description, kind)``. | ||
| ALL_PATTERNS: list[tuple[re.Pattern[str], str, str, str]] = [ | ||
| (p.regex, tool.name, p.description, p.kind) | ||
| for tool in ALL_KNOWN_TOOLS | ||
| for p in tool.patterns | ||
| ] | ||
|
|
||
|
|
||
| def detect_ai_signatures(message: str) -> list[dict[str, str]]: | ||
| """Scan *message* for known AI tool signatures. | ||
|
|
||
| :param message: The full commit message (subject + body) to scan. | ||
| :returns: A list of dicts, one per matched signature, each with keys | ||
| ``"tool"``, ``"kind"``, ``"description"``, and ``"matched_text"``. | ||
| Returns an empty list when no signatures are found. | ||
|
|
||
| Example:: | ||
|
|
||
| >>> detect_ai_signatures( | ||
| ... "feat: init\\n\\nCo-authored-by: Claude <noreply@anthropic.com>" | ||
| ... ) | ||
| [{'tool': 'Claude Code', 'kind': 'trailer', ...}] | ||
| """ | ||
| results: list[dict[str, str]] = [] | ||
| seen: set[str] = set() | ||
|
|
||
| for regex, tool_name, desc, kind in ALL_PATTERNS: | ||
| for match in regex.finditer(message): | ||
| matched = match.group(0).strip() | ||
| if matched not in seen: | ||
| seen.add(matched) | ||
| results.append( | ||
| { | ||
| "tool": tool_name, | ||
| "kind": kind, | ||
| "description": desc, | ||
| "matched_text": matched, | ||
| } | ||
| ) | ||
|
|
||
| return results | ||
|
|
||
|
|
||
| def has_ai_signature(message: str) -> bool: | ||
| """Return ``True`` if *message* contains any known AI signature.""" | ||
| for regex, _tool_name, _desc, _kind in ALL_PATTERNS: | ||
| if regex.search(message): | ||
| return True | ||
| return False |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| """Known AI tool signatures — pure data, no detection logic. | ||
|
|
||
| This module defines the data structures and the curated registry of known AI | ||
| coding tool signatures. To add a new tool, define a ``KnownAiTool`` entry | ||
| with its patterns and add it to ``ALL_KNOWN_TOOLS``. | ||
|
|
||
| The detection logic lives in :mod:`commit_check.ai_signatures`. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from dataclasses import dataclass, field | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class AiSignaturePattern: | ||
| """A single pattern that identifies AI tool usage in a commit message. | ||
|
|
||
| :param regex: A compiled regex that, if matched anywhere in the commit | ||
| message body, indicates the corresponding tool was involved. | ||
| :param kind: ``"trailer"`` for structured ``Key: value`` footer lines | ||
| (matched case-insensitively), ``"body_marker"`` for any other text | ||
| marker. | ||
| :param description: Human-readable description of what is matched. | ||
| """ | ||
|
|
||
| regex: re.Pattern[str] | ||
| kind: str # "trailer" | "body_marker" | ||
| description: str = "" | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class KnownAiTool: | ||
| """A known AI coding tool and its commit-message signatures. | ||
|
|
||
| :param name: Short display name (e.g. ``"Claude Code"``, ``"GitHub Copilot"``). | ||
| :param patterns: One or more signature patterns that indicate this tool. | ||
| """ | ||
|
|
||
| name: str | ||
| patterns: list[AiSignaturePattern] = field(default_factory=list) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Pattern helpers | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _trailer( | ||
| key: str, value_pattern: str = r".*", description: str = "" | ||
| ) -> AiSignaturePattern: | ||
| """Build a trailer pattern for a structured ``Key: value`` line. | ||
|
|
||
| The match is case-insensitive and anchors the key at the start of a line. | ||
| """ | ||
| raw = rf"^{re.escape(key)}:\s*{value_pattern}\s*$" | ||
| return AiSignaturePattern( | ||
| regex=re.compile(raw, re.IGNORECASE | re.MULTILINE), | ||
| kind="trailer", | ||
| description=description or f"``{key}:`` trailer", | ||
| ) | ||
|
|
||
|
|
||
| def _body_marker(pattern: str, description: str = "") -> AiSignaturePattern: | ||
| """Build a free-text body marker pattern.""" | ||
| return AiSignaturePattern( | ||
| regex=re.compile(pattern, re.MULTILINE), | ||
| kind="body_marker", | ||
| description=description, | ||
| ) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Known tool signatures | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| # --- Anthropic Claude Code / Claude CLI --- | ||
| CLAUDE_CODE = KnownAiTool( | ||
| name="Claude Code", | ||
| patterns=[ | ||
| # Standard Co-authored-by trailer added by Claude Code. | ||
| # When an email is present, anchor to known AI noreply addresses | ||
| # to avoid false positives with human co-authors named Claude. | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Claude(?: Code)?" | ||
| r"(?:\s*<(?:noreply@anthropic\.com" | ||
| r"|\d+\+Claude@users\.noreply\.github\.com)>)?", | ||
| "``Co-authored-by: Claude`` trailer", | ||
| ), | ||
| # Assisted-by trailer (Linux kernel style, with optional tool list) | ||
| _trailer( | ||
| "Assisted-by", | ||
| r"Claude:\S+(?:\s+\S+)*", | ||
| "``Assisted-by: Claude:<model> [tools]`` trailer", | ||
| ), | ||
| # Body marker: generated-with notice | ||
| _body_marker( | ||
| r"🤖\s*Generated\s+(?:with|by)\s+\[?Claude", | ||
| "``🤖 Generated with Claude`` body marker", | ||
| ), | ||
| # Session ID trailer (Claude Code sometimes adds this) | ||
| _trailer("Claude-Session", r"\S+", "``Claude-Session:`` trailer"), | ||
| # Workflow ID trailer | ||
| _trailer("Claude-Workflow", r"\S+", "``Claude-Workflow:`` trailer"), | ||
| ], | ||
| ) | ||
|
|
||
| # --- GitHub Copilot --- | ||
| COPILOT = KnownAiTool( | ||
| name="GitHub Copilot", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Copilot" | ||
| r"(?:\s*<\d+\+Copilot@users\.noreply\.github\.com>)?", | ||
| "``Co-authored-by: Copilot`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- OpenAI Codex --- | ||
| CODEX = KnownAiTool( | ||
| name="OpenAI Codex", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Codex\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: Codex`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- Gemini (Google) --- | ||
| GEMINI = KnownAiTool( | ||
| name="Gemini", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Gemini\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: Gemini`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- Cursor --- | ||
| CURSOR = KnownAiTool( | ||
| name="Cursor", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Cursor\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: Cursor`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- Devin --- | ||
| DEVIN = KnownAiTool( | ||
| name="Devin", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Devin\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: Devin`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- Aider --- | ||
| AIDER = KnownAiTool( | ||
| name="Aider", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Aider\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: Aider`` trailer", | ||
| ), | ||
| # aider appends "(aider)" to the author name | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"[^<]+\(aider\)\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: ... (aider)`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- Windsurf (Codeium) --- | ||
| WINDSURF = KnownAiTool( | ||
| name="Windsurf", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Windsurf\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: Windsurf`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- Tabby --- | ||
| TABBY = KnownAiTool( | ||
| name="Tabby", | ||
| patterns=[ | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"Tabby\s*(?:<[^>]*>)?", | ||
| "``Co-authored-by: Tabby`` trailer", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --- Generic / catch-all AI patterns --- | ||
| GENERIC_AI = KnownAiTool( | ||
| name="Generic AI", | ||
| patterns=[ | ||
| # Catch AI agent model identifiers in Co-authored-by | ||
| # (e.g. claude-sonnet-4, gpt-4-turbo, gemini-1.5-pro). | ||
| # A hyphenated model suffix is required so bare human first names | ||
| # ("Claude", "Gemini") are NOT flagged, regardless of the email. | ||
| _trailer( | ||
| "Co-authored-by", | ||
| r"(?:claude|gpt|gemini)[\w.]*-[\w.-]+(?:\s*<[^>]*>)?", | ||
| "``Co-authored-by`` with AI model name", | ||
| ), | ||
| # Catch Assisted-by trailer (Linux kernel style) regardless of agent, | ||
| # with optional trailing tool list. | ||
| _trailer( | ||
| "Assisted-by", | ||
| r"\S+:\S+(?:\s+\S+)*", | ||
| "``Assisted-by: <tool>:<model> [tools]`` trailer (kernel style)", | ||
| ), | ||
| # Catch common body markers | ||
| _body_marker( | ||
| r"^Generated\s+(?:by|with)\s+(?:AI|artificial intelligence)", | ||
| "``Generated by AI`` body marker", | ||
| ), | ||
| ], | ||
| ) | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Master registry — ordered by specificity (most specific first) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| #: All known AI tools, ordered so that more specific patterns are checked first. | ||
| ALL_KNOWN_TOOLS: list[KnownAiTool] = [ | ||
| CLAUDE_CODE, | ||
| COPILOT, | ||
| CODEX, | ||
| GEMINI, | ||
| CURSOR, | ||
| DEVIN, | ||
| AIDER, | ||
| WINDSURF, | ||
| TABBY, | ||
| GENERIC_AI, | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Devin AI Cognition commit co-authored-by bot email address💡 Result:
Devin AI, developed by Cognition, uses a Co-authored-by trailer in its git commit messages to provide attribution for changes made by the AI agent [1][2]. Regarding the email address associated with this trailer, official documentation for Devin notes a correction in its changelog: the system was updated to use the correct GitHub App bot email address instead of the previously used noreply@cognition.ai address [3]. The standard format used for the trailer is: Co-authored-by: Devin devin@cognition.ai While earlier or alternative configurations may have utilized other patterns, current documentation emphasizes the transition to official GitHub App bot integrations for improved attribution and compatibility [3]. Depending on the specific integration setup (such as the Devin GitHub App), the trailer may reference the official GitHub App bot identity (e.g., devin-ai-integration[bot]) to ensure clear provenance within repository histories [4]. Users are encouraged to check their specific repository's Devin integration documentation for the most accurate, environment-specific attribution string [5][3].
Citations:
Restrict
DEVINto a bot identityDevin\s*(?:<[^>]*>)?still matches any human co-author named Devin with any email, soCo-authored-by: Devin <devin@company.com>is classified as AI and rejected inforbidmode. Anchor this to Devin’s bot email/identity instead of a bare name.🤖 Prompt for AI Agents