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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies = [
"lfx-ibm>=0.3.0,<1.0.0",
"lfx-docling>=0.2.1,<1.0.0",
"lfx-datastax>=0.2.0,<1.0.0",
"lfx-toolguard>=0.2.0,<1.0.0",
"lfx-toolguard>=0.2.2,<1.0.0",
"lfx-openai>=0.2.0,<1.0.0",
"lfx-anthropic>=0.2.0,<1.0.0",
"lfx-azure>=0.2.0,<1.0.0",
Expand Down Expand Up @@ -183,7 +183,7 @@ Documentation = "https://docs.langflow.org"

[project.optional-dependencies]
bundles = [
"lfx-bundles[all-no-torch]>=1.2.3,<2.0",
"lfx-bundles[all-no-torch]>=1.2.4,<2.0",
"lfx-arxiv>=0.2.0,<1.0.0",
"lfx-confluent>=0.2.0,<1.0.0",
"lfx-duckduckgo>=0.2.0,<1.0.0",
Expand Down Expand Up @@ -261,7 +261,7 @@ constraint-dependencies = [
"langgraph-sdk>=0.4.6", # GHSA-fvww-7h3r-vfhp / CVE-2026-104873, GHSA-3fx2-cqw9-xr3c
# GHSA-7hp6-4w63-5g45 is fixed in 1.103.1 and 1.104.0rc2.
# Keep Opik's upper bound while enforcing the patched LiteLLM floor.
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0",
"multidict>=6.9.1", # GHSA-54p9-h82j-f925 / CVE-2026-104874
# Security floors for every workspace dependency path, including optional bundles.
"oauthlib>=4.0.0,<5.0.0", # CVE-2026-49264, CVE-2026-49265
Expand Down
8 changes: 4 additions & 4 deletions scripts/ci/bundle_profile_locks/enterprise-hardened.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@
"providers": [
"lfx-toolguard"
],
"requested": ">=0.2.0,<1.0.0",
"requirement": "lfx-toolguard==0.2.0",
"resolved_version": "0.2.0"
"requested": ">=0.2.2,<1.0.0",
"requirement": "lfx-toolguard==0.2.2",
"resolved_version": "0.2.2"
},
{
"distribution": "lfx-vllm",
Expand All @@ -180,7 +180,7 @@
"https://pypi.org/simple"
],
"profile": "enterprise-hardened",
"profile_digest": "sha256:f668394b03a1ec2e",
"profile_digest": "sha256:a5568d72b77fce01",
"providers": [
"lfx-amazon",
"lfx-anthropic",
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/bundle_profiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
{
"distribution": "lfx-toolguard",
"extras": [],
"version": ">=0.2.0,<1.0.0",
"version": ">=0.2.2,<1.0.0",
"providers": [
"lfx-toolguard"
]
Expand Down
129 changes: 129 additions & 0 deletions scripts/ci/test_litellm_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Block compromised LiteLLM releases through normal package requirements."""

from __future__ import annotations

import json
import os
import subprocess
import sys
import zipfile
from pathlib import Path

import pytest
import tomllib
from packaging.requirements import Requirement

ROOT = Path(__file__).resolve().parents[2]
MANIFESTS = [
"src/backend/base/pyproject.toml",
"src/bundles/toolguard/pyproject.toml",
"src/bundles/lfx-bundles/pyproject.toml",
]


@pytest.fixture(params=MANIFESTS)
def litellm_guard(request: pytest.FixtureRequest) -> Requirement:
project = tomllib.loads((ROOT / request.param).read_text())["project"]
dependencies = (
project["optional-dependencies"]["cuga"] if project["name"] == "lfx-bundles" else project["dependencies"]
)
guards = [Requirement(value) for value in dependencies if Requirement(value).name == "litellm"]
assert guards, "Standalone installs need a published requirement, not just a workspace constraint"
for guard in guards:
assert "1.107.0" not in guard.specifier
assert "1.103.0" not in guard.specifier
assert "1.103.2" in guard.specifier
assert "1.106.2" in guard.specifier, "Do not freeze installs to the current lockfile version"
if project["name"] != "lfx-bundles":
assert guard.marker is None
# Exercise CUGA's version restriction independently of its platform markers.
guard = guards[0]
guard.marker = None
return guard


def test_workspace_excludes_compromised_release_without_overriding_upstream_caps() -> None:
settings = tomllib.loads((ROOT / "pyproject.toml").read_text())["tool"]["uv"]
assert not any(Requirement(value).name == "litellm" for value in settings["override-dependencies"])
guard = next(
Requirement(value) for value in settings["constraint-dependencies"] if Requirement(value).name == "litellm"
)
assert "1.107.0" not in guard.specifier
lock = tomllib.loads((ROOT / "uv.lock").read_text())
versions = {package["version"] for package in lock["package"] if package["name"] == "litellm"}
assert versions
assert all(version in guard.specifier for version in versions)


@pytest.fixture
def offline_resolver(tmp_path: Path):
"""Resolve harmless metadata-only wheels with pip; never download or execute the malicious artifact."""

def resolve(guard: Requirement | None, *, force_compromised: bool = False) -> subprocess.CompletedProcess[str]:
dependencies = ["upstream"] + ([str(guard)] if guard else [])
for name, version, requirements in (
("litellm", "1.103.2", []),
("litellm", "1.106.2", []),
("litellm", "1.107.0", []),
("upstream", "1.0.0", ["litellm>=1.103.1"]),
("consumer", "1.0.0", dependencies),
):
dist_info = f"{name}-{version}.dist-info"
metadata = f"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n"
metadata += "".join(f"Requires-Dist: {requirement}\n" for requirement in requirements)
with zipfile.ZipFile(tmp_path / f"{name}-{version}-py3-none-any.whl", "w") as wheel:
wheel.writestr(f"{dist_info}/METADATA", metadata)
wheel.writestr(f"{dist_info}/WHEEL", "Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n")
wheel.writestr(f"{dist_info}/RECORD", "")
env = {key: value for key, value in os.environ.items() if not key.startswith(("UV_", "PIP_"))}
env["PIP_CONFIG_FILE"] = os.devnull
return subprocess.run( # noqa: S603
[
sys.executable,
"-m",
"pip",
"install",
"--dry-run",
"--ignore-installed",
"--no-index",
"--no-cache-dir",
"--disable-pip-version-check",
"--find-links",
str(tmp_path),
"--report",
str(tmp_path / "resolved.json"),
"consumer",
*(["litellm==1.107.0"] if force_compromised else []),
],
cwd=tmp_path,
env=env,
check=False,
capture_output=True,
text=True,
timeout=30,
)

return resolve


def test_published_requirement_protects_transitive_install_without_lockfile(
litellm_guard: Requirement, offline_resolver, tmp_path: Path
) -> None:
unprotected = offline_resolver(None)
assert unprotected.returncode == 0, unprotected.stdout + unprotected.stderr
before = json.loads((tmp_path / "resolved.json").read_text())
assert any(item["metadata"]["version"] == "1.107.0" for item in before["install"])

protected = offline_resolver(litellm_guard)
assert protected.returncode == 0, protected.stdout + protected.stderr
after = json.loads((tmp_path / "resolved.json").read_text())
versions = {item["metadata"]["name"]: item["metadata"]["version"] for item in after["install"]}
assert versions["litellm"] == "1.106.2"


def test_published_requirement_rejects_explicit_compromised_release(
litellm_guard: Requirement, offline_resolver
) -> None:
result = offline_resolver(litellm_guard, force_compromised=True)
assert result.returncode != 0
assert "ResolutionImpossible" in result.stderr
4 changes: 2 additions & 2 deletions scripts/migrate/consolidate_bundles.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@
# Temporary compatibility valve: newer releases require cryptography<49.
"langchain-litellm==0.5.1; sys_platform != 'darwin' and python_version < '3.14'",
"langchain-litellm==0.5.1; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", # noqa: E501
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1; sys_platform != 'darwin' and python_version < '3.14'",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", # noqa: E501
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0; sys_platform != 'darwin' and python_version < '3.14'",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", # noqa: E501
],
# --- tranche 9: langwatch evaluator (pure httpx REST; the langwatch SDK extra
# is for the tracing service, not this component) ---
Expand Down
9 changes: 6 additions & 3 deletions src/backend/base/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ dependencies = [
"arize-phoenix-otel>=0.6.1",
"openinference-instrumentation-langchain>=0.1.29",
"opik>=2.0.0,<3.0.0",
# Opik installs LiteLLM even without the optional litellm extra.
# Block the compromised 1.107.0 release in published wheel metadata.
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0",
# Newer Opik releases cap LiteLLM below its security floor on Python 3.10.
# Publish the compatible version constraint so fresh wheel installs stay valid.
"opik==2.2.7; python_version < '3.11'",
Expand Down Expand Up @@ -332,7 +335,7 @@ pgvector = [
"psycopg[binary]>=3.1,<4.0",
]
litellm = [
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0",
# Runtime deps from litellm[proxy] that langflow-ide hits when logging
# to langfuse via litellm.proxy.proxy_server (issue #12228). The full
# Pull in only the modules Langflow uses from litellm[proxy] rather than
Expand Down Expand Up @@ -416,8 +419,8 @@ cuga = [
# Temporary compatibility valve: newer langchain-litellm releases require cryptography<49.
"langchain-litellm==0.5.1; sys_platform != 'darwin' and python_version < '3.14'",
"langchain-litellm==0.5.1; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1; sys_platform != 'darwin' and python_version < '3.14'",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0; sys_platform != 'darwin' and python_version < '3.14'",
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'",
]
mlx = [
"mlx>=0.29.0; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version >= '3.12'",
Expand Down
4 changes: 2 additions & 2 deletions src/bundles/lfx-bundles/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lfx-bundles"
version = "1.2.3"
version = "1.2.4"
description = "Langflow's long-tail provider bundles as a single manifest-less metapackage (the langchain-community model)."
readme = "README.md"
requires-python = ">=3.10,<3.15"
Expand Down Expand Up @@ -46,7 +46,7 @@ cometapi = ["langchain-openai>=1.1.6", "requests>=2.32.0"]
composio = ["composio==0.16.0", "composio-langchain==0.16.0"]
confluence = ["atlassian-python-api==3.41.16", "langchain-community>=0.4.1,<1.0.0"]
couchbase = ["couchbase>=4.2.1", "dnspython>=2.6,<3", "langchain-community>=0.4.1,<1.0.0"]
cuga = ["cuga>=0.2.20,<0.3.0; sys_platform != 'darwin' and python_version < '3.14'", "cuga>=0.2.20,<0.3.0; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", "langchain-litellm==0.5.1; sys_platform != 'darwin' and python_version < '3.14'", "langchain-litellm==0.5.1; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", "litellm>=1.103.1,<2.0.0,!=1.104.0rc1; sys_platform != 'darwin' and python_version < '3.14'", "litellm>=1.103.1,<2.0.0,!=1.104.0rc1; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'"]
cuga = ["cuga>=0.2.20,<0.3.0; sys_platform != 'darwin' and python_version < '3.14'", "cuga>=0.2.20,<0.3.0; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", "langchain-litellm==0.5.1; sys_platform != 'darwin' and python_version < '3.14'", "langchain-litellm==0.5.1; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", "litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0; sys_platform != 'darwin' and python_version < '3.14'", "litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'"]
deepseek = ["langchain-openai>=1.1.6", "openai>=1.68.2,<3.0.0", "requests>=2.32.0"]
elastic = ["elasticsearch~=8.19", "langchain-elasticsearch~=1.0.0", "opensearch-py==2.8.0"]
faiss = ["faiss-cpu==1.9.0.post1; python_version < '3.14'", "faiss-cpu>=1.13.2; python_version >= '3.14'", "langchain-community>=0.4.1,<1.0.0"]
Expand Down
7 changes: 5 additions & 2 deletions src/bundles/toolguard/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lfx-toolguard"
version = "0.2.0"
version = "0.2.2"
description = "Langflow Policies component powered by ToolGuard."
readme = "README.md"
requires-python = ">=3.10,<3.15"
Expand All @@ -10,8 +10,11 @@ authors = [
]
keywords = ["langflow", "lfx", "extension", "policies", "toolguard"]
dependencies = [
"lfx>=1.13.0.dev0,<2.0.0",
# Keep this 1.13 backport from replacing the bundle published for 1.14.
"lfx>=1.13.0.dev0,<1.14.0.dev0",
"toolguard>=0.2.20,<1.0.0",
# ToolGuard installs LiteLLM; workspace constraints do not protect standalone installs.
"litellm>=1.103.1,<2.0.0,!=1.104.0rc1,!=1.107.0",
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion src/bundles/toolguard/src/lfx_toolguard/extension.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schemas.langflow.org/extension/v1.json",
"id": "lfx-toolguard",
"version": "0.2.0",
"version": "0.2.2",
"name": "ToolGuard",
"description": "Langflow Policies component powered by ToolGuard.",
"lfx": {
Expand Down
2 changes: 1 addition & 1 deletion src/lfx/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ sandbox = [
# Relocated out of langflow-base[complete], where they were the de-facto provider
# for these components after the bundle split.
cassandra = ["lfx-datastax>=0.1.3,<1.0.0"] # Compatibility alias for the Cassandra extension
toolguard = ["lfx-toolguard>=0.1.0,<1.0.0"] # Compatibility alias for the Policies extension
toolguard = ["lfx-toolguard>=0.2.2,<1.0.0"] # Compatibility alias for the Policies extension
# Application observability: OTLP export of traces, metrics and logs for ``lfx serve`` / ``lfx
# run``. lfx.observability guards every import, so a bare ``pip install lfx`` skips these and
# the whole path no-ops; installing this extra turns it on. The full ``langflow`` distribution
Expand Down
20 changes: 12 additions & 8 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading