Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ openant set-api-key <your-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 API calls so TLS verification succeeds behind the proxy.

## Data directories

OpenAnt creates two directories:
Expand Down
5 changes: 3 additions & 2 deletions libs/openant-core/context/application_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@
from pathlib import Path
from typing import Any

from anthropic import Anthropic
from dotenv import load_dotenv

from utilities.anthropic_http import create_anthropic_client

# Load environment variables
load_dotenv()

Expand Down Expand Up @@ -503,7 +504,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,
Expand Down
3 changes: 2 additions & 1 deletion libs/openant-core/core/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,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)
Expand Down
29 changes: 25 additions & 4 deletions libs/openant-core/core/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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,
Expand Down Expand Up @@ -463,8 +482,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:
Expand All @@ -473,8 +493,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
Expand Down
5 changes: 3 additions & 2 deletions libs/openant-core/generate_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@
import os
from datetime import datetime

import anthropic
from dotenv import load_dotenv

from utilities.anthropic_http import create_anthropic_client

# Load environment variables from .env file
load_dotenv()

Expand Down Expand Up @@ -142,7 +143,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,
Expand Down
4 changes: 2 additions & 2 deletions libs/openant-core/openant/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,9 +554,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
Expand Down Expand Up @@ -777,7 +777,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,
Expand Down
1 change: 1 addition & 0 deletions libs/openant-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions libs/openant-core/report/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import json
import os
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

load_dotenv()
Expand Down Expand Up @@ -135,7 +136,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")
Expand All @@ -158,7 +159,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")

Expand Down
3 changes: 2 additions & 1 deletion libs/openant-core/utilities/agentic_enhancer/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
84 changes: 84 additions & 0 deletions libs/openant-core/utilities/anthropic_http.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion libs/openant-core/utilities/context_enhancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -569,7 +570,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]
Expand Down
4 changes: 2 additions & 2 deletions libs/openant-core/utilities/finding_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion libs/openant-core/utilities/llm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down