mirror of
https://github.com/hwchase17/langchain.git
synced 2026-04-16 18:02:57 +00:00
Compare commits
6 Commits
langchain-
...
cc/test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09fe394d46 | ||
|
|
690c6ca2ce | ||
|
|
4be8744950 | ||
|
|
ffe4def5e4 | ||
|
|
efa97e598b | ||
|
|
6443612fa3 |
14
AGENTS.md
14
AGENTS.md
@@ -194,6 +194,16 @@ def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
|
||||
- Ensure American English spelling (e.g., "behavior", not "behaviour")
|
||||
- Do NOT use Sphinx-style double backtick formatting (` ``code`` `). Use single backticks (`` `code` ``) for inline code references in docstrings and comments.
|
||||
|
||||
#### Model references in docs and examples
|
||||
|
||||
Always use the latest generally available (GA) models when referencing LLMs in docstrings and illustrative code snippets. Avoid preview or beta identifiers unless the model has no GA equivalent. Outdated model names signal stale code and confuse users.
|
||||
|
||||
Before writing or updating model references, verify current model IDs against the provider's official docs. Do not rely on memorized or cached model names — they go stale quickly.
|
||||
|
||||
Changing **shipped default parameter values** in code (e.g., a `model=` kwarg default in a class constructor) may constitute a breaking change — see "Maintain stable public interfaces" above. This guidance applies to documentation and examples, not code defaults.
|
||||
|
||||
For model *profile data* (capability flags, context windows), use the `langchain-profiles` CLI described below.
|
||||
|
||||
## Model profiles
|
||||
|
||||
Model profiles are generated using the `langchain-profiles` CLI in `libs/model-profiles`. The `--data-dir` must point to the directory containing `profile_augmentations.toml`, not the top-level package directory.
|
||||
@@ -247,6 +257,10 @@ When adding a new partner package, update these files:
|
||||
- `.github/workflows/integration_tests.yml` – Add integration test config
|
||||
- `.github/workflows/pr_lint.yml` – Add to allowed scopes
|
||||
|
||||
## GitHub Actions & Workflows
|
||||
|
||||
This repository require actions to be pinned to a full-length commit SHA. Attempting to use a tag will fail. Use the `gh` cli to query. Verify tags are not annotated tag objects (which would need dereferencing).
|
||||
|
||||
## Additional resources
|
||||
|
||||
- **Documentation:** https://docs.langchain.com/oss/python/langchain/overview and source at https://github.com/langchain-ai/docs or `../docs/`. Prefer the local install and use file search tools for best results. If needed, use the docs MCP server as defined in `.mcp.json` for programmatic access.
|
||||
|
||||
14
CLAUDE.md
14
CLAUDE.md
@@ -194,6 +194,16 @@ def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
|
||||
- Ensure American English spelling (e.g., "behavior", not "behaviour")
|
||||
- Do NOT use Sphinx-style double backtick formatting (` ``code`` `). Use single backticks (`` `code` ``) for inline code references in docstrings and comments.
|
||||
|
||||
#### Model references in docs and examples
|
||||
|
||||
Always use the latest generally available (GA) models when referencing LLMs in docstrings and illustrative code snippets. Avoid preview or beta identifiers unless the model has no GA equivalent. Outdated model names signal stale code and confuse users.
|
||||
|
||||
Before writing or updating model references, verify current model IDs against the provider's official docs. Do not rely on memorized or cached model names — they go stale quickly.
|
||||
|
||||
Changing **shipped default parameter values** in code (e.g., a `model=` kwarg default in a class constructor) may constitute a breaking change — see "Maintain stable public interfaces" above. This guidance applies to documentation and examples, not code defaults.
|
||||
|
||||
For model *profile data* (capability flags, context windows), use the `langchain-profiles` CLI described below.
|
||||
|
||||
## Model profiles
|
||||
|
||||
Model profiles are generated using the `langchain-profiles` CLI in `libs/model-profiles`. The `--data-dir` must point to the directory containing `profile_augmentations.toml`, not the top-level package directory.
|
||||
@@ -247,6 +257,10 @@ When adding a new partner package, update these files:
|
||||
- `.github/workflows/integration_tests.yml` – Add integration test config
|
||||
- `.github/workflows/pr_lint.yml` – Add to allowed scopes
|
||||
|
||||
## GitHub Actions & Workflows
|
||||
|
||||
This repository require actions to be pinned to a full-length commit SHA. Attempting to use a tag will fail. Use the `gh` cli to query. Verify tags are not annotated tag objects (which would need dereferencing).
|
||||
|
||||
## Additional resources
|
||||
|
||||
- **Documentation:** https://docs.langchain.com/oss/python/langchain/overview and source at https://github.com/langchain-ai/docs or `../docs/`. Prefer the local install and use file search tools for best results. If needed, use the docs MCP server as defined in `.mcp.json` for programmatic access.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""SSRF protection and security utilities.
|
||||
|
||||
This is an **internal** module (note the `_security` prefix). It is NOT part of
|
||||
the public `langchain-core` API and may change or be removed at any time without
|
||||
notice. External code should not import from or depend on anything in this
|
||||
module. Any vulnerability reports should target the public APIs that use these
|
||||
utilities, not this internal module directly.
|
||||
"""
|
||||
|
||||
from langchain_core._security._exceptions import SSRFBlockedError
|
||||
from langchain_core._security._policy import (
|
||||
SSRFPolicy,
|
||||
validate_hostname,
|
||||
validate_resolved_ip,
|
||||
validate_url,
|
||||
validate_url_sync,
|
||||
)
|
||||
from langchain_core._security._transport import (
|
||||
SSRFSafeSyncTransport,
|
||||
SSRFSafeTransport,
|
||||
ssrf_safe_async_client,
|
||||
ssrf_safe_client,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"SSRFBlockedError",
|
||||
"SSRFPolicy",
|
||||
"SSRFSafeSyncTransport",
|
||||
"SSRFSafeTransport",
|
||||
"ssrf_safe_async_client",
|
||||
"ssrf_safe_client",
|
||||
"validate_hostname",
|
||||
"validate_resolved_ip",
|
||||
"validate_url",
|
||||
"validate_url_sync",
|
||||
]
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""SSRF protection exceptions."""
|
||||
|
||||
|
||||
class SSRFBlockedError(Exception):
|
||||
"""Raised when a request is blocked by SSRF protection policy."""
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
super().__init__(f"SSRF blocked: {reason}")
|
||||
@@ -1,290 +0,0 @@
|
||||
"""SSRF protection policy with IP validation and DNS-aware URL checking."""
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import ipaddress
|
||||
import os
|
||||
import socket
|
||||
import urllib.parse
|
||||
|
||||
from langchain_core._security._exceptions import SSRFBlockedError
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blocklist constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_BLOCKED_IPV4_NETWORKS: tuple[ipaddress.IPv4Network, ...] = tuple(
|
||||
ipaddress.IPv4Network(n)
|
||||
for n in (
|
||||
"10.0.0.0/8", # RFC 1918 - private class A
|
||||
"172.16.0.0/12", # RFC 1918 - private class B
|
||||
"192.168.0.0/16", # RFC 1918 - private class C
|
||||
"127.0.0.0/8", # RFC 1122 - loopback
|
||||
"169.254.0.0/16", # RFC 3927 - link-local
|
||||
"0.0.0.0/8", # RFC 1122 - "this network"
|
||||
"100.64.0.0/10", # RFC 6598 - shared/CGN address space
|
||||
"192.0.0.0/24", # RFC 6890 - IETF protocol assignments
|
||||
"192.0.2.0/24", # RFC 5737 - TEST-NET-1 (documentation)
|
||||
"198.18.0.0/15", # RFC 2544 - benchmarking
|
||||
"198.51.100.0/24", # RFC 5737 - TEST-NET-2 (documentation)
|
||||
"203.0.113.0/24", # RFC 5737 - TEST-NET-3 (documentation)
|
||||
"224.0.0.0/4", # RFC 5771 - multicast
|
||||
"240.0.0.0/4", # RFC 1112 - reserved for future use
|
||||
"255.255.255.255/32", # RFC 919 - limited broadcast
|
||||
)
|
||||
)
|
||||
|
||||
_BLOCKED_IPV6_NETWORKS: tuple[ipaddress.IPv6Network, ...] = tuple(
|
||||
ipaddress.IPv6Network(n)
|
||||
for n in (
|
||||
"::1/128", # RFC 4291 - loopback
|
||||
"fc00::/7", # RFC 4193 - unique local addresses (ULA)
|
||||
"fe80::/10", # RFC 4291 - link-local
|
||||
"ff00::/8", # RFC 4291 - multicast
|
||||
"::ffff:0:0/96", # RFC 4291 - IPv4-mapped IPv6 addresses
|
||||
"::0.0.0.0/96", # RFC 4291 - IPv4-compatible IPv6 (deprecated)
|
||||
"64:ff9b::/96", # RFC 6052 - NAT64 well-known prefix
|
||||
"64:ff9b:1::/48", # RFC 8215 - NAT64 discovery prefix
|
||||
)
|
||||
)
|
||||
|
||||
_CLOUD_METADATA_IPS: frozenset[str] = frozenset(
|
||||
{
|
||||
"169.254.169.254",
|
||||
"169.254.170.2",
|
||||
"100.100.100.200",
|
||||
"fd00:ec2::254",
|
||||
}
|
||||
)
|
||||
|
||||
_CLOUD_METADATA_HOSTNAMES: frozenset[str] = frozenset(
|
||||
{
|
||||
"metadata.google.internal",
|
||||
"metadata.amazonaws.com",
|
||||
"metadata",
|
||||
"instance-data",
|
||||
}
|
||||
)
|
||||
|
||||
_LOCALHOST_NAMES: frozenset[str] = frozenset(
|
||||
{
|
||||
"localhost",
|
||||
"localhost.localdomain",
|
||||
"host.docker.internal",
|
||||
}
|
||||
)
|
||||
|
||||
_K8S_SUFFIX = ".svc.cluster.local"
|
||||
|
||||
_LOOPBACK_IPV4 = ipaddress.IPv4Network("127.0.0.0/8")
|
||||
_LOOPBACK_IPV6 = ipaddress.IPv6Address("::1")
|
||||
|
||||
# NAT64 well-known prefixes
|
||||
_NAT64_PREFIX = ipaddress.IPv6Network("64:ff9b::/96")
|
||||
_NAT64_DISCOVERY_PREFIX = ipaddress.IPv6Network("64:ff9b:1::/48")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSRFPolicy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class SSRFPolicy:
|
||||
"""Immutable policy controlling which URLs/IPs are considered safe."""
|
||||
|
||||
allowed_schemes: frozenset[str] = frozenset({"http", "https"})
|
||||
block_private_ips: bool = True
|
||||
block_localhost: bool = True
|
||||
block_cloud_metadata: bool = True
|
||||
block_k8s_internal: bool = True
|
||||
allowed_hosts: frozenset[str] = frozenset()
|
||||
additional_blocked_cidrs: tuple[
|
||||
ipaddress.IPv4Network | ipaddress.IPv6Network, ...
|
||||
] = ()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _extract_embedded_ipv4(
|
||||
addr: ipaddress.IPv6Address,
|
||||
) -> ipaddress.IPv4Address | None:
|
||||
"""Extract an embedded IPv4 from IPv4-mapped or NAT64 IPv6 addresses."""
|
||||
# Check ipv4_mapped first (covers ::ffff:x.x.x.x)
|
||||
if addr.ipv4_mapped is not None:
|
||||
return addr.ipv4_mapped
|
||||
|
||||
# Check NAT64 prefixes — embedded IPv4 is in the last 4 bytes
|
||||
if addr in _NAT64_PREFIX or addr in _NAT64_DISCOVERY_PREFIX:
|
||||
raw = addr.packed
|
||||
return ipaddress.IPv4Address(raw[-4:])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _ip_in_blocked_networks(
|
||||
addr: ipaddress.IPv4Address | ipaddress.IPv6Address,
|
||||
policy: SSRFPolicy,
|
||||
) -> str | None:
|
||||
"""Return a reason string if *addr* falls in a blocked range, else None."""
|
||||
# NOTE: if profiling shows this is a hot path, consider memoising with
|
||||
# @functools.lru_cache (key on (addr, id(policy))).
|
||||
if isinstance(addr, ipaddress.IPv4Address):
|
||||
if policy.block_private_ips:
|
||||
for net in _BLOCKED_IPV4_NETWORKS:
|
||||
if addr in net:
|
||||
return "private IP range"
|
||||
for net in policy.additional_blocked_cidrs: # type: ignore[assignment]
|
||||
if isinstance(net, ipaddress.IPv4Network) and addr in net:
|
||||
return "blocked CIDR"
|
||||
else:
|
||||
if policy.block_private_ips:
|
||||
for net in _BLOCKED_IPV6_NETWORKS: # type: ignore[assignment]
|
||||
if addr in net:
|
||||
return "private IP range"
|
||||
for net in policy.additional_blocked_cidrs: # type: ignore[assignment]
|
||||
if isinstance(net, ipaddress.IPv6Network) and addr in net:
|
||||
return "blocked CIDR"
|
||||
|
||||
# Loopback check — independent of block_private_ips so that
|
||||
# block_localhost=True still catches 127.x.x.x / ::1 even when
|
||||
# private IPs are allowed.
|
||||
if policy.block_localhost:
|
||||
if isinstance(addr, ipaddress.IPv4Address) and (
|
||||
addr in _LOOPBACK_IPV4 or addr in ipaddress.IPv4Network("0.0.0.0/8")
|
||||
):
|
||||
return "localhost address"
|
||||
if isinstance(addr, ipaddress.IPv6Address) and addr == _LOOPBACK_IPV6:
|
||||
return "localhost address"
|
||||
|
||||
# Cloud metadata IP check
|
||||
if policy.block_cloud_metadata and str(addr) in _CLOUD_METADATA_IPS:
|
||||
return "cloud metadata endpoint"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public validation functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def validate_resolved_ip(ip_str: str, policy: SSRFPolicy) -> None:
|
||||
"""Validate a resolved IP address against the SSRF policy.
|
||||
|
||||
Raises SSRFBlockedError if the IP is blocked.
|
||||
"""
|
||||
try:
|
||||
addr = ipaddress.ip_address(ip_str)
|
||||
except ValueError as exc:
|
||||
raise SSRFBlockedError("invalid IP address") from exc
|
||||
|
||||
if isinstance(addr, ipaddress.IPv6Address):
|
||||
inner = _extract_embedded_ipv4(addr)
|
||||
if inner is not None:
|
||||
addr = inner
|
||||
|
||||
reason = _ip_in_blocked_networks(addr, policy)
|
||||
if reason is not None:
|
||||
raise SSRFBlockedError(reason)
|
||||
|
||||
|
||||
def validate_hostname(hostname: str, policy: SSRFPolicy) -> None:
|
||||
"""Validate a hostname against the SSRF policy.
|
||||
|
||||
Raises SSRFBlockedError if the hostname is blocked.
|
||||
"""
|
||||
lower = hostname.lower()
|
||||
|
||||
if policy.block_localhost and lower in _LOCALHOST_NAMES:
|
||||
raise SSRFBlockedError("localhost address")
|
||||
|
||||
if policy.block_cloud_metadata and lower in _CLOUD_METADATA_HOSTNAMES:
|
||||
raise SSRFBlockedError("cloud metadata endpoint")
|
||||
|
||||
if policy.block_k8s_internal and lower.endswith(_K8S_SUFFIX):
|
||||
raise SSRFBlockedError("Kubernetes internal DNS")
|
||||
|
||||
|
||||
def _effective_allowed_hosts(policy: SSRFPolicy) -> frozenset[str]:
|
||||
"""Return allowed_hosts, augmented for local environments."""
|
||||
extra: set[str] = set()
|
||||
if os.environ.get("LANGCHAIN_ENV", "").startswith("local"):
|
||||
extra.update({"localhost", "testserver"})
|
||||
if extra:
|
||||
return policy.allowed_hosts | frozenset(extra)
|
||||
return policy.allowed_hosts
|
||||
|
||||
|
||||
async def validate_url(url: str, policy: SSRFPolicy = SSRFPolicy()) -> None:
|
||||
"""Validate a URL against the SSRF policy, including DNS resolution.
|
||||
|
||||
This is the primary entry-point for async code paths. It delegates
|
||||
scheme/hostname/allowed-hosts checks to ``validate_url_sync``, then
|
||||
resolves DNS and validates every resolved IP.
|
||||
|
||||
Raises:
|
||||
SSRFBlockedError: If the URL violates the policy.
|
||||
"""
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
hostname = parsed.hostname or ""
|
||||
|
||||
validate_url_sync(url, policy)
|
||||
|
||||
allowed = {h.lower() for h in _effective_allowed_hosts(policy)}
|
||||
if hostname.lower() in allowed:
|
||||
return
|
||||
|
||||
scheme = (parsed.scheme or "").lower()
|
||||
port = parsed.port or (443 if scheme == "https" else 80)
|
||||
try:
|
||||
addrinfo = await asyncio.to_thread(
|
||||
socket.getaddrinfo, hostname, port, type=socket.SOCK_STREAM
|
||||
)
|
||||
except socket.gaierror as exc:
|
||||
msg = "DNS resolution failed"
|
||||
raise SSRFBlockedError(msg) from exc
|
||||
|
||||
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
||||
validate_resolved_ip(str(sockaddr[0]), policy)
|
||||
|
||||
|
||||
def validate_url_sync(url: str, policy: SSRFPolicy = SSRFPolicy()) -> None:
|
||||
"""Synchronous URL validation (no DNS resolution).
|
||||
|
||||
Suitable for Pydantic validators and other sync contexts. Checks scheme
|
||||
and hostname patterns only - use ``validate_url`` for full DNS-aware checking.
|
||||
|
||||
Raises:
|
||||
SSRFBlockedError: If the URL violates the policy.
|
||||
"""
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
|
||||
scheme = (parsed.scheme or "").lower()
|
||||
if scheme not in policy.allowed_schemes:
|
||||
msg = f"scheme '{scheme}' not allowed"
|
||||
raise SSRFBlockedError(msg)
|
||||
|
||||
hostname = parsed.hostname
|
||||
if not hostname:
|
||||
msg = "missing hostname"
|
||||
raise SSRFBlockedError(msg)
|
||||
|
||||
allowed = _effective_allowed_hosts(policy)
|
||||
if hostname.lower() in {h.lower() for h in allowed}:
|
||||
return
|
||||
|
||||
try:
|
||||
ipaddress.ip_address(hostname)
|
||||
validate_resolved_ip(hostname, policy)
|
||||
except SSRFBlockedError:
|
||||
raise
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
|
||||
validate_hostname(hostname, policy)
|
||||
@@ -1,8 +1,28 @@
|
||||
"""SSRF Protection - thin wrapper raising ValueError for internal callers.
|
||||
"""SSRF Protection for validating URLs against Server-Side Request Forgery attacks.
|
||||
|
||||
Delegates all validation to `langchain_core._security._policy`.
|
||||
This module provides utilities to validate user-provided URLs and prevent SSRF attacks
|
||||
by blocking requests to:
|
||||
- Private IP ranges (RFC 1918, loopback, link-local)
|
||||
- Cloud metadata endpoints (AWS, GCP, Azure, etc.)
|
||||
- Localhost addresses
|
||||
- Invalid URL schemes
|
||||
|
||||
Usage:
|
||||
from lc_security.ssrf_protection import validate_safe_url, is_safe_url
|
||||
|
||||
# Validate a URL (raises ValueError if unsafe)
|
||||
safe_url = validate_safe_url("https://example.com/webhook")
|
||||
|
||||
# Check if URL is safe (returns bool)
|
||||
if is_safe_url("http://192.168.1.1"):
|
||||
# URL is safe
|
||||
pass
|
||||
|
||||
# Allow private IPs for development/testing (still blocks cloud metadata)
|
||||
safe_url = validate_safe_url("http://localhost:8080", allow_private=True)
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import os
|
||||
import socket
|
||||
from typing import Annotated, Any
|
||||
@@ -14,28 +34,141 @@ from pydantic import (
|
||||
HttpUrl,
|
||||
)
|
||||
|
||||
from langchain_core._security._exceptions import SSRFBlockedError
|
||||
from langchain_core._security._policy import (
|
||||
SSRFPolicy,
|
||||
)
|
||||
from langchain_core._security._policy import (
|
||||
validate_resolved_ip as _validate_resolved_ip,
|
||||
)
|
||||
from langchain_core._security._policy import (
|
||||
validate_url_sync as _validate_url_sync,
|
||||
)
|
||||
# Private IP ranges (RFC 1918, RFC 4193, RFC 3927, loopback)
|
||||
PRIVATE_IP_RANGES = [
|
||||
ipaddress.ip_network("10.0.0.0/8"), # Private Class A
|
||||
ipaddress.ip_network("172.16.0.0/12"), # Private Class B
|
||||
ipaddress.ip_network("192.168.0.0/16"), # Private Class C
|
||||
ipaddress.ip_network("127.0.0.0/8"), # Loopback
|
||||
ipaddress.ip_network("169.254.0.0/16"), # Link-local (includes cloud metadata)
|
||||
ipaddress.ip_network("0.0.0.0/8"), # Current network
|
||||
ipaddress.ip_network("::1/128"), # IPv6 loopback
|
||||
ipaddress.ip_network("fc00::/7"), # IPv6 unique local
|
||||
ipaddress.ip_network("fe80::/10"), # IPv6 link-local
|
||||
ipaddress.ip_network("ff00::/8"), # IPv6 multicast
|
||||
]
|
||||
|
||||
# Cloud provider metadata endpoints
|
||||
CLOUD_METADATA_RANGES = [
|
||||
ipaddress.ip_network(
|
||||
"169.254.0.0/16"
|
||||
), # IPv4 link-local (used by metadata services)
|
||||
]
|
||||
|
||||
CLOUD_METADATA_IPS = [
|
||||
"169.254.169.254", # AWS, GCP, Azure, DigitalOcean, Oracle Cloud
|
||||
"169.254.170.2", # AWS ECS task metadata
|
||||
"169.254.170.23", # AWS EKS Pod Identity Agent
|
||||
"100.100.100.200", # Alibaba Cloud metadata
|
||||
"fd00:ec2::254", # AWS EC2 IMDSv2 over IPv6 (Nitro instances)
|
||||
"fd00:ec2::23", # AWS EKS Pod Identity Agent (IPv6)
|
||||
"fe80::a9fe:a9fe", # OpenStack Nova metadata (IPv6 link-local equiv of
|
||||
# 169.254.169.254)
|
||||
]
|
||||
|
||||
CLOUD_METADATA_HOSTNAMES = [
|
||||
"metadata.google.internal", # GCP
|
||||
"metadata", # Generic
|
||||
"instance-data", # AWS EC2
|
||||
]
|
||||
|
||||
# Localhost variations
|
||||
LOCALHOST_NAMES = [
|
||||
"localhost",
|
||||
"localhost.localdomain",
|
||||
]
|
||||
|
||||
|
||||
def _policy_for(*, allow_private: bool, allow_http: bool) -> SSRFPolicy:
|
||||
"""Build an `SSRFPolicy` from the legacy flag interface."""
|
||||
schemes = frozenset({"http", "https"}) if allow_http else frozenset({"https"})
|
||||
return SSRFPolicy(
|
||||
allowed_schemes=schemes,
|
||||
block_private_ips=not allow_private,
|
||||
block_localhost=not allow_private,
|
||||
block_cloud_metadata=True,
|
||||
block_k8s_internal=True,
|
||||
)
|
||||
def _normalize_ip(ip_str: str) -> str:
|
||||
"""Normalize IP strings for consistent SSRF checks.
|
||||
|
||||
Args:
|
||||
ip_str: IP address as a string.
|
||||
|
||||
Returns:
|
||||
Canonical string form, converting IPv6-mapped IPv4 to plain IPv4.
|
||||
"""
|
||||
ip = ipaddress.ip_address(ip_str)
|
||||
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped is not None:
|
||||
return str(ip.ipv4_mapped)
|
||||
return str(ip)
|
||||
|
||||
|
||||
def is_private_ip(ip_str: str) -> bool:
|
||||
"""Check if an IP address is in a private range.
|
||||
|
||||
Args:
|
||||
ip_str: IP address as a string (e.g., "192.168.1.1")
|
||||
|
||||
Returns:
|
||||
True if IP is in a private range, False otherwise
|
||||
"""
|
||||
try:
|
||||
ip = ipaddress.ip_address(_normalize_ip(ip_str))
|
||||
return any(ip in range_ for range_ in PRIVATE_IP_RANGES)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_cloud_metadata(hostname: str, ip_str: str | None = None) -> bool:
|
||||
"""Check if hostname or IP is a cloud metadata endpoint.
|
||||
|
||||
Args:
|
||||
hostname: Hostname to check
|
||||
ip_str: Optional IP address to check
|
||||
|
||||
Returns:
|
||||
True if hostname or IP is a known cloud metadata endpoint
|
||||
"""
|
||||
# Check hostname
|
||||
if hostname.lower() in CLOUD_METADATA_HOSTNAMES:
|
||||
return True
|
||||
|
||||
# Check IP
|
||||
if ip_str:
|
||||
try:
|
||||
normalized_ip = _normalize_ip(ip_str)
|
||||
if normalized_ip in CLOUD_METADATA_IPS:
|
||||
return True
|
||||
|
||||
ip = ipaddress.ip_address(normalized_ip)
|
||||
if any(ip in range_ for range_ in CLOUD_METADATA_RANGES):
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_localhost(hostname: str, ip_str: str | None = None) -> bool:
|
||||
"""Check if hostname or IP is localhost.
|
||||
|
||||
Args:
|
||||
hostname: Hostname to check
|
||||
ip_str: Optional IP address to check
|
||||
|
||||
Returns:
|
||||
True if hostname or IP is localhost
|
||||
"""
|
||||
# Check hostname
|
||||
if hostname.lower() in LOCALHOST_NAMES:
|
||||
return True
|
||||
|
||||
# Check IP
|
||||
if ip_str:
|
||||
try:
|
||||
normalized_ip = _normalize_ip(ip_str)
|
||||
ip = ipaddress.ip_address(normalized_ip)
|
||||
# Check if loopback
|
||||
if ip.is_loopback:
|
||||
return True
|
||||
# Also check common localhost IPs
|
||||
if normalized_ip in ("127.0.0.1", "::1", "0.0.0.0"): # noqa: S104
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def validate_safe_url(
|
||||
@@ -50,22 +183,54 @@ def validate_safe_url(
|
||||
by blocking requests to private networks and cloud metadata endpoints.
|
||||
|
||||
Args:
|
||||
url: The URL to validate (string or Pydantic HttpUrl).
|
||||
allow_private: If ``True``, allows private IPs and localhost (for development).
|
||||
url: The URL to validate (string or Pydantic HttpUrl)
|
||||
allow_private: If True, allows private IPs and localhost (for development).
|
||||
Cloud metadata endpoints are ALWAYS blocked.
|
||||
allow_http: If ``True``, allows both HTTP and HTTPS. If ``False``, only HTTPS.
|
||||
allow_http: If True, allows both HTTP and HTTPS. If False, only HTTPS.
|
||||
|
||||
Returns:
|
||||
The validated URL as a string.
|
||||
The validated URL as a string
|
||||
|
||||
Raises:
|
||||
ValueError: If URL is invalid or potentially dangerous.
|
||||
ValueError: If URL is invalid or potentially dangerous
|
||||
|
||||
Examples:
|
||||
>>> validate_safe_url("https://hooks.slack.com/services/xxx")
|
||||
'https://hooks.slack.com/services/xxx'
|
||||
|
||||
>>> validate_safe_url("http://127.0.0.1:8080")
|
||||
ValueError: Localhost URLs are not allowed
|
||||
|
||||
>>> validate_safe_url("http://192.168.1.1")
|
||||
ValueError: URL resolves to private IP: 192.168.1.1
|
||||
|
||||
>>> validate_safe_url("http://169.254.169.254/latest/meta-data/")
|
||||
ValueError: URL resolves to cloud metadata IP: 169.254.169.254
|
||||
|
||||
>>> validate_safe_url("http://localhost:8080", allow_private=True)
|
||||
'http://localhost:8080'
|
||||
"""
|
||||
url_str = str(url)
|
||||
parsed = urlparse(url_str)
|
||||
hostname = parsed.hostname or ""
|
||||
|
||||
# Test-environment bypass (preserved from original implementation)
|
||||
# Validate URL scheme
|
||||
if not allow_http and parsed.scheme != "https":
|
||||
msg = "Only HTTPS URLs are allowed"
|
||||
raise ValueError(msg)
|
||||
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
msg = f"Only HTTP/HTTPS URLs are allowed, got scheme: {parsed.scheme}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Extract hostname
|
||||
hostname = parsed.hostname
|
||||
if not hostname:
|
||||
msg = "URL must have a valid hostname"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Special handling for test environments - allow test server hostnames
|
||||
# testserver is used by FastAPI/Starlette test clients and doesn't resolve via DNS
|
||||
# Only enabled when LANGCHAIN_ENV=local_test (set in conftest.py)
|
||||
if (
|
||||
os.environ.get("LANGCHAIN_ENV") == "local_test"
|
||||
and hostname.startswith("test")
|
||||
@@ -73,34 +238,52 @@ def validate_safe_url(
|
||||
):
|
||||
return url_str
|
||||
|
||||
policy = _policy_for(allow_private=allow_private, allow_http=allow_http)
|
||||
# ALWAYS block cloud metadata endpoints (even with allow_private=True)
|
||||
if is_cloud_metadata(hostname):
|
||||
msg = f"Cloud metadata endpoints are not allowed: {hostname}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Synchronous scheme + hostname checks
|
||||
try:
|
||||
_validate_url_sync(url_str, policy)
|
||||
except SSRFBlockedError as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
# Check for localhost
|
||||
if is_localhost(hostname) and not allow_private:
|
||||
msg = f"Localhost URLs are not allowed: {hostname}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# DNS resolution and IP validation
|
||||
# Resolve hostname to IP addresses and validate each one.
|
||||
# Note: DNS resolution results are cached by the OS, so repeated calls are fast.
|
||||
try:
|
||||
# Get all IP addresses for this hostname
|
||||
addr_info = socket.getaddrinfo(
|
||||
hostname,
|
||||
parsed.port or (443 if parsed.scheme == "https" else 80),
|
||||
socket.AF_UNSPEC,
|
||||
socket.AF_UNSPEC, # Allow both IPv4 and IPv6
|
||||
socket.SOCK_STREAM,
|
||||
)
|
||||
|
||||
for result in addr_info:
|
||||
ip_str: str = result[4][0] # type: ignore[assignment]
|
||||
try:
|
||||
_validate_resolved_ip(ip_str, policy)
|
||||
except SSRFBlockedError as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
normalized_ip = _normalize_ip(ip_str)
|
||||
|
||||
# ALWAYS block cloud metadata IPs
|
||||
if is_cloud_metadata(hostname, normalized_ip):
|
||||
msg = f"URL resolves to cloud metadata IP: {normalized_ip}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Check for localhost IPs
|
||||
if is_localhost(hostname, normalized_ip) and not allow_private:
|
||||
msg = f"URL resolves to localhost IP: {normalized_ip}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Check for private IPs
|
||||
if not allow_private and is_private_ip(normalized_ip):
|
||||
msg = f"URL resolves to private IP address: {normalized_ip}"
|
||||
raise ValueError(msg)
|
||||
|
||||
except socket.gaierror as e:
|
||||
# DNS resolution failed - fail closed for security
|
||||
msg = f"Failed to resolve hostname '{hostname}': {e}"
|
||||
raise ValueError(msg) from e
|
||||
except OSError as e:
|
||||
# Other network errors - fail closed
|
||||
msg = f"Network error while validating URL: {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
@@ -113,7 +296,26 @@ def is_safe_url(
|
||||
allow_private: bool = False,
|
||||
allow_http: bool = True,
|
||||
) -> bool:
|
||||
"""Non-throwing version of `validate_safe_url`."""
|
||||
"""Check if a URL is safe (non-throwing version of validate_safe_url).
|
||||
|
||||
Args:
|
||||
url: The URL to check
|
||||
allow_private: If True, allows private IPs and localhost
|
||||
allow_http: If True, allows both HTTP and HTTPS
|
||||
|
||||
Returns:
|
||||
True if URL is safe, False otherwise
|
||||
|
||||
Examples:
|
||||
>>> is_safe_url("https://example.com")
|
||||
True
|
||||
|
||||
>>> is_safe_url("http://127.0.0.1:8080")
|
||||
False
|
||||
|
||||
>>> is_safe_url("http://localhost:8080", allow_private=True)
|
||||
True
|
||||
"""
|
||||
try:
|
||||
validate_safe_url(url, allow_private=allow_private, allow_http=allow_http)
|
||||
except ValueError:
|
||||
@@ -130,6 +332,7 @@ def _validate_url_ssrf_strict(v: Any) -> Any:
|
||||
|
||||
|
||||
def _validate_url_ssrf_https_only(v: Any) -> Any:
|
||||
"""Validate URL for SSRF protection (HTTPS only, strict mode)."""
|
||||
if isinstance(v, str):
|
||||
validate_safe_url(v, allow_private=False, allow_http=False)
|
||||
return v
|
||||
@@ -144,12 +347,52 @@ def _validate_url_ssrf_relaxed(v: Any) -> Any:
|
||||
|
||||
# Annotated types with SSRF protection
|
||||
SSRFProtectedUrl = Annotated[HttpUrl, BeforeValidator(_validate_url_ssrf_strict)]
|
||||
"""A Pydantic HttpUrl type with built-in SSRF protection.
|
||||
|
||||
This blocks private IPs, localhost, and cloud metadata endpoints.
|
||||
|
||||
Example:
|
||||
class WebhookSchema(BaseModel):
|
||||
url: SSRFProtectedUrl # Automatically validated for SSRF
|
||||
headers: dict[str, str] | None = None
|
||||
"""
|
||||
|
||||
SSRFProtectedUrlRelaxed = Annotated[
|
||||
HttpUrl, BeforeValidator(_validate_url_ssrf_relaxed)
|
||||
]
|
||||
"""A Pydantic HttpUrl with relaxed SSRF protection (allows private IPs).
|
||||
|
||||
Use this for development/testing webhooks where localhost/private IPs are needed.
|
||||
Cloud metadata endpoints are still blocked.
|
||||
|
||||
Example:
|
||||
class DevWebhookSchema(BaseModel):
|
||||
url: SSRFProtectedUrlRelaxed # Allows localhost, blocks cloud metadata
|
||||
"""
|
||||
|
||||
SSRFProtectedHttpsUrl = Annotated[
|
||||
HttpUrl, BeforeValidator(_validate_url_ssrf_https_only)
|
||||
]
|
||||
"""A Pydantic HttpUrl with SSRF protection that only allows HTTPS.
|
||||
|
||||
This blocks private IPs, localhost, cloud metadata endpoints, and HTTP URLs.
|
||||
|
||||
Example:
|
||||
class SecureWebhookSchema(BaseModel):
|
||||
url: SSRFProtectedHttpsUrl # Only HTTPS, blocks private IPs
|
||||
"""
|
||||
|
||||
SSRFProtectedHttpsUrlStr = Annotated[
|
||||
str, BeforeValidator(_validate_url_ssrf_https_only)
|
||||
]
|
||||
"""A string type with SSRF protection that only allows HTTPS URLs.
|
||||
|
||||
Same as SSRFProtectedHttpsUrl but returns a string instead of HttpUrl.
|
||||
Useful for FastAPI query parameters where you need a string URL.
|
||||
|
||||
Example:
|
||||
@router.get("/proxy")
|
||||
async def proxy_get(url: SSRFProtectedHttpsUrlStr):
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url)
|
||||
"""
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
"""SSRF-safe httpx transport with DNS resolution and IP pinning."""
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
import httpx
|
||||
|
||||
from langchain_core._security._exceptions import SSRFBlockedError
|
||||
from langchain_core._security._policy import (
|
||||
SSRFPolicy,
|
||||
_effective_allowed_hosts,
|
||||
validate_resolved_ip,
|
||||
validate_url_sync,
|
||||
)
|
||||
|
||||
# Keys that AsyncHTTPTransport accepts (forwarded from factory kwargs).
|
||||
_TRANSPORT_KWARGS = frozenset(
|
||||
{
|
||||
"verify",
|
||||
"cert",
|
||||
"trust_env",
|
||||
"http1",
|
||||
"http2",
|
||||
"limits",
|
||||
"retries",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SSRFSafeTransport(httpx.AsyncBaseTransport):
|
||||
"""httpx async transport that validates DNS results against an SSRF policy.
|
||||
|
||||
For every outgoing request the transport:
|
||||
1. Checks the URL scheme against ``policy.allowed_schemes``.
|
||||
2. Validates the hostname against blocked patterns.
|
||||
3. Resolves DNS and validates **all** returned IPs.
|
||||
4. Rewrites the request to connect to the first valid IP while
|
||||
preserving the original ``Host`` header and TLS SNI hostname.
|
||||
|
||||
Redirects are re-validated on each hop because ``follow_redirects``
|
||||
is set on the *client*, causing ``handle_async_request`` to be called
|
||||
again for each redirect target.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
policy: SSRFPolicy = SSRFPolicy(),
|
||||
**transport_kwargs: object,
|
||||
) -> None:
|
||||
self._policy = policy
|
||||
self._inner = httpx.AsyncHTTPTransport(**transport_kwargs) # type: ignore[arg-type]
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Core request handler
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def handle_async_request(
|
||||
self,
|
||||
request: httpx.Request,
|
||||
) -> httpx.Response:
|
||||
hostname = request.url.host or ""
|
||||
scheme = request.url.scheme.lower()
|
||||
|
||||
# 1-3. Scheme, hostname, and pattern checks (reuse sync validator).
|
||||
try:
|
||||
validate_url_sync(str(request.url), self._policy)
|
||||
except SSRFBlockedError:
|
||||
raise
|
||||
|
||||
# Allowed-hosts bypass - skip DNS/IP validation entirely.
|
||||
allowed = {h.lower() for h in _effective_allowed_hosts(self._policy)}
|
||||
if hostname.lower() in allowed:
|
||||
return await self._inner.handle_async_request(request)
|
||||
|
||||
# 4. DNS resolution
|
||||
port = request.url.port or (443 if scheme == "https" else 80)
|
||||
try:
|
||||
addrinfo = await asyncio.to_thread(
|
||||
socket.getaddrinfo,
|
||||
hostname,
|
||||
port,
|
||||
type=socket.SOCK_STREAM,
|
||||
)
|
||||
except socket.gaierror as exc:
|
||||
raise SSRFBlockedError("DNS resolution failed") from exc
|
||||
|
||||
if not addrinfo:
|
||||
raise SSRFBlockedError("DNS resolution returned no results")
|
||||
|
||||
# 5. Validate ALL resolved IPs - any blocked means reject.
|
||||
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
||||
ip_str: str = sockaddr[0] # type: ignore[assignment]
|
||||
validate_resolved_ip(ip_str, self._policy)
|
||||
|
||||
# 6. Pin to first resolved IP.
|
||||
pinned_ip = addrinfo[0][4][0]
|
||||
|
||||
# 7. Rewrite URL to use pinned IP, preserving Host header and SNI.
|
||||
pinned_url = request.url.copy_with(host=pinned_ip)
|
||||
|
||||
# Build extensions dict, adding sni_hostname for HTTPS so TLS
|
||||
# certificate validation uses the original hostname.
|
||||
extensions = dict(request.extensions)
|
||||
if scheme == "https":
|
||||
extensions["sni_hostname"] = hostname.encode("ascii")
|
||||
|
||||
pinned_request = httpx.Request(
|
||||
method=request.method,
|
||||
url=pinned_url,
|
||||
headers=request.headers, # Host header already set to original
|
||||
content=request.content,
|
||||
extensions=extensions,
|
||||
)
|
||||
|
||||
return await self._inner.handle_async_request(pinned_request)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Lifecycle
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._inner.aclose()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Factory
|
||||
# ---------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class SSRFSafeSyncTransport(httpx.BaseTransport):
|
||||
"""httpx sync transport that validates DNS results against an SSRF policy.
|
||||
|
||||
Sync mirror of `SSRFSafeTransport`. See that class for full documentation.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
policy: SSRFPolicy = SSRFPolicy(),
|
||||
**transport_kwargs: object,
|
||||
) -> None:
|
||||
self._policy = policy
|
||||
self._inner = httpx.HTTPTransport(**transport_kwargs) # type: ignore[arg-type]
|
||||
|
||||
def handle_request(
|
||||
self,
|
||||
request: httpx.Request,
|
||||
) -> httpx.Response:
|
||||
hostname = request.url.host or ""
|
||||
scheme = request.url.scheme.lower()
|
||||
|
||||
validate_url_sync(str(request.url), self._policy)
|
||||
|
||||
allowed = {h.lower() for h in _effective_allowed_hosts(self._policy)}
|
||||
if hostname.lower() in allowed:
|
||||
return self._inner.handle_request(request)
|
||||
|
||||
port = request.url.port or (443 if scheme == "https" else 80)
|
||||
try:
|
||||
addrinfo = socket.getaddrinfo(
|
||||
hostname,
|
||||
port,
|
||||
type=socket.SOCK_STREAM,
|
||||
)
|
||||
except socket.gaierror as exc:
|
||||
raise SSRFBlockedError("DNS resolution failed") from exc
|
||||
|
||||
if not addrinfo:
|
||||
raise SSRFBlockedError("DNS resolution returned no results")
|
||||
|
||||
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
||||
ip_str: str = sockaddr[0] # type: ignore[assignment]
|
||||
validate_resolved_ip(ip_str, self._policy)
|
||||
|
||||
pinned_ip = addrinfo[0][4][0]
|
||||
pinned_url = request.url.copy_with(host=pinned_ip)
|
||||
|
||||
extensions = dict(request.extensions)
|
||||
if scheme == "https":
|
||||
extensions["sni_hostname"] = hostname.encode("ascii")
|
||||
|
||||
pinned_request = httpx.Request(
|
||||
method=request.method,
|
||||
url=pinned_url,
|
||||
headers=request.headers,
|
||||
content=request.content,
|
||||
extensions=extensions,
|
||||
)
|
||||
|
||||
return self._inner.handle_request(pinned_request)
|
||||
|
||||
def close(self) -> None:
|
||||
self._inner.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Factories
|
||||
# ---------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def ssrf_safe_client(
|
||||
policy: SSRFPolicy = SSRFPolicy(),
|
||||
**kwargs: object,
|
||||
) -> httpx.Client:
|
||||
"""Create an `httpx.Client` with SSRF protection."""
|
||||
transport_kwargs: dict[str, object] = {}
|
||||
client_kwargs: dict[str, object] = {}
|
||||
for key, value in kwargs.items():
|
||||
if key in _TRANSPORT_KWARGS:
|
||||
transport_kwargs[key] = value
|
||||
else:
|
||||
client_kwargs[key] = value
|
||||
|
||||
transport = SSRFSafeSyncTransport(policy=policy, **transport_kwargs)
|
||||
|
||||
client_kwargs.setdefault("follow_redirects", True)
|
||||
client_kwargs.setdefault("max_redirects", 10)
|
||||
|
||||
return httpx.Client(
|
||||
transport=transport,
|
||||
**client_kwargs, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def ssrf_safe_async_client(
|
||||
policy: SSRFPolicy = SSRFPolicy(),
|
||||
**kwargs: object,
|
||||
) -> httpx.AsyncClient:
|
||||
"""Create an ``httpx.AsyncClient`` with SSRF protection.
|
||||
|
||||
Drop-in replacement for ``httpx.AsyncClient(...)`` - callers just swap
|
||||
the constructor call. Transport-specific kwargs (``verify``, ``cert``,
|
||||
``retries``, etc.) are forwarded to the inner ``AsyncHTTPTransport``;
|
||||
everything else goes to the ``AsyncClient``.
|
||||
"""
|
||||
transport_kwargs: dict[str, object] = {}
|
||||
client_kwargs: dict[str, object] = {}
|
||||
for key, value in kwargs.items():
|
||||
if key in _TRANSPORT_KWARGS:
|
||||
transport_kwargs[key] = value
|
||||
else:
|
||||
client_kwargs[key] = value
|
||||
|
||||
transport = SSRFSafeTransport(policy=policy, **transport_kwargs)
|
||||
|
||||
# Apply defaults only if not overridden by caller.
|
||||
client_kwargs.setdefault("follow_redirects", True)
|
||||
client_kwargs.setdefault("max_redirects", 10)
|
||||
|
||||
return httpx.AsyncClient(
|
||||
transport=transport,
|
||||
**client_kwargs, # type: ignore[arg-type]
|
||||
)
|
||||
@@ -335,9 +335,10 @@ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
|
||||
|
||||
# Reasoning
|
||||
if reasoning := message.additional_kwargs.get("reasoning"):
|
||||
if "type" not in reasoning:
|
||||
reasoning = {**reasoning, "type": "reasoning"}
|
||||
buckets["reasoning"].append(reasoning)
|
||||
if isinstance(message, AIMessageChunk) and message.chunk_position != "last":
|
||||
buckets["reasoning"].append({**reasoning, "type": "reasoning"})
|
||||
else:
|
||||
buckets["reasoning"].append(reasoning)
|
||||
|
||||
# Refusal
|
||||
if refusal := message.additional_kwargs.get("refusal"):
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""langchain-core version information and utilities."""
|
||||
|
||||
VERSION = "1.2.30"
|
||||
VERSION = "1.2.28"
|
||||
|
||||
@@ -21,7 +21,7 @@ classifiers = [
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
|
||||
version = "1.2.30"
|
||||
version = "1.2.28"
|
||||
requires-python = ">=3.10.0,<4.0.0"
|
||||
dependencies = [
|
||||
"langsmith>=0.3.45,<1.0.0",
|
||||
@@ -135,10 +135,8 @@ ignore-var-parameters = true # ignore missing documentation for *args and **kwa
|
||||
"langchain_core/utils/mustache.py" = [ "PLW0603",]
|
||||
"langchain_core/sys_info.py" = [ "T201",]
|
||||
"tests/unit_tests/test_tools.py" = [ "ARG",]
|
||||
"tests/**" = [ "ARG", "D1", "PLR2004", "S", "SLF",]
|
||||
"tests/**" = [ "D1", "PLR2004", "S", "SLF",]
|
||||
"scripts/**" = [ "INP", "S", "T201",]
|
||||
"langchain_core/_security/_policy.py" = [ "EM101", "EM102", "TRY003", "B008", "TRY300",]
|
||||
"langchain_core/_security/_transport.py" = [ "EM101", "EM102", "TRY003", "TRY203", "B008",]
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = [ "tests/*",]
|
||||
|
||||
@@ -2843,7 +2843,7 @@ async def test_tool_error_event_includes_tool_call_id() -> None:
|
||||
"""Test that on_tool_error event includes tool_call_id when provided."""
|
||||
|
||||
@tool
|
||||
def failing_tool(x: int) -> str:
|
||||
def failing_tool(x: int) -> str: # noqa: ARG001
|
||||
"""A tool that always fails."""
|
||||
msg = "Tool execution failed"
|
||||
raise ValueError(msg)
|
||||
@@ -2883,7 +2883,7 @@ async def test_tool_error_event_tool_call_id_is_none_when_not_provided() -> None
|
||||
"""Test that on_tool_error event has tool_call_id=None when not provided."""
|
||||
|
||||
@tool
|
||||
def failing_tool_no_id(x: int) -> str:
|
||||
def failing_tool_no_id(x: int) -> str: # noqa: ARG001
|
||||
"""A tool that always fails."""
|
||||
msg = "Tool execution failed"
|
||||
raise ValueError(msg)
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
import socket
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from langchain_core._security import (
|
||||
SSRFBlockedError,
|
||||
SSRFPolicy,
|
||||
SSRFSafeSyncTransport,
|
||||
SSRFSafeTransport,
|
||||
ssrf_safe_async_client,
|
||||
ssrf_safe_client,
|
||||
validate_hostname,
|
||||
validate_resolved_ip,
|
||||
validate_url_sync,
|
||||
)
|
||||
|
||||
|
||||
def _fake_addrinfo(ip: str, port: int = 80) -> list[Any]:
|
||||
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, port))]
|
||||
|
||||
|
||||
def _fake_addrinfo_v6(ip: str, port: int = 80) -> list[Any]:
|
||||
return [(socket.AF_INET6, socket.SOCK_STREAM, 6, "", (ip, port, 0, 0))]
|
||||
|
||||
|
||||
def _ok_response(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, text="ok")
|
||||
|
||||
|
||||
def test_validate_resolved_ip_blocks_nat64_embedded_private_ip() -> None:
|
||||
policy = SSRFPolicy()
|
||||
|
||||
with pytest.raises(SSRFBlockedError, match="private IP range"):
|
||||
validate_resolved_ip("64:ff9b::c0a8:101", policy)
|
||||
|
||||
|
||||
def test_validate_resolved_ip_blocks_cgnat() -> None:
|
||||
policy = SSRFPolicy()
|
||||
|
||||
with pytest.raises(SSRFBlockedError, match="private IP range"):
|
||||
validate_resolved_ip("100.64.0.1", policy)
|
||||
|
||||
|
||||
def test_validate_hostname_blocks_kubernetes_internal_dns() -> None:
|
||||
policy = SSRFPolicy()
|
||||
|
||||
with pytest.raises(SSRFBlockedError, match="Kubernetes internal DNS"):
|
||||
validate_hostname("api.default.svc.cluster.local", policy)
|
||||
|
||||
|
||||
def test_validate_url_sync_allows_explicit_allowed_host() -> None:
|
||||
policy = SSRFPolicy(allowed_hosts=frozenset({"metadata.google.internal"}))
|
||||
|
||||
validate_url_sync("http://metadata.google.internal/path", policy)
|
||||
|
||||
|
||||
def test_validate_url_sync_blocks_metadata_without_allowlist() -> None:
|
||||
policy = SSRFPolicy()
|
||||
|
||||
with pytest.raises(SSRFBlockedError, match="cloud metadata endpoint"):
|
||||
validate_url_sync("http://metadata.google.internal/path", policy)
|
||||
|
||||
|
||||
class _RecordingAsyncTransport(httpx.AsyncBaseTransport):
|
||||
def __init__(self) -> None:
|
||||
self.requests: list[httpx.Request] = []
|
||||
|
||||
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
||||
self.requests.append(request)
|
||||
return httpx.Response(200, request=request, text="ok")
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ssrf_safe_transport_pins_ip_and_sets_sni() -> None:
|
||||
transport = SSRFSafeTransport()
|
||||
recorder = _RecordingAsyncTransport()
|
||||
transport._inner = recorder # type: ignore[assignment]
|
||||
|
||||
addrinfo = [
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
6,
|
||||
"",
|
||||
("93.184.216.34", 443),
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"langchain_core._security._transport.socket.getaddrinfo",
|
||||
return_value=addrinfo,
|
||||
):
|
||||
request = httpx.Request("GET", "https://example.com/resource")
|
||||
response = await transport.handle_async_request(request)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(recorder.requests) == 1
|
||||
pinned_request = recorder.requests[0]
|
||||
assert pinned_request.url.host == "93.184.216.34"
|
||||
assert pinned_request.headers["host"] == "example.com"
|
||||
assert pinned_request.extensions["sni_hostname"] == b"example.com"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ssrf_safe_transport_blocks_private_resolution() -> None:
|
||||
transport = SSRFSafeTransport()
|
||||
|
||||
addrinfo = [
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
6,
|
||||
"",
|
||||
("127.0.0.1", 443),
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"langchain_core._security._transport.socket.getaddrinfo",
|
||||
return_value=addrinfo,
|
||||
):
|
||||
request = httpx.Request("GET", "https://example.com/resource")
|
||||
with pytest.raises(SSRFBlockedError, match="private IP range"):
|
||||
await transport.handle_async_request(request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ssrf_safe_async_client_sets_redirect_defaults() -> None:
|
||||
client = ssrf_safe_async_client()
|
||||
try:
|
||||
assert client.follow_redirects is True
|
||||
assert client.max_redirects == 10
|
||||
finally:
|
||||
await client.aclose()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Policy toggle: block_private_ips=False still blocks loopback/metadata/k8s
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
"http://10.0.0.1:8080/api",
|
||||
"http://172.16.0.1:3000/",
|
||||
"http://192.168.1.100/webhook",
|
||||
],
|
||||
)
|
||||
def test_private_ip_allowed_when_block_disabled(url: str) -> None:
|
||||
policy = SSRFPolicy(block_private_ips=False)
|
||||
validate_url_sync(url, policy)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
"http://127.0.0.1:8080/",
|
||||
"http://127.0.0.2/",
|
||||
"http://[::1]:8080/",
|
||||
],
|
||||
)
|
||||
def test_loopback_still_blocked_when_private_ips_allowed(url: str) -> None:
|
||||
policy = SSRFPolicy(block_private_ips=False)
|
||||
with pytest.raises(SSRFBlockedError):
|
||||
validate_url_sync(url, policy)
|
||||
|
||||
|
||||
def test_docker_internal_blocked() -> None:
|
||||
policy = SSRFPolicy()
|
||||
with pytest.raises(SSRFBlockedError, match="localhost"):
|
||||
validate_url_sync("http://host.docker.internal:8080/", policy)
|
||||
|
||||
|
||||
def test_metadata_still_blocked_when_private_ips_allowed() -> None:
|
||||
policy = SSRFPolicy(block_private_ips=False)
|
||||
with pytest.raises(SSRFBlockedError):
|
||||
validate_url_sync("http://metadata.google.internal/", policy)
|
||||
|
||||
|
||||
def test_k8s_still_blocked_when_private_ips_allowed() -> None:
|
||||
policy = SSRFPolicy(block_private_ips=False)
|
||||
with pytest.raises(SSRFBlockedError):
|
||||
validate_url_sync("http://myservice.default.svc.cluster.local/", policy)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transport: redirect to private IP blocked
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_to_private_ip_blocked(monkeypatch: Any) -> None:
|
||||
call_count = 0
|
||||
|
||||
def _routing_addrinfo(*args: Any, **kwargs: Any) -> list[Any]:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
return _fake_addrinfo("93.184.216.34")
|
||||
return _fake_addrinfo("127.0.0.1")
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", _routing_addrinfo)
|
||||
|
||||
def _redirect_responder(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
302,
|
||||
headers={"Location": "http://evil.com/pwned"},
|
||||
)
|
||||
|
||||
transport = SSRFSafeTransport()
|
||||
transport._inner = httpx.MockTransport(_redirect_responder) # type: ignore[assignment]
|
||||
|
||||
client = httpx.AsyncClient(
|
||||
transport=transport,
|
||||
follow_redirects=True,
|
||||
max_redirects=5,
|
||||
)
|
||||
|
||||
with pytest.raises(SSRFBlockedError):
|
||||
await client.get("http://safe.com/start")
|
||||
|
||||
await client.aclose()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transport: IPv6-mapped IPv4, scheme rejection, DNS fail-closed
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ipv6_mapped_ipv4_blocked(monkeypatch: Any) -> None:
|
||||
monkeypatch.setattr(
|
||||
socket,
|
||||
"getaddrinfo",
|
||||
lambda *a, **kw: _fake_addrinfo_v6("::ffff:127.0.0.1"),
|
||||
)
|
||||
|
||||
transport = SSRFSafeTransport()
|
||||
request = httpx.Request("GET", "http://evil.com/")
|
||||
with pytest.raises(SSRFBlockedError):
|
||||
await transport.handle_async_request(request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scheme_blocked() -> None:
|
||||
transport = SSRFSafeTransport()
|
||||
request = httpx.Request("GET", "ftp://evil.com/file")
|
||||
with pytest.raises(SSRFBlockedError, match="scheme"):
|
||||
await transport.handle_async_request(request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unresolvable_host_blocked(monkeypatch: Any) -> None:
|
||||
monkeypatch.setattr(
|
||||
socket,
|
||||
"getaddrinfo",
|
||||
lambda *a, **kw: (_ for _ in ()).throw(
|
||||
socket.gaierror("Name or service not known")
|
||||
),
|
||||
)
|
||||
|
||||
transport = SSRFSafeTransport()
|
||||
request = httpx.Request("GET", "http://nonexistent.invalid/")
|
||||
with pytest.raises(SSRFBlockedError, match="DNS resolution failed"):
|
||||
await transport.handle_async_request(request)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transport: allowed_hosts bypass and local env behavior
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_allowed_host_bypass() -> None:
|
||||
policy = SSRFPolicy(allowed_hosts=frozenset({"special.host"}))
|
||||
transport = SSRFSafeTransport(policy=policy)
|
||||
transport._inner = httpx.MockTransport(_ok_response) # type: ignore[assignment]
|
||||
|
||||
request = httpx.Request("GET", "http://special.host/api")
|
||||
response = await transport.handle_async_request(request)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("env", ["local_dev", "local_test", "local_docker"])
|
||||
async def test_localhost_allowed_in_local_env(monkeypatch: Any, env: str) -> None:
|
||||
monkeypatch.setenv("LANGCHAIN_ENV", env)
|
||||
transport = SSRFSafeTransport()
|
||||
transport._inner = httpx.MockTransport(_ok_response) # type: ignore[assignment]
|
||||
|
||||
request = httpx.Request("GET", "http://localhost:8084/mcp")
|
||||
response = await transport.handle_async_request(request)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_localhost_blocked_in_production(monkeypatch: Any) -> None:
|
||||
monkeypatch.setenv("LANGCHAIN_ENV", "production")
|
||||
transport = SSRFSafeTransport()
|
||||
|
||||
request = httpx.Request("GET", "http://localhost:8084/mcp")
|
||||
with pytest.raises(SSRFBlockedError):
|
||||
await transport.handle_async_request(request)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sync transport tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_sync_transport_pins_ip_and_sets_sni() -> None:
|
||||
transport = SSRFSafeSyncTransport()
|
||||
transport._inner = httpx.MockTransport(_ok_response) # type: ignore[assignment]
|
||||
|
||||
addrinfo = [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 443))]
|
||||
|
||||
with patch(
|
||||
"langchain_core._security._transport.socket.getaddrinfo",
|
||||
return_value=addrinfo,
|
||||
):
|
||||
request = httpx.Request("GET", "https://example.com/resource")
|
||||
response = transport.handle_request(request)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_sync_transport_blocks_private_resolution() -> None:
|
||||
transport = SSRFSafeSyncTransport()
|
||||
|
||||
addrinfo = [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", 443))]
|
||||
|
||||
with patch(
|
||||
"langchain_core._security._transport.socket.getaddrinfo",
|
||||
return_value=addrinfo,
|
||||
):
|
||||
request = httpx.Request("GET", "https://example.com/resource")
|
||||
with pytest.raises(SSRFBlockedError, match="private IP range"):
|
||||
transport.handle_request(request)
|
||||
|
||||
|
||||
def test_sync_transport_redirect_to_private_blocked(monkeypatch: Any) -> None:
|
||||
call_count = 0
|
||||
|
||||
def _routing_addrinfo(*args: Any, **kwargs: Any) -> list[Any]:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
return _fake_addrinfo("93.184.216.34")
|
||||
return _fake_addrinfo("127.0.0.1")
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", _routing_addrinfo)
|
||||
|
||||
def _redirect_responder(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
302,
|
||||
headers={"Location": "http://evil.com/pwned"},
|
||||
)
|
||||
|
||||
transport = SSRFSafeSyncTransport()
|
||||
transport._inner = httpx.MockTransport(_redirect_responder) # type: ignore[assignment]
|
||||
|
||||
client = httpx.Client(
|
||||
transport=transport,
|
||||
follow_redirects=True,
|
||||
max_redirects=5,
|
||||
)
|
||||
|
||||
with pytest.raises(SSRFBlockedError):
|
||||
client.get("http://safe.com/start")
|
||||
|
||||
client.close()
|
||||
|
||||
|
||||
def test_ssrf_safe_client_sets_redirect_defaults() -> None:
|
||||
client = ssrf_safe_client()
|
||||
try:
|
||||
assert client.follow_redirects is True
|
||||
assert client.max_redirects == 10
|
||||
finally:
|
||||
client.close()
|
||||
@@ -8,11 +8,89 @@ from pydantic import BaseModel, ValidationError
|
||||
from langchain_core._security._ssrf_protection import (
|
||||
SSRFProtectedUrl,
|
||||
SSRFProtectedUrlRelaxed,
|
||||
is_cloud_metadata,
|
||||
is_localhost,
|
||||
is_private_ip,
|
||||
is_safe_url,
|
||||
validate_safe_url,
|
||||
)
|
||||
|
||||
|
||||
class TestIPValidation:
|
||||
"""Tests for IP address validation functions."""
|
||||
|
||||
def test_is_private_ip_ipv4(self) -> None:
|
||||
"""Test private IPv4 address detection."""
|
||||
assert is_private_ip("10.0.0.1") is True
|
||||
assert is_private_ip("172.16.0.1") is True
|
||||
assert is_private_ip("192.168.1.1") is True
|
||||
assert is_private_ip("127.0.0.1") is True
|
||||
assert is_private_ip("169.254.169.254") is True
|
||||
assert is_private_ip("0.0.0.1") is True
|
||||
|
||||
def test_is_private_ip_ipv6(self) -> None:
|
||||
"""Test private IPv6 address detection."""
|
||||
assert is_private_ip("::1") is True # Loopback
|
||||
assert is_private_ip("fc00::1") is True # Unique local
|
||||
assert is_private_ip("fe80::1") is True # Link-local
|
||||
assert is_private_ip("ff00::1") is True # Multicast
|
||||
|
||||
def test_is_private_ip_public(self) -> None:
|
||||
"""Test that public IPs are not flagged as private."""
|
||||
assert is_private_ip("8.8.8.8") is False
|
||||
assert is_private_ip("1.1.1.1") is False
|
||||
assert is_private_ip("151.101.1.140") is False
|
||||
|
||||
def test_is_private_ip_invalid(self) -> None:
|
||||
"""Test handling of invalid IP addresses."""
|
||||
assert is_private_ip("not-an-ip") is False
|
||||
assert is_private_ip("999.999.999.999") is False
|
||||
|
||||
def test_is_cloud_metadata_ips(self) -> None:
|
||||
"""Test cloud metadata IP detection."""
|
||||
assert is_cloud_metadata("example.com", "169.254.169.254") is True
|
||||
assert is_cloud_metadata("example.com", "169.254.170.2") is True
|
||||
assert is_cloud_metadata("example.com", "169.254.170.23") is True
|
||||
assert is_cloud_metadata("example.com", "100.100.100.200") is True
|
||||
assert is_cloud_metadata("example.com", "fd00:ec2::254") is True
|
||||
assert is_cloud_metadata("example.com", "fd00:ec2::23") is True
|
||||
assert is_cloud_metadata("example.com", "fe80::a9fe:a9fe") is True
|
||||
|
||||
def test_is_cloud_metadata_link_local_range(self) -> None:
|
||||
"""Test that IPv4 link-local is flagged as cloud metadata."""
|
||||
assert is_cloud_metadata("example.com", "169.254.1.2") is True
|
||||
assert is_cloud_metadata("example.com", "169.254.255.254") is True
|
||||
|
||||
def test_is_cloud_metadata_hostnames(self) -> None:
|
||||
"""Test cloud metadata hostname detection."""
|
||||
assert is_cloud_metadata("metadata.google.internal") is True
|
||||
assert is_cloud_metadata("metadata") is True
|
||||
assert is_cloud_metadata("instance-data") is True
|
||||
assert is_cloud_metadata("METADATA.GOOGLE.INTERNAL") is True # Case insensitive
|
||||
|
||||
def test_is_cloud_metadata_safe(self) -> None:
|
||||
"""Test that normal URLs are not flagged as cloud metadata."""
|
||||
assert is_cloud_metadata("example.com", "8.8.8.8") is False
|
||||
assert is_cloud_metadata("google.com") is False
|
||||
|
||||
def test_is_localhost_hostnames(self) -> None:
|
||||
"""Test localhost hostname detection."""
|
||||
assert is_localhost("localhost") is True
|
||||
assert is_localhost("LOCALHOST") is True
|
||||
assert is_localhost("localhost.localdomain") is True
|
||||
|
||||
def test_is_localhost_ips(self) -> None:
|
||||
"""Test localhost IP detection."""
|
||||
assert is_localhost("example.com", "127.0.0.1") is True
|
||||
assert is_localhost("example.com", "::1") is True
|
||||
assert is_localhost("example.com", "0.0.0.0") is True
|
||||
|
||||
def test_is_localhost_safe(self) -> None:
|
||||
"""Test that normal hosts are not flagged as localhost."""
|
||||
assert is_localhost("example.com", "8.8.8.8") is False
|
||||
assert is_localhost("google.com") is False
|
||||
|
||||
|
||||
class TestValidateSafeUrl:
|
||||
"""Tests for validate_safe_url function."""
|
||||
|
||||
@@ -30,10 +108,10 @@ class TestValidateSafeUrl:
|
||||
|
||||
def test_localhost_blocked_by_default(self) -> None:
|
||||
"""Test that localhost URLs are blocked by default."""
|
||||
with pytest.raises(ValueError, match="localhost"):
|
||||
with pytest.raises(ValueError, match="Localhost"):
|
||||
validate_safe_url("http://localhost:8080/webhook")
|
||||
|
||||
with pytest.raises(ValueError, match="private IP"):
|
||||
with pytest.raises(ValueError, match="localhost"):
|
||||
validate_safe_url("http://127.0.0.1:8080/webhook")
|
||||
|
||||
def test_localhost_allowed_with_flag(self) -> None:
|
||||
@@ -64,11 +142,11 @@ class TestValidateSafeUrl:
|
||||
|
||||
def test_cloud_metadata_always_blocked(self) -> None:
|
||||
"""Test that cloud metadata endpoints are always blocked."""
|
||||
with pytest.raises(ValueError, match="SSRF blocked"):
|
||||
with pytest.raises(ValueError, match="metadata"):
|
||||
validate_safe_url("http://169.254.169.254/latest/meta-data/")
|
||||
|
||||
# Even with allow_private=True
|
||||
with pytest.raises(ValueError, match="SSRF blocked"):
|
||||
with pytest.raises(ValueError, match="metadata"):
|
||||
validate_safe_url(
|
||||
"http://169.254.169.254/latest/meta-data/",
|
||||
allow_private=True,
|
||||
@@ -76,12 +154,12 @@ class TestValidateSafeUrl:
|
||||
|
||||
def test_ipv6_mapped_ipv4_localhost_blocked(self) -> None:
|
||||
"""Test that IPv6-mapped IPv4 localhost is blocked."""
|
||||
with pytest.raises(ValueError, match="SSRF blocked"):
|
||||
with pytest.raises(ValueError, match="localhost"):
|
||||
validate_safe_url("http://[::ffff:127.0.0.1]:8080/webhook")
|
||||
|
||||
def test_ipv6_mapped_ipv4_cloud_metadata_blocked(self) -> None:
|
||||
"""Test that IPv6-mapped IPv4 cloud metadata is blocked."""
|
||||
with pytest.raises(ValueError, match="SSRF blocked"):
|
||||
with pytest.raises(ValueError, match="metadata"):
|
||||
validate_safe_url("http://[::ffff:169.254.169.254]/latest/meta-data/")
|
||||
|
||||
def test_invalid_scheme_blocked(self) -> None:
|
||||
@@ -97,7 +175,7 @@ class TestValidateSafeUrl:
|
||||
|
||||
def test_https_only_mode(self) -> None:
|
||||
"""Test that HTTP is blocked when allow_http=False."""
|
||||
with pytest.raises(ValueError, match="scheme"):
|
||||
with pytest.raises(ValueError, match="HTTPS"):
|
||||
validate_safe_url("http://example.com/webhook", allow_http=False)
|
||||
|
||||
# HTTPS should still work
|
||||
|
||||
4
libs/core/uv.lock
generated
4
libs/core/uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.10.0, <4.0.0"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14' and platform_python_implementation == 'PyPy'",
|
||||
@@ -995,7 +995,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.2.30"
|
||||
version = "1.2.28"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
|
||||
104
libs/langchain/uv.lock
generated
104
libs/langchain/uv.lock
generated
@@ -873,62 +873,62 @@ toml = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.6"
|
||||
version = "46.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2601,7 +2601,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.2.24"
|
||||
version = "1.2.28"
|
||||
source = { editable = "../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -2847,7 +2847,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-tests"
|
||||
version = "1.1.5"
|
||||
version = "1.1.6"
|
||||
source = { editable = "../standard-tests" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
104
libs/langchain_v1/uv.lock
generated
104
libs/langchain_v1/uv.lock
generated
@@ -897,62 +897,62 @@ toml = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.6"
|
||||
version = "46.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2209,7 +2209,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.2.26"
|
||||
version = "1.2.28"
|
||||
source = { editable = "../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -2455,7 +2455,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-tests"
|
||||
version = "1.1.5"
|
||||
version = "1.1.6"
|
||||
source = { editable = "../standard-tests" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
@@ -21,7 +21,7 @@ classifiers = [
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
|
||||
version = "1.1.6"
|
||||
version = "1.1.7"
|
||||
requires-python = ">=3.10.0,<4.0.0"
|
||||
dependencies = [
|
||||
"langchain-core>=1.2.27,<2.0.0",
|
||||
|
||||
Reference in New Issue
Block a user