mirror of
https://github.com/anthropics/claude-code-sdk-python.git
synced 2025-12-23 09:19:52 +00:00
Create sandbox adapter interface for Python SDK (#363)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Some checks are pending
Lint / lint (push) Waiting to run
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Adds programmatic sandbox configuration to the Python SDK, matching the
TypeScript SDK's approach.
Changes:
- Add SandboxSettings, SandboxNetworkConfig, SandboxIgnoreViolations
types
- Add sandbox field to ClaudeAgentOptions
- Merge sandbox into --settings CLI flag in SubprocessCLITransport
- Export sandbox types from package __init__.py
- Add comprehensive tests for sandbox settings
**Important:** Filesystem and network restrictions are configured via
permission rules (Read/Edit/WebFetch), not via these sandbox settings.
The sandbox settings control sandbox behavior (enabled, auto-allow,
excluded commands, etc.).
Example usage:
```python
from claude_agent_sdk import query, SandboxSettings
result = query(
prompt='Build and test the project',
options=ClaudeAgentOptions(
sandbox={
'enabled': True,
'autoAllowBashIfSandboxed': True,
'excludedCommands': ['docker'],
'network': {
'allowLocalBinding': True,
'allowUnixSockets': ['/var/run/docker.sock']
}
}
)
)
```
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d553184ef6
commit
f21f63e181
4 changed files with 293 additions and 2 deletions
|
|
@ -39,6 +39,9 @@ from .types import (
|
|||
PreCompactHookInput,
|
||||
PreToolUseHookInput,
|
||||
ResultMessage,
|
||||
SandboxIgnoreViolations,
|
||||
SandboxNetworkConfig,
|
||||
SandboxSettings,
|
||||
SdkPluginConfig,
|
||||
SettingSource,
|
||||
StopHookInput,
|
||||
|
|
@ -342,6 +345,10 @@ __all__ = [
|
|||
"SettingSource",
|
||||
# Plugin support
|
||||
"SdkPluginConfig",
|
||||
# Sandbox support
|
||||
"SandboxSettings",
|
||||
"SandboxNetworkConfig",
|
||||
"SandboxIgnoreViolations",
|
||||
# MCP Server Support
|
||||
"create_sdk_mcp_server",
|
||||
"tool",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,60 @@ class SubprocessCLITransport(Transport):
|
|||
|
||||
return None
|
||||
|
||||
def _build_settings_value(self) -> str | None:
|
||||
"""Build settings value, merging sandbox settings if provided.
|
||||
|
||||
Returns the settings value as either:
|
||||
- A JSON string (if sandbox is provided or settings is JSON)
|
||||
- A file path (if only settings path is provided without sandbox)
|
||||
- None if neither settings nor sandbox is provided
|
||||
"""
|
||||
has_settings = self._options.settings is not None
|
||||
has_sandbox = self._options.sandbox is not None
|
||||
|
||||
if not has_settings and not has_sandbox:
|
||||
return None
|
||||
|
||||
# If only settings path and no sandbox, pass through as-is
|
||||
if has_settings and not has_sandbox:
|
||||
return self._options.settings
|
||||
|
||||
# If we have sandbox settings, we need to merge into a JSON object
|
||||
settings_obj: dict[str, Any] = {}
|
||||
|
||||
if has_settings:
|
||||
assert self._options.settings is not None
|
||||
settings_str = self._options.settings.strip()
|
||||
# Check if settings is a JSON string or a file path
|
||||
if settings_str.startswith("{") and settings_str.endswith("}"):
|
||||
# Parse JSON string
|
||||
try:
|
||||
settings_obj = json.loads(settings_str)
|
||||
except json.JSONDecodeError:
|
||||
# If parsing fails, treat as file path
|
||||
logger.warning(
|
||||
f"Failed to parse settings as JSON, treating as file path: {settings_str}"
|
||||
)
|
||||
# Read the file
|
||||
settings_path = Path(settings_str)
|
||||
if settings_path.exists():
|
||||
with settings_path.open(encoding="utf-8") as f:
|
||||
settings_obj = json.load(f)
|
||||
else:
|
||||
# It's a file path - read and parse
|
||||
settings_path = Path(settings_str)
|
||||
if settings_path.exists():
|
||||
with settings_path.open(encoding="utf-8") as f:
|
||||
settings_obj = json.load(f)
|
||||
else:
|
||||
logger.warning(f"Settings file not found: {settings_path}")
|
||||
|
||||
# Merge sandbox settings
|
||||
if has_sandbox:
|
||||
settings_obj["sandbox"] = self._options.sandbox
|
||||
|
||||
return json.dumps(settings_obj)
|
||||
|
||||
def _build_command(self) -> list[str]:
|
||||
"""Build CLI command with arguments."""
|
||||
cmd = [self._cli_path, "--output-format", "stream-json", "--verbose"]
|
||||
|
|
@ -163,8 +217,10 @@ class SubprocessCLITransport(Transport):
|
|||
if self._options.resume:
|
||||
cmd.extend(["--resume", self._options.resume])
|
||||
|
||||
if self._options.settings:
|
||||
cmd.extend(["--settings", self._options.settings])
|
||||
# Handle settings and sandbox: merge sandbox into settings if both are provided
|
||||
settings_value = self._build_settings_value()
|
||||
if settings_value:
|
||||
cmd.extend(["--settings", settings_value])
|
||||
|
||||
if self._options.add_dirs:
|
||||
# Convert all paths to strings and add each directory
|
||||
|
|
|
|||
|
|
@ -419,6 +419,83 @@ class SdkPluginConfig(TypedDict):
|
|||
path: str
|
||||
|
||||
|
||||
# Sandbox configuration types
|
||||
class SandboxNetworkConfig(TypedDict, total=False):
|
||||
"""Network configuration for sandbox.
|
||||
|
||||
Attributes:
|
||||
allowUnixSockets: Unix socket paths accessible in sandbox (e.g., SSH agents).
|
||||
allowAllUnixSockets: Allow all Unix sockets (less secure).
|
||||
allowLocalBinding: Allow binding to localhost ports (macOS only).
|
||||
httpProxyPort: HTTP proxy port if bringing your own proxy.
|
||||
socksProxyPort: SOCKS5 proxy port if bringing your own proxy.
|
||||
"""
|
||||
|
||||
allowUnixSockets: list[str]
|
||||
allowAllUnixSockets: bool
|
||||
allowLocalBinding: bool
|
||||
httpProxyPort: int
|
||||
socksProxyPort: int
|
||||
|
||||
|
||||
class SandboxIgnoreViolations(TypedDict, total=False):
|
||||
"""Violations to ignore in sandbox.
|
||||
|
||||
Attributes:
|
||||
file: File paths for which violations should be ignored.
|
||||
network: Network hosts for which violations should be ignored.
|
||||
"""
|
||||
|
||||
file: list[str]
|
||||
network: list[str]
|
||||
|
||||
|
||||
class SandboxSettings(TypedDict, total=False):
|
||||
"""Sandbox settings configuration.
|
||||
|
||||
This controls how Claude Code sandboxes bash commands for filesystem
|
||||
and network isolation.
|
||||
|
||||
**Important:** Filesystem and network restrictions are configured via permission
|
||||
rules, not via these sandbox settings:
|
||||
- Filesystem read restrictions: Use Read deny rules
|
||||
- Filesystem write restrictions: Use Edit allow/deny rules
|
||||
- Network restrictions: Use WebFetch allow/deny rules
|
||||
|
||||
Attributes:
|
||||
enabled: Enable bash sandboxing (macOS/Linux only). Default: False
|
||||
autoAllowBashIfSandboxed: Auto-approve bash commands when sandboxed. Default: True
|
||||
excludedCommands: Commands that should run outside the sandbox (e.g., ["git", "docker"])
|
||||
allowUnsandboxedCommands: Allow commands to bypass sandbox via dangerouslyDisableSandbox.
|
||||
When False, all commands must run sandboxed (or be in excludedCommands). Default: True
|
||||
network: Network configuration for sandbox.
|
||||
ignoreViolations: Violations to ignore.
|
||||
enableWeakerNestedSandbox: Enable weaker sandbox for unprivileged Docker environments
|
||||
(Linux only). Reduces security. Default: False
|
||||
|
||||
Example:
|
||||
```python
|
||||
sandbox_settings: SandboxSettings = {
|
||||
"enabled": True,
|
||||
"autoAllowBashIfSandboxed": True,
|
||||
"excludedCommands": ["docker"],
|
||||
"network": {
|
||||
"allowUnixSockets": ["/var/run/docker.sock"],
|
||||
"allowLocalBinding": True
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
enabled: bool
|
||||
autoAllowBashIfSandboxed: bool
|
||||
excludedCommands: list[str]
|
||||
allowUnsandboxedCommands: bool
|
||||
network: SandboxNetworkConfig
|
||||
ignoreViolations: SandboxIgnoreViolations
|
||||
enableWeakerNestedSandbox: bool
|
||||
|
||||
|
||||
# Content block types
|
||||
@dataclass
|
||||
class TextBlock:
|
||||
|
|
@ -569,6 +646,10 @@ class ClaudeAgentOptions:
|
|||
agents: dict[str, AgentDefinition] | None = None
|
||||
# Setting sources to load (user, project, local)
|
||||
setting_sources: list[SettingSource] | None = None
|
||||
# Sandbox configuration for bash command isolation.
|
||||
# Filesystem and network restrictions are derived from permission rules (Read/Edit/WebFetch),
|
||||
# not from these sandbox settings.
|
||||
sandbox: SandboxSettings | None = None
|
||||
# Plugin configurations for custom plugins
|
||||
plugins: list[SdkPluginConfig] = field(default_factory=list)
|
||||
# Max tokens for thinking blocks
|
||||
|
|
|
|||
|
|
@ -500,3 +500,150 @@ class TestSubprocessCLITransport:
|
|||
assert user_passed == "claude"
|
||||
|
||||
anyio.run(_test)
|
||||
|
||||
def test_build_command_with_sandbox_only(self):
|
||||
"""Test building CLI command with sandbox settings (no existing settings)."""
|
||||
import json
|
||||
|
||||
from claude_agent_sdk import SandboxSettings
|
||||
|
||||
sandbox: SandboxSettings = {
|
||||
"enabled": True,
|
||||
"autoAllowBashIfSandboxed": True,
|
||||
"network": {
|
||||
"allowLocalBinding": True,
|
||||
"allowUnixSockets": ["/var/run/docker.sock"],
|
||||
},
|
||||
}
|
||||
|
||||
transport = SubprocessCLITransport(
|
||||
prompt="test",
|
||||
options=make_options(sandbox=sandbox),
|
||||
)
|
||||
|
||||
cmd = transport._build_command()
|
||||
|
||||
# Should have --settings with sandbox merged in
|
||||
assert "--settings" in cmd
|
||||
settings_idx = cmd.index("--settings")
|
||||
settings_value = cmd[settings_idx + 1]
|
||||
|
||||
# Parse and verify
|
||||
parsed = json.loads(settings_value)
|
||||
assert "sandbox" in parsed
|
||||
assert parsed["sandbox"]["enabled"] is True
|
||||
assert parsed["sandbox"]["autoAllowBashIfSandboxed"] is True
|
||||
assert parsed["sandbox"]["network"]["allowLocalBinding"] is True
|
||||
assert parsed["sandbox"]["network"]["allowUnixSockets"] == [
|
||||
"/var/run/docker.sock"
|
||||
]
|
||||
|
||||
def test_build_command_with_sandbox_and_settings_json(self):
|
||||
"""Test building CLI command with sandbox merged into existing settings JSON."""
|
||||
import json
|
||||
|
||||
from claude_agent_sdk import SandboxSettings
|
||||
|
||||
# Existing settings as JSON string
|
||||
existing_settings = (
|
||||
'{"permissions": {"allow": ["Bash(ls:*)"]}, "verbose": true}'
|
||||
)
|
||||
|
||||
sandbox: SandboxSettings = {
|
||||
"enabled": True,
|
||||
"excludedCommands": ["git", "docker"],
|
||||
}
|
||||
|
||||
transport = SubprocessCLITransport(
|
||||
prompt="test",
|
||||
options=make_options(settings=existing_settings, sandbox=sandbox),
|
||||
)
|
||||
|
||||
cmd = transport._build_command()
|
||||
|
||||
# Should have merged settings
|
||||
assert "--settings" in cmd
|
||||
settings_idx = cmd.index("--settings")
|
||||
settings_value = cmd[settings_idx + 1]
|
||||
|
||||
parsed = json.loads(settings_value)
|
||||
|
||||
# Original settings should be preserved
|
||||
assert parsed["permissions"] == {"allow": ["Bash(ls:*)"]}
|
||||
assert parsed["verbose"] is True
|
||||
|
||||
# Sandbox should be merged in
|
||||
assert "sandbox" in parsed
|
||||
assert parsed["sandbox"]["enabled"] is True
|
||||
assert parsed["sandbox"]["excludedCommands"] == ["git", "docker"]
|
||||
|
||||
def test_build_command_with_settings_file_and_no_sandbox(self):
|
||||
"""Test that settings file path is passed through when no sandbox."""
|
||||
transport = SubprocessCLITransport(
|
||||
prompt="test",
|
||||
options=make_options(settings="/path/to/settings.json"),
|
||||
)
|
||||
|
||||
cmd = transport._build_command()
|
||||
|
||||
# Should pass path directly, not parse it
|
||||
assert "--settings" in cmd
|
||||
settings_idx = cmd.index("--settings")
|
||||
assert cmd[settings_idx + 1] == "/path/to/settings.json"
|
||||
|
||||
def test_build_command_sandbox_minimal(self):
|
||||
"""Test sandbox with minimal configuration."""
|
||||
import json
|
||||
|
||||
from claude_agent_sdk import SandboxSettings
|
||||
|
||||
sandbox: SandboxSettings = {"enabled": True}
|
||||
|
||||
transport = SubprocessCLITransport(
|
||||
prompt="test",
|
||||
options=make_options(sandbox=sandbox),
|
||||
)
|
||||
|
||||
cmd = transport._build_command()
|
||||
|
||||
assert "--settings" in cmd
|
||||
settings_idx = cmd.index("--settings")
|
||||
settings_value = cmd[settings_idx + 1]
|
||||
|
||||
parsed = json.loads(settings_value)
|
||||
assert parsed == {"sandbox": {"enabled": True}}
|
||||
|
||||
def test_sandbox_network_config(self):
|
||||
"""Test sandbox with full network configuration."""
|
||||
import json
|
||||
|
||||
from claude_agent_sdk import SandboxSettings
|
||||
|
||||
sandbox: SandboxSettings = {
|
||||
"enabled": True,
|
||||
"network": {
|
||||
"allowUnixSockets": ["/tmp/ssh-agent.sock"],
|
||||
"allowAllUnixSockets": False,
|
||||
"allowLocalBinding": True,
|
||||
"httpProxyPort": 8080,
|
||||
"socksProxyPort": 8081,
|
||||
},
|
||||
}
|
||||
|
||||
transport = SubprocessCLITransport(
|
||||
prompt="test",
|
||||
options=make_options(sandbox=sandbox),
|
||||
)
|
||||
|
||||
cmd = transport._build_command()
|
||||
settings_idx = cmd.index("--settings")
|
||||
settings_value = cmd[settings_idx + 1]
|
||||
|
||||
parsed = json.loads(settings_value)
|
||||
network = parsed["sandbox"]["network"]
|
||||
|
||||
assert network["allowUnixSockets"] == ["/tmp/ssh-agent.sock"]
|
||||
assert network["allowAllUnixSockets"] is False
|
||||
assert network["allowLocalBinding"] is True
|
||||
assert network["httpProxyPort"] == 8080
|
||||
assert network["socksProxyPort"] == 8081
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue