cocoonstack-sandbox-openai 0.1.0__py3-none-any.whl
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.
- cocoonsandbox_openai/__init__.py +23 -0
- cocoonsandbox_openai/adapter.py +179 -0
- cocoonstack_sandbox_openai-0.1.0.dist-info/METADATA +29 -0
- cocoonstack_sandbox_openai-0.1.0.dist-info/RECORD +6 -0
- cocoonstack_sandbox_openai-0.1.0.dist-info/WHEEL +5 -0
- cocoonstack_sandbox_openai-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""OpenAI Agents SDK sandbox provider for cocoon microVMs.
|
|
2
|
+
|
|
3
|
+
from agents.sandbox import SandboxRunConfig # SDK side
|
|
4
|
+
from cocoonsandbox_openai import CocoonSandboxClient, CocoonSandboxClientOptions
|
|
5
|
+
|
|
6
|
+
client = CocoonSandboxClient()
|
|
7
|
+
session = await client.create(options=CocoonSandboxClientOptions(
|
|
8
|
+
addr="10.0.0.5:7777", api_token="...", template="ghcr.io/cocoonstack/sandbox/rt:24.04"))
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .adapter import (
|
|
12
|
+
CocoonSandboxClient,
|
|
13
|
+
CocoonSandboxClientOptions,
|
|
14
|
+
CocoonSandboxSession,
|
|
15
|
+
CocoonSandboxSessionState,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CocoonSandboxClient",
|
|
20
|
+
"CocoonSandboxClientOptions",
|
|
21
|
+
"CocoonSandboxSession",
|
|
22
|
+
"CocoonSandboxSessionState",
|
|
23
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""OpenAI Agents SDK sandbox provider backed by cocoon microVMs: implements
|
|
2
|
+
the SDK's BaseSandboxClient/BaseSandboxSession pair on top of the sync
|
|
3
|
+
cocoonsandbox SDK, bridged with asyncio.to_thread. One session is one
|
|
4
|
+
claimed sandbox; delete releases it, resume reattaches by id + token."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import io
|
|
10
|
+
import socket
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
from agents.sandbox.manifest import Manifest
|
|
16
|
+
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
|
|
17
|
+
from agents.sandbox.session.sandbox_client import BaseSandboxClient, BaseSandboxClientOptions
|
|
18
|
+
from agents.sandbox.session.sandbox_session import SandboxSession
|
|
19
|
+
from agents.sandbox.session.sandbox_session_state import SandboxSessionState
|
|
20
|
+
from agents.sandbox.snapshot import SnapshotBase, SnapshotSpec, resolve_snapshot
|
|
21
|
+
from agents.sandbox.types import ExecResult, ExposedPortEndpoint, User
|
|
22
|
+
from cocoonsandbox import APIError, Client, Sandbox, SilkdError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CocoonSandboxClientOptions(BaseSandboxClientOptions):
|
|
26
|
+
"""Connection settings for a sandboxd node (or cluster entry node)."""
|
|
27
|
+
|
|
28
|
+
type: Literal["cocoon"] = "cocoon"
|
|
29
|
+
addr: str = "127.0.0.1:7777"
|
|
30
|
+
api_token: str = ""
|
|
31
|
+
template: str = "rt:24.04"
|
|
32
|
+
net: str = ""
|
|
33
|
+
ttl_seconds: int = 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CocoonSandboxSessionState(SandboxSessionState):
|
|
37
|
+
"""Everything needed to reattach: the claim's id, token, and owner."""
|
|
38
|
+
|
|
39
|
+
type: Literal["cocoon"] = "cocoon"
|
|
40
|
+
addr: str = ""
|
|
41
|
+
api_token: str = ""
|
|
42
|
+
sandbox_id: str = ""
|
|
43
|
+
sandbox_token: str = ""
|
|
44
|
+
owner: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CocoonSandboxSession(BaseSandboxSession):
|
|
48
|
+
"""One claimed microVM behind the Agents SDK session surface."""
|
|
49
|
+
|
|
50
|
+
state: CocoonSandboxSessionState
|
|
51
|
+
|
|
52
|
+
def __init__(self, *, state: CocoonSandboxSessionState) -> None:
|
|
53
|
+
self.state = state
|
|
54
|
+
self._proxies: list[socket.socket] = []
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_state(cls, state: CocoonSandboxSessionState) -> CocoonSandboxSession:
|
|
58
|
+
return cls(state=state)
|
|
59
|
+
|
|
60
|
+
async def _prepare_backend_workspace(self) -> None:
|
|
61
|
+
sb = self._sandbox()
|
|
62
|
+
await asyncio.to_thread(sb.mkdir, str(self.state.manifest.root), True)
|
|
63
|
+
|
|
64
|
+
async def _exec_internal(self, *command: str | Path, timeout: float | None = None) -> ExecResult:
|
|
65
|
+
# Bound the blocking SDK call by the socket timeout too, so a
|
|
66
|
+
# wait_for cancellation is matched by the worker thread actually
|
|
67
|
+
# unblocking (recv wakes) instead of lingering.
|
|
68
|
+
sb = self._sandbox(timeout=timeout)
|
|
69
|
+
argv = [str(part) for part in command]
|
|
70
|
+
stdout, stderr = bytearray(), bytearray()
|
|
71
|
+
|
|
72
|
+
def run() -> int:
|
|
73
|
+
return sb.run(argv, on_stdout=stdout.extend, on_stderr=stderr.extend)
|
|
74
|
+
|
|
75
|
+
code = await asyncio.wait_for(asyncio.to_thread(run), timeout=timeout)
|
|
76
|
+
return ExecResult(stdout=bytes(stdout), stderr=bytes(stderr), exit_code=code)
|
|
77
|
+
|
|
78
|
+
async def read(self, path: Path, *, user: str | User | None = None) -> io.IOBase:
|
|
79
|
+
sb = self._sandbox()
|
|
80
|
+
try:
|
|
81
|
+
data = await asyncio.to_thread(sb.read_file, str(self._abs(path)))
|
|
82
|
+
except SilkdError as exc:
|
|
83
|
+
if exc.kind == "not_found":
|
|
84
|
+
raise FileNotFoundError(str(path)) from exc
|
|
85
|
+
raise
|
|
86
|
+
return io.BytesIO(data)
|
|
87
|
+
|
|
88
|
+
async def write(self, path: Path, data: io.IOBase, *, user: str | User | None = None) -> None:
|
|
89
|
+
sb = self._sandbox()
|
|
90
|
+
payload = data.read()
|
|
91
|
+
await asyncio.to_thread(sb.write_file, str(self._abs(path)), payload)
|
|
92
|
+
|
|
93
|
+
async def running(self) -> bool:
|
|
94
|
+
try:
|
|
95
|
+
await asyncio.to_thread(self._sandbox().stat, "/")
|
|
96
|
+
except (APIError, SilkdError, OSError):
|
|
97
|
+
return False
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
async def persist_workspace(self) -> io.IOBase:
|
|
101
|
+
sb = self._sandbox()
|
|
102
|
+
tar = await asyncio.to_thread(sb.pull, str(self.state.manifest.root))
|
|
103
|
+
return io.BytesIO(tar)
|
|
104
|
+
|
|
105
|
+
async def hydrate_workspace(self, data: io.IOBase) -> None:
|
|
106
|
+
sb = self._sandbox()
|
|
107
|
+
await asyncio.to_thread(sb.push, str(self.state.manifest.root), data.read())
|
|
108
|
+
|
|
109
|
+
async def _resolve_exposed_port(self, port: int) -> ExposedPortEndpoint:
|
|
110
|
+
sb = self._sandbox()
|
|
111
|
+
listener = await asyncio.to_thread(sb.proxy_port, "127.0.0.1:0", port)
|
|
112
|
+
self._proxies.append(listener)
|
|
113
|
+
return ExposedPortEndpoint(host="127.0.0.1", port=listener.getsockname()[1], tls=False)
|
|
114
|
+
|
|
115
|
+
async def _shutdown_backend(self) -> None:
|
|
116
|
+
# The sandbox itself outlives shutdown (delete releases it); only the
|
|
117
|
+
# local port proxies belong to this process.
|
|
118
|
+
for listener in self._proxies:
|
|
119
|
+
listener.close()
|
|
120
|
+
self._proxies.clear()
|
|
121
|
+
|
|
122
|
+
def _sandbox(self, timeout: float | None = None) -> Sandbox:
|
|
123
|
+
s = self.state
|
|
124
|
+
client = Client(s.addr, api_token=s.api_token, timeout=timeout or 120.0)
|
|
125
|
+
return Sandbox(client=client, id=s.sandbox_id, token=s.sandbox_token, owner=s.owner or s.addr)
|
|
126
|
+
|
|
127
|
+
def _abs(self, path: Path | str) -> Path:
|
|
128
|
+
# The SDK hands paths as str or Path; a relative one roots at the
|
|
129
|
+
# manifest workspace.
|
|
130
|
+
path = Path(path)
|
|
131
|
+
if path.is_absolute():
|
|
132
|
+
return path
|
|
133
|
+
return Path(self.state.manifest.root) / path
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class CocoonSandboxClient(BaseSandboxClient[CocoonSandboxClientOptions]):
|
|
137
|
+
"""Claims a sandbox per session; delete releases, resume reattaches."""
|
|
138
|
+
|
|
139
|
+
async def create(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
snapshot: SnapshotSpec | SnapshotBase | None = None,
|
|
143
|
+
manifest: Manifest | None = None,
|
|
144
|
+
options: CocoonSandboxClientOptions,
|
|
145
|
+
) -> SandboxSession:
|
|
146
|
+
client = Client(options.addr, api_token=options.api_token)
|
|
147
|
+
sb = await asyncio.to_thread(
|
|
148
|
+
client.new, options.template, net=options.net, ttl_seconds=options.ttl_seconds
|
|
149
|
+
)
|
|
150
|
+
if manifest is None:
|
|
151
|
+
manifest = Manifest(root="/workspace")
|
|
152
|
+
session_id = uuid.uuid4()
|
|
153
|
+
state = CocoonSandboxSessionState(
|
|
154
|
+
session_id=session_id,
|
|
155
|
+
manifest=manifest,
|
|
156
|
+
snapshot=resolve_snapshot(snapshot, str(session_id)),
|
|
157
|
+
addr=options.addr,
|
|
158
|
+
api_token=options.api_token,
|
|
159
|
+
sandbox_id=sb.id,
|
|
160
|
+
sandbox_token=sb.token,
|
|
161
|
+
owner=sb.owner,
|
|
162
|
+
)
|
|
163
|
+
return self._wrap_session(CocoonSandboxSession.from_state(state))
|
|
164
|
+
|
|
165
|
+
async def delete(self, session: SandboxSession) -> SandboxSession:
|
|
166
|
+
inner = session._inner
|
|
167
|
+
if not isinstance(inner, CocoonSandboxSession):
|
|
168
|
+
raise TypeError("CocoonSandboxClient.delete expects a CocoonSandboxSession")
|
|
169
|
+
await inner._shutdown_backend() # close any local port proxies
|
|
170
|
+
await asyncio.to_thread(inner._sandbox().close) # 404-safe in the SDK
|
|
171
|
+
return session
|
|
172
|
+
|
|
173
|
+
async def resume(self, state: SandboxSessionState) -> SandboxSession:
|
|
174
|
+
if not isinstance(state, CocoonSandboxSessionState):
|
|
175
|
+
raise TypeError("CocoonSandboxClient.resume expects a CocoonSandboxSessionState")
|
|
176
|
+
return self._wrap_session(CocoonSandboxSession.from_state(state))
|
|
177
|
+
|
|
178
|
+
def deserialize_session_state(self, payload: dict[str, object]) -> SandboxSessionState:
|
|
179
|
+
return CocoonSandboxSessionState.model_validate(payload)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cocoonstack-sandbox-openai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenAI Agents SDK sandbox provider backed by cocoon microVMs
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: openai-agents>=0.17
|
|
9
|
+
Requires-Dist: cocoonstack-sandbox>=0.1
|
|
10
|
+
|
|
11
|
+
# cocoonstack-sandbox-openai
|
|
12
|
+
|
|
13
|
+
OpenAI Agents SDK sandbox provider backed by cocoon microVMs: implements
|
|
14
|
+
the SDK's `BaseSandboxClient`/`BaseSandboxSession` pair over the sync
|
|
15
|
+
`cocoonstack-sandbox` SDK, bridged with `asyncio.to_thread`.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from cocoonsandbox_openai import CocoonSandboxClient, CocoonSandboxClientOptions
|
|
19
|
+
|
|
20
|
+
options = CocoonSandboxClientOptions(addr="10.0.0.5:7777", api_token="...")
|
|
21
|
+
session = await CocoonSandboxClient().create(options=options)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
One session is one claimed sandbox; `delete` releases it, `resume`
|
|
25
|
+
reattaches from serialized state (id + token). Exec maps to streaming
|
|
26
|
+
`run`, workspace persist/hydrate to tar pull/push, exposed ports to local
|
|
27
|
+
port proxies. Requires Python >= 3.10 (the `openai-agents` floor).
|
|
28
|
+
|
|
29
|
+
Full reference: https://cocoonstack.github.io/sandbox/openai-adapter
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
cocoonsandbox_openai/__init__.py,sha256=Cx3fhPIUU59TvX0EP9cqnzUj9-JAWdSCnRweD0uQMk4,689
|
|
2
|
+
cocoonsandbox_openai/adapter.py,sha256=QCi0VE1tyy_TDWahJprBi7Ub8FGtLNa7d1N45LhLQ0M,7281
|
|
3
|
+
cocoonstack_sandbox_openai-0.1.0.dist-info/METADATA,sha256=YjvA3tgFpA6JOTCY5T9cM5Aeu9wscEZttjyxuKnmsqw,1118
|
|
4
|
+
cocoonstack_sandbox_openai-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
cocoonstack_sandbox_openai-0.1.0.dist-info/top_level.txt,sha256=20FDa0xTWo9JRIPNLzYob5noojOHfT5f01xPMzj5HIo,21
|
|
6
|
+
cocoonstack_sandbox_openai-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cocoonsandbox_openai
|