diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml
index b43b4a2c..cb2185e2 100644
--- a/.github/workflows/claude-code-review.yml
+++ b/.github/workflows/claude-code-review.yml
@@ -11,7 +11,7 @@ name: Claude Code Review
# the safe path. If you add steps here, preserve this invariant.
on:
pull_request_target:
- types: [opened, synchronize, ready_for_review, reopened]
+ types: [opened, synchronize, ready_for_review, reopened, labeled]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
@@ -21,11 +21,13 @@ on:
jobs:
claude-review:
- # Optional: Filter by PR author
- # if: |
- # github.event.pull_request.user.login == 'external-contributor' ||
- # github.event.pull_request.user.login == 'new-developer' ||
- # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
+ # Maintainers apply this label to explicitly approve privileged review on fork PRs.
+ if: |
+ github.event.pull_request.head.repo.full_name == github.repository ||
+ github.event.pull_request.author_association == 'OWNER' ||
+ github.event.pull_request.author_association == 'MEMBER' ||
+ github.event.pull_request.author_association == 'COLLABORATOR' ||
+ contains(github.event.pull_request.labels.*.name, 'safe-to-run-claude-review')
runs-on: ubuntu-latest
permissions:
@@ -36,13 +38,13 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
- uses: anthropics/claude-code-action@v1
+ uses: anthropics/claude-code-action@51ea8ea73a139f2a74ff649e3092c25a904aed7e
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
diff --git a/clients/desktop/tests/conftest.py b/clients/desktop/tests/conftest.py
index a72e4eb8..48080789 100644
--- a/clients/desktop/tests/conftest.py
+++ b/clients/desktop/tests/conftest.py
@@ -1,10 +1,14 @@
import pytest
-import wx
@pytest.fixture(scope="session")
def wx_app():
"""Ensure a wx App exists for dialog tests."""
+ try:
+ import wx
+ except ModuleNotFoundError:
+ pytest.skip("wxPython is not installed")
+
if not wx.App.IsDisplayAvailable():
pytest.skip("GUI display is not available for wx dialogs")
app = wx.App(False)
diff --git a/clients/desktop/tests/test_markdown_viewer_security.py b/clients/desktop/tests/test_markdown_viewer_security.py
new file mode 100644
index 00000000..9dd55913
--- /dev/null
+++ b/clients/desktop/tests/test_markdown_viewer_security.py
@@ -0,0 +1,253 @@
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+import re
+import sys
+import types
+
+
+def _load_markdown_viewer_dialog_module():
+ if "markdown" not in sys.modules:
+ markdown_module = types.ModuleType("markdown")
+
+ def _markdown_to_html(text, extensions=None):
+ text = re.sub(
+ r"!\[([^\]]*)\]\(([^)]+)\)",
+ lambda match: f'',
+ text,
+ )
+ text = re.sub(
+ r"\[([^\]]+)\]\(([^)]+)\)",
+ lambda match: f'{match.group(1)}',
+ text,
+ )
+ blocks = [block.strip() for block in text.split("\n\n") if block.strip()]
+ html_blocks = []
+ for block in blocks:
+ if block.startswith("<"):
+ html_blocks.append(block)
+ else:
+ html_blocks.append(f"
{block}
") + return "\n".join(html_blocks) + + markdown_module.markdown = _markdown_to_html + sys.modules["markdown"] = markdown_module + + if "nh3" not in sys.modules: + nh3_module = types.ModuleType("nh3") + + def _clean_html(html, tags=None, attributes=None, url_schemes=None, link_rel=None): + cleaned = re.sub(r"