Преглед на файлове

Merge pull request #2055 from anthropics/fix-windows-agentic-reviewer

security-guidance: enable agentic commit reviewer on Windows
Mohamed Hegazy преди 3 месеца
родител
ревизия
2a3dd81146
променени са 2 файла, в които са добавени 82 реда и са изтрити 24 реда
  1. 19 10
      plugins/security-guidance/hooks/ensure_agent_sdk.py
  2. 63 14
      plugins/security-guidance/hooks/llm.py

+ 19 - 10
plugins/security-guidance/hooks/ensure_agent_sdk.py

@@ -28,7 +28,9 @@ NOOP_SYSTEM = 0      # claude_agent_sdk already importable in system python
 NOOP_VENV = 1        # venv already built and SDK imports from it
 BUILT = 2            # venv created + SDK pip-installed this run
 BUILD_FAILED = 3     # venv create or pip install raised/timed out
-SKIP_WIN32 = 4       # Windows; consumer glob doesn't handle Lib/ layout
+# Outcome 4 was previously SKIP_WIN32; retired now that the consumer glob in
+# llm.py also matches Windows venv layout (Lib/site-packages). Don't reuse the
+# value — telemetry rows from older plugin builds still emit 4.
 SKIP_SENTINEL = 5    # another SessionStart is currently building
 
 
@@ -60,13 +62,6 @@ def main() -> tuple[int, str, str]:
     err_phase / err_kind are non-empty only on BUILD_FAILED — they let
     telemetry split bootstrap failures by root cause.
     """
-    # Windows venv layout (Lib/site-packages, no python* subdir) isn't
-    # handled by the consumer's glob in security_reminder_hook.py; skip the
-    # bootstrap entirely rather than build a venv that's never read.
-    if sys.platform == "win32":
-        return SKIP_WIN32, "", ""
-
-
     if _sdk_on_syspath():
         return NOOP_SYSTEM, "", ""
 
@@ -75,7 +70,11 @@ def main() -> tuple[int, str, str]:
         or os.path.expanduser("~/.claude/security")
     )
     venv = state_dir / "agent-sdk-venv"
-    venv_py = venv / "bin" / "python"
+    # Windows venvs put the interpreter at Scripts\python.exe; POSIX uses bin/python.
+    if sys.platform == "win32":
+        venv_py = venv / "Scripts" / "python.exe"
+    else:
+        venv_py = venv / "bin" / "python"
 
     # Another SessionStart (concurrent CC instance, same plugin) may already
     # be building. The sentinel lives NEXT TO the venv, not inside it —
@@ -125,10 +124,20 @@ def main() -> tuple[int, str, str]:
         # the user's machine, pip's own default registry applies — that's the same
         # exposure the user would have running `pip install` themselves, so
         # we're not widening the supply-chain surface.
+        #
+        # --prefer-binary: on ARM64 Windows, pip's default resolver picks a
+        # `cryptography` version with no published binary wheel and tries to
+        # build from source, which needs Rust/Cargo (almost never present
+        # on user machines). The build fails and the whole bootstrap returns
+        # BUILD_FAILED. A binary wheel exists on PyPI for an adjacent
+        # version (`cryptography-46.0.3-cp311-abi3-win_arm64.whl`);
+        # --prefer-binary tells pip to pick it. Cross-platform safe: no-op
+        # on platforms where the latest version already has a wheel.
         err_phase = "pip"
         subprocess.run(
             [str(venv_py), "-m", "pip", "install", "--quiet",
-             "--disable-pip-version-check", "claude-agent-sdk"],
+             "--disable-pip-version-check", "--prefer-binary",
+             "claude-agent-sdk"],
             capture_output=True, timeout=120, check=True,
         )
         return BUILT, "", ""

+ 63 - 14
plugins/security-guidance/hooks/llm.py

@@ -31,6 +31,67 @@ from _base import debug_log, _record_usage, _PV, PROVENANCE_TAG  # noqa: F401
 from session_state import with_locked_state
 
 
+def _inject_agent_sdk_venv_into_syspath(state_dir):
+    """Prepend the agent-SDK venv's site-packages to sys.path so the SDK
+    import below picks it up when the user's system Python doesn't have it.
+
+    Called from two fallback sites (3P SDK + agentic_review); shared here so
+    Windows pywin32 handling stays in one place.
+
+    Returns True if any path was added.
+
+    POSIX venv layout: `agent-sdk-venv/lib/pythonX.Y/site-packages`
+    Windows venv layout: `agent-sdk-venv/Lib/site-packages` (capital L, no
+    pythonX.Y subdir). The SDK transitively imports pywin32 on Windows, and
+    pywin32's `.pth` files (which add `win32/`, `win32/lib/` to sys.path and
+    register the DLL search dir via `pywin32_bootstrap.py`) are processed
+    ONLY by Python's `site.py` at interpreter startup — not when we manually
+    insert a path here. Without the bootstrap, the SDK's
+    `mcp.client.stdio → mcp.os.win32.utilities → pywintypes` import chain
+    fails with `ModuleNotFoundError: pywintypes` and the agentic reviewer
+    falls back to single-shot silently. Replicate what site.py would do.
+    """
+    venv_root = os.path.join(state_dir, "agent-sdk-venv")
+    candidates = (
+        glob.glob(os.path.join(venv_root, "lib", "python*", "site-packages"))
+        + glob.glob(os.path.join(venv_root, "Lib", "site-packages"))
+    )
+    added = False
+    for sp in candidates:
+        if not os.path.isdir(sp) or sp in sys.path:
+            continue
+        sys.path.insert(0, sp)
+        added = True
+        if sys.platform == "win32":
+            _bootstrap_pywin32(sp)
+    return added
+
+
+def _bootstrap_pywin32(site_packages_dir):
+    """Manually replicate the pywin32 `.pth` bootstrap so a venv added via
+    `sys.path.insert()` (not site.py) can still import `pywintypes`. No-op
+    when the venv doesn't include pywin32. Failures are swallowed — the
+    SDK import below will raise its own ImportError and the caller's
+    fallback path handles it cleanly."""
+    try:
+        win32 = os.path.join(site_packages_dir, "win32")
+        win32_lib = os.path.join(win32, "lib")
+        for d in (win32, win32_lib):
+            if os.path.isdir(d) and d not in sys.path:
+                sys.path.insert(0, d)
+        bootstrap = os.path.join(win32_lib, "pywin32_bootstrap.py")
+        if os.path.isfile(bootstrap):
+            import importlib.util
+            spec = importlib.util.spec_from_file_location(
+                "pywin32_bootstrap", bootstrap,
+            )
+            if spec and spec.loader:
+                mod = importlib.util.module_from_spec(spec)
+                spec.loader.exec_module(mod)
+    except Exception as e:
+        debug_log(f"pywin32 bootstrap failed (may break SDK import on Windows): {e}")
+
+
 # Plan Security Check Configuration
 ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
 # OAuth access token — Claude Code passes this for /login users.
@@ -298,12 +359,7 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None)
             "SECURITY_WARNINGS_STATE_DIR",
             os.path.expanduser("~/.claude/security"),
         )
-        for _sp in glob.glob(
-            os.path.join(_state_dir, "agent-sdk-venv", "lib",
-                         "python*", "site-packages")
-        ):
-            if os.path.isdir(_sp) and _sp not in sys.path:
-                sys.path.insert(0, _sp)
+        _inject_agent_sdk_venv_into_syspath(_state_dir)
         try:
             import asyncio as _asyncio  # noqa: F811
             from claude_agent_sdk import (  # noqa: F401,F811
@@ -1089,18 +1145,11 @@ def agentic_review(
         # ~/.claude/security/ with the SDK installed; try that as a fallback
         # before giving up. The system import is attempted first so users
         # who DO have it never touch the venv.
-        _venv_tried = False
         _state_dir = os.environ.get(
             "SECURITY_WARNINGS_STATE_DIR",
             os.path.expanduser("~/.claude/security"),
         )
-        for _sp in glob.glob(
-            os.path.join(_state_dir, "agent-sdk-venv", "lib",
-                         "python*", "site-packages")
-        ):
-            if os.path.isdir(_sp) and _sp not in sys.path:
-                sys.path.insert(0, _sp)
-                _venv_tried = True
+        _venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
         try:
             import asyncio as _asyncio  # noqa: F811