Address Windows verification: --prefer-binary + pywin32 bootstrap

The first round of this PR removed SKIP_WIN32, fixed venv_py to use
Scripts/python.exe, and added Lib/site-packages to the consumer glob —
all necessary. Windows verification (Win11 ARM64, Py 3.13, Git Bash)
showed two more blockers, both addressed here.

1. Pip dependency resolver picks unbuildable cryptography on ARM64.

   Without --prefer-binary, pip picks a cryptography version with no
   published ARM64 wheel and tries to build it from source. That needs
   Rust/Cargo, almost never present on user machines → BUILD_FAILED
   with err_kind=other:cryptography. A binary wheel exists 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
   where the latest version already has a wheel).

2. pywin32 .pth files aren't processed by sys.path.insert().

   With the venv built, ensure_agent_sdk.py's post-build probe passes
   (it runs from venv_py, where Python's site.py at startup processes
   pywin32.pth and registers win32/, win32/lib/ plus runs
   pywin32_bootstrap.py to set the DLL search dir). But llm.py runs in
   the hook's SYSTEM Python and adds the venv via sys.path.insert(),
   which doesn't trigger site.py at all. Without the bootstrap, the
   SDK's mcp.client.stdio → mcp.os.win32.utilities chain raises
   ModuleNotFoundError: pywintypes and the agentic reviewer falls back
   to single-shot silently — exactly the symptom this PR is trying to
   fix. The probe says NOOP_VENV; the actual consumer fails. Probe and
   consumer use different Pythons.

   Replicate what site.py would do: after inserting site-packages,
   also insert win32/ and win32/lib/, then exec pywin32_bootstrap.py.
   Pulled into a shared helper _inject_agent_sdk_venv_into_syspath()
   so both consumer sites (3P SDK fallback, agentic_review fallback)
   call the same code — Windows handling stays in one place.

Verified on macOS (POSIX path unchanged):
- Helper end-to-end test: POSIX-layout venv detected + fake package
  imports successfully via the injected path
- Windows-layout venv also detected; win32 branch correctly skipped
  via sys.platform check
- Both files pass py_compile

Credit: @mhegazy verified the previous commit on Win11 ARM64 / Py 3.13
/ Git Bash, surfaced both issues end-to-end, and provided the exact
fix patterns. This commit applies them with the pywin32 part factored
into a shared helper (vs. inlining at both consumer sites).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mohamed Hegazy 2026-05-27 15:07:33 -07:00
parent 4decd2e3b2
commit c11244778d
No known key found for this signature in database
2 changed files with 74 additions and 21 deletions

View File

@ -124,10 +124,20 @@ def main() -> tuple[int, str, str]:
# the user's machine, pip's own default registry applies — that's the same # the user's machine, pip's own default registry applies — that's the same
# exposure the user would have running `pip install` themselves, so # exposure the user would have running `pip install` themselves, so
# we're not widening the supply-chain surface. # 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" err_phase = "pip"
subprocess.run( subprocess.run(
[str(venv_py), "-m", "pip", "install", "--quiet", [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, capture_output=True, timeout=120, check=True,
) )
return BUILT, "", "" return BUILT, "", ""

View File

@ -31,6 +31,67 @@ from _base import debug_log, _record_usage, _PV, PROVENANCE_TAG # noqa: F401
from session_state import with_locked_state 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 # Plan Security Check Configuration
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "") ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
# OAuth access token — Claude Code passes this for /login users. # OAuth access token — Claude Code passes this for /login users.
@ -298,15 +359,7 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None)
"SECURITY_WARNINGS_STATE_DIR", "SECURITY_WARNINGS_STATE_DIR",
os.path.expanduser("~/.claude/security"), os.path.expanduser("~/.claude/security"),
) )
# POSIX venv: lib/pythonX.Y/site-packages _inject_agent_sdk_venv_into_syspath(_state_dir)
# Windows venv: Lib/site-packages (capital L, no pythonX.Y subdir)
_venv_root = os.path.join(_state_dir, "agent-sdk-venv")
for _sp in (
glob.glob(os.path.join(_venv_root, "lib", "python*", "site-packages"))
+ glob.glob(os.path.join(_venv_root, "Lib", "site-packages"))
):
if os.path.isdir(_sp) and _sp not in sys.path:
sys.path.insert(0, _sp)
try: try:
import asyncio as _asyncio # noqa: F811 import asyncio as _asyncio # noqa: F811
from claude_agent_sdk import ( # noqa: F401,F811 from claude_agent_sdk import ( # noqa: F401,F811
@ -1092,21 +1145,11 @@ def agentic_review(
# ~/.claude/security/ with the SDK installed; try that as a fallback # ~/.claude/security/ with the SDK installed; try that as a fallback
# before giving up. The system import is attempted first so users # before giving up. The system import is attempted first so users
# who DO have it never touch the venv. # who DO have it never touch the venv.
_venv_tried = False
_state_dir = os.environ.get( _state_dir = os.environ.get(
"SECURITY_WARNINGS_STATE_DIR", "SECURITY_WARNINGS_STATE_DIR",
os.path.expanduser("~/.claude/security"), os.path.expanduser("~/.claude/security"),
) )
# POSIX venv: lib/pythonX.Y/site-packages _venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
# Windows venv: Lib/site-packages (capital L, no pythonX.Y subdir)
_venv_root = os.path.join(_state_dir, "agent-sdk-venv")
for _sp in (
glob.glob(os.path.join(_venv_root, "lib", "python*", "site-packages"))
+ glob.glob(os.path.join(_venv_root, "Lib", "site-packages"))
):
if os.path.isdir(_sp) and _sp not in sys.path:
sys.path.insert(0, _sp)
_venv_tried = True
try: try:
import asyncio as _asyncio # noqa: F811 import asyncio as _asyncio # noqa: F811