roche-sandbox 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ src/roche_sandbox/generated/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ .venv/
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: roche-sandbox
3
+ Version: 0.1.0
4
+ Summary: Universal sandbox orchestrator for AI agents — Python SDK
5
+ Project-URL: Homepage, https://github.com/substratum-labs/roche
6
+ Project-URL: Repository, https://github.com/substratum-labs/roche
7
+ Author: Substratum Labs
8
+ License: Apache-2.0
9
+ Keywords: agent,ai,docker,orchestrator,sandbox,wasm
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: System :: Emulators
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: grpcio>=1.60.0
20
+ Requires-Dist: protobuf>=4.25.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: grpcio-tools>=1.60.0; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
24
+ Requires-Dist: pytest>=7.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # roche-sandbox
28
+
29
+ Python SDK for [Roche](https://github.com/substratum-labs/roche) -- universal sandbox orchestrator for AI agents.
30
+
31
+ ## Requirements
32
+
33
+ - Python >= 3.10
34
+ - Roche CLI on `PATH` (or Roche daemon running)
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install roche-sandbox
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ from roche_sandbox import Roche
46
+
47
+ roche = Roche()
48
+ sandbox = roche.create(image="python:3.12-slim")
49
+ output = sandbox.exec(["python3", "-c", "print('Hello from Roche!')"])
50
+ print(output.stdout) # Hello from Roche!
51
+ sandbox.destroy()
52
+ ```
53
+
54
+ ### Context Manager (auto-cleanup)
55
+
56
+ ```python
57
+ with roche.create(image="python:3.12-slim") as sandbox:
58
+ output = sandbox.exec(["echo", "hello"])
59
+ ```
60
+
61
+ ### Async API
62
+
63
+ ```python
64
+ import asyncio
65
+ from roche_sandbox import AsyncRoche
66
+
67
+ async def main():
68
+ roche = AsyncRoche()
69
+ sandbox = await roche.create(image="python:3.12-slim")
70
+ output = await sandbox.exec(["echo", "hello"])
71
+ await sandbox.destroy()
72
+
73
+ asyncio.run(main())
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ ```python
79
+ sandbox = roche.create(
80
+ image="python:3.12-slim",
81
+ memory="512m",
82
+ cpus=1.0,
83
+ timeout_secs=600,
84
+ network=False, # default: AI-safe
85
+ writable=False, # default: AI-safe
86
+ env={"API_KEY": "secret"},
87
+ )
88
+ ```
89
+
90
+ ## Transport
91
+
92
+ The SDK auto-detects whether the Roche gRPC daemon is running and connects to it. If the daemon is unavailable, it falls back to invoking the Roche CLI as a subprocess.
93
+
94
+ You can force CLI mode explicitly:
95
+
96
+ ```python
97
+ roche = Roche(mode="direct")
98
+ ```
99
+
100
+ ## API Styles
101
+
102
+ The SDK provides two API styles:
103
+
104
+ - **Async-first**: `AsyncRoche` and `AsyncSandbox` -- native `async`/`await` support.
105
+ - **Sync wrapper**: `Roche` and `Sandbox` -- blocking equivalents for scripts and notebooks.
106
+
107
+ ## Public Exports
108
+
109
+ ```python
110
+ from roche_sandbox import (
111
+ Roche, AsyncRoche,
112
+ Sandbox, AsyncSandbox,
113
+ SandboxConfig, ExecOutput, SandboxInfo,
114
+ Mount, SandboxStatus,
115
+ RocheError, SandboxNotFound, SandboxPaused,
116
+ ProviderUnavailable, TimeoutError, UnsupportedOperation,
117
+ )
118
+ ```
119
+
120
+ ## License
121
+
122
+ Apache-2.0 -- see [LICENSE](https://github.com/substratum-labs/roche/blob/main/LICENSE).
@@ -0,0 +1,96 @@
1
+ # roche-sandbox
2
+
3
+ Python SDK for [Roche](https://github.com/substratum-labs/roche) -- universal sandbox orchestrator for AI agents.
4
+
5
+ ## Requirements
6
+
7
+ - Python >= 3.10
8
+ - Roche CLI on `PATH` (or Roche daemon running)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install roche-sandbox
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```python
19
+ from roche_sandbox import Roche
20
+
21
+ roche = Roche()
22
+ sandbox = roche.create(image="python:3.12-slim")
23
+ output = sandbox.exec(["python3", "-c", "print('Hello from Roche!')"])
24
+ print(output.stdout) # Hello from Roche!
25
+ sandbox.destroy()
26
+ ```
27
+
28
+ ### Context Manager (auto-cleanup)
29
+
30
+ ```python
31
+ with roche.create(image="python:3.12-slim") as sandbox:
32
+ output = sandbox.exec(["echo", "hello"])
33
+ ```
34
+
35
+ ### Async API
36
+
37
+ ```python
38
+ import asyncio
39
+ from roche_sandbox import AsyncRoche
40
+
41
+ async def main():
42
+ roche = AsyncRoche()
43
+ sandbox = await roche.create(image="python:3.12-slim")
44
+ output = await sandbox.exec(["echo", "hello"])
45
+ await sandbox.destroy()
46
+
47
+ asyncio.run(main())
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ ```python
53
+ sandbox = roche.create(
54
+ image="python:3.12-slim",
55
+ memory="512m",
56
+ cpus=1.0,
57
+ timeout_secs=600,
58
+ network=False, # default: AI-safe
59
+ writable=False, # default: AI-safe
60
+ env={"API_KEY": "secret"},
61
+ )
62
+ ```
63
+
64
+ ## Transport
65
+
66
+ The SDK auto-detects whether the Roche gRPC daemon is running and connects to it. If the daemon is unavailable, it falls back to invoking the Roche CLI as a subprocess.
67
+
68
+ You can force CLI mode explicitly:
69
+
70
+ ```python
71
+ roche = Roche(mode="direct")
72
+ ```
73
+
74
+ ## API Styles
75
+
76
+ The SDK provides two API styles:
77
+
78
+ - **Async-first**: `AsyncRoche` and `AsyncSandbox` -- native `async`/`await` support.
79
+ - **Sync wrapper**: `Roche` and `Sandbox` -- blocking equivalents for scripts and notebooks.
80
+
81
+ ## Public Exports
82
+
83
+ ```python
84
+ from roche_sandbox import (
85
+ Roche, AsyncRoche,
86
+ Sandbox, AsyncSandbox,
87
+ SandboxConfig, ExecOutput, SandboxInfo,
88
+ Mount, SandboxStatus,
89
+ RocheError, SandboxNotFound, SandboxPaused,
90
+ ProviderUnavailable, TimeoutError, UnsupportedOperation,
91
+ )
92
+ ```
93
+
94
+ ## License
95
+
96
+ Apache-2.0 -- see [LICENSE](https://github.com/substratum-labs/roche/blob/main/LICENSE).
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "roche-sandbox"
7
+ version = "0.1.0"
8
+ description = "Universal sandbox orchestrator for AI agents — Python SDK"
9
+ license = {text = "Apache-2.0"}
10
+ requires-python = ">=3.10"
11
+ readme = "README.md"
12
+ keywords = ["sandbox", "docker", "ai", "agent", "wasm", "orchestrator"]
13
+ authors = [{name = "Substratum Labs"}]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries",
22
+ "Topic :: System :: Emulators",
23
+ ]
24
+ dependencies = [
25
+ "grpcio>=1.60.0",
26
+ "protobuf>=4.25.0",
27
+ ]
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/roche_sandbox"]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.0",
35
+ "pytest-asyncio>=0.23.0",
36
+ "grpcio-tools>=1.60.0",
37
+ ]
38
+
39
+ [tool.pytest.ini_options]
40
+ asyncio_mode = "strict"
41
+ testpaths = ["tests"]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/substratum-labs/roche"
45
+ Repository = "https://github.com/substratum-labs/roche"
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROTO_DIR="$(cd "$(dirname "$0")/../../.." && pwd)/proto"
5
+ OUT_DIR="$(cd "$(dirname "$0")/.." && pwd)/src/roche_sandbox/generated"
6
+
7
+ rm -rf "$OUT_DIR"
8
+ mkdir -p "$OUT_DIR/roche/v1"
9
+ touch "$OUT_DIR/__init__.py"
10
+ touch "$OUT_DIR/roche/__init__.py"
11
+ touch "$OUT_DIR/roche/v1/__init__.py"
12
+
13
+ python -m grpc_tools.protoc \
14
+ -I "$PROTO_DIR" \
15
+ --python_out="$OUT_DIR" \
16
+ --grpc_python_out="$OUT_DIR" \
17
+ --pyi_out="$OUT_DIR" \
18
+ "$PROTO_DIR/roche/v1/sandbox.proto"
@@ -0,0 +1,33 @@
1
+ """Roche — Universal sandbox orchestrator for AI agents (Python SDK)."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from roche_sandbox.client import AsyncRoche, Roche
6
+ from roche_sandbox.errors import (
7
+ ProviderUnavailable,
8
+ RocheError,
9
+ SandboxNotFound,
10
+ SandboxPaused,
11
+ TimeoutError,
12
+ UnsupportedOperation,
13
+ )
14
+ from roche_sandbox.sandbox import AsyncSandbox, Sandbox
15
+ from roche_sandbox.types import ExecOutput, Mount, SandboxConfig, SandboxInfo, SandboxStatus
16
+
17
+ __all__ = [
18
+ "AsyncRoche",
19
+ "Roche",
20
+ "AsyncSandbox",
21
+ "Sandbox",
22
+ "RocheError",
23
+ "SandboxNotFound",
24
+ "SandboxPaused",
25
+ "ProviderUnavailable",
26
+ "TimeoutError",
27
+ "UnsupportedOperation",
28
+ "SandboxConfig",
29
+ "ExecOutput",
30
+ "SandboxInfo",
31
+ "SandboxStatus",
32
+ "Mount",
33
+ ]
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING
5
+
6
+ from roche_sandbox.daemon import detect_daemon
7
+ from roche_sandbox.sandbox import AsyncSandbox, Sandbox
8
+ from roche_sandbox.transport.cli import CliTransport
9
+ from roche_sandbox.transport.grpc import GrpcTransport
10
+ from roche_sandbox.types import ExecOutput, SandboxConfig, SandboxInfo
11
+
12
+ if TYPE_CHECKING:
13
+ from roche_sandbox.transport import Transport
14
+
15
+
16
+ class AsyncRoche:
17
+ def __init__(
18
+ self,
19
+ *,
20
+ mode: str = "auto",
21
+ daemon_port: int | None = None,
22
+ provider: str = "docker",
23
+ binary: str = "roche",
24
+ transport: Transport | None = None,
25
+ ):
26
+ self._provider = provider
27
+ if transport is not None:
28
+ self._transport = transport
29
+ elif mode == "direct":
30
+ self._transport = CliTransport(binary=binary)
31
+ elif daemon_port is not None:
32
+ self._transport = GrpcTransport(port=daemon_port)
33
+ else:
34
+ daemon = detect_daemon()
35
+ if daemon is not None:
36
+ self._transport = GrpcTransport(port=daemon["port"])
37
+ else:
38
+ self._transport = CliTransport(binary=binary)
39
+
40
+ @property
41
+ def transport(self) -> Transport:
42
+ return self._transport
43
+
44
+ async def create(
45
+ self,
46
+ *,
47
+ provider: str | None = None,
48
+ image: str = "python:3.12-slim",
49
+ memory: str | None = None,
50
+ cpus: float | None = None,
51
+ timeout_secs: int = 300,
52
+ network: bool = False,
53
+ writable: bool = False,
54
+ env: dict[str, str] | None = None,
55
+ mounts: list | None = None,
56
+ kernel: str | None = None,
57
+ rootfs: str | None = None,
58
+ ) -> AsyncSandbox:
59
+ effective_provider = provider or self._provider
60
+ config = SandboxConfig(
61
+ provider=effective_provider,
62
+ image=image,
63
+ memory=memory,
64
+ cpus=cpus,
65
+ timeout_secs=timeout_secs,
66
+ network=network,
67
+ writable=writable,
68
+ env=env or {},
69
+ mounts=mounts or [],
70
+ kernel=kernel,
71
+ rootfs=rootfs,
72
+ )
73
+ sandbox_id = await self._transport.create(config, effective_provider)
74
+ return AsyncSandbox(sandbox_id, effective_provider, self._transport)
75
+
76
+ async def create_id(self, **kwargs) -> str:
77
+ sb = await self.create(**kwargs)
78
+ return sb.id
79
+
80
+ async def exec(
81
+ self, sandbox_id: str, command: list[str], timeout_secs: int | None = None
82
+ ) -> ExecOutput:
83
+ return await self._transport.exec(sandbox_id, command, self._provider, timeout_secs)
84
+
85
+ async def destroy(self, sandbox_id: str) -> None:
86
+ await self._transport.destroy([sandbox_id], self._provider)
87
+
88
+ async def list(self) -> list[SandboxInfo]:
89
+ return await self._transport.list(self._provider)
90
+
91
+ async def gc(self, dry_run: bool = False, all: bool = False) -> list[str]:
92
+ return await self._transport.gc(self._provider, dry_run, all)
93
+
94
+
95
+ class Roche:
96
+ def __init__(self, **kwargs):
97
+ self._async = AsyncRoche(**kwargs)
98
+
99
+ def create(self, **kwargs) -> Sandbox:
100
+ sb = asyncio.run(self._async.create(**kwargs))
101
+ return Sandbox(sb.id, sb.provider, self._async.transport)
102
+
103
+ def create_id(self, **kwargs) -> str:
104
+ return asyncio.run(self._async.create_id(**kwargs))
105
+
106
+ def exec(
107
+ self, sandbox_id: str, command: list[str], timeout_secs: int | None = None
108
+ ) -> ExecOutput:
109
+ return asyncio.run(self._async.exec(sandbox_id, command, timeout_secs))
110
+
111
+ def destroy(self, sandbox_id: str) -> None:
112
+ asyncio.run(self._async.destroy(sandbox_id))
113
+
114
+ def list(self) -> list[SandboxInfo]:
115
+ return asyncio.run(self._async.list())
116
+
117
+ def gc(self, dry_run: bool = False, all: bool = False) -> list[str]:
118
+ return asyncio.run(self._async.gc(dry_run, all))
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import TypedDict
7
+
8
+
9
+ class DaemonInfo(TypedDict):
10
+ pid: int
11
+ port: int
12
+
13
+
14
+ def daemon_json_path() -> Path:
15
+ return Path.home() / ".roche" / "daemon.json"
16
+
17
+
18
+ def detect_daemon() -> DaemonInfo | None:
19
+ path = daemon_json_path()
20
+ if not path.exists():
21
+ return None
22
+ try:
23
+ data = json.loads(path.read_text())
24
+ except (json.JSONDecodeError, OSError):
25
+ return None
26
+ pid = data.get("pid")
27
+ port = data.get("port")
28
+ if not isinstance(pid, int) or not isinstance(port, int):
29
+ return None
30
+ if not _is_process_alive(pid):
31
+ return None
32
+ return DaemonInfo(pid=pid, port=port)
33
+
34
+
35
+ def _is_process_alive(pid: int) -> bool:
36
+ try:
37
+ os.kill(pid, 0)
38
+ return True
39
+ except (OSError, ProcessLookupError):
40
+ return False
@@ -0,0 +1,28 @@
1
+ class RocheError(Exception):
2
+ def __init__(self, message: str):
3
+ super().__init__(message)
4
+
5
+
6
+ class SandboxNotFound(RocheError):
7
+ def __init__(self, detail: str):
8
+ super().__init__(f"Sandbox not found: {detail}")
9
+
10
+
11
+ class SandboxPaused(RocheError):
12
+ def __init__(self, detail: str):
13
+ super().__init__(f"Sandbox is paused: {detail}")
14
+
15
+
16
+ class ProviderUnavailable(RocheError):
17
+ def __init__(self, detail: str):
18
+ super().__init__(f"Provider unavailable: {detail}")
19
+
20
+
21
+ class TimeoutError(RocheError):
22
+ def __init__(self, detail: str):
23
+ super().__init__(f"Operation timed out: {detail}")
24
+
25
+
26
+ class UnsupportedOperation(RocheError):
27
+ def __init__(self, detail: str):
28
+ super().__init__(f"Unsupported operation: {detail}")
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING
5
+
6
+ from roche_sandbox.types import ExecOutput
7
+
8
+ if TYPE_CHECKING:
9
+ from roche_sandbox.transport import Transport
10
+
11
+
12
+ class AsyncSandbox:
13
+ def __init__(self, id: str, provider: str, transport: Transport):
14
+ self._id = id
15
+ self._provider = provider
16
+ self._transport = transport
17
+
18
+ @property
19
+ def id(self) -> str:
20
+ return self._id
21
+
22
+ @property
23
+ def provider(self) -> str:
24
+ return self._provider
25
+
26
+ async def exec(self, command: list[str], timeout_secs: int | None = None) -> ExecOutput:
27
+ return await self._transport.exec(self._id, command, self._provider, timeout_secs)
28
+
29
+ async def pause(self) -> None:
30
+ await self._transport.pause(self._id, self._provider)
31
+
32
+ async def unpause(self) -> None:
33
+ await self._transport.unpause(self._id, self._provider)
34
+
35
+ async def destroy(self) -> None:
36
+ await self._transport.destroy([self._id], self._provider)
37
+
38
+ async def copy_to(self, host_path: str, sandbox_path: str) -> None:
39
+ await self._transport.copy_to(self._id, host_path, sandbox_path, self._provider)
40
+
41
+ async def copy_from(self, sandbox_path: str, host_path: str) -> None:
42
+ await self._transport.copy_from(self._id, sandbox_path, host_path, self._provider)
43
+
44
+ async def __aenter__(self) -> AsyncSandbox:
45
+ return self
46
+
47
+ async def __aexit__(self, *exc: object) -> None:
48
+ await self.destroy()
49
+
50
+
51
+ class Sandbox:
52
+ def __init__(self, id: str, provider: str, transport: Transport):
53
+ self._inner = AsyncSandbox(id, provider, transport)
54
+
55
+ @property
56
+ def id(self) -> str:
57
+ return self._inner.id
58
+
59
+ @property
60
+ def provider(self) -> str:
61
+ return self._inner.provider
62
+
63
+ def exec(self, command: list[str], timeout_secs: int | None = None) -> ExecOutput:
64
+ return asyncio.run(self._inner.exec(command, timeout_secs))
65
+
66
+ def pause(self) -> None:
67
+ asyncio.run(self._inner.pause())
68
+
69
+ def unpause(self) -> None:
70
+ asyncio.run(self._inner.unpause())
71
+
72
+ def destroy(self) -> None:
73
+ asyncio.run(self._inner.destroy())
74
+
75
+ def copy_to(self, host_path: str, sandbox_path: str) -> None:
76
+ asyncio.run(self._inner.copy_to(host_path, sandbox_path))
77
+
78
+ def copy_from(self, sandbox_path: str, host_path: str) -> None:
79
+ asyncio.run(self._inner.copy_from(sandbox_path, host_path))
80
+
81
+ def __enter__(self) -> Sandbox:
82
+ return self
83
+
84
+ def __exit__(self, *exc: object) -> None:
85
+ self.destroy()
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from roche_sandbox.types import ExecOutput, SandboxConfig, SandboxInfo
6
+
7
+
8
+ class Transport(Protocol):
9
+ async def create(self, config: SandboxConfig, provider: str) -> str: ...
10
+ async def exec(self, sandbox_id: str, command: list[str], provider: str, timeout_secs: int | None = None) -> ExecOutput: ...
11
+ async def destroy(self, sandbox_ids: list[str], provider: str, all: bool = False) -> list[str]: ...
12
+ async def list(self, provider: str) -> list[SandboxInfo]: ...
13
+ async def pause(self, sandbox_id: str, provider: str) -> None: ...
14
+ async def unpause(self, sandbox_id: str, provider: str) -> None: ...
15
+ async def gc(self, provider: str, dry_run: bool = False, all: bool = False) -> list[str]: ...
16
+ async def copy_to(self, sandbox_id: str, host_path: str, sandbox_path: str, provider: str) -> None: ...
17
+ async def copy_from(self, sandbox_id: str, sandbox_path: str, host_path: str, provider: str) -> None: ...