Skip to content
Draft
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 .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 131
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-6d5c96a475b06ff067a4491fc2258181f0426af6c64bcfd4be5d167a8c0d9d16.yml
openapi_spec_hash: d2deb0fef6a15bf53cc6c53f07973a54
config_hash: 447f76a00c2affe40a8b4c43129e17a3
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-c48ea225ef1d1685f19d56df1b647b69026a203fe6ad6cb913c832856d223418.yml
openapi_spec_hash: e9c540d82f06430a4b554f73df4ef914
config_hash: d335041db7fb23dedd07cf9ff13a1565
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ Most of the SDK is generated code. Modifications to code will be persisted betwe
result in merge conflicts between manual patches and changes from the generator. The generator will never
modify the contents of the `src/anthropic/lib/` and `examples/` directories.

### Spawning external programs

Never pass a bare program name (`"rg"`, `"git"`, `"tar"`) to a process API (`subprocess.*`,
`anyio.open_process` / `anyio.run_process`, `asyncio.create_subprocess_exec`, `os.exec*` / `os.spawn*`).
The OS-level lookup those trigger searches the current working directory on Windows (and `.`/empty/relative
`PATH` entries everywhere), so a look-alike binary planted in whatever directory the user happens to run from
would be executed. Instead, resolve the program with
[`anthropic.lib._executable`](src/anthropic/lib/_executable.py) and spawn the absolute path it returns:

```py
from anthropic.lib._executable import find_executable, resolve_argv, run

rg = find_executable("rg") # absolute path or None — only absolute PATH entries, never the CWD
proc = await anyio.run_process(resolve_argv(["git", "status"])) # async: resolve first, then spawn
result = run(["git", "status"], capture_output=True) # sync: resolves and runs in one call
```

Absolute paths you construct yourself (e.g. `/bin/bash`) are fine as-is. CI enforces this: ruff bans
`shutil.which` / `distutils.spawn.find_executable` (`flake8-tidy-imports.banned-api` in `pyproject.toml`), and
`tests/lib/test_executable_policy.py` fails on any spawn call whose program is a non-absolute string literal.
The module docstring lists the exact guarantees (G1–G5, D1), which are shared with the other Anthropic SDKs.

## Adding and running examples

All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.
Expand Down
5 changes: 4 additions & 1 deletion helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,10 @@ tools = [t for t in beta_agent_toolset_20260401(env) if t.name != "bash"]
The `bash` tool runs an unrestricted `/bin/bash` and executes file operations and shell commands
directly on the host. Run the worker inside a container or other isolation boundary you control.
(The file tools — `read`/`write`/`edit`/`glob`/`grep` — confine to the workdir with a symlink-aware
check, so they are safe without a sandbox; `bash` is not.) `bash` does not inherit the runner's
check, so they are safe without a sandbox; `bash` is not.) Helper binaries the file tools shell out
to (`rg` for `grep`) are looked up on absolute `PATH` entries only — never in the working directory
or the tree being searched — so a look-alike `rg`/`rg.exe` planted in the workspace is never executed;
without ripgrep installed, `grep` falls back to a pure-Python search. `bash` does not inherit the runner's
`ANTHROPIC_*` credentials; pass `AgentToolContext(env=...)` to control the subprocess environment.

See [`examples/managed-agents-self-hosted-sandbox-worker.py`](examples/managed-agents-self-hosted-sandbox-worker.py) for a complete example.
15 changes: 10 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies = [
"typing-extensions>=4.14, <5",
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
"sniffio>=1, <2",
"jiter>=0.4.0, <1",
"docstring-parser>=0.15,<1",
]
Expand All @@ -39,11 +39,11 @@ classifiers = [
]

[project.optional-dependencies]
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9, <1"]
aiohttp = ["aiohttp>=3, <4", "httpx_aiohttp>=0.1.9, <1"]
vertex = ["google-auth[requests] >=2, <3"]
google_cloud = ["google-auth[requests] >=2, <3"]
aws = ["boto3 >= 1.28.57", "botocore >= 1.31.57"]
bedrock = ["boto3 >= 1.28.57", "botocore >= 1.31.57"]
aws = ["boto3 >=1.28.57, <2", "botocore >=1.31.57, <2"]
bedrock = ["boto3 >=1.28.57, <2", "botocore >=1.31.57, <2"]
mcp = ["mcp>=1.0, <3; python_version >= '3.10'"]
webhooks = ["standardwebhooks >= 1.0.1, < 2"]

Expand All @@ -55,6 +55,7 @@ Repository = "https://github.com/anthropics/anthropic-sdk-python"
[tool.uv]
managed = true
required-version = ">=0.9"
add-bounds = "major"
# Ensure the lockfile always uses public PyPI, regardless of contributor's global uv config
index = [{ url = "https://pypi.org/simple", default = true }]
conflicts = [
Expand Down Expand Up @@ -86,6 +87,8 @@ dev = [
"inline-snapshot>=0.28.0",
"griffe>=1",
"http-snapshot[httpx]==0.1.9",
"packaging",
"tomli; python_version < '3.11'",
]
pydantic-v1 = [
"pydantic>=1.9.0,<2",
Expand All @@ -96,7 +99,7 @@ pydantic-v2 = [
]

[build-system]
requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"]
requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme>=22.4, <26"]
build-backend = "hatchling.build"

[tool.hatch.build]
Expand Down Expand Up @@ -270,6 +273,8 @@ extend-safe-fixes = ["FA102"]

[tool.ruff.lint.flake8-tidy-imports.banned-api]
"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead"
"shutil.which".msg = "Searches the current directory on Windows (and via '.'/empty/relative PATH entries everywhere); use `find_executable` from `anthropic.lib._executable` instead"
"distutils.spawn.find_executable".msg = "Searches the current directory; use `find_executable` from `anthropic.lib._executable` instead"

[tool.ruff.lint.isort]
length-sort = true
Expand Down
3 changes: 3 additions & 0 deletions scripts/lint
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ else
uv run ruff check .
fi

echo "==> Checking dependency version caps"
uv run python scripts/utils/check-dependency-caps.py

echo "==> Running pyright"
uv run pyright

Expand Down
57 changes: 57 additions & 0 deletions scripts/utils/check-dependency-caps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Ensure every production and build dependency in pyproject.toml has an upper version bound.

Unbounded requirements (e.g. `httpx>=0.25.0`) mean a future major release of a
dependency can break users at install time without us noticing, so we require an
explicit cap (e.g. `httpx>=0.25.0,<1`).
"""

from __future__ import annotations

import sys
from typing import Any
from pathlib import Path

from packaging.requirements import Requirement

if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib

# operators that constrain the maximum installable version
UPPER_BOUND_OPERATORS = {"<", "<=", "==", "===", "~="}


def has_upper_bound(requirement: Requirement) -> bool:
return any(spec.operator in UPPER_BOUND_OPERATORS for spec in requirement.specifier)


def main() -> None:
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
with open(pyproject_path, "rb") as f:
pyproject = tomllib.load(f)

project: dict[str, Any] = pyproject["project"]
build_system: dict[str, Any] = pyproject.get("build-system", {})

requirement_tables: dict[str, list[str]] = {"project.dependencies": project.get("dependencies", [])}
optional_dependencies: dict[str, list[str]] = project.get("optional-dependencies", {})
for extra, requirements in optional_dependencies.items():
requirement_tables[f"project.optional-dependencies.{extra}"] = requirements
requirement_tables["build-system.requires"] = build_system.get("requires", [])

missing_caps: list[str] = []
for group, requirements in requirement_tables.items():
for raw in requirements:
requirement = Requirement(raw)
if not has_upper_bound(requirement):
missing_caps.append(f" [{group}] {raw}")

if missing_caps:
print("The following dependencies are missing an upper version bound (e.g. `>=2, <3`):")
print("\n".join(missing_caps))
sys.exit(1)


if __name__ == "__main__":
main()
Loading
Loading