adk-code-mode-sandbox 1.2.0__tar.gz → 1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adk-code-mode-sandbox
3
- Version: 1.2.0
3
+ Version: 1.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
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- __version__ = "1.2.0" # x-release-please-version
4
+ __version__ = "1.3.0" # x-release-please-version
@@ -23,8 +23,10 @@ its stdout/stderr captured and shipped back in a per-block ``OutputFrame``; any
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ import importlib.metadata
26
27
  import io
27
28
  import os
29
+ import platform
28
30
  import socket
29
31
  import sys
30
32
  import traceback
@@ -40,6 +42,11 @@ from adk_code_mode_sandbox.protocol import (
40
42
  ShutdownFrame,
41
43
  )
42
44
 
45
+ # Distributions that ship with the sandbox itself rather than being installed by
46
+ # whoever built the image. Reporting them would tell the model it has tools it
47
+ # was never meant to use (`websockets` is this wheel's only dependency).
48
+ _PACKAGE_DENYLIST = frozenset({"adk-code-mode-sandbox", "websockets", "pip", "setuptools", "wheel"})
49
+
43
50
  CONTROL_FD_ENV = "ADK_CODE_MODE_CONTROL_FD"
44
51
  CONTROL_SOCKET_ENV = "ADK_CODE_MODE_CONTROL_SOCKET"
45
52
  CONTROL_TCP_ENV = "ADK_CODE_MODE_CONTROL_TCP"
@@ -50,6 +57,35 @@ TOOLS_DIR = os.environ.get(TOOLS_DIR_ENV, "/tools")
50
57
  WORKDIR = os.environ.get(WORKDIR_ENV, "/workspace")
51
58
 
52
59
 
60
+ def ready_frame() -> ReadyFrame:
61
+ """Build the ``ReadyFrame`` announcing this image's Python and packages.
62
+
63
+ Both transports send this, so image introspection stays in one place.
64
+ """
65
+ distributions = {
66
+ dist.name.casefold(): (dist.name, dist.version)
67
+ for dist in sorted(
68
+ importlib.metadata.distributions(),
69
+ key=lambda dist: (dist.name.casefold(), dist.name, dist.version),
70
+ )
71
+ if dist.name.casefold() not in _PACKAGE_DENYLIST
72
+ }
73
+ packages: dict[str, dict[str, str]] = {}
74
+ package_distributions = importlib.metadata.packages_distributions()
75
+ for import_name in sorted(package_distributions, key=lambda name: (name.casefold(), name)):
76
+ providers: dict[str, str] = {}
77
+ for distribution_name in sorted(
78
+ package_distributions[import_name],
79
+ key=lambda name: (name.casefold(), name),
80
+ ):
81
+ distribution = distributions.get(distribution_name.casefold())
82
+ if distribution is not None:
83
+ providers[distribution[0]] = distribution[1]
84
+ if providers:
85
+ packages[import_name] = providers
86
+ return ReadyFrame(python_version=platform.python_version(), packages=packages)
87
+
88
+
53
89
  def _open_control_streams() -> tuple[Any, Any]:
54
90
  """Open the host control pipe as a (reader, writer) pair."""
55
91
  tcp_endpoint = os.environ.get(CONTROL_TCP_ENV)
@@ -204,7 +240,7 @@ def main() -> int:
204
240
  # One persistent globals dict + one /workspace for the whole connection, so
205
241
  # variables and files carry across the turn's successive code blocks.
206
242
  globs = _make_globals()
207
- client.send(ReadyFrame())
243
+ client.send(ready_frame())
208
244
 
209
245
  while True:
210
246
  frame = client.recv()
@@ -49,6 +49,7 @@ from adk_code_mode_sandbox._entry import (
49
49
  _prepare_sys_path,
50
50
  _prepare_workdir,
51
51
  _sanitize_environ,
52
+ ready_frame,
52
53
  run_block,
53
54
  )
54
55
  from adk_code_mode_sandbox._rpc_client import RpcClient
@@ -57,7 +58,6 @@ from adk_code_mode_sandbox.protocol import (
57
58
  DoneFrame,
58
59
  OutputFrame,
59
60
  ProtocolError,
60
- ReadyFrame,
61
61
  RunFrame,
62
62
  ShutdownFrame,
63
63
  decode,
@@ -188,8 +188,9 @@ async def _handle_connection(ws: Any) -> None:
188
188
  globs = _make_globals()
189
189
  loop = asyncio.get_running_loop()
190
190
 
191
- # 4. Announce readiness once (carries the protocol version).
192
- await ws.send(encode(ReadyFrame()).decode("utf-8"))
191
+ # 4. Announce readiness once (carries the protocol version, Python
192
+ # version, and installed packages).
193
+ await ws.send(encode(ready_frame()).decode("utf-8"))
193
194
 
194
195
  # 5. Run code blocks until the client ends the turn or disconnects.
195
196
  while await _run_one_block(ws, loop, globs, workspace_dir, max_workspace):
@@ -39,10 +39,20 @@ FrameKind = Literal[
39
39
 
40
40
  @dataclass(frozen=True)
41
41
  class ReadyFrame:
42
- """Sandbox → host. Sent once after boot."""
42
+ """Sandbox → host. Sent once after boot.
43
+
44
+ ``python_version`` / ``packages`` describe the image the sandbox runs in so
45
+ the host can tell the model what it can import. ``packages`` maps each
46
+ top-level import name to the distributions that provide it and their
47
+ versions. Both are additive fields with defaults, so a host and a sandbox
48
+ on different releases still speak: ``decode`` drops unknown keys, and an
49
+ older sandbox that sends neither just leaves them empty.
50
+ """
43
51
 
44
52
  kind: Literal["ready"] = "ready"
45
53
  protocol_version: int = PROTOCOL_VERSION
54
+ python_version: str = ""
55
+ packages: dict[str, dict[str, str]] = field(default_factory=dict)
46
56
 
47
57
 
48
58
  @dataclass(frozen=True)