From ff904c701a7f763827eb4c50165ab751d4b531be Mon Sep 17 00:00:00 2001 From: Petre Ghita Date: Wed, 22 Apr 2026 09:18:04 +0200 Subject: [PATCH 1/3] fix: trust corporate TLS for Anthropic (NODE_EXTRA_CA_CERTS) --- README.md | 10 ++- .../context/application_context.py | 5 +- libs/openant-core/core/analyzer.py | 3 +- libs/openant-core/core/scanner.py | 29 ++++++- libs/openant-core/generate_report.py | 5 +- libs/openant-core/openant/cli.py | 4 +- libs/openant-core/pyproject.toml | 1 + libs/openant-core/report/generator.py | 7 +- .../utilities/agentic_enhancer/agent.py | 3 +- libs/openant-core/utilities/anthropic_http.py | 84 +++++++++++++++++++ .../utilities/context_enhancer.py | 3 +- .../utilities/finding_verifier.py | 4 +- libs/openant-core/utilities/llm_client.py | 3 +- 13 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 libs/openant-core/utilities/anthropic_http.py diff --git a/README.md b/README.md index 66c5806..a3896eb 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,19 @@ openant set-api-key **The key must have access to the Claude Opus 4.6 model.** Get a key at [console.anthropic.com](https://console.anthropic.com/settings/keys). +If HTTPS traffic to Anthropic is intercepted (for example by Zscaler), export your corporate root CA in PEM form -- the same variable many tools use with Node: + +```bash +export NODE_EXTRA_CA_CERTS=/path/to/your-corporate-root-ca.pem +``` + +OpenAnt's Python runtime loads that bundle for **Anthropic Python SDK** API calls so TLS verification succeeds behind the proxy. When this variable is set, Python 3.13+'s strict X.509 validation is relaxed to match Node's behavior, so Zscaler-class CAs (whose Basic Constraints extension is sometimes non-critical) validate -- a deliberate trade toward compatibility with typical corporate intercept roots and Node-like trust behavior. + ### Python runtime OpenAnt's parsing, enhancement, analysis, and reporting code is Python 3.11+. The Go CLI picks an interpreter in this order: -1. `OPENANT_PYTHON` env var (set this to pin a specific interpreter — e.g. `OPENANT_PYTHON=python3.11`). +1. `OPENANT_PYTHON` env var (set this to pin a specific interpreter -- e.g. `OPENANT_PYTHON=python3.11`). 2. Managed venv at `~/.openant/venv/` (auto-created on first use). The CLI uses `bin/python` on Linux/macOS and `Scripts\python.exe` on Windows. 3. `python3` / `python` on `PATH`. diff --git a/libs/openant-core/context/application_context.py b/libs/openant-core/context/application_context.py index 11940db..7a08389 100644 --- a/libs/openant-core/context/application_context.py +++ b/libs/openant-core/context/application_context.py @@ -29,10 +29,11 @@ from pathlib import Path from typing import Any -from anthropic import Anthropic from dotenv import load_dotenv from utilities.file_io import open_utf8, read_json, write_json +from utilities.anthropic_http import create_anthropic_client + # Load environment variables load_dotenv() @@ -509,7 +510,7 @@ def generate_application_context( # Call LLM print(f"Generating context with {model}...", file=sys.stderr) - client = Anthropic() + client = create_anthropic_client() response = client.messages.create( model=model, max_tokens=2000, diff --git a/libs/openant-core/core/analyzer.py b/libs/openant-core/core/analyzer.py index f8255f1..2a64177 100644 --- a/libs/openant-core/core/analyzer.py +++ b/libs/openant-core/core/analyzer.py @@ -317,8 +317,9 @@ def run_analysis( model_id = "claude-opus-4-6" if model == "opus" else "claude-sonnet-4-20250514" print(f"[Analyze] Model: {model_id}", file=sys.stderr) - # Initialize client + # Initialize client (uses global token tracker by default) client = AnthropicClient(model=model_id) + tracker = get_global_tracker() # Initialize JSON corrector json_corrector = JSONCorrector(client) diff --git a/libs/openant-core/core/scanner.py b/libs/openant-core/core/scanner.py index 0424672..4305fe6 100644 --- a/libs/openant-core/core/scanner.py +++ b/libs/openant-core/core/scanner.py @@ -40,6 +40,25 @@ HAS_APP_CONTEXT = False +def _format_exception_chain(exc: BaseException, limit: int = 500) -> str: + """Readable error text; Anthropic often raises APIConnectionError with a hidden __cause__.""" + parts: list[str] = [] + cur: BaseException | None = exc + seen: set[str] = set() + depth = 0 + while cur is not None and depth < 6: + msg = (str(cur) or type(cur).__name__).strip() + if msg and msg not in seen: + parts.append(msg) + seen.add(msg) + cur = cur.__cause__ or cur.__context__ + depth += 1 + text = " | ".join(parts) if parts else type(exc).__name__ + if len(text) > limit: + return text[: limit - 3] + "..." + return text + + def scan_repository( repo_path: str, output_dir: str, @@ -620,8 +639,9 @@ def _step_label(name: str) -> str: outputs["summary_path"] = summary_path print(f" Summary: {summary_path}", file=sys.stderr) except Exception as e: - print(f" WARNING: Summary report failed: {e}", file=sys.stderr) - ctx.errors.append(f"Summary report: {e}") + detail = _format_exception_chain(e) + print(f" WARNING: Summary report failed: {detail}", file=sys.stderr) + ctx.errors.append(f"Summary report: {detail}") # Only generate disclosures if there are findings if has_findings: @@ -630,8 +650,9 @@ def _step_label(name: str) -> str: outputs["disclosures_dir"] = disclosures_dir print(f" Disclosures: {disclosures_dir}", file=sys.stderr) except Exception as e: - print(f" WARNING: Disclosure docs failed: {e}", file=sys.stderr) - ctx.errors.append(f"Disclosure docs: {e}") + detail = _format_exception_chain(e) + print(f" WARNING: Disclosure docs failed: {detail}", file=sys.stderr) + ctx.errors.append(f"Disclosure docs: {detail}") ctx.summary = {"formats_generated": list(outputs.keys())} ctx.outputs = outputs diff --git a/libs/openant-core/generate_report.py b/libs/openant-core/generate_report.py index 5af97f9..8495e80 100644 --- a/libs/openant-core/generate_report.py +++ b/libs/openant-core/generate_report.py @@ -29,10 +29,11 @@ import os from datetime import datetime -import anthropic from dotenv import load_dotenv from utilities.file_io import read_json +from utilities.anthropic_http import create_anthropic_client + # Load environment variables from .env file load_dotenv() @@ -201,7 +202,7 @@ def generate_remediation_guidance(findings: list) -> str: if not api_key: raise ValueError("ANTHROPIC_API_KEY not found in environment") - client = anthropic.Anthropic(api_key=api_key) + client = create_anthropic_client(api_key=api_key) response = client.messages.create( model=REPORT_MODEL, max_tokens=MAX_TOKENS, diff --git a/libs/openant-core/openant/cli.py b/libs/openant-core/openant/cli.py index c303c64..c6f67a5 100644 --- a/libs/openant-core/openant/cli.py +++ b/libs/openant-core/openant/cli.py @@ -590,9 +590,9 @@ def cmd_report_data(args): and step reports — everything display-ready. """ import html as html_mod - import anthropic from core.schemas import success, error from core.step_report import step_context + from utilities.anthropic_http import create_anthropic_client from utilities.llm_client import get_global_tracker results_path = args.results @@ -809,7 +809,7 @@ def cmd_report_data(args): {findings_text} """ print("[Report] Generating remediation guidance (LLM)...", file=sys.stderr) - client = anthropic.Anthropic() + client = create_anthropic_client() response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=4096, diff --git a/libs/openant-core/pyproject.toml b/libs/openant-core/pyproject.toml index bf0377a..fbed61a 100644 --- a/libs/openant-core/pyproject.toml +++ b/libs/openant-core/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "anthropic>=0.40.0", + "certifi>=2024.0.0", "python-dotenv>=1.0.0", "pydantic>=2.0.0", "httpx>=0.24.0", diff --git a/libs/openant-core/report/generator.py b/libs/openant-core/report/generator.py index 9f08b87..a5cb258 100644 --- a/libs/openant-core/report/generator.py +++ b/libs/openant-core/report/generator.py @@ -8,10 +8,11 @@ import os import re import sys -import anthropic from pathlib import Path from dotenv import load_dotenv +from utilities.anthropic_http import create_anthropic_client + from .schema import validate_pipeline_output, ValidationError from utilities.file_io import open_utf8, read_json @@ -138,7 +139,7 @@ def generate_summary_report(pipeline_data: dict) -> tuple[str, dict]: output_tokens, total_tokens, cost_usd. """ _check_api_key() - client = anthropic.Anthropic() + client = create_anthropic_client() summary_data = _compact_for_summary(pipeline_data) system_prompt = load_prompt("system") @@ -201,7 +202,7 @@ def generate_disclosure(vulnerability_data: dict, product_name: str) -> tuple[st (disclosure_text, usage_dict) """ _check_api_key() - client = anthropic.Anthropic() + client = create_anthropic_client() system_prompt = load_prompt("system") diff --git a/libs/openant-core/utilities/agentic_enhancer/agent.py b/libs/openant-core/utilities/agentic_enhancer/agent.py index 62061b7..28e9bfc 100644 --- a/libs/openant-core/utilities/agentic_enhancer/agent.py +++ b/libs/openant-core/utilities/agentic_enhancer/agent.py @@ -16,6 +16,7 @@ import anthropic +from ..anthropic_http import create_anthropic_client from ..llm_client import TokenTracker, get_global_tracker from ..rate_limiter import get_rate_limiter from .repository_index import RepositoryIndex @@ -126,7 +127,7 @@ def __init__( self.tool_executor = ToolExecutor(index) self.entry_points = entry_points or set() self.reachability = reachability - self.client = client or anthropic.Anthropic(max_retries=5) + self.client = client or create_anthropic_client(max_retries=5) def analyze_unit( self, diff --git a/libs/openant-core/utilities/anthropic_http.py b/libs/openant-core/utilities/anthropic_http.py new file mode 100644 index 0000000..c48e9f1 --- /dev/null +++ b/libs/openant-core/utilities/anthropic_http.py @@ -0,0 +1,84 @@ +"""TLS configuration for Anthropic API calls. + +TLS inspection (for example Zscaler) terminates HTTPS with a corporate root +that is not in the default trust store. Node and Claude Code honor +``NODE_EXTRA_CA_CERTS`` (a PEM file of extra CA certificates). The Anthropic +Python SDK uses httpx; this module applies the same PEM when the variable is +set so verification succeeds behind those proxies. + +Python 3.13+ enables ``VERIFY_X509_STRICT`` on default contexts, which rejects +many corporate intercept CAs (e.g. Zscaler) whose Basic Constraints extension +is not marked critical. When using ``NODE_EXTRA_CA_CERTS``, that strict bit is +cleared so TLS still verifies the chain while matching typical Node behavior. +""" + +from __future__ import annotations + +import os +import ssl +from typing import Any + +import httpx + +try: + from anthropic import DefaultHttpxClient +except ImportError: # pragma: no cover - extremely old anthropic + DefaultHttpxClient = httpx.Client # type: ignore[misc, assignment] + +try: + import certifi +except ImportError: # pragma: no cover - pulled in by httpx + certifi = None # type: ignore[assignment] + + +def _relax_x509_strict_for_corporate_cas(ctx: ssl.SSLContext) -> None: + """Turn off VERIFY_X509_STRICT so Zscaler-like CAs validate (Python 3.13+).""" + strict = getattr(ssl, "VERIFY_X509_STRICT", 0) + if strict: + ctx.verify_flags &= ~strict + + +def _ssl_context_from_node_extra_ca_certs() -> ssl.SSLContext | None: + """Build TLS context: public CA bundle plus PEM from ``NODE_EXTRA_CA_CERTS``. + + Node appends ``NODE_EXTRA_CA_CERTS`` to its built-in store. On some platforms + ``ssl.create_default_context()`` then ``load_verify_locations(file)`` does not + stack the way we need; anchoring with certifi's bundle then loading the extra + file matches the Node / Claude Code behavior more reliably (e.g. macOS + + Zscaler). + """ + path = (os.environ.get("NODE_EXTRA_CA_CERTS") or "").strip() + if not path or not os.path.isfile(path): + return None + try: + if certifi is not None: + ctx = ssl.create_default_context(cafile=certifi.where()) + else: + ctx = ssl.create_default_context() + ctx.load_verify_locations(path) + _relax_x509_strict_for_corporate_cas(ctx) + except OSError: + return None + return ctx + + +def anthropic_http_client_from_env() -> httpx.Client | None: + """Return an httpx client with extra CAs, or None if not configured.""" + ctx = _ssl_context_from_node_extra_ca_certs() + if ctx is None: + return None + return DefaultHttpxClient(verify=ctx) + + +def create_anthropic_client(**kwargs: Any): + """Construct ``anthropic.Anthropic`` honoring ``NODE_EXTRA_CA_CERTS``. + + If the caller passes ``http_client``, it is left unchanged. + """ + import anthropic + + if kwargs.get("http_client") is None: + http_client = anthropic_http_client_from_env() + if http_client is not None: + kwargs["http_client"] = http_client + return anthropic.Anthropic(**kwargs) diff --git a/libs/openant-core/utilities/context_enhancer.py b/libs/openant-core/utilities/context_enhancer.py index 2f7dea2..ba1e9a2 100644 --- a/libs/openant-core/utilities/context_enhancer.py +++ b/libs/openant-core/utilities/context_enhancer.py @@ -25,6 +25,7 @@ import anthropic +from .anthropic_http import create_anthropic_client from .llm_client import AnthropicClient, TokenTracker, get_global_tracker, reset_global_tracker from .agentic_enhancer import RepositoryIndex, enhance_unit_with_agent, load_index_from_file from .rate_limiter import get_rate_limiter, is_rate_limit_error, is_retryable_error @@ -584,7 +585,7 @@ def enhance_dataset_agentic( # which spawns a new httpx connection pool. With 1000+ units and 8 workers, # this exhausted file descriptors (macOS limit ~256). The httpx.Client # underlying anthropic.Anthropic is thread-safe, so sharing is correct. - shared_client = anthropic.Anthropic(max_retries=5) + shared_client = create_anthropic_client(max_retries=5) # Filter to unprocessed units units_to_process = [(i, unit) for i, unit in enumerate(units) if unit.get("id") not in processed_ids] diff --git a/libs/openant-core/utilities/finding_verifier.py b/libs/openant-core/utilities/finding_verifier.py index 2e66b7c..f5c40ec 100644 --- a/libs/openant-core/utilities/finding_verifier.py +++ b/libs/openant-core/utilities/finding_verifier.py @@ -39,7 +39,7 @@ from typing import Callable, Optional import anthropic - +from .anthropic_http import create_anthropic_client from .llm_client import TokenTracker, get_global_tracker from .rate_limiter import get_rate_limiter @@ -271,7 +271,7 @@ def __init__( self.verbose = verbose self.app_context = app_context self.tool_executor = ToolExecutor(index) - self.client = client or anthropic.Anthropic(max_retries=5) + self.client = client or create_anthropic_client(max_retries=5) self.logger = logger or _null_logger self._use_logger = logger is not None diff --git a/libs/openant-core/utilities/llm_client.py b/libs/openant-core/utilities/llm_client.py index ea356bf..5c97387 100644 --- a/libs/openant-core/utilities/llm_client.py +++ b/libs/openant-core/utilities/llm_client.py @@ -23,6 +23,7 @@ import anthropic from dotenv import load_dotenv +from .anthropic_http import create_anthropic_client from .rate_limiter import get_rate_limiter @@ -204,7 +205,7 @@ def __init__(self, model: str = "claude-opus-4-20250514", tracker: TokenTracker if not api_key: raise ValueError("ANTHROPIC_API_KEY not found in environment") - self.client = anthropic.Anthropic(api_key=api_key, max_retries=5) + self.client = create_anthropic_client(api_key=api_key, max_retries=5) self.model = model self.tracker = tracker or _global_tracker self.last_call = None # Store last call details From b7cbbb1f183c1e03051dcb91cc6b5e1ea4caeb19 Mon Sep 17 00:00:00 2001 From: Petre Ghita Date: Fri, 15 May 2026 10:42:14 +0200 Subject: [PATCH 2/3] test: cover anthropic_http TLS and client injection (#27) Add regression tests for NODE_EXTRA_CA_CERTS, merge README guidance with upstream Python runtime docs and document the Python 3.13+ X.509 tradeoff, and drop the analyzer tracker change from the TLS PR per review. Co-authored-by: Cursor --- README.md | 14 +-- libs/openant-core/core/analyzer.py | 3 +- .../openant-core/tests/test_anthropic_http.py | 89 +++++++++++++++++++ 3 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 libs/openant-core/tests/test_anthropic_http.py diff --git a/README.md b/README.md index a3896eb..5885253 100644 --- a/README.md +++ b/README.md @@ -92,27 +92,27 @@ If none yield Python 3.11+, the command exits with an error pointing at [python. OpenAnt creates two directories: -- **`~/.config/openant/`** — CLI configuration (`config.json`). Stores your API key, active project, and preferences. File permissions are restricted to `0600`. -- **`~/.openant/`** — Project data. Each initialized project gets a workspace under `~/.openant/projects///` containing `project.json` and a `scans/` directory with per-commit outputs. +- **`~/.config/openant/`** - CLI configuration (`config.json`). Stores your API key, active project, and preferences. File permissions are restricted to `0600`. +- **`~/.openant/`** - Project data. Each initialized project gets a workspace under `~/.openant/projects///` containing `project.json` and a `scans/` directory with per-commit outputs. ## Analyzing a project ### 1. Initialize -Point OpenAnt at a repository. The `-l` flag (language) is required — use `go` or `python`. +Point OpenAnt at a repository. The `-l` flag (language) is required -- use `go` or `python`. ```bash -# Remote — clones the repo +# Remote -- clones the repo openant init -l go -# Remote — pin to a specific commit +# Remote -- pin to a specific commit openant init -l go --commit -# Local — references the directory in-place +# Local -- references the directory in-place openant init -l go --name ``` -This creates a project workspace and sets it as the active project. All subsequent commands operate on the active project automatically — no path arguments needed. +This creates a project workspace and sets it as the active project. All subsequent commands operate on the active project automatically -- no path arguments needed. ### 2. Run the pipeline diff --git a/libs/openant-core/core/analyzer.py b/libs/openant-core/core/analyzer.py index 2a64177..f8255f1 100644 --- a/libs/openant-core/core/analyzer.py +++ b/libs/openant-core/core/analyzer.py @@ -317,9 +317,8 @@ def run_analysis( model_id = "claude-opus-4-6" if model == "opus" else "claude-sonnet-4-20250514" print(f"[Analyze] Model: {model_id}", file=sys.stderr) - # Initialize client (uses global token tracker by default) + # Initialize client client = AnthropicClient(model=model_id) - tracker = get_global_tracker() # Initialize JSON corrector json_corrector = JSONCorrector(client) diff --git a/libs/openant-core/tests/test_anthropic_http.py b/libs/openant-core/tests/test_anthropic_http.py new file mode 100644 index 0000000..32aae1d --- /dev/null +++ b/libs/openant-core/tests/test_anthropic_http.py @@ -0,0 +1,89 @@ +"""Tests for utilities.anthropic_http (corporate TLS / NODE_EXTRA_CA_CERTS).""" + +from __future__ import annotations + +import os +import ssl +import tempfile +from typing import Any + +import certifi +import pytest + +from utilities.anthropic_http import _ssl_context_from_node_extra_ca_certs, create_anthropic_client + +# Self-signed test CA (generated for this test module only; not a production trust anchor). +_TEST_CA_PEM = """\ +-----BEGIN CERTIFICATE----- +MIIDGTCCAgGgAwIBAgIUb/55TWJ6Dq5Md9inc2PMh795YUkwDQYJKoZIhvcNAQEL +BQAwHDEaMBgGA1UEAwwRVGVzdCBDb3Jwb3JhdGUgQ0EwHhcNMjYwNTE1MDgzMzM4 +WhcNMjcwNTE1MDgzMzM4WjAcMRowGAYDVQQDDBFUZXN0IENvcnBvcmF0ZSBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANT6B1N+UkVlx4u4MWA/NKmJ +XVHULjJiOqdgEAQpJ0a1LMWHBmabthkdxVtzXOvbvZMeV2MmoMtN0fUKRvY4TRvF +ktPKzyuopz7T2KdVF062wLsq6+T0essOJ5DcUJSsyWlfX9myfGYqAq+XA6lufh6E +S6nroeYxbXvmIt7yMOG/TTHFghXHNPvnwOk7iy1QpPJt5dFlvXuANsgfJ5nBDyLS +P3kU+/P/Q3TQ3Qe6aEwF5MdvVnGhDQ7waKj3HHdhAabwkAzzAxoHxkzbm+GT5y/G +rmd9i7xOdztpmNxQ1xE2kk/7qDqmgQumNDsUOPpGnUfRh4lbsXmp9YoqRB1EYlMC +AwEAAaNTMFEwHQYDVR0OBBYEFEm8vvB1B2V/Jsctb6P+eMxM4sbqMB8GA1UdIwQY +MBaAFEm8vvB1B2V/Jsctb6P+eMxM4sbqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBAIbcE0Pcb7+azH9acQhsuRy3EEHaZYgBFmpfcnRqtgNHAfF3 +hyevLhjCr+auiQ9NtAMEHeoRFGdqxqXG4BzqqIuoFrBc+cCwBxbeNVyWN0+6qqFi +ObYmZQu+wB3lPYdwlShNIGu+rRBNCsqymLtKIuLEqW1Rk1D5B4HiJCYDqBa0VXgY +IpGQR7qvDcgvH/AfYdfY3GSCMgAWDjAt28o6NV3lZC9gcfKmo3Vt1ID+W0DdE4TZ +8XqADeHTYAnJUQs4r81vbemLE8vbwuQR+/lSWyJ18jCAmoAjHl+VlHyaFdoFBCD+ +bbc7He5nRua65XEUMIbTgpvT83E9KhHsJz+K96A= +-----END CERTIFICATE----- +""" + + +@pytest.fixture +def corporate_ca_pem_path() -> str: + with tempfile.NamedTemporaryFile("w", suffix=".pem", delete=False) as f: + f.write(_TEST_CA_PEM) + path = f.name + yield path + try: + os.unlink(path) + except OSError: + pass + + +def test_ssl_context_loads_extra_ca_into_trust_store(monkeypatch: pytest.MonkeyPatch, corporate_ca_pem_path: str): + monkeypatch.setenv("NODE_EXTRA_CA_CERTS", corporate_ca_pem_path) + base = ssl.create_default_context(cafile=certifi.where()).cert_store_stats()["x509"] + ctx = _ssl_context_from_node_extra_ca_certs() + assert ctx is not None + assert ctx.cert_store_stats()["x509"] >= base + 1 + + +def test_create_anthropic_client_injects_http_client_when_env_set( + monkeypatch: pytest.MonkeyPatch, corporate_ca_pem_path: str, +): + monkeypatch.setenv("NODE_EXTRA_CA_CERTS", corporate_ca_pem_path) + captured: dict[str, Any] = {} + + class _StubAnthropic: + def __init__(self, **kwargs: Any) -> None: + captured.update(kwargs) + + import anthropic + + monkeypatch.setattr(anthropic, "Anthropic", _StubAnthropic) + create_anthropic_client(api_key="test-key") + assert "http_client" in captured + assert captured["http_client"] is not None + + +def test_create_anthropic_client_skips_http_client_without_env(monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("NODE_EXTRA_CA_CERTS", raising=False) + captured: dict[str, Any] = {} + + class _StubAnthropic: + def __init__(self, **kwargs: Any) -> None: + captured.update(kwargs) + + import anthropic + + monkeypatch.setattr(anthropic, "Anthropic", _StubAnthropic) + create_anthropic_client(api_key="test-key") + assert "http_client" not in captured From 137725a0428fa341c0fc39d3b459e7b89cfc64d9 Mon Sep 17 00:00:00 2001 From: Petre Ghita Date: Mon, 18 May 2026 10:02:18 +0200 Subject: [PATCH 3/3] test: mock create_anthropic_client in disclosure fidelity tests report.generator no longer exposes module-level anthropic after TLS factory wiring; patch create_anthropic_client so tier-2 disclosure tests keep a stable fake client. Co-authored-by: Cursor --- .../tests/report/test_disclosure_source_fidelity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/openant-core/tests/report/test_disclosure_source_fidelity.py b/libs/openant-core/tests/report/test_disclosure_source_fidelity.py index 462f958..0589853 100644 --- a/libs/openant-core/tests/report/test_disclosure_source_fidelity.py +++ b/libs/openant-core/tests/report/test_disclosure_source_fidelity.py @@ -309,7 +309,11 @@ class _Usage: @pytest.fixture def patched_anthropic(monkeypatch): monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test-key") - monkeypatch.setattr(generator.anthropic, "Anthropic", _FakeAnthropic) + monkeypatch.setattr( + generator, + "create_anthropic_client", + lambda **_kwargs: _FakeAnthropic(), + ) def test_generate_disclosure_output_has_real_code(patched_anthropic, pipeline_output):