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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dependencies = [
"microsoft-agents-activity",
"microsoft-agents-hosting-core",
"pydantic",
# Publish the security floor for the hosting SDK's transitive dependency.
"PyJWT",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ dependencies = [
"opentelemetry-sdk",
"opentelemetry-instrumentation",
"wrapt",
# Publish security floors for LangChain's transitive dependencies.
"langsmith",
"anyio",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ dependencies = [
"opentelemetry-api",
"opentelemetry-sdk",
"opentelemetry-instrumentation",
# Publish security floors for the framework's transitive dependencies.
"mcp",
"anyio",
"python-multipart",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ dependencies = [
"opentelemetry-api",
"opentelemetry-sdk",
"opentelemetry-instrumentation",
# Publish security floors for the framework's transitive dependencies.
"mcp",
"anyio",
"python-multipart",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ dependencies = [
"azure-identity",
"typing-extensions",
"httpx",
# Publish security floors for the framework's transitive dependencies.
"mcp",
"anyio",
"python-multipart",
"pyasn1",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ dependencies = [
"microsoft-agents-a365-tooling",
"microsoft-agents-hosting-core",
"google-adk",
# Publish security floors for the framework's transitive dependencies.
"mcp",
"anyio",
"python-multipart",
"pyasn1",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies = [
"microsoft-agents-a365-tooling",
"openai-agents",
"asyncio-throttle",
# Publish security floors for the framework's transitive dependencies.
"mcp",
"anyio",
"python-multipart",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies = [
"microsoft-agents-a365-tooling",
"semantic-kernel",
"aiohttp",
# Publish security floors for the framework's transitive dependencies.
"mcp",
"anyio",
"python-multipart",
]

[project.urls]
Expand Down
2 changes: 2 additions & 0 deletions libraries/microsoft-agents-a365-tooling/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ dependencies = [
"pydantic",
"typing-extensions",
"microsoft-agents-hosting-core",
# Publish the security floor for the hosting SDK's transitive dependency.
"PyJWT",
]

[project.urls]
Expand Down
11 changes: 9 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ override-dependencies = [
# Centralized version constraints for all external dependencies
# Individual package pyproject.toml files declare dependencies by name only (no version)
# uv applies these constraints during dependency resolution
# Security-critical transitive dependencies must also be declared by affected libraries
# so the build backend includes their minimum versions in published wheel metadata.
constraint-dependencies = [
# --- Observability (OpenTelemetry) ---
"opentelemetry-api >= 1.36.0",
Expand All @@ -89,7 +91,9 @@ constraint-dependencies = [
# --- AI Frameworks ---
"agent-framework >= 1.0.0",
"agent-framework-core >= 1.0.0",
"langchain-core >= 0.1.0",
"langchain-core >= 1.3.3",
"langsmith >= 0.8.18",
"mcp >= 1.28.1",
Comment on lines +95 to +96
"openai-agents >= 0.2.6",
"semantic-kernel >= 1.39.3",
# google-adk >=1.28.1,<2.0.0 - security remediation. Fixes CVE-2026-4810
Expand All @@ -111,10 +115,13 @@ constraint-dependencies = [

# --- Data Validation & Utilities ---
"aiohttp >= 3.8.0",
"anyio >= 4.14.2",
"asyncio-throttle >= 1.0.0",
"httpx >= 0.27.0",
"pyasn1 >= 0.6.4",
"pydantic >= 2.0.0",
"PyJWT >= 2.8.0",
"PyJWT >= 2.14.0",
"python-multipart >= 0.0.30",
"typing-extensions >= 4.0.0",

# --- Tox ---
Expand Down
167 changes: 167 additions & 0 deletions tests/test_published_security_constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Ensure published wheels enforce security floors without the workspace lockfile."""

import os
import shutil
import subprocess
import sys
import tomllib
from collections.abc import Iterator
from email.parser import BytesParser
from pathlib import Path
from zipfile import ZipFile

import pytest
from packaging.requirements import Requirement
from packaging.utils import canonicalize_name
from packaging.version import Version

REPO_ROOT = Path(__file__).resolve().parent.parent
PACKAGE_PREFIX = "microsoft-agents-a365-"
SECURITY_FLOORS = {
"anyio": "4.14.2",
"langchain-core": "1.3.3",
"langsmith": "0.8.18",
"mcp": "1.28.1",
"pyasn1": "0.6.4",
"pyjwt": "2.14.0",
"python-multipart": "0.0.30",
}
MCP_REQUIREMENTS = {"mcp", "anyio", "python-multipart", "pyjwt"}
EXPECTED_REQUIREMENTS = {
"notifications": {"pyjwt"},
"observability-core": {"pyjwt"},
"observability-extensions-agent-framework": {"pyjwt"},
"observability-extensions-langchain": {"langchain-core", "langsmith", "anyio", "pyjwt"},
"observability-extensions-openai": MCP_REQUIREMENTS,
"observability-extensions-semantic-kernel": MCP_REQUIREMENTS,
"observability-hosting": {"pyjwt"},
"runtime": {"pyjwt"},
"tooling": {"pyjwt"},
"tooling-extensions-agentframework": MCP_REQUIREMENTS | {"pyasn1"},
"tooling-extensions-azureaifoundry": {"pyjwt"},
"tooling-extensions-googleadk": MCP_REQUIREMENTS | {"pyasn1"},
"tooling-extensions-openai": MCP_REQUIREMENTS,
"tooling-extensions-semantickernel": MCP_REQUIREMENTS,
}


@pytest.fixture(scope="module")
def published_requirements(
tmp_path_factory: pytest.TempPathFactory,
) -> dict[str, list[Requirement]]:
"""Build real wheels offline in a copy so backend rewrites cannot affect the checkout."""
workspace = tmp_path_factory.mktemp("wheels")
shutil.copy2(REPO_ROOT / "pyproject.toml", workspace / "pyproject.toml")
shutil.copy2(REPO_ROOT / "LICENSE.md", workspace / "LICENSE.md")
shutil.copytree(
REPO_ROOT / "versioning" / "helper",
workspace / "versioning" / "helper",
ignore=shutil.ignore_patterns("__pycache__"),
)
# Short directory names leave room for setuptools build paths on Windows.
for index, manifest in enumerate(sorted((REPO_ROOT / "libraries").glob("*/pyproject.toml"))):
shutil.copytree(
manifest.parent,
workspace / "libraries" / f"p{index}",
ignore=shutil.ignore_patterns("build", "dist", "*.egg-info", "__pycache__"),
)
wheel_dir = workspace / "wheels"
wheel_dir.mkdir()
env = {
**os.environ,
"PYTHONPATH": str(workspace / "versioning" / "helper"),
"AGENT365_PYTHON_SDK_PACKAGE_VERSION": "0.0.0",
}
requirements: dict[str, list[Requirement]] = {}
for pyproject in sorted((workspace / "libraries").glob("*/pyproject.toml")):
original = pyproject.read_bytes()
result = subprocess.run(
[
sys.executable,
"-c",
"import build_backend, sys; build_backend.build_wheel(sys.argv[1])",
str(wheel_dir),
],
cwd=pyproject.parent,
env=env,
capture_output=True,
text=True,
timeout=120,
check=False,
)
assert result.returncode == 0, (
f"Wheel build failed for {pyproject.parent.name}:\n{result.stdout}\n{result.stderr}"
)
assert pyproject.read_bytes() == original, f"Build did not restore {pyproject}"

for wheel in sorted(wheel_dir.glob("*.whl")):
with ZipFile(wheel) as archive:
metadata_files = [
name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
]
assert len(metadata_files) == 1, f"Expected one METADATA file in {wheel}"
metadata = BytesParser().parsebytes(archive.read(metadata_files[0]))
name = metadata["Name"]
assert name is not None
requirements[canonicalize_name(name)] = [
Requirement(value) for value in metadata.get_all("Requires-Dist", [])
]

assert set(requirements) == {PACKAGE_PREFIX + suffix for suffix in EXPECTED_REQUIREMENTS}
return requirements


def _runtime_requirements(
name: str, published: dict[str, list[Requirement]]
) -> Iterator[Requirement]:
"""Follow internal wheel requirements, never the lockfile or optional development extras."""
pending = [name]
visited: set[str] = set()
while pending:
package = pending.pop()
if package in visited:
continue
visited.add(package)
for requirement in published[package]:
if requirement.marker is not None:
continue
dependency = canonicalize_name(requirement.name)
if dependency.startswith(PACKAGE_PREFIX):
assert str(requirement.specifier) == "==0.0.0"
pending.append(dependency)
else:
yield requirement


@pytest.mark.parametrize("package_suffix", EXPECTED_REQUIREMENTS)
def test_published_security_floors(
package_suffix: str, published_requirements: dict[str, list[Requirement]]
) -> None:
with (REPO_ROOT / "pyproject.toml").open("rb") as file:
root = tomllib.load(file)
constraints = {
canonicalize_name(requirement.name): requirement
for value in root["tool"]["uv"]["constraint-dependencies"]
for requirement in [Requirement(value)]
}
requirements = {
canonicalize_name(requirement.name): requirement
for requirement in _runtime_requirements(
PACKAGE_PREFIX + package_suffix, published_requirements
)
}
# Also reject adding unrelated framework dependencies to lightweight packages.
assert requirements.keys() & SECURITY_FLOORS.keys() == EXPECTED_REQUIREMENTS[package_suffix]
for name in EXPECTED_REQUIREMENTS[package_suffix]:
requirement = requirements[name]
assert requirement.specifier == constraints[name].specifier, (
f"{package_suffix} does not publish the centralized {name} constraint"
)
assert any(
specifier.operator == ">="
and Version(specifier.version) >= Version(SECURITY_FLOORS[name])
for specifier in requirement.specifier
), f"{package_suffix} allows vulnerable versions of {name}: {requirement.specifier}"
Loading
Loading