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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.111.0"
".": "0.112.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 116
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-2ecf157aa324d82fa8f3636a325aea5bd96ecab93193f6e37864ebe665c48685.yml
openapi_spec_hash: 29873ea69a87e047c4f742a960648bf0
config_hash: 48f7cbc6648bf7f1e6c68ad3dab477fc
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-106b972584d8eb2dc0b0319a2cfc0a2528544e35083d3264b82f8e22ae3300b8.yml
openapi_spec_hash: 79b933da300d4134d1964d83f33afb0c
config_hash: ce2cd5d2f03228adacf04ebcceb14465
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 0.112.0 (2026-06-23)

Full Changelog: [v0.111.0...v0.112.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.111.0...v0.112.0)

### Features

* **client:** add support for system.message streaming events ([2450d59](https://github.com/anthropics/anthropic-sdk-python/commit/2450d595731f9532080bb94eb8a43c0bd5189659))


### Bug Fixes

* **memory tool:** create parent directories with the correct permissions ([#135](https://github.com/anthropics/anthropic-sdk-python/issues/135)) ([f2fc2a9](https://github.com/anthropics/anthropic-sdk-python/commit/f2fc2a9e0ad8507e4108e9a6b85d023416c2f14c))


### Chores

* **api:** add support for sending User Profile ID in request headers ([83319be](https://github.com/anthropics/anthropic-sdk-python/commit/83319bed74f4414d54e0f4237d70b945ed671008))

## 0.111.0 (2026-06-18)

Full Changelog: [v0.110.0...v0.111.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.110.0...v0.111.0)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "anthropic"
version = "0.111.0"
version = "0.112.0"
description = "The official Python library for the anthropic API"
dynamic = ["readme"]
license = "MIT"
Expand Down
2 changes: 2 additions & 0 deletions src/anthropic/_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def __stream__(self) -> Iterator[_T]:
or sse.event == "session.thread_status_idle"
or sse.event == "session.thread_status_rescheduled"
or sse.event == "session.thread_status_terminated"
or sse.event == "system.message"
):
data = sse.json()
if is_dict(data) and "type" not in data:
Expand Down Expand Up @@ -304,6 +305,7 @@ async def __stream__(self) -> AsyncIterator[_T]:
or sse.event == "session.thread_status_idle"
or sse.event == "session.thread_status_rescheduled"
or sse.event == "session.thread_status_terminated"
or sse.event == "system.message"
):
data = sse.json()
if is_dict(data) and "type" not in data:
Expand Down
2 changes: 1 addition & 1 deletion src/anthropic/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "anthropic"
__version__ = "0.111.0" # x-release-please-version
__version__ = "0.112.0" # x-release-please-version
51 changes: 43 additions & 8 deletions src/anthropic/lib/tools/_beta_builtin_memory_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,37 @@ def _atomic_write_file(target_path: Path, content: str) -> None:
raise


def _secure_mkdir(path: Path, mode: int = _DIR_CREATE_MODE) -> None:
"""Create ``path`` and any missing parents with ``mode``, regardless of umask.

``Path.mkdir(parents=True, mode=...)`` and ``os.makedirs(mode=...)`` apply the
requested mode only to the final (leaf) directory; intermediate parents are
created with the process umask default, which can be world-writable under a
permissive umask. We create each missing component explicitly so the entire
newly-created chain has restrictive permissions — closing a symlink-swap hole
where an attacker with write access to a world-writable parent could replace
the sandbox root and defeat path validation.
"""
missing: list[Path] = []
current = path
while not current.exists():
missing.append(current)
parent = current.parent
if parent == current: # reached filesystem root
break
current = parent
for directory in reversed(missing):
try:
directory.mkdir(mode=mode)
except FileExistsError:
# Created concurrently between our exists() check and mkdir(); skip.
continue
# mkdir() is subject to umask; chmod is not. Enforce the exact mode so a
# restrictive umask can't strip owner bits. (We only chmod dirs we just
# created and therefore own — never pre-existing dirs.)
os.chmod(directory, mode)


def _validate_no_symlink_escape(target_path: Path, memory_root: Path) -> None:
resolved_root = memory_root.resolve()

Expand Down Expand Up @@ -351,7 +382,7 @@ def __init__(self, base_path: str = "./memory"):
super().__init__()
self.base_path = Path(base_path)
self.memory_root = self.base_path / "memories"
self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
_secure_mkdir(self.memory_root)

def _validate_path(self, path: str) -> Path:
"""Validate and resolve memory paths"""
Expand Down Expand Up @@ -443,7 +474,7 @@ def collect_items(dir_path: Path, relative_path: str, depth: int) -> None:
def create(self, command: BetaMemoryTool20250818CreateCommand) -> str:
full_path = self._validate_path(command.path)

full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
_secure_mkdir(full_path.parent)

try:
fd = os.open(full_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, _FILE_CREATE_MODE)
Expand Down Expand Up @@ -558,7 +589,7 @@ def rename(self, command: BetaMemoryTool20250818RenameCommand) -> str:
if new_full_path.exists():
raise ToolError(f"The destination {command.new_path} already exists")

new_full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
_secure_mkdir(new_full_path.parent)

try:
old_full_path.rename(new_full_path)
Expand All @@ -572,7 +603,7 @@ def clear_all_memory(self) -> str:
"""Override the base implementation to provide file system clearing."""
if self.memory_root.exists():
shutil.rmtree(self.memory_root)
self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
_secure_mkdir(self.memory_root)
return "All memory cleared"


Expand Down Expand Up @@ -613,6 +644,10 @@ async def _async_validate_no_symlink_escape(target_path: AsyncPath, memory_root:
await run_sync(_validate_no_symlink_escape, sync_target, sync_root)


async def _async_secure_mkdir(path: AsyncPath, mode: int = _DIR_CREATE_MODE) -> None:
await run_sync(_secure_mkdir, Path(str(path)), mode)


async def _async_read_file_content(full_path: AsyncPath, memory_path: str) -> str:
try:
return await full_path.read_text(encoding="utf-8")
Expand All @@ -633,7 +668,7 @@ def __init__(self, base_path: str = "./memory"):

async def _ensure_memory_root(self) -> None:
"""Ensure the memory root directory exists"""
await self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
await _async_secure_mkdir(self.memory_root)

async def _validate_path(self, path: str) -> AsyncPath:
"""Validate and resolve memory paths"""
Expand Down Expand Up @@ -733,7 +768,7 @@ async def create(self, command: BetaMemoryTool20250818CreateCommand) -> str:
await self._ensure_memory_root()
full_path = await self._validate_path(command.path)

await full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
await _async_secure_mkdir(full_path.parent)

try:
sync_full_path = Path(str(full_path))
Expand Down Expand Up @@ -857,7 +892,7 @@ async def rename(self, command: BetaMemoryTool20250818RenameCommand) -> str:
if await new_full_path.exists():
raise ToolError(f"The destination {command.new_path} already exists")

await new_full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
await _async_secure_mkdir(new_full_path.parent)

try:
await old_full_path.rename(new_full_path)
Expand All @@ -871,5 +906,5 @@ async def clear_all_memory(self) -> str:
"""Override the base implementation to provide file system clearing."""
if await self.memory_root.exists():
await run_sync(shutil.rmtree, str(self.memory_root))
await self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
await _async_secure_mkdir(self.memory_root)
return "All memory cleared"
4 changes: 1 addition & 3 deletions src/anthropic/lib/tools/_beta_session_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,7 @@ def _scoped_client(client: AsyncAnthropic, environment_key: str | None) -> Async
mutated).
"""
if environment_key is not None:
return _copy_client_with_bearer_auth(
client, auth_token=environment_key, helper="session-tool-runner"
)
return _copy_client_with_bearer_auth(client, auth_token=environment_key, helper="session-tool-runner")
return client.with_options(default_headers=helper_header("session-tool-runner"))


Expand Down
22 changes: 16 additions & 6 deletions src/anthropic/resources/beta/agents/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ def create(
description: Description of what the agent does.

mcp_servers: MCP servers this agent connects to. Maximum 20. Names must be unique within the
array.
array. Every server must be referenced by an `mcp_toolset` in `tools`;
unreferenced servers are rejected. See the
[MCP connector guide](https://platform.claude.com/docs/en/managed-agents/mcp-connector).

metadata: Arbitrary key-value metadata. Maximum 16 pairs, keys up to 64 chars, values up
to 512 chars.
Expand Down Expand Up @@ -245,8 +247,11 @@ def update(

description: Description. Omit to preserve; send empty string or null to clear.

mcp_servers: MCP servers. Full replacement. Omit to preserve; send empty array or null to
clear. Names must be unique. Maximum 20.
mcp_servers: MCP servers. Full replacement. Omit to preserve; send empty array or `null` to
clear. Names must be unique. Maximum 20. Every server must be referenced by an
`mcp_toolset` in the agent's resulting `tools`; unreferenced servers are
rejected. See the
[MCP connector guide](https://platform.claude.com/docs/en/managed-agents/mcp-connector).

metadata: Metadata patch. Set a key to a string to upsert it, or to null to delete it.
Omit the field to preserve. The stored bag is limited to 16 keys (up to 64 chars
Expand Down Expand Up @@ -496,7 +501,9 @@ async def create(
description: Description of what the agent does.

mcp_servers: MCP servers this agent connects to. Maximum 20. Names must be unique within the
array.
array. Every server must be referenced by an `mcp_toolset` in `tools`;
unreferenced servers are rejected. See the
[MCP connector guide](https://platform.claude.com/docs/en/managed-agents/mcp-connector).

metadata: Arbitrary key-value metadata. Maximum 16 pairs, keys up to 64 chars, values up
to 512 chars.
Expand Down Expand Up @@ -642,8 +649,11 @@ async def update(

description: Description. Omit to preserve; send empty string or null to clear.

mcp_servers: MCP servers. Full replacement. Omit to preserve; send empty array or null to
clear. Names must be unique. Maximum 20.
mcp_servers: MCP servers. Full replacement. Omit to preserve; send empty array or `null` to
clear. Names must be unique. Maximum 20. Every server must be referenced by an
`mcp_toolset` in the agent's resulting `tools`; unreferenced servers are
rejected. See the
[MCP connector guide](https://platform.claude.com/docs/en/managed-agents/mcp-connector).

metadata: Metadata patch. Set a key to a string to upsert it, or to null to delete it.
Omit the field to preserve. The stored bag is limited to 16 keys (up to 64 chars
Expand Down
18 changes: 16 additions & 2 deletions src/anthropic/resources/beta/messages/batches.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def create(
*,
requests: Iterable[batch_create_params.Request],
betas: List[AnthropicBetaParam] | Omit = omit,
user_profile_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand All @@ -74,6 +75,11 @@ def create(

betas: Optional header to specify the beta version(s) you want to use.

user_profile_id: The user profile ID to attribute the requests in this batch to. Use when acting
on behalf of a party other than your organization. Requires the `user-profiles`
beta header. Applies to every request in the batch; an individual request whose
`user_profile_id` body field conflicts with this header is errored.

extra_headers: Send extra headers

extra_query: Add additional query parameters to the request
Expand All @@ -87,7 +93,8 @@ def create(
{
"anthropic-beta": ",".join(chain((str(e) for e in betas), ["message-batches-2024-09-24"]))
if is_given(betas)
else not_given
else not_given,
"anthropic-user-profile-id": user_profile_id,
}
),
**(extra_headers or {}),
Expand Down Expand Up @@ -440,6 +447,7 @@ async def create(
*,
requests: Iterable[batch_create_params.Request],
betas: List[AnthropicBetaParam] | Omit = omit,
user_profile_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand All @@ -463,6 +471,11 @@ async def create(

betas: Optional header to specify the beta version(s) you want to use.

user_profile_id: The user profile ID to attribute the requests in this batch to. Use when acting
on behalf of a party other than your organization. Requires the `user-profiles`
beta header. Applies to every request in the batch; an individual request whose
`user_profile_id` body field conflicts with this header is errored.

extra_headers: Send extra headers

extra_query: Add additional query parameters to the request
Expand All @@ -476,7 +489,8 @@ async def create(
{
"anthropic-beta": ",".join(chain((str(e) for e in betas), ["message-batches-2024-09-24"]))
if is_given(betas)
else not_given
else not_given,
"anthropic-user-profile-id": user_profile_id,
}
),
**(extra_headers or {}),
Expand Down
Loading
Loading