adk-code-mode 0.1.0__tar.gz → 0.2.1__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.
Files changed (77) hide show
  1. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.github/workflows/release-please.yml +26 -1
  2. adk_code_mode-0.2.1/.release-please-manifest.json +3 -0
  3. adk_code_mode-0.2.1/CHANGELOG.md +37 -0
  4. adk_code_mode-0.2.1/PKG-INFO +469 -0
  5. adk_code_mode-0.2.1/README.md +442 -0
  6. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/docker/Dockerfile +6 -0
  7. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/pyproject.toml +9 -1
  8. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/sandbox-wheel/pyproject.toml +4 -2
  9. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/sandbox-wheel/src/adk_code_mode_sandbox/__about__.py +1 -1
  10. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/sandbox-wheel/src/adk_code_mode_sandbox/_entry.py +51 -9
  11. adk_code_mode-0.2.1/sandbox-wheel/src/adk_code_mode_sandbox/_http_server.py +292 -0
  12. {adk_code_mode-0.1.0/src/adk_code_mode/runtime → adk_code_mode-0.2.1/sandbox-wheel/src/adk_code_mode_sandbox}/protocol.py +23 -3
  13. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/__about__.py +1 -1
  14. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/__init__.py +10 -4
  15. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/callback.py +3 -3
  16. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/executor.py +54 -22
  17. adk_code_mode-0.2.1/src/adk_code_mode/runtime/__init__.py +10 -0
  18. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/runtime/base.py +8 -8
  19. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/runtime/docker.py +12 -9
  20. {adk_code_mode-0.1.0/sandbox-wheel/src/adk_code_mode_sandbox → adk_code_mode-0.2.1/src/adk_code_mode/runtime}/protocol.py +23 -3
  21. adk_code_mode-0.2.1/src/adk_code_mode/runtime/remote.py +200 -0
  22. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/_fake_runtime.py +5 -5
  23. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_agent_with_fake_llm.py +2 -2
  24. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_callback.py +7 -7
  25. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_docker_integration.py +3 -3
  26. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_host_loop.py +2 -2
  27. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_integration.py +13 -13
  28. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_protocol.py +6 -0
  29. adk_code_mode-0.2.1/tests/test_remote_integration.py +199 -0
  30. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/uv.lock +12 -3
  31. adk_code_mode-0.1.0/.release-please-manifest.json +0 -3
  32. adk_code_mode-0.1.0/CHANGELOG.md +0 -13
  33. adk_code_mode-0.1.0/PKG-INFO +0 -351
  34. adk_code_mode-0.1.0/README.md +0 -326
  35. adk_code_mode-0.1.0/src/adk_code_mode/runtime/__init__.py +0 -9
  36. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.githooks/pre-commit +0 -0
  37. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.githooks/pre-push +0 -0
  38. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.github/workflows/ci.yml +0 -0
  39. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.gitignore +0 -0
  40. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.prettierrc +0 -0
  41. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.vscode/settings.json +0 -0
  42. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/.zed/settings.json +0 -0
  43. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/AGENTS.md +0 -0
  44. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/CLAUDE.md +0 -0
  45. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/LICENSE +0 -0
  46. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/Makefile +0 -0
  47. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/release-please-config.json +0 -0
  48. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/sandbox-wheel/README.md +0 -0
  49. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/sandbox-wheel/src/adk_code_mode_sandbox/__init__.py +0 -0
  50. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/sandbox-wheel/src/adk_code_mode_sandbox/__main__.py +0 -0
  51. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/sandbox-wheel/src/adk_code_mode_sandbox/_rpc_client.py +0 -0
  52. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/scripts/install-git-hooks.sh +0 -0
  53. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/_artifact_tools.py +0 -0
  54. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/output.py +0 -0
  55. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/py.typed +0 -0
  56. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/tools/__init__.py +0 -0
  57. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/tools/catalog.py +0 -0
  58. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/tools/dispatcher.py +0 -0
  59. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/tools/namespacing.py +0 -0
  60. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/tools/normaliser.py +0 -0
  61. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/tools/stubs.py +0 -0
  62. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/workspace/__init__.py +0 -0
  63. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/src/adk_code_mode/workspace/files.py +0 -0
  64. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/__init__.py +0 -0
  65. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/_docker_helpers.py +0 -0
  66. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_artifact_tools.py +0 -0
  67. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_catalog.py +0 -0
  68. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_dispatcher.py +0 -0
  69. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_docker_image.py +0 -0
  70. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_namespacing.py +0 -0
  71. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_normaliser.py +0 -0
  72. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_output.py +0 -0
  73. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_runtime_docker.py +0 -0
  74. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_sandbox_rpc_client.py +0 -0
  75. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_stubs.py +0 -0
  76. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_version.py +0 -0
  77. {adk_code_mode-0.1.0 → adk_code_mode-0.2.1}/tests/test_workspace_files.py +0 -0
@@ -33,11 +33,18 @@ jobs:
33
33
  uses: astral-sh/setup-uv@v5
34
34
  - name: Build package
35
35
  run: uv build
36
+ - name: Build sandbox package
37
+ run: uv build --out-dir sandbox-wheel/dist sandbox-wheel
36
38
  - name: Upload distribution artifacts
37
39
  uses: actions/upload-artifact@v4
38
40
  with:
39
41
  name: dist
40
42
  path: dist/
43
+ - name: Upload sandbox distribution artifacts
44
+ uses: actions/upload-artifact@v4
45
+ with:
46
+ name: sandbox-dist
47
+ path: sandbox-wheel/dist/
41
48
 
42
49
  publish:
43
50
  name: Publish to PyPI
@@ -55,9 +62,27 @@ jobs:
55
62
  - name: Publish to PyPI
56
63
  uses: pypa/gh-action-pypi-publish@release/v1
57
64
 
65
+ publish-sandbox:
66
+ name: Publish sandbox package to PyPI
67
+ needs: build
68
+ runs-on: ubuntu-latest
69
+ environment: pypi
70
+ permissions:
71
+ id-token: write
72
+ steps:
73
+ - name: Download sandbox distribution artifacts
74
+ uses: actions/download-artifact@v4
75
+ with:
76
+ name: sandbox-dist
77
+ path: sandbox-wheel/dist/
78
+ - name: Publish sandbox package to PyPI
79
+ uses: pypa/gh-action-pypi-publish@release/v1
80
+ with:
81
+ packages-dir: sandbox-wheel/dist/
82
+
58
83
  docker-publish:
59
84
  name: Publish base image to GitHub Container Registry
60
- needs: [release-please, build, publish]
85
+ needs: [release-please, build, publish, publish-sandbox]
61
86
  if: needs.release-please.outputs.release_created == 'true'
62
87
  runs-on: ubuntu-latest
63
88
  permissions:
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.1"
3
+ }
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ ## [0.2.1](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.2.0...adk-code-mode-v0.2.1) (2026-05-05)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * rename `<tools>` tag to `<code-mode>` and update `CODE_MODE_SYSTEM_INSTRUCTION` ([0a06bfb](https://github.com/a2anet/adk-code-mode/commit/0a06bfbdf22db6e0226e477d0ed074a6cc4d1515))
9
+
10
+
11
+ ### Documentation
12
+
13
+ * update `README.md` with deployment instructions ([1dd7a95](https://github.com/a2anet/adk-code-mode/commit/1dd7a95197fd56231ba97182526a825baf18508a))
14
+
15
+ ## [0.2.0](https://github.com/a2anet/adk-code-mode/compare/adk-code-mode-v0.1.0...adk-code-mode-v0.2.0) (2026-05-03)
16
+
17
+
18
+ ### Features
19
+
20
+ * add `RemoteBackend`, sandbox hardening, and make `README.md` more concise ([a1a70ee](https://github.com/a2anet/adk-code-mode/commit/a1a70eec0686f164ce3796ba1d82bb82b7fbda59))
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * improve `CODE_MODE_SYSTEM_INSTRUCTION` ([0d00863](https://github.com/a2anet/adk-code-mode/commit/0d008636e2e273b438192f737e20e207c165a610))
26
+
27
+ ## 0.1.0 (2026-04-29)
28
+
29
+
30
+ ### Features
31
+
32
+ * ADK Code Mode: `CodeModeExecutor`, `DockerRuntime`, and more ([6ab1ac2](https://github.com/a2anet/adk-code-mode/commit/6ab1ac28236d21ecb740d8e920e08e4d9bd969c5))
33
+
34
+
35
+ ### Documentation
36
+
37
+ * shorten ADK code executor comparison table headers ([28116cf](https://github.com/a2anet/adk-code-mode/commit/28116cf9f40b3623c31782a55885fa5c4f8c6bbf))
@@ -0,0 +1,469 @@
1
+ Metadata-Version: 2.4
2
+ Name: adk-code-mode
3
+ Version: 0.2.1
4
+ Summary: A "Code Mode" Code Executor for ADK for agents to interact with tools, files, and custom packages with Python
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
+ License-File: LICENSE
11
+ Keywords: a2a,adk,agents,code-executor,code-mode
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: Implementation :: CPython
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: google-adk<2.0.0,>=1.0.0
21
+ Requires-Dist: google-genai>=1.0.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Requires-Dist: websockets>=14.0
24
+ Provides-Extra: docker
25
+ Requires-Dist: docker>=7.0.0; extra == 'docker'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # ADK Code Mode
29
+
30
+ A [Code Mode](https://blog.cloudflare.com/code-mode/) code executor for [Agent Development Kit (ADK)](https://github.com/google/adk-python).
31
+
32
+ The `CodeModeCodeExecutor` allows ADK agents to write Python code to call tools and read and write files.
33
+ Code runs inside a sandboxed container, and tools (and their credentials) are executed on the host.
34
+ The base image comes with the stdlib and can be extended with any Python package you want.
35
+ It also supports `input_files` and `output_files`, and the sandboxed container can list, load, and save ADK Artifacts.
36
+
37
+ Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) and Anthropic's [Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp).
38
+
39
+ ## ✨ Features
40
+
41
+ - **Call ADK tools from sandbox code** — imports against the `tools` package proxy back to the host and run through ADK's `before_tool` / `after_tool` / `on_error` callbacks and the plugin manager exactly as direct tool calls would.
42
+ - **Bake any Python package into the image** — extend the published base image with anything the model's code needs to `import`, no runtime `pip install` required.
43
+ - **Cross-turn persistence via ADK Artifacts** — `save_artifact` / `load_artifact` / `list_artifacts` are auto-injected and route through your configured `ArtifactService`.
44
+ - **Bounded stdout/stderr** — overflow lands in a session artifact instead of poisoning the prompt.
45
+ - **Production-ready remote sandbox** — `RemoteBackend` connects to an isolated, single-use container over WebSocket. Deploy on any cloud platform (Cloud Run, Fargate, ACI, Kubernetes, Fly.io, etc.).
46
+ - **Local development** — `UnsafeLocalDockerBackend` runs the sandbox against your local Docker daemon for fast iteration. **Not for production** — see [Safety](#-safety).
47
+
48
+ | | BuiltIn | AgentEngineSandbox | VertexAi | Container | Gke | CodeMode |
49
+ | ----------------------------------- | ------- | ------------------------------- | ------------------------------- | --------- | --- | ------------------------ |
50
+ | Call ADK tools from code | no | no | no | no | no | yes (with limitations) |
51
+ | Extra Python packages | no | no (more than stdlib but fixed) | no (more than stdlib but fixed) | yes | yes | yes |
52
+ | Variables are stateful | no | yes | yes | no | no | no |
53
+ | Input files | no | yes | yes | no | no | yes |
54
+ | Output files | no | yes | yes | no | no | yes |
55
+ | Storage | no | yes (via variables) | yes (via variables) | no | no | yes (via ADK Artifacts) |
56
+ | Local development version available | no | no | no | yes | yes | yes |
57
+ | Bounded stdout/stderr | no | no | no | no | no | yes (`max_output_chars`) |
58
+
59
+ ## 📦 Install
60
+
61
+ ```bash
62
+ pip install adk-code-mode
63
+ ```
64
+
65
+ Or with uv:
66
+
67
+ ```bash
68
+ uv add adk-code-mode
69
+ ```
70
+
71
+ For local development with `UnsafeLocalDockerBackend`, install the `docker` extra:
72
+
73
+ ```bash
74
+ pip install adk-code-mode[docker]
75
+ ```
76
+
77
+ Requires Python 3.10+. Local development requires [Docker](https://docs.docker.com/get-docker/); remote deployment only needs network access to the sandbox URL.
78
+
79
+ ## 🚀 Usage
80
+
81
+ Build a `CodeModeCodeExecutor`, then wire three things into the agent:
82
+
83
+ - **`CODE_MODE_SYSTEM_INSTRUCTION`** — append to the agent's `instruction`. Teaches the model how to write code blocks and use artifacts.
84
+ - **`code_mode_before_model_callback`** — set as `before_model_callback`. Injects the tool catalog (`<code-mode>` block) into the system prompt on every model turn.
85
+ - **`generate_content_config`** with `function_calling_config.mode="NONE"` — disables native function calling so the model writes Python instead of attempting tool calls that fail with `MALFORMED_FUNCTION_CALL` (since `tools=[]`).
86
+
87
+ ### Production (remote sandbox)
88
+
89
+ ```python
90
+ from google.adk.agents import LlmAgent
91
+ from google.genai import types as genai_types
92
+ from adk_code_mode import (
93
+ CODE_MODE_SYSTEM_INSTRUCTION,
94
+ CodeModeCodeExecutor,
95
+ RemoteBackend,
96
+ code_mode_before_model_callback,
97
+ )
98
+
99
+ executor = CodeModeCodeExecutor(
100
+ tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
101
+ backend=RemoteBackend(
102
+ url="https://sandbox-xyz.run.app", # your deployed sandbox URL
103
+ token="your-secret-token", # bearer token for auth
104
+ ),
105
+ )
106
+
107
+ root_agent = LlmAgent(
108
+ name="assistant",
109
+ model="gemini-2.5-pro",
110
+ instruction=f"You are a helpful assistant.\n\n{CODE_MODE_SYSTEM_INSTRUCTION}",
111
+ tools=[], # do NOT also bind tools here; the executor owns them.
112
+ code_executor=executor,
113
+ generate_content_config=genai_types.GenerateContentConfig(
114
+ tool_config=genai_types.ToolConfig(
115
+ function_calling_config=genai_types.FunctionCallingConfig(mode="NONE"),
116
+ ),
117
+ ),
118
+ before_model_callback=code_mode_before_model_callback(executor),
119
+ )
120
+ ```
121
+
122
+ ### Local development only
123
+
124
+ > **`UnsafeLocalDockerBackend` is not safe for production or multi-tenant use.** See [Safety](#-safety).
125
+
126
+ ```python
127
+ from adk_code_mode import (
128
+ CODE_MODE_SYSTEM_INSTRUCTION,
129
+ CodeModeCodeExecutor,
130
+ UnsafeLocalDockerBackend,
131
+ code_mode_before_model_callback,
132
+ )
133
+
134
+ executor = CodeModeCodeExecutor(
135
+ tools=[my_fn_tool, McpToolset(...), OpenAPIToolset(...)],
136
+ backend=UnsafeLocalDockerBackend(image="ghcr.io/a2anet/adk-code-mode:latest"),
137
+ )
138
+ ```
139
+
140
+ Inside the sandbox, the model writes code like:
141
+
142
+ ```python
143
+ from tools.slack import send_message
144
+ print(send_message(channel="C123", text="hi"))
145
+ ```
146
+
147
+ ## 🌐 Remote Deployment
148
+
149
+ **Every execution runs in its own container.** The container accepts exactly one WebSocket connection, executes the user's code, returns results, and exits. The hosting platform destroys the container after each request — no cross-tenant data leakage, no residual state. You **must** configure your platform for one container per request (`--concurrency 1` on Cloud Run, or equivalent).
150
+
151
+ Setting `ADK_CODE_MODE_CONTROL_HTTP=1` activates HTTP mode. The container:
152
+
153
+ 1. Starts a WebSocket server on port 8080 (configurable via `PORT`)
154
+ 2. Accepts exactly one connection (rejects further connections with 503)
155
+ 3. Receives tools and workspace as tar archives over binary WebSocket frames
156
+ 4. Sanitizes the environment (strips all env vars except a safe allowlist)
157
+ 5. Executes user code with tools proxied back to the host over the same WebSocket
158
+ 6. Returns stdout/stderr and updated workspace files
159
+ 7. Exits
160
+
161
+ ### Deploy to Cloud Run
162
+
163
+ ```bash
164
+ # Push the sandbox image to Artifact Registry
165
+ gcloud auth configure-docker <region>-docker.pkg.dev
166
+ docker pull --platform linux/amd64 ghcr.io/a2anet/adk-code-mode:latest
167
+ docker tag ghcr.io/a2anet/adk-code-mode:latest \
168
+ <region>-docker.pkg.dev/<project>/<repository>/adk-code-mode-sandbox:latest
169
+ docker push <region>-docker.pkg.dev/<project>/<repository>/adk-code-mode-sandbox:latest
170
+
171
+ # Create a VPC connector with no egress routes (blocks outbound network from sandbox)
172
+ gcloud compute networks create adk-sandbox-vpc --subnet-mode=custom
173
+ gcloud compute networks subnets create adk-sandbox-subnet \
174
+ --network=adk-sandbox-vpc \
175
+ --region=<region> \
176
+ --range=10.8.0.0/28
177
+ gcloud compute firewall-rules create adk-sandbox-deny-all-egress \
178
+ --network=adk-sandbox-vpc \
179
+ --direction=EGRESS \
180
+ --action=DENY \
181
+ --rules=all \
182
+ --priority=1000
183
+ gcloud compute networks vpc-access connectors create adk-sandbox-connector \
184
+ --region=<region> \
185
+ --subnet=adk-sandbox-subnet
186
+
187
+ # Deploy — note --concurrency 1 and --vpc-egress=all-traffic
188
+ gcloud run deploy adk-code-mode-sandbox \
189
+ --image <region>-docker.pkg.dev/<project>/<repository>/adk-code-mode-sandbox:latest \
190
+ --region <region> \
191
+ --port 8080 \
192
+ --cpu 1 \
193
+ --memory 1Gi \
194
+ --concurrency 1 \
195
+ --allow-unauthenticated \
196
+ --vpc-connector=adk-sandbox-connector \
197
+ --vpc-egress=all-traffic \
198
+ --set-env-vars "ADK_CODE_MODE_CONTROL_HTTP=1" \
199
+ --set-secrets "ADK_CODE_MODE_AUTH_TOKEN=<your-secret-name>:latest"
200
+ ```
201
+
202
+ Then in your agent:
203
+
204
+ ```python
205
+ RemoteBackend(
206
+ url="https://adk-code-mode-sandbox-xxxxx.run.app",
207
+ token="<your-secret>",
208
+ )
209
+ ```
210
+
211
+ > **`--concurrency 1` is critical for security.** Without this flag, Cloud Run may route multiple requests to the same container. The sandbox rejects the second connection, but the misconfiguration itself is a risk.
212
+
213
+ > **`--vpc-egress=all-traffic` with a deny-all VPC is critical for security.** Without it, user code can make arbitrary outbound requests — including hitting the GCP metadata endpoint (`169.254.169.254`) to steal the service account token, exfiltrating data, or scanning your VPC. The sandbox only needs to _accept_ inbound connections; it never needs outbound access.
214
+
215
+ ### Deploy on other platforms
216
+
217
+ The same pattern works on any platform that runs Docker containers as HTTP services (AWS Fargate/ECS, Azure Container Instances, Kubernetes, Fly.io, etc.):
218
+
219
+ 1. **One container per request.** Each container handles exactly one execution and exits.
220
+ 2. **Block all outbound network access.** Without egress restrictions, user code can exfiltrate data, access cloud metadata endpoints, or scan internal networks.
221
+ 3. **Set a read-only root filesystem** where the platform supports it (e.g., `readOnlyRootFilesystem: true` in Kubernetes). The sandbox only writes to `/workspace`.
222
+ 4. **Authenticate connections.** Set `ADK_CODE_MODE_AUTH_TOKEN` and layer platform-level auth (IAM, NetworkPolicy, security groups) on top.
223
+
224
+ Required env vars:
225
+
226
+ | Env var | Required | Default | Purpose |
227
+ | ------------------------------------ | -------- | ------- | -------------------------------- |
228
+ | `ADK_CODE_MODE_CONTROL_HTTP` | yes | — | Set to `1` to activate HTTP mode |
229
+ | `ADK_CODE_MODE_AUTH_TOKEN` | yes | — | Bearer token for WebSocket auth |
230
+ | `PORT` | no | `8080` | Listen port |
231
+ | `ADK_CODE_MODE_MAX_UPLOAD_TOOLS` | no | 100 MiB | Max tools tar archive size |
232
+ | `ADK_CODE_MODE_MAX_UPLOAD_WORKSPACE` | no | 100 MiB | Max workspace tar archive size |
233
+
234
+ The same upload limits (plus a download limit) are configurable on `RemoteBackend`:
235
+
236
+ ```python
237
+ RemoteBackend(
238
+ url="...",
239
+ token="...",
240
+ max_upload_tools_bytes=100 * 1024 * 1024, # 100 MiB (default)
241
+ max_upload_workspace_bytes=100 * 1024 * 1024, # 100 MiB (default)
242
+ max_download_workspace_bytes=100 * 1024 * 1024, # 100 MiB (default)
243
+ )
244
+ ```
245
+
246
+ ## 🗂️ Storage
247
+
248
+ Code Mode exposes two file surfaces:
249
+
250
+ - **`/workspace`** — per-run working directory. ADK `input_files` are staged here before code runs (`open("input.csv")` works). Files created or modified under `/workspace` are returned as `CodeExecutionResult.output_files` but are not re-hydrated next turn unless persisted via `save_artifact`.
251
+
252
+ - **ADK Artifacts** — persistent cross-turn storage. `CodeModeCodeExecutor` injects three tools into the catalog:
253
+
254
+ ```python
255
+ import json
256
+ from tools import save_artifact, load_artifact, list_artifacts
257
+
258
+ save_artifact(
259
+ filename="report.json",
260
+ content=json.dumps({"status": "ready"}),
261
+ mime_type="application/json",
262
+ )
263
+ print(list_artifacts())
264
+ report = load_artifact(filename="report.json")
265
+ if report is not None and report["kind"] == "text":
266
+ payload = json.loads(report["data"])
267
+ ```
268
+
269
+ Pass `include_artifact_tools=False` to opt out. To react when the model saves an artifact, pass `on_artifacts_saved`:
270
+
271
+ ```python
272
+ async def on_saved(invocation_context, delta):
273
+ # delta is {filename: version} for everything saved this turn.
274
+ ...
275
+
276
+ CodeModeCodeExecutor(tools=..., backend=..., on_artifacts_saved=on_saved)
277
+ ```
278
+
279
+ ## 🐳 Sandbox Image
280
+
281
+ The published base image (`ghcr.io/a2anet/adk-code-mode`) works as-is for tools whose execution is fully host-side. To bake in extra Python packages:
282
+
283
+ ```dockerfile
284
+ FROM ghcr.io/a2anet/adk-code-mode:latest
285
+ RUN pip install --no-cache-dir pandas==2.2.*
286
+ ```
287
+
288
+ The same image works for both `RemoteBackend` and `UnsafeLocalDockerBackend`. To build directly from this repo, run `make docker-image`.
289
+
290
+ ## ⚙️ Configuration
291
+
292
+ ### Catalog overflow
293
+
294
+ `max_catalog_chars` (default `50_000`) is a soft cap on the rendered tool catalog in the system prompt. When exceeded, the per-tool sections are replaced with a short note telling the model how to navigate `/tools/` from Python.
295
+
296
+ ```python
297
+ CodeModeCodeExecutor(tools=..., backend=..., max_catalog_chars=20_000)
298
+ ```
299
+
300
+ ### Output truncation
301
+
302
+ `max_output_chars` (default `50_000`) caps stdout and stderr handed back to the model. Overflow is saved as a session artifact at `code_mode/stdout/<execution-id>.txt`, and the model sees a head-and-tail view with a marker pointing to it.
303
+
304
+ ```python
305
+ from tools import load_artifact
306
+ spilled = load_artifact(filename="code_mode/stdout/<execution-id>.txt")
307
+ print(spilled["data"][-2000:])
308
+ ```
309
+
310
+ ### Code size limit
311
+
312
+ `max_code_chars` (default `1_000_000`) rejects oversized code payloads before starting a container.
313
+
314
+ ### Timeouts
315
+
316
+ `timeout_seconds` caps overall execution time; `per_tool_timeout_seconds` caps each individual tool call. Both default to `None` (relying on platform timeouts). Set them explicitly for defense in depth:
317
+
318
+ ```python
319
+ CodeModeCodeExecutor(
320
+ tools=...,
321
+ backend=...,
322
+ timeout_seconds=30,
323
+ per_tool_timeout_seconds=10,
324
+ )
325
+ ```
326
+
327
+ ## 🏗️ Architecture
328
+
329
+ **Host wheel (`adk-code-mode`).** Lives in the same process as your `LlmAgent`. The `before_model_callback` resolves tools, renders the catalog, and appends it to the system prompt. At execution time, it generates a `tools/` Python package of thin stubs, stages `input_files` into `/workspace`, and launches the sandbox.
330
+
331
+ **Sandbox wheel (`adk-code-mode-sandbox`).** Pre-installed in the container image. When model code calls a stub, it sends a JSON-Lines frame over the control connection; the host runs the real tool (with callbacks and plugins) and sends the result back.
332
+
333
+ The only things crossing the boundary are: code, tool call arguments, tool return values, and log frames.
334
+
335
+ | Backend | Transport | Multi-tenant safe? | When to use |
336
+ | -------------------------- | ---------------------- | ------------------ | ------------------------------- |
337
+ | `RemoteBackend` | WebSocket over HTTPS | **Yes** | Production — any cloud platform |
338
+ | `UnsafeLocalDockerBackend` | TCP over Docker bridge | No | Local development only |
339
+
340
+ ### What the model sees
341
+
342
+ Your `instruction` (containing `CODE_MODE_SYSTEM_INSTRUCTION`) followed by a `<code-mode>` block appended by the callback:
343
+
344
+ ```
345
+ …your instruction…
346
+
347
+ # How to execute code and use tools
348
+ Code you write in a fenced Python block (i.e. ```python) will be executed in a sandbox.
349
+ The Python Standard Library and a custom set of tools are available to you.
350
+ To see the result of your code, you need to print it.
351
+
352
+ For example, if you had the following tool:
353
+
354
+ ```
355
+ from tools.slack import send_message
356
+
357
+ def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any:
358
+ """Send a message to a Slack channel."""
359
+ ...
360
+ ```
361
+
362
+ To call the tool, you should write:
363
+
364
+ """
365
+ ```python
366
+ from tools.slack import send_message
367
+
368
+ print(send_message(channel="C123", text="hi"))
369
+ ```
370
+ """
371
+
372
+ # How to use files and variables in between executions
373
+ Code is executed in a new environment each time.
374
+ To list available Artifacts, use the `list_artifacts` tool. To save an Artifact, use the `save_artifact` tool, and to load an Artifact, use the `load_artifact` tool.
375
+
376
+ <code-mode>
377
+
378
+ # tools.slack
379
+
380
+ from tools.slack import list_channels, send_message
381
+
382
+ def list_channels() -> Any:
383
+ """List Slack channels."""
384
+ ...
385
+
386
+ def send_message(*, channel: str, text: str, thread_ts: str | None = ...) -> Any:
387
+ """Send a message to a Slack channel."""
388
+ ...
389
+
390
+ # tools
391
+
392
+ from tools import save_artifact, load_artifact, list_artifacts
393
+
394
+
395
+ </code-mode>
396
+ ```
397
+
398
+ When the rendered catalog exceeds `max_catalog_chars`, the per-tool sections are replaced with:
399
+
400
+ ```
401
+ <code-mode>
402
+ A `tools` package is available in the sandbox. List `/tools/` with
403
+ `pathlib.Path('/tools').iterdir()`. Each entry is either a `.py` file
404
+ (a top-level tool, importable as `from tools import <name>`) or a
405
+ subdirectory (a namespace, with tools importable as
406
+ `from tools.<namespace> import <name>`). To see a tool's signature and
407
+ docstring, read its `.py` file with `open(...).read()`.
408
+ </code-mode>
409
+ ```
410
+
411
+ Text and JSON-like MIME types travel as plain strings in artifact tools; binary content is base64-encoded. `load_artifact` returns `{"kind": "text" | "bytes", "data": str, "mime_type": str | None}`.
412
+
413
+ ## 🛡️ Safety
414
+
415
+ ### `RemoteBackend` (production)
416
+
417
+ `RemoteBackend` is designed for multi-tenant production use where untrusted users submit arbitrary Python code:
418
+
419
+ - **One container per execution.** Fresh container per request — no shared filesystem, memory, or processes.
420
+ - **Environment sanitization.** All env vars are stripped except a safe allowlist (`PATH`, `HOME`, `USER`, locale vars, Python config) before user code runs.
421
+ - **Credentials never enter the sandbox.** API keys, OAuth tokens, and connection strings stay in the host process. The container only receives tool results.
422
+ - **Bearer token authentication.** WebSocket connections without a valid token are rejected. Always set `ADK_CODE_MODE_AUTH_TOKEN` and layer platform-level auth on top.
423
+ - **Hardened tar extraction.** Path traversal (`../`), symlinks, hardlinks, and absolute paths are rejected.
424
+ - **Non-root user.** The sandbox runs as `sandbox`, not root.
425
+ - **Tool dispatch runs ADK's guard callbacks.** `before_tool`, `after_tool`, `on_error`, and the plugin manager all fire normally.
426
+ - **Bounded inputs and outputs.** See [Configuration](#-configuration) for `max_code_chars`, `max_output_chars`, `timeout_seconds`, `per_tool_timeout_seconds`, and upload/download size limits.
427
+
428
+ ### `UnsafeLocalDockerBackend` (development only)
429
+
430
+ > **Do not use in production or for multi-tenant workloads.**
431
+
432
+ Named "Unsafe" intentionally: it binds a TCP listener on `0.0.0.0`, communicates over unencrypted TCP, and relies on the local Docker daemon. It does still sanitize env vars, run as non-root, drop all Linux capabilities (`cap_drop=["ALL"]`), and mount the root filesystem read-only — but it is not a security boundary for untrusted users.
433
+
434
+ ### What this does NOT protect against
435
+
436
+ - **Network egress (if you skip egress restrictions).** The sandbox does NOT block outbound network by itself — configure this at the platform level. Without it, user code can exfiltrate data, access cloud metadata endpoints (`169.254.169.254`), or scan internal networks. See [Remote Deployment](#-remote-deployment).
437
+ - **Container runtime escapes.** Keep your container runtime patched.
438
+ - **Exfiltration through legitimate tool calls.** If your tool surface includes `send_email`, a prompt-injected payload could use it. Keep your tool surface least-privilege.
439
+ - **Denial of service within resource limits.** User code can consume its full CPU/memory allocation. Set platform-level limits.
440
+
441
+ ## ⚠️ Limitations
442
+
443
+ - **No credential-requesting tools.** Tools that need ADK to request credentials, confirmations, UI widgets, agent transfer, escalation, or that yield without an immediate response are rejected with a structured error.
444
+ - **No state across executions.** Variables don't survive between turns. Use `save_artifact` / `load_artifact` to persist, or `/workspace` within a single run.
445
+ - **No runtime package installation.** The sandbox ships with the Python Standard Library and the runtime's own dependencies only. Extra packages must be baked into the image at build time.
446
+
447
+ ## 🛠️ Development
448
+
449
+ ```bash
450
+ make install # uv sync --group dev
451
+ make ci # ruff + mypy + pytest
452
+ ```
453
+
454
+ Docker integration tests are opt-in:
455
+
456
+ ```bash
457
+ uv run pytest -m docker
458
+ ```
459
+
460
+ ## 📄 License
461
+
462
+ `adk-code-mode` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
463
+
464
+ ## 🤝 Join the A2A Net Community
465
+
466
+ 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!
467
+
468
+ - 🌍 Site: [A2A Net](https://a2anet.com)
469
+ - 🤖 Discord: [Join the Discord](https://discord.gg/674NGXpAjU)