cocoonstack-sandbox-openai 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.
- cocoonstack_sandbox_openai-0.1.0/PKG-INFO +29 -0
- cocoonstack_sandbox_openai-0.1.0/README.md +19 -0
- cocoonstack_sandbox_openai-0.1.0/cocoonsandbox_openai/__init__.py +23 -0
- cocoonstack_sandbox_openai-0.1.0/cocoonsandbox_openai/adapter.py +179 -0
- cocoonstack_sandbox_openai-0.1.0/cocoonstack_sandbox_openai.egg-info/PKG-INFO +29 -0
- cocoonstack_sandbox_openai-0.1.0/cocoonstack_sandbox_openai.egg-info/SOURCES.txt +10 -0
- cocoonstack_sandbox_openai-0.1.0/cocoonstack_sandbox_openai.egg-info/dependency_links.txt +1 -0
- cocoonstack_sandbox_openai-0.1.0/cocoonstack_sandbox_openai.egg-info/requires.txt +2 -0
- cocoonstack_sandbox_openai-0.1.0/cocoonstack_sandbox_openai.egg-info/top_level.txt +1 -0
- cocoonstack_sandbox_openai-0.1.0/pyproject.toml +22 -0
- cocoonstack_sandbox_openai-0.1.0/setup.cfg +4 -0
- cocoonstack_sandbox_openai-0.1.0/tests/test_adapter.py +136 -0
|
@@ -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,19 @@
|
|
|
1
|
+
# cocoonstack-sandbox-openai
|
|
2
|
+
|
|
3
|
+
OpenAI Agents SDK sandbox provider backed by cocoon microVMs: implements
|
|
4
|
+
the SDK's `BaseSandboxClient`/`BaseSandboxSession` pair over the sync
|
|
5
|
+
`cocoonstack-sandbox` SDK, bridged with `asyncio.to_thread`.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from cocoonsandbox_openai import CocoonSandboxClient, CocoonSandboxClientOptions
|
|
9
|
+
|
|
10
|
+
options = CocoonSandboxClientOptions(addr="10.0.0.5:7777", api_token="...")
|
|
11
|
+
session = await CocoonSandboxClient().create(options=options)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
One session is one claimed sandbox; `delete` releases it, `resume`
|
|
15
|
+
reattaches from serialized state (id + token). Exec maps to streaming
|
|
16
|
+
`run`, workspace persist/hydrate to tar pull/push, exposed ports to local
|
|
17
|
+
port proxies. Requires Python >= 3.10 (the `openai-agents` floor).
|
|
18
|
+
|
|
19
|
+
Full reference: https://cocoonstack.github.io/sandbox/openai-adapter
|
|
@@ -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,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cocoonsandbox_openai/__init__.py
|
|
4
|
+
cocoonsandbox_openai/adapter.py
|
|
5
|
+
cocoonstack_sandbox_openai.egg-info/PKG-INFO
|
|
6
|
+
cocoonstack_sandbox_openai.egg-info/SOURCES.txt
|
|
7
|
+
cocoonstack_sandbox_openai.egg-info/dependency_links.txt
|
|
8
|
+
cocoonstack_sandbox_openai.egg-info/requires.txt
|
|
9
|
+
cocoonstack_sandbox_openai.egg-info/top_level.txt
|
|
10
|
+
tests/test_adapter.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cocoonsandbox_openai
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cocoonstack-sandbox-openai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "OpenAI Agents SDK sandbox provider backed by cocoon microVMs"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
dependencies = ["openai-agents>=0.17", "cocoonstack-sandbox>=0.1"]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.packages.find]
|
|
15
|
+
include = ["cocoonsandbox_openai*"]
|
|
16
|
+
|
|
17
|
+
[tool.ruff]
|
|
18
|
+
line-length = 120
|
|
19
|
+
target-version = "py310"
|
|
20
|
+
|
|
21
|
+
[tool.ruff.lint]
|
|
22
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Adapter unit tests: exercise the BaseSandboxSession hooks against an
|
|
2
|
+
in-process fake sandboxd (control plane over http.server, data plane over
|
|
3
|
+
the SDK's own conn). No real VM — this binds the adapter's mapping onto the
|
|
4
|
+
cocoonsandbox surface, not cocoon itself."""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import threading
|
|
10
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from cocoonsandbox import SilkdError
|
|
15
|
+
|
|
16
|
+
from cocoonsandbox_openai import CocoonSandboxClient, CocoonSandboxClientOptions, CocoonSandboxSessionState
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakeNode(BaseHTTPRequestHandler):
|
|
20
|
+
def do_POST(self):
|
|
21
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
22
|
+
_ = self.rfile.read(length)
|
|
23
|
+
if self.path == "/v1/claim":
|
|
24
|
+
self._reply(200, {"id": "sb_1", "token": "tok", "owner_addr": self.headers["Host"]})
|
|
25
|
+
elif self.path.endswith("/release"):
|
|
26
|
+
self._reply(204, None)
|
|
27
|
+
else:
|
|
28
|
+
self._reply(404, {"error": "no route"})
|
|
29
|
+
|
|
30
|
+
def _reply(self, code, payload):
|
|
31
|
+
self.send_response(code)
|
|
32
|
+
if payload is not None:
|
|
33
|
+
raw = json.dumps(payload).encode()
|
|
34
|
+
self.send_header("Content-Type", "application/json")
|
|
35
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
36
|
+
self.end_headers()
|
|
37
|
+
self.wfile.write(raw)
|
|
38
|
+
else:
|
|
39
|
+
self.end_headers()
|
|
40
|
+
|
|
41
|
+
def log_message(self, *args):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def node():
|
|
47
|
+
server = HTTPServer(("127.0.0.1", 0), FakeNode)
|
|
48
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
49
|
+
yield f"127.0.0.1:{server.server_port}"
|
|
50
|
+
server.shutdown()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_create_claims_and_state_round_trips(node):
|
|
54
|
+
async def go():
|
|
55
|
+
client = CocoonSandboxClient()
|
|
56
|
+
session = await client.create(options=CocoonSandboxClientOptions(addr=node, template="rt:24.04"))
|
|
57
|
+
inner = session._inner
|
|
58
|
+
assert inner.state.sandbox_id == "sb_1" and inner.state.sandbox_token == "tok"
|
|
59
|
+
|
|
60
|
+
# serialize → deserialize must reattach to the same claim.
|
|
61
|
+
payload = client.serialize_session_state(inner.state)
|
|
62
|
+
restored = client.deserialize_session_state(payload)
|
|
63
|
+
assert isinstance(restored, CocoonSandboxSessionState)
|
|
64
|
+
assert restored.sandbox_id == "sb_1" and restored.owner == node
|
|
65
|
+
|
|
66
|
+
await client.delete(session)
|
|
67
|
+
|
|
68
|
+
asyncio.run(go())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_exec_maps_stdio_and_exit(node, monkeypatch):
|
|
72
|
+
|
|
73
|
+
class FakeSandbox:
|
|
74
|
+
def __init__(self, **kw):
|
|
75
|
+
self.id = "sb_1"
|
|
76
|
+
|
|
77
|
+
def run(self, argv, on_stdout=None, on_stderr=None):
|
|
78
|
+
assert argv == ["echo", "hi"]
|
|
79
|
+
on_stdout(b"hi\n")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
async def go():
|
|
83
|
+
client = CocoonSandboxClient()
|
|
84
|
+
session = await client.create(options=CocoonSandboxClientOptions(addr=node))
|
|
85
|
+
inner = session._inner
|
|
86
|
+
monkeypatch.setattr(inner, "_sandbox", lambda timeout=None: FakeSandbox())
|
|
87
|
+
result = await inner._exec_internal("echo", "hi")
|
|
88
|
+
assert result.exit_code == 0 and result.stdout == b"hi\n"
|
|
89
|
+
assert result.ok()
|
|
90
|
+
|
|
91
|
+
asyncio.run(go())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_read_missing_maps_to_filenotfound(node, monkeypatch):
|
|
95
|
+
class FakeSandbox:
|
|
96
|
+
def read_file(self, path):
|
|
97
|
+
raise SilkdError("not_found", path)
|
|
98
|
+
|
|
99
|
+
async def go():
|
|
100
|
+
client = CocoonSandboxClient()
|
|
101
|
+
session = await client.create(options=CocoonSandboxClientOptions(addr=node))
|
|
102
|
+
inner = session._inner
|
|
103
|
+
monkeypatch.setattr(inner, "_sandbox", lambda timeout=None: FakeSandbox())
|
|
104
|
+
with pytest.raises(FileNotFoundError):
|
|
105
|
+
await inner.read(Path("/nope"))
|
|
106
|
+
|
|
107
|
+
asyncio.run(go())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_write_and_persist_use_tree_verbs(node, monkeypatch):
|
|
111
|
+
calls = {}
|
|
112
|
+
|
|
113
|
+
class FakeSandbox:
|
|
114
|
+
def write_file(self, path, data):
|
|
115
|
+
calls["write"] = (path, data)
|
|
116
|
+
|
|
117
|
+
def pull(self, path):
|
|
118
|
+
calls["pull"] = path
|
|
119
|
+
return b"tar-bytes"
|
|
120
|
+
|
|
121
|
+
def push(self, dest, data):
|
|
122
|
+
calls["push"] = (dest, data)
|
|
123
|
+
|
|
124
|
+
async def go():
|
|
125
|
+
client = CocoonSandboxClient()
|
|
126
|
+
session = await client.create(options=CocoonSandboxClientOptions(addr=node))
|
|
127
|
+
inner = session._inner
|
|
128
|
+
monkeypatch.setattr(inner, "_sandbox", lambda timeout=None: FakeSandbox())
|
|
129
|
+
await inner.write(Path("/workspace/a.txt"), io.BytesIO(b"body"))
|
|
130
|
+
assert calls["write"] == ("/workspace/a.txt", b"body")
|
|
131
|
+
tar = await inner.persist_workspace()
|
|
132
|
+
assert tar.read() == b"tar-bytes" and calls["pull"] == "/workspace"
|
|
133
|
+
await inner.hydrate_workspace(io.BytesIO(b"tar-in"))
|
|
134
|
+
assert calls["push"] == ("/workspace", b"tar-in")
|
|
135
|
+
|
|
136
|
+
asyncio.run(go())
|