adk-code-mode-sandbox 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,4 @@
1
+ # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ __version__ = "0.1.0" # x-release-please-version
@@ -0,0 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from adk_code_mode_sandbox.__about__ import __version__
5
+
6
+ __all__ = ["__version__"]
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from adk_code_mode_sandbox._entry import main
5
+
6
+ if __name__ == "__main__":
7
+ raise SystemExit(main())
@@ -0,0 +1,155 @@
1
+ # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Sandbox entry point.
5
+
6
+ Invoked as ``python -m adk_code_mode_sandbox``. Three control-pipe transports
7
+ are supported, chosen by env var (checked in order):
8
+
9
+ - ``ADK_CODE_MODE_CONTROL_TCP`` → ``host:port`` TCP endpoint on the host
10
+ (used by the Docker runtime; works on mac/Windows/Linux uniformly).
11
+ - ``ADK_CODE_MODE_CONTROL_SOCKET`` → Unix domain socket path (used by the
12
+ in-process test ``FakeRuntime``; broken on Docker Desktop macOS so not
13
+ used by ``DockerRuntime``).
14
+ - ``ADK_CODE_MODE_CONTROL_FD`` → inherited fd(s). Accepts one fd
15
+ (bidirectional) or two comma-separated fds (read,write).
16
+
17
+ Installs the RPC client, announces readiness, then waits for a ``run`` frame.
18
+ On receipt, execs the user code with stdout/stderr on the normal streams and
19
+ any ``tools.*`` calls funnelling through the RPC client.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import socket
26
+ import sys
27
+ import traceback
28
+ from typing import Any
29
+
30
+ from adk_code_mode_sandbox import _rpc_client
31
+ from adk_code_mode_sandbox._rpc_client import RpcClient
32
+ from adk_code_mode_sandbox.protocol import (
33
+ DoneFrame,
34
+ ReadyFrame,
35
+ RunFrame,
36
+ ShutdownFrame,
37
+ )
38
+
39
+ CONTROL_FD_ENV = "ADK_CODE_MODE_CONTROL_FD"
40
+ CONTROL_SOCKET_ENV = "ADK_CODE_MODE_CONTROL_SOCKET"
41
+ CONTROL_TCP_ENV = "ADK_CODE_MODE_CONTROL_TCP"
42
+ CONTROL_TOKEN_ENV = "ADK_CODE_MODE_CONTROL_TOKEN"
43
+ TOOLS_DIR_ENV = "ADK_CODE_MODE_TOOLS_DIR"
44
+ WORKDIR_ENV = "ADK_CODE_MODE_WORKDIR"
45
+ TOOLS_DIR = os.environ.get(TOOLS_DIR_ENV, "/tools")
46
+ WORKDIR = os.environ.get(WORKDIR_ENV, "/workspace")
47
+
48
+
49
+ def _open_control_streams() -> tuple[Any, Any]:
50
+ """Open the host control pipe as a (reader, writer) pair."""
51
+ tcp_endpoint = os.environ.get(CONTROL_TCP_ENV)
52
+ if tcp_endpoint:
53
+ host, _, port_s = tcp_endpoint.rpartition(":")
54
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
55
+ sock.connect((host, int(port_s)))
56
+ token = os.environ.get(CONTROL_TOKEN_ENV, "")
57
+ sock.sendall(token.encode("ascii") + b"\n")
58
+ return sock.makefile("rb", buffering=0), sock.makefile("wb", buffering=0)
59
+ sock_path = os.environ.get(CONTROL_SOCKET_ENV)
60
+ if sock_path:
61
+ usock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
62
+ usock.connect(sock_path)
63
+ return usock.makefile("rb", buffering=0), usock.makefile("wb", buffering=0)
64
+ raw = os.environ.get(CONTROL_FD_ENV, "3")
65
+ if "," in raw:
66
+ read_fd_s, write_fd_s = raw.split(",", 1)
67
+ read_fd, write_fd = int(read_fd_s), int(write_fd_s)
68
+ else:
69
+ read_fd = write_fd = int(raw)
70
+ return (
71
+ os.fdopen(read_fd, "rb", buffering=0, closefd=False),
72
+ os.fdopen(write_fd, "wb", buffering=0, closefd=False),
73
+ )
74
+
75
+
76
+ def _prepare_sys_path() -> None:
77
+ """Make the generated tools directory importable from user code."""
78
+ if os.path.isdir(TOOLS_DIR):
79
+ # TOOLS_DIR is the package directory itself (normally /tools), not a
80
+ # parent that contains tools/. Add the parent so `import tools` resolves
81
+ # directly to /tools/__init__.py instead of requiring /tools/tools.
82
+ package_parent = os.path.dirname(os.path.abspath(TOOLS_DIR)) or os.sep
83
+ if package_parent not in sys.path:
84
+ sys.path.insert(0, package_parent)
85
+
86
+
87
+ def _prepare_workdir() -> None:
88
+ os.makedirs(WORKDIR, exist_ok=True)
89
+ os.chdir(WORKDIR)
90
+
91
+
92
+ def _make_globals() -> dict[str, Any]:
93
+ """Build the globals dict for ``exec``. Looks like a normal ``__main__``."""
94
+ return {
95
+ "__name__": "__main__",
96
+ "__doc__": None,
97
+ "__package__": None,
98
+ "__loader__": None,
99
+ "__spec__": None,
100
+ "__builtins__": __builtins__,
101
+ }
102
+
103
+
104
+ def _run_code(code: str) -> int:
105
+ """Execute user code. Returns the exit code (0 ok, 1 on exception)."""
106
+ globs = _make_globals()
107
+ try:
108
+ compiled = compile(code, "<code-mode>", "exec")
109
+ except SyntaxError:
110
+ traceback.print_exc()
111
+ return 1
112
+ try:
113
+ exec(compiled, globs)
114
+ except SystemExit as exc:
115
+ code_val = exc.code
116
+ if code_val is None:
117
+ return 0
118
+ if isinstance(code_val, int):
119
+ return code_val
120
+ print(code_val, file=sys.stderr)
121
+ return 1
122
+ except BaseException:
123
+ traceback.print_exc()
124
+ return 1
125
+ return 0
126
+
127
+
128
+ def main() -> int:
129
+ reader, writer = _open_control_streams()
130
+ client = RpcClient(reader=reader, writer=writer)
131
+ _rpc_client.install(client)
132
+ _prepare_sys_path()
133
+ _prepare_workdir()
134
+
135
+ client.send(ReadyFrame())
136
+
137
+ while True:
138
+ frame = client.recv()
139
+ if isinstance(frame, ShutdownFrame):
140
+ return 0
141
+ if isinstance(frame, RunFrame):
142
+ exit_code = _run_code(frame.code)
143
+ sys.stdout.flush()
144
+ sys.stderr.flush()
145
+ client.send(DoneFrame(exit_code=exit_code))
146
+ continue
147
+ # Any other frame kind is a host protocol error; keep going but log.
148
+ print(
149
+ f"[adk-code-mode-sandbox] unexpected frame kind: {type(frame).__name__}",
150
+ file=sys.stderr,
151
+ )
152
+
153
+
154
+ if __name__ == "__main__":
155
+ raise SystemExit(main())
@@ -0,0 +1,129 @@
1
+ # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Blocking RPC client used by generated tool stubs inside the sandbox.
5
+
6
+ All tool stubs funnel through ``call(name, args)``. The global client instance
7
+ is installed by ``_entry`` before user code runs. Stubs import it lazily:
8
+
9
+ .. code-block:: python
10
+
11
+ from adk_code_mode_sandbox._rpc_client import call
12
+ result = call("slack.send_message", {"channel": "C1", "text": "hi"})
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections import deque
18
+ import threading
19
+ import uuid
20
+ from typing import Any
21
+
22
+ from adk_code_mode_sandbox.protocol import (
23
+ Frame,
24
+ ProtocolError,
25
+ ToolCallFrame,
26
+ ToolResultFrame,
27
+ decode,
28
+ encode,
29
+ )
30
+
31
+
32
+ class ToolError(RuntimeError):
33
+ """Raised inside the sandbox when a host-side tool call fails."""
34
+
35
+ def __init__(self, error_type: str, message: str, trace: str | None = None) -> None:
36
+ super().__init__(f"{error_type}: {message}")
37
+ self.error_type = error_type
38
+ self.trace = trace
39
+
40
+
41
+ class RpcClient:
42
+ """Synchronous, thread-safe RPC client over the sandbox control pipe.
43
+
44
+ The control pipe is a pair of unbuffered streams: ``_out`` for frames going
45
+ to the host and ``_in`` for frames coming back. Both are line-oriented.
46
+ """
47
+
48
+ def __init__(self, *, reader: Any, writer: Any) -> None:
49
+ self._reader = reader
50
+ self._writer = writer
51
+ self._write_lock = threading.Lock()
52
+ self._state_lock = threading.Lock()
53
+ self._state_changed = threading.Condition(self._state_lock)
54
+ self._general_frames: deque[Frame] = deque()
55
+ self._pending_results: dict[str, ToolResultFrame] = {}
56
+ self._closed_error: Exception | None = None
57
+ self._reader_thread = threading.Thread(target=self._read_loop, daemon=True)
58
+ self._reader_thread.start()
59
+
60
+ def send(self, frame: Frame) -> None:
61
+ payload = encode(frame)
62
+ with self._write_lock:
63
+ self._writer.write(payload)
64
+ self._writer.flush()
65
+
66
+ def recv(self) -> Frame:
67
+ with self._state_changed:
68
+ while not self._general_frames and self._closed_error is None:
69
+ self._state_changed.wait()
70
+ if self._general_frames:
71
+ return self._general_frames.popleft()
72
+ raise self._closed_error or ProtocolError("control pipe closed by host")
73
+
74
+ def call(self, name: str, args: dict[str, Any], timeout: float | None = None) -> Any:
75
+ call_id = uuid.uuid4().hex
76
+ self.send(ToolCallFrame(id=call_id, name=name, args=args, timeout=timeout))
77
+ with self._state_changed:
78
+ while call_id not in self._pending_results and self._closed_error is None:
79
+ self._state_changed.wait()
80
+ frame = self._pending_results.pop(call_id, None)
81
+ if frame is None:
82
+ raise self._closed_error or ProtocolError("control pipe closed by host")
83
+ if frame.ok:
84
+ return frame.value
85
+ err = frame.error
86
+ if err is None:
87
+ raise ToolError("Error", "tool call failed without error payload")
88
+ raise ToolError(err.type, err.message, err.trace)
89
+
90
+ def _read_loop(self) -> None:
91
+ try:
92
+ while True:
93
+ line = self._reader.readline()
94
+ if not line:
95
+ raise ProtocolError("control pipe closed by host")
96
+ frame = decode(line)
97
+ with self._state_changed:
98
+ if isinstance(frame, ToolResultFrame):
99
+ self._pending_results[frame.id] = frame
100
+ else:
101
+ self._general_frames.append(frame)
102
+ self._state_changed.notify_all()
103
+ except Exception as exc:
104
+ with self._state_changed:
105
+ self._closed_error = exc
106
+ self._state_changed.notify_all()
107
+
108
+
109
+ _CLIENT: RpcClient | None = None
110
+
111
+
112
+ def install(client: RpcClient) -> None:
113
+ """Set the process-wide RPC client. Called by ``_entry`` at boot."""
114
+ global _CLIENT
115
+ _CLIENT = client
116
+
117
+
118
+ def get() -> RpcClient:
119
+ if _CLIENT is None:
120
+ raise RuntimeError("RPC client not installed; sandbox is not in a valid state")
121
+ return _CLIENT
122
+
123
+
124
+ def call(name: str, args: dict[str, Any], timeout: float | None = None) -> Any:
125
+ """Convenience: invoke a tool through the installed client."""
126
+ return get().call(name, args, timeout)
127
+
128
+
129
+ __all__ = ["RpcClient", "ToolError", "call", "get", "install"]
@@ -0,0 +1,193 @@
1
+ # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """JSON-lines wire protocol between the host and the sandbox.
5
+
6
+ This module is the shared kernel: a byte-identical copy lives in the
7
+ ``adk_code_mode_sandbox`` wheel. A CI check enforces the two files are
8
+ identical on every release.
9
+
10
+ Design rules:
11
+
12
+ - Zero dependencies outside the Python standard library. The sandbox wheel
13
+ must remain stdlib-only so the container image does not pull ADK / google-genai.
14
+ - Frames are JSON objects, one per newline-terminated UTF-8 line.
15
+ - Every frame carries a ``kind`` discriminator. Correlated pairs (e.g.
16
+ ``tool_call`` / ``tool_result``) carry a string ``id``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from dataclasses import asdict, dataclass, field
23
+ from typing import Any, Literal
24
+
25
+ PROTOCOL_VERSION = 1
26
+
27
+
28
+ FrameKind = Literal[
29
+ "ready",
30
+ "run",
31
+ "tool_call",
32
+ "tool_result",
33
+ "log",
34
+ "done",
35
+ "shutdown",
36
+ ]
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ReadyFrame:
41
+ """Sandbox → host. Sent once after boot."""
42
+
43
+ kind: Literal["ready"] = "ready"
44
+ protocol_version: int = PROTOCOL_VERSION
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class RunFrame:
49
+ """Host → sandbox. Instructs the sandbox to execute the given code."""
50
+
51
+ code: str = ""
52
+ kind: Literal["run"] = "run"
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class ToolCallFrame:
57
+ """Sandbox → host. Request to invoke a host-side tool."""
58
+
59
+ id: str = ""
60
+ name: str = ""
61
+ args: dict[str, Any] = field(default_factory=dict)
62
+ timeout: float | None = None
63
+ kind: Literal["tool_call"] = "tool_call"
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class ToolErrorPayload:
68
+ """The structured error body inside a failing ``tool_result`` frame."""
69
+
70
+ type: str
71
+ message: str
72
+ trace: str | None = None
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class ToolResultFrame:
77
+ """Host → sandbox. Result for a previously issued ``tool_call``."""
78
+
79
+ id: str = ""
80
+ ok: bool = True
81
+ value: Any = None
82
+ error: ToolErrorPayload | None = None
83
+ kind: Literal["tool_result"] = "tool_result"
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class LogFrame:
88
+ """Sandbox → host. Optional structured log line."""
89
+
90
+ level: str = "info"
91
+ msg: str = ""
92
+ kind: Literal["log"] = "log"
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class DoneFrame:
97
+ """Sandbox → host. User code finished. Host drains stdout/stderr next."""
98
+
99
+ exit_code: int = 0
100
+ kind: Literal["done"] = "done"
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class ShutdownFrame:
105
+ """Host → sandbox. Graceful shutdown."""
106
+
107
+ kind: Literal["shutdown"] = "shutdown"
108
+
109
+
110
+ Frame = (
111
+ ReadyFrame | RunFrame | ToolCallFrame | ToolResultFrame | LogFrame | DoneFrame | ShutdownFrame
112
+ )
113
+
114
+
115
+ _KIND_TO_CLS: dict[str, type] = {
116
+ "ready": ReadyFrame,
117
+ "run": RunFrame,
118
+ "tool_call": ToolCallFrame,
119
+ "tool_result": ToolResultFrame,
120
+ "log": LogFrame,
121
+ "done": DoneFrame,
122
+ "shutdown": ShutdownFrame,
123
+ }
124
+
125
+
126
+ class ProtocolError(Exception):
127
+ """Raised when a frame cannot be decoded or is semantically invalid."""
128
+
129
+
130
+ def encode(frame: Frame) -> bytes:
131
+ """Serialise a frame to a newline-terminated UTF-8 byte string."""
132
+ payload = asdict(frame)
133
+ return (json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8")
134
+
135
+
136
+ def decode(line: bytes | str) -> Frame:
137
+ """Deserialise a single frame line.
138
+
139
+ Accepts either bytes or str; lines may or may not have a trailing newline.
140
+ """
141
+ if isinstance(line, (bytes, bytearray)):
142
+ text = line.decode("utf-8")
143
+ else:
144
+ text = line
145
+ text = text.rstrip("\r\n")
146
+ if not text:
147
+ raise ProtocolError("empty frame")
148
+ try:
149
+ obj = json.loads(text)
150
+ except json.JSONDecodeError as exc:
151
+ raise ProtocolError(f"invalid JSON: {exc}") from exc
152
+ if not isinstance(obj, dict):
153
+ raise ProtocolError(f"frame must be a JSON object, got {type(obj).__name__}")
154
+ kind = obj.get("kind")
155
+ if not isinstance(kind, str):
156
+ raise ProtocolError("frame missing string `kind`")
157
+ cls = _KIND_TO_CLS.get(kind)
158
+ if cls is None:
159
+ raise ProtocolError(f"unknown frame kind: {kind!r}")
160
+ if cls is ToolResultFrame and obj.get("error") is not None:
161
+ err = obj["error"]
162
+ if not isinstance(err, dict):
163
+ raise ProtocolError("tool_result.error must be an object")
164
+ obj = dict(obj)
165
+ obj["error"] = ToolErrorPayload(
166
+ type=str(err.get("type", "Error")),
167
+ message=str(err.get("message", "")),
168
+ trace=err.get("trace"),
169
+ )
170
+ valid_fields = {f for f in cls.__dataclass_fields__} # type: ignore[attr-defined]
171
+ filtered = {k: v for k, v in obj.items() if k in valid_fields}
172
+ try:
173
+ return cls(**filtered) # type: ignore[no-any-return]
174
+ except TypeError as exc:
175
+ raise ProtocolError(f"invalid frame body for kind {kind!r}: {exc}") from exc
176
+
177
+
178
+ __all__ = [
179
+ "PROTOCOL_VERSION",
180
+ "FrameKind",
181
+ "Frame",
182
+ "ReadyFrame",
183
+ "RunFrame",
184
+ "ToolCallFrame",
185
+ "ToolResultFrame",
186
+ "ToolErrorPayload",
187
+ "LogFrame",
188
+ "DoneFrame",
189
+ "ShutdownFrame",
190
+ "ProtocolError",
191
+ "encode",
192
+ "decode",
193
+ ]
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: adk-code-mode-sandbox
3
+ Version: 0.1.0
4
+ Summary: In-sandbox runtime shim for adk-code-mode. Stdlib-only. Installs inside the container image the host runtime launches.
5
+ Project-URL: Documentation, https://github.com/A2ANet/adk-code-mode#readme
6
+ Project-URL: Issues, https://github.com/A2ANet/adk-code-mode/issues
7
+ Project-URL: Source, https://github.com/A2ANet/adk-code-mode
8
+ Author-email: A2A Net <hello@a2anet.com>
9
+ License-Expression: Apache-2.0
10
+ Keywords: adk,code-mode,sandbox
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ADK Code Mode Sandbox
22
+
23
+ In-sandbox shim for [`adk-code-mode`](https://github.com/A2ANet/adk-code-mode). The host package launches a container that has this wheel installed, then talks to it over a TCP control connection so model code can call ADK tools that run on the host.
24
+
25
+ ## 📄 License
26
+
27
+ `adk-code-mode-sandbox` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
28
+
29
+ ## 🤝 Join the A2A Net Community
30
+
31
+ A2A Net is a site to find and share AI agents and open-source community. Join to share your A2A agents, ask questions, stay up-to-date with the latest A2A news, be the first to hear about open-source releases, tutorials, and more!
32
+
33
+ - 🌍 Site: [A2A Net](https://a2anet.com)
34
+ - 🤖 Discord: [Join the Discord](https://discord.gg/674NGXpAjU)
@@ -0,0 +1,9 @@
1
+ adk_code_mode_sandbox/__about__.py,sha256=0H6GNBbcL8zIcc4LIjpBwyFNtiAFeTkZ5LAqPOl2X4o,156
2
+ adk_code_mode_sandbox/__init__.py,sha256=7Mn8z9QKZHsNlw4XGbsj41lkkrgSEyNlTbSNeBdXnyk,189
3
+ adk_code_mode_sandbox/__main__.py,sha256=lLkW6NeCK3IqjnQ8-DXLiqoWHSk9nW5lu1DoU9myr88,209
4
+ adk_code_mode_sandbox/_entry.py,sha256=_7CJ6vAvtFBERJ-2CBaz6KfBcSFoathLPi46k7YGnE0,5220
5
+ adk_code_mode_sandbox/_rpc_client.py,sha256=_zZBUXq3AOTEN4qHV35tiCgGq1BLMCMb6xXNlkSuczg,4562
6
+ adk_code_mode_sandbox/protocol.py,sha256=gybzBlaedv46mTMUF7s5Ux-Hsm4B_j42FnkOPJARLkk,5147
7
+ adk_code_mode_sandbox-0.1.0.dist-info/METADATA,sha256=XIZqJX7XzsD31xb6wGRezuNgC6y5XAlsH92hC0dUdxI,1727
8
+ adk_code_mode_sandbox-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ adk_code_mode_sandbox-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any