mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-13 22:26:03 -03:00
Merge pull request #2055 from anthropics/fix-windows-agentic-reviewer
security-guidance: enable agentic commit reviewer on Windows
This commit is contained in:
commit
2a3dd81146
@ -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
|
NOOP_VENV = 1 # venv already built and SDK imports from it
|
||||||
BUILT = 2 # venv created + SDK pip-installed this run
|
BUILT = 2 # venv created + SDK pip-installed this run
|
||||||
BUILD_FAILED = 3 # venv create or pip install raised/timed out
|
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
|
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
|
err_phase / err_kind are non-empty only on BUILD_FAILED — they let
|
||||||
telemetry split bootstrap failures by root cause.
|
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():
|
if _sdk_on_syspath():
|
||||||
return NOOP_SYSTEM, "", ""
|
return NOOP_SYSTEM, "", ""
|
||||||
|
|
||||||
@ -75,6 +70,10 @@ def main() -> tuple[int, str, str]:
|
|||||||
or os.path.expanduser("~/.claude/security")
|
or os.path.expanduser("~/.claude/security")
|
||||||
)
|
)
|
||||||
venv = state_dir / "agent-sdk-venv"
|
venv = state_dir / "agent-sdk-venv"
|
||||||
|
# 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"
|
venv_py = venv / "bin" / "python"
|
||||||
|
|
||||||
# Another SessionStart (concurrent CC instance, same plugin) may already
|
# Another SessionStart (concurrent CC instance, same plugin) may already
|
||||||
@ -125,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, "", ""
|
||||||
|
|||||||
@ -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,12 +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"),
|
||||||
)
|
)
|
||||||
for _sp in glob.glob(
|
_inject_agent_sdk_venv_into_syspath(_state_dir)
|
||||||
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)
|
|
||||||
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
|
||||||
@ -1089,18 +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"),
|
||||||
)
|
)
|
||||||
for _sp in glob.glob(
|
_venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
import asyncio as _asyncio # noqa: F811
|
import asyncio as _asyncio # noqa: F811
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user