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.
- adk_code_mode_sandbox/__about__.py +4 -0
- adk_code_mode_sandbox/__init__.py +6 -0
- adk_code_mode_sandbox/__main__.py +7 -0
- adk_code_mode_sandbox/_entry.py +155 -0
- adk_code_mode_sandbox/_rpc_client.py +129 -0
- adk_code_mode_sandbox/protocol.py +193 -0
- adk_code_mode_sandbox-0.1.0.dist-info/METADATA +34 -0
- adk_code_mode_sandbox-0.1.0.dist-info/RECORD +9 -0
- adk_code_mode_sandbox-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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,,
|