adk-code-mode-sandbox 0.2.2__tar.gz → 0.3.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.
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/PKG-INFO +1 -1
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/__about__.py +1 -1
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/_entry.py +38 -9
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/_http_server.py +101 -68
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/protocol.py +6 -2
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/.gitignore +0 -0
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/README.md +0 -0
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/pyproject.toml +0 -0
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/__init__.py +0 -0
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/__main__.py +0 -0
- {adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adk-code-mode-sandbox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: In-sandbox runtime shim for adk-code-mode. Installs inside the container image the host runtime launches.
|
|
5
5
|
Project-URL: Documentation, https://github.com/A2ANet/adk-code-mode#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/A2ANet/adk-code-mode/issues
|
{adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/_entry.py
RENAMED
|
@@ -14,13 +14,16 @@ are supported, chosen by env var (checked in order):
|
|
|
14
14
|
- ``ADK_CODE_MODE_CONTROL_FD`` → inherited fd(s). Accepts one fd
|
|
15
15
|
(bidirectional) or two comma-separated fds (read,write).
|
|
16
16
|
|
|
17
|
-
Installs the RPC client, announces readiness, then
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Installs the RPC client, announces readiness once, then loops handling ``run``
|
|
18
|
+
frames until a ``shutdown`` frame (or disconnect) ends the turn. Each block
|
|
19
|
+
runs against one persistent globals dict (state carries across the turn) with
|
|
20
|
+
its stdout/stderr captured and shipped back in a per-block ``OutputFrame``; any
|
|
21
|
+
``tools.*`` calls funnel through the RPC client.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
24
|
from __future__ import annotations
|
|
23
25
|
|
|
26
|
+
import io
|
|
24
27
|
import os
|
|
25
28
|
import socket
|
|
26
29
|
import sys
|
|
@@ -31,6 +34,7 @@ from adk_code_mode_sandbox import _rpc_client
|
|
|
31
34
|
from adk_code_mode_sandbox._rpc_client import RpcClient
|
|
32
35
|
from adk_code_mode_sandbox.protocol import (
|
|
33
36
|
DoneFrame,
|
|
37
|
+
OutputFrame,
|
|
34
38
|
ReadyFrame,
|
|
35
39
|
RunFrame,
|
|
36
40
|
ShutdownFrame,
|
|
@@ -132,9 +136,12 @@ def _make_globals() -> dict[str, Any]:
|
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
|
|
135
|
-
def
|
|
136
|
-
"""Execute user code
|
|
137
|
-
|
|
139
|
+
def _exec_into(code: str, globs: dict[str, Any]) -> int:
|
|
140
|
+
"""Execute user code into ``globs``. Returns the exit code (0 ok, 1 on exception).
|
|
141
|
+
|
|
142
|
+
``globs`` is the persistent per-connection globals dict, so names bound by
|
|
143
|
+
one code block are visible to later blocks in the same turn.
|
|
144
|
+
"""
|
|
138
145
|
try:
|
|
139
146
|
compiled = compile(code, "<code-mode>", "exec")
|
|
140
147
|
except SyntaxError:
|
|
@@ -156,6 +163,26 @@ def _run_code(code: str) -> int:
|
|
|
156
163
|
return 0
|
|
157
164
|
|
|
158
165
|
|
|
166
|
+
def run_block(code: str, globs: dict[str, Any]) -> tuple[str, str, int]:
|
|
167
|
+
"""Run one code block with its stdout/stderr captured.
|
|
168
|
+
|
|
169
|
+
Redirects ``sys.stdout`` / ``sys.stderr`` to in-memory buffers for the
|
|
170
|
+
duration of the block so the captured text can be shipped back in an
|
|
171
|
+
``OutputFrame`` (both the TCP and HTTP transports emit one per block).
|
|
172
|
+
"""
|
|
173
|
+
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
174
|
+
capture_out = io.StringIO()
|
|
175
|
+
capture_err = io.StringIO()
|
|
176
|
+
sys.stdout = capture_out
|
|
177
|
+
sys.stderr = capture_err
|
|
178
|
+
try:
|
|
179
|
+
exit_code = _exec_into(code, globs)
|
|
180
|
+
finally:
|
|
181
|
+
sys.stdout = old_stdout
|
|
182
|
+
sys.stderr = old_stderr
|
|
183
|
+
return capture_out.getvalue(), capture_err.getvalue(), exit_code
|
|
184
|
+
|
|
185
|
+
|
|
159
186
|
def main() -> int:
|
|
160
187
|
if os.environ.get("ADK_CODE_MODE_CONTROL_HTTP"):
|
|
161
188
|
import asyncio
|
|
@@ -174,6 +201,9 @@ def main() -> int:
|
|
|
174
201
|
_prepare_workdir()
|
|
175
202
|
_sanitize_environ()
|
|
176
203
|
|
|
204
|
+
# One persistent globals dict + one /workspace for the whole connection, so
|
|
205
|
+
# variables and files carry across the turn's successive code blocks.
|
|
206
|
+
globs = _make_globals()
|
|
177
207
|
client.send(ReadyFrame())
|
|
178
208
|
|
|
179
209
|
while True:
|
|
@@ -181,10 +211,9 @@ def main() -> int:
|
|
|
181
211
|
if isinstance(frame, ShutdownFrame):
|
|
182
212
|
return 0
|
|
183
213
|
if isinstance(frame, RunFrame):
|
|
184
|
-
exit_code =
|
|
185
|
-
sys.stdout.flush()
|
|
186
|
-
sys.stderr.flush()
|
|
214
|
+
stdout_text, stderr_text, exit_code = run_block(frame.code, globs)
|
|
187
215
|
client.send(DoneFrame(exit_code=exit_code))
|
|
216
|
+
client.send(OutputFrame(stdout=stdout_text, stderr=stderr_text, exit_code=exit_code))
|
|
188
217
|
continue
|
|
189
218
|
# Any other frame kind is a host protocol error; keep going but log.
|
|
190
219
|
print(
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
"""HTTP/WebSocket server mode for the sandbox (
|
|
4
|
+
"""HTTP/WebSocket server mode for the sandbox (one connection per turn).
|
|
5
5
|
|
|
6
6
|
Activated by ``ADK_CODE_MODE_CONTROL_HTTP=1``. The container accepts exactly
|
|
7
|
-
one WebSocket connection
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
one WebSocket connection and holds it open for the whole turn, executing one or
|
|
8
|
+
more code blocks in-process with a persistent globals dict and ``/workspace``,
|
|
9
|
+
then exits when the turn ends (``ShutdownFrame`` or disconnect). The hosting
|
|
10
|
+
platform (Cloud Run, Fargate, K8s, etc.) creates a new container per turn.
|
|
10
11
|
|
|
11
12
|
Files (tools and workspace) are transferred as tar archives over binary
|
|
12
13
|
WebSocket frames; control frames use the standard JSON-lines protocol
|
|
@@ -14,11 +15,16 @@ over text WebSocket frames.
|
|
|
14
15
|
|
|
15
16
|
Connection protocol::
|
|
16
17
|
|
|
17
|
-
1. Client sends binary: tools tar.gz
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
1. Client sends binary: tools tar.gz (once)
|
|
19
|
+
2. Server sends text: ReadyFrame (once)
|
|
20
|
+
3. Per code block, in fixed frame order:
|
|
21
|
+
a. Client sends binary: workspace tar.gz (this block's inputs; may be empty)
|
|
22
|
+
b. Client sends text: RunFrame
|
|
23
|
+
c. Bidirectional text frames: JSON-lines tool-call protocol
|
|
24
|
+
d. Server sends text: DoneFrame
|
|
25
|
+
e. Server sends text: OutputFrame
|
|
26
|
+
f. Server sends binary: workspace tar.gz (updated files)
|
|
27
|
+
4. Client sends text: ShutdownFrame (or disconnects) to end the turn.
|
|
22
28
|
"""
|
|
23
29
|
|
|
24
30
|
from __future__ import annotations
|
|
@@ -29,7 +35,6 @@ import logging
|
|
|
29
35
|
import os
|
|
30
36
|
import queue as queue_mod
|
|
31
37
|
import shutil
|
|
32
|
-
import sys
|
|
33
38
|
import tarfile
|
|
34
39
|
import tempfile
|
|
35
40
|
from http import HTTPStatus
|
|
@@ -39,18 +44,22 @@ import websockets
|
|
|
39
44
|
import websockets.asyncio.server
|
|
40
45
|
|
|
41
46
|
from adk_code_mode_sandbox._entry import (
|
|
47
|
+
_make_globals,
|
|
42
48
|
_prepare_sys_path,
|
|
43
49
|
_prepare_workdir,
|
|
44
|
-
_run_code,
|
|
45
50
|
_sanitize_environ,
|
|
51
|
+
run_block,
|
|
46
52
|
)
|
|
47
53
|
from adk_code_mode_sandbox._rpc_client import RpcClient
|
|
48
54
|
from adk_code_mode_sandbox import _rpc_client
|
|
49
55
|
from adk_code_mode_sandbox.protocol import (
|
|
50
56
|
DoneFrame,
|
|
51
57
|
OutputFrame,
|
|
58
|
+
ProtocolError,
|
|
52
59
|
ReadyFrame,
|
|
53
60
|
RunFrame,
|
|
61
|
+
ShutdownFrame,
|
|
62
|
+
decode,
|
|
54
63
|
encode,
|
|
55
64
|
)
|
|
56
65
|
|
|
@@ -156,7 +165,7 @@ async def _handle_connection(ws: Any) -> None:
|
|
|
156
165
|
workspace_dir = tempfile.mkdtemp(prefix="adk-cm-ws-")
|
|
157
166
|
|
|
158
167
|
try:
|
|
159
|
-
# 1. Receive and extract tools tar
|
|
168
|
+
# 1. Receive and extract the tools tar (once for the whole connection).
|
|
160
169
|
tools_data = await ws.recv()
|
|
161
170
|
if not isinstance(tools_data, (bytes, bytearray)):
|
|
162
171
|
raise ValueError("expected binary frame for tools tar")
|
|
@@ -166,83 +175,107 @@ async def _handle_connection(ws: Any) -> None:
|
|
|
166
175
|
)
|
|
167
176
|
_extract_tar(tools_data, tools_dir)
|
|
168
177
|
|
|
169
|
-
# 2.
|
|
170
|
-
ws_data = await ws.recv()
|
|
171
|
-
if not isinstance(ws_data, (bytes, bytearray)):
|
|
172
|
-
raise ValueError("expected binary frame for workspace tar")
|
|
173
|
-
if len(ws_data) > max_workspace:
|
|
174
|
-
raise ValueError(
|
|
175
|
-
f"workspace archive ({len(ws_data):,} bytes) exceeds limit ({max_workspace:,} bytes)"
|
|
176
|
-
)
|
|
177
|
-
_extract_tar(ws_data, workspace_dir)
|
|
178
|
-
|
|
179
|
-
# 3. Sanitize environment before user code can read it
|
|
178
|
+
# 2. Sanitize environment, then prepare sys.path + a persistent /workspace.
|
|
180
179
|
_sanitize_environ()
|
|
181
|
-
|
|
182
|
-
# 4. Prepare sandbox (sys.path, workdir)
|
|
183
180
|
_prepare_sys_path(tools_dir)
|
|
184
181
|
_prepare_workdir(workspace_dir)
|
|
185
182
|
|
|
186
|
-
#
|
|
183
|
+
# 3. One persistent globals dict for the connection, so variables set in
|
|
184
|
+
# one block are visible to later blocks in the same turn.
|
|
185
|
+
globs = _make_globals()
|
|
187
186
|
loop = asyncio.get_running_loop()
|
|
188
|
-
bridge_reader = _WsBridgeReader()
|
|
189
|
-
bridge_writer = _WsBridgeWriter(loop, ws)
|
|
190
|
-
client = RpcClient(reader=bridge_reader, writer=bridge_writer)
|
|
191
|
-
_rpc_client.install(client)
|
|
192
187
|
|
|
193
|
-
#
|
|
194
|
-
|
|
188
|
+
# 4. Announce readiness once (carries the protocol version).
|
|
189
|
+
await ws.send(encode(ReadyFrame()).decode("utf-8"))
|
|
195
190
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
pump_task.cancel()
|
|
201
|
-
try:
|
|
202
|
-
await pump_task
|
|
203
|
-
except asyncio.CancelledError:
|
|
191
|
+
# 5. Run code blocks until the client ends the turn or disconnects.
|
|
192
|
+
while await _run_one_block(ws, loop, globs, workspace_dir, max_workspace):
|
|
204
193
|
pass
|
|
205
|
-
|
|
206
|
-
# 7. Send OutputFrame + updated workspace tar
|
|
207
|
-
output = OutputFrame(
|
|
208
|
-
stdout=stdout_text,
|
|
209
|
-
stderr=stderr_text,
|
|
210
|
-
exit_code=exit_code,
|
|
211
|
-
)
|
|
212
|
-
await ws.send(encode(output).decode("utf-8"))
|
|
213
|
-
await ws.send(_create_tar(workspace_dir))
|
|
214
|
-
|
|
215
194
|
finally:
|
|
216
195
|
shutil.rmtree(tools_base, ignore_errors=True)
|
|
217
196
|
shutil.rmtree(workspace_dir, ignore_errors=True)
|
|
218
197
|
|
|
219
198
|
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
199
|
+
async def _run_one_block(
|
|
200
|
+
ws: Any,
|
|
201
|
+
loop: asyncio.AbstractEventLoop,
|
|
202
|
+
globs: dict[str, Any],
|
|
203
|
+
workspace_dir: str,
|
|
204
|
+
max_workspace: int,
|
|
205
|
+
) -> bool:
|
|
206
|
+
"""Handle one code block. Returns ``False`` when the turn should end.
|
|
226
207
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
208
|
+
The per-block frame order is fixed — the binary workspace tar first, then
|
|
209
|
+
the text ``RunFrame`` — which is what keeps the WebSocket from desyncing
|
|
210
|
+
across blocks.
|
|
211
|
+
"""
|
|
212
|
+
# a. This block's workspace tar (binary), or a control frame ending the turn.
|
|
232
213
|
try:
|
|
233
|
-
|
|
214
|
+
first = await ws.recv()
|
|
215
|
+
except websockets.ConnectionClosed:
|
|
216
|
+
return False
|
|
217
|
+
if isinstance(first, str):
|
|
218
|
+
try:
|
|
219
|
+
frame = decode(first)
|
|
220
|
+
except ProtocolError:
|
|
221
|
+
return True
|
|
222
|
+
if isinstance(frame, ShutdownFrame):
|
|
223
|
+
return False
|
|
224
|
+
return True # unexpected control frame between blocks; ignore
|
|
225
|
+
if len(first) > max_workspace:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"workspace archive ({len(first):,} bytes) exceeds limit ({max_workspace:,} bytes)"
|
|
228
|
+
)
|
|
229
|
+
_extract_tar(first, workspace_dir)
|
|
230
|
+
|
|
231
|
+
# b. The RunFrame (text).
|
|
232
|
+
run_msg = await ws.recv()
|
|
233
|
+
if not isinstance(run_msg, str):
|
|
234
|
+
raise ValueError("expected text frame for RunFrame")
|
|
235
|
+
run_frame = decode(run_msg)
|
|
236
|
+
if not isinstance(run_frame, RunFrame):
|
|
237
|
+
raise ValueError(f"expected RunFrame, got {type(run_frame).__name__}")
|
|
238
|
+
|
|
239
|
+
# c. Run user code on a worker thread while pumping incoming text frames
|
|
240
|
+
# (tool results) into the RPC bridge. A fresh bridge/client per block
|
|
241
|
+
# keeps the reader-thread lifecycle simple; the globals dict persists.
|
|
242
|
+
bridge_reader = _WsBridgeReader()
|
|
243
|
+
bridge_writer = _WsBridgeWriter(loop, ws)
|
|
244
|
+
client = RpcClient(reader=bridge_reader, writer=bridge_writer)
|
|
245
|
+
_rpc_client.install(client)
|
|
246
|
+
pump_task = asyncio.create_task(_pump_ws_to_reader(ws, bridge_reader))
|
|
247
|
+
try:
|
|
248
|
+
stdout_text, stderr_text, exit_code = await loop.run_in_executor(
|
|
249
|
+
None, run_block, run_frame.code, globs
|
|
250
|
+
)
|
|
234
251
|
finally:
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
pump_task.cancel()
|
|
253
|
+
try:
|
|
254
|
+
await pump_task
|
|
255
|
+
except asyncio.CancelledError:
|
|
256
|
+
pass
|
|
237
257
|
|
|
238
|
-
|
|
239
|
-
|
|
258
|
+
# d-f. DoneFrame, OutputFrame, then the updated workspace tar (binary).
|
|
259
|
+
await ws.send(encode(DoneFrame(exit_code=exit_code)).decode("utf-8"))
|
|
260
|
+
await ws.send(
|
|
261
|
+
encode(OutputFrame(stdout=stdout_text, stderr=stderr_text, exit_code=exit_code)).decode(
|
|
262
|
+
"utf-8"
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
await ws.send(_create_tar(workspace_dir))
|
|
266
|
+
return True
|
|
240
267
|
|
|
241
268
|
|
|
242
269
|
async def _pump_ws_to_reader(ws: Any, reader: _WsBridgeReader) -> None:
|
|
243
|
-
"""Forward incoming WS text frames to the bridge reader queue.
|
|
270
|
+
"""Forward incoming WS text frames (tool results) to the bridge reader queue.
|
|
271
|
+
|
|
272
|
+
Runs only while a block executes. Uses an explicit ``recv()`` loop (rather
|
|
273
|
+
than ``async for``) so it can be cancelled and a fresh pump started for the
|
|
274
|
+
next block. Binary frames are not expected mid-block and are ignored.
|
|
275
|
+
"""
|
|
244
276
|
try:
|
|
245
|
-
|
|
277
|
+
while True:
|
|
278
|
+
msg = await ws.recv()
|
|
246
279
|
if isinstance(msg, str):
|
|
247
280
|
data = msg.encode("utf-8")
|
|
248
281
|
if not data.endswith(b"\n"):
|
{adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/protocol.py
RENAMED
|
@@ -22,7 +22,7 @@ import json
|
|
|
22
22
|
from dataclasses import asdict, dataclass, field
|
|
23
23
|
from typing import Any, Literal
|
|
24
24
|
|
|
25
|
-
PROTOCOL_VERSION =
|
|
25
|
+
PROTOCOL_VERSION = 2
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
FrameKind = Literal[
|
|
@@ -103,7 +103,11 @@ class DoneFrame:
|
|
|
103
103
|
|
|
104
104
|
@dataclass(frozen=True)
|
|
105
105
|
class OutputFrame:
|
|
106
|
-
"""
|
|
106
|
+
"""Sandbox → host. Captured stdout/stderr for one executed code block.
|
|
107
|
+
|
|
108
|
+
Sent once per block, right after that block's ``DoneFrame``. Under the
|
|
109
|
+
turn-scoped protocol (v2) a single connection produces one ``OutputFrame``
|
|
110
|
+
per ``RunFrame`` it handles."""
|
|
107
111
|
|
|
108
112
|
stdout: str = ""
|
|
109
113
|
stderr: str = ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/__init__.py
RENAMED
|
File without changes
|
{adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/__main__.py
RENAMED
|
File without changes
|
{adk_code_mode_sandbox-0.2.2 → adk_code_mode_sandbox-0.3.0}/src/adk_code_mode_sandbox/_rpc_client.py
RENAMED
|
File without changes
|