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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cocoonsandbox_openai