Skip to content

Commit 8c01c1a

Browse files
opencolinclaude
andcommitted
Add AG2 sandbox extension example
Covers AG2 1.0's native Tenki backend (ag2.extensions.tenki, merged in ag2ai/ag2#3096): one TenkiEnvironment powering SandboxShellTool and SandboxCodeTool, with a live no-LLM-key verify that exercises the real factory open/exec/put_file/aclose path. Pins ag2 to the merge commit since the 1.0.1 release predates the extension. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1c86a86 commit 8c01c1a

5 files changed

Lines changed: 196 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Give an agent a sandboxed place to run the code it writes.
5353
| [OpenAI Agents SDK](examples/openai-agents-sdk/) | Sandboxed execution as an agent tool |
5454
| [LlamaIndex](examples/llamaindex/) | Sandboxed execution as a `FunctionTool` |
5555
| [Hugging Face smolagents](examples/smolagents/) | Remote Python executor for a `CodeAgent` |
56+
| [AG2](examples/ag2-tenki/) | Native sandbox backend for shell + code tools |
5657

5758
### Developer tools
5859

examples/ag2-tenki/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# AG2 agents with a Tenki sandbox
2+
3+
[AG2](https://github.com/ag2ai/ag2) 1.0 is a ground-up rewrite of the framework (`from ag2 import Agent` — not the old AutoGen 0.x `ConversableAgent` API) with first-class sandbox tools: `SandboxShellTool` and `SandboxCodeTool` run whatever the model writes inside a pluggable sandbox backend. This example uses AG2's **native Tenki backend**`ag2.extensions.tenki` (merged in [ag2ai/ag2#3096](https://github.com/ag2ai/ag2/pull/3096), maintained with Tenki) — so every shell command and code snippet executes in an isolated [Tenki](https://tenki.cloud) microVM instead of on your machine.
4+
5+
## The agent
6+
7+
One `TenkiEnvironment` powers both tools:
8+
9+
```python
10+
import asyncio
11+
12+
from ag2 import Agent
13+
from ag2.config import AnthropicConfig
14+
from ag2.extensions.tenki import TenkiEnvironment
15+
from ag2.tools import SandboxCodeTool, SandboxShellTool
16+
17+
async def main() -> None:
18+
env = TenkiEnvironment()
19+
async with env:
20+
agent = Agent(
21+
"developer",
22+
config=AnthropicConfig(model="claude-sonnet-4-6"),
23+
tools=[SandboxShellTool(env), SandboxCodeTool(env.code_environment())],
24+
)
25+
reply = await agent.ask("Write hello.txt, then read it with Python.")
26+
print(await reply.content())
27+
28+
if __name__ == "__main__":
29+
asyncio.run(main())
30+
```
31+
32+
(Running this agent needs an LLM key — `verify.py` below doesn't.)
33+
34+
`env.code_environment()` matters: Tenki's default image ships `python3` (not `python`), and the returned `CodeAdapter` maps the `python` runner accordingly. Pass the env itself to `SandboxShellTool`, the adapter to `SandboxCodeTool`.
35+
36+
## Setup (Python 3.10+)
37+
38+
```bash
39+
uv venv # or: python3.11 -m venv .venv
40+
uv pip install -r requirements.txt # ag2 (git pin, see note) + tenki SDK
41+
export TENKI_API_KEY=tk_... # from the Tenki dashboard
42+
```
43+
44+
> **Release note (2026-08):** the Tenki extension merged into ag2 `main` on 2026-08-13, after the latest PyPI release (1.0.1, 2026-07-29) — so `requirements.txt` pins the merge commit via git. Once the next ag2 release ships `ag2/extensions/tenki`, this becomes simply:
45+
>
46+
> ```bash
47+
> pip install "ag2[anthropic]" "tenki>=0.5.4,<1"
48+
> ```
49+
50+
## Configuring `TenkiEnvironment`
51+
52+
```python
53+
from ag2.extensions.tenki import TenkiEnvironment, TenkiResources
54+
55+
env = TenkiEnvironment(
56+
workspace_id="your-workspace-id", # auto-detected when the key sees one workspace
57+
name="my-agent-sandbox",
58+
image="workspace/image:tag",
59+
env_vars={"APP_ENV": "sandbox"},
60+
resources=TenkiResources(cpu_cores=2, memory_mb=4096, disk_size_gb=5),
61+
timeout=60,
62+
max_duration=900,
63+
)
64+
```
65+
66+
| Parameter | Default | Purpose |
67+
| --- | --- | --- |
68+
| `api_key` | `TENKI_API_KEY` | API authentication |
69+
| `api_url` | `TENKI_API_URL` or production | API endpoint |
70+
| `workspace_id` | Auto-detected when unique | Tenki workspace scope |
71+
| `name` | `ag2` | Sandbox name in the Tenki dashboard |
72+
| `image` | Tenki default image | Registry image reference |
73+
| `env_vars` | `{}` | Environment variables baked into the sandbox |
74+
| `resources` | Tenki defaults | CPU / memory / disk overrides |
75+
| `timeout` | `60` s | Sandbox startup + per-command timeout |
76+
| `max_duration` | `900` s | Server-enforced sandbox lifetime backstop |
77+
| `workdir` | `/home/tenki` | Working directory for commands and files |
78+
79+
## What the backend does
80+
81+
- **One environment, both tools.** The same `TenkiEnvironment` serves `SandboxShellTool` directly and `SandboxCodeTool` via `code_environment()` — one sandbox, one cleanup path.
82+
- **Files persist across tool calls.** The factory caches sandboxes by their resolved parameters, so a file the agent writes in tool call 1 is still there in tool call 10. The sandbox lives until `aclose()` (the `async with env` exit), not per call.
83+
- **Cleanup has three layers.** Creation is failure-atomic (a sandbox that never becomes ready is terminated immediately), an `atexit` hook catches interpreter shutdown if `aclose()` never ran, and `max_duration` reclaims the VM server-side even if the client process dies. Sandboxes are created with inbound networking off, outbound on.
84+
85+
## Verify
86+
87+
```bash
88+
node verify.mjs # or: .venv/bin/python verify.py
89+
```
90+
91+
`verify.py` drives the real integration classes against live Tenki; no LLM key needed. It imports `ag2.extensions.tenki`, constructs `TenkiEnvironment` + `TenkiResources`, builds the `CodeAdapter`, then does what `SandboxShellTool` does per call — `env.open()` → live sandbox → `exec` (`42`) — plus a file round-trip across two `open()` calls to prove the caching, and asserts teardown on scope exit. The agent loop on top is covered by ag2's own CI.
92+
93+
## Notes
94+
95+
- The extension uses Tenki's **new [`tenki`](https://pypi.org/project/tenki/) Python SDK** (`tenki>=0.5.4,<1`) — the canonical successor namespace to the older `tenki-sandbox` package some earlier cookbook examples use. Don't install `tenki-sandbox` for this one.
96+
- Auth: a `tk_` API key works as-is. A `tenki login` browser session token must be prefixed `cookie:` for the Python SDK (`verify.py` handles this).
97+
- All credential/selection parameters also accept AG2 `Variable`s for per-request (multi-tenant) resolution — see the [AG2 Tenki docs](https://github.com/ag2ai/ag2/blob/main/website/docs/user-guide/extensions/tenki.mdx).
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ag2's latest release (1.0.1, 2026-07-29) predates the Tenki extension merge
2+
# (ag2ai/ag2#3096, 2026-08-13). Pin the merge commit until the next ag2 release
3+
# ships ag2/extensions/tenki, then switch to a plain `ag2` pin.
4+
ag2 @ git+https://github.com/ag2ai/ag2.git@535dfb303cf1db9f862ac099a9f7887d3f8f9b39
5+
tenki>=0.5.4,<1

examples/ag2-tenki/verify.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Shim so the cookbook's Node verify harness can run this Python example.
3+
* Runs verify.py with the example's local venv if present, else `python3`.
4+
* Setup (Python 3.10+): `uv venv && uv pip install -r requirements.txt` (see README).
5+
*/
6+
import { spawnSync } from "node:child_process";
7+
import { existsSync } from "node:fs";
8+
9+
const py = existsSync(".venv/bin/python") ? ".venv/bin/python" : process.env.PYTHON || "python3";
10+
const r = spawnSync(py, ["verify.py"], { stdio: "inherit" });
11+
process.exit(r.status ?? 1);

examples/ag2-tenki/verify.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
Smoke-verify the AG2 Tenki extension, live and without any LLM key:
3+
1. `ag2.extensions.tenki` imports (the integration shipped in this ag2 build),
4+
2. `TenkiEnvironment` + `TenkiResources` construct and `code_environment()`
5+
builds the CodeAdapter that `SandboxCodeTool` consumes, and
6+
3. the real factory path works end to end: `env.open()` creates a live Tenki
7+
sandbox (exactly what `SandboxShellTool` does per call), a second `open()`
8+
returns the same cached sandbox, put_file/exec/remove_file round-trip, and
9+
leaving `async with env` tears the sandbox down via `aclose()`.
10+
11+
No agent or model call is made — the tools' sandbox plumbing is the part a
12+
Tenki outage or an API change would break, and it is fully exercised here.
13+
14+
Token/workspace from env (CI) or ~/.config/tenki/config.yaml (local `tenki login`).
15+
"""
16+
17+
import asyncio
18+
import os
19+
import sys
20+
from pathlib import PurePosixPath
21+
22+
from ag2.extensions.tenki import TenkiEnvironment, TenkiResources # proves the extension shipped
23+
24+
25+
def cfg(key):
26+
try:
27+
with open(os.path.expanduser("~/.config/tenki/config.yaml")) as f:
28+
for line in f:
29+
if line.startswith(key + ":"):
30+
return line.split(":", 1)[1].strip()
31+
except Exception:
32+
pass
33+
return ""
34+
35+
36+
token = os.environ.get("TENKI_AUTH_TOKEN") or os.environ.get("TENKI_API_KEY") or cfg("auth_token")
37+
if not token:
38+
print("No token. Set TENKI_API_KEY, or run `tenki login`.")
39+
sys.exit(1)
40+
41+
# `tenki` SDK auth gap: a bare `tenki login` browser session token is sent as a
42+
# Bearer header (rejected); prefix it `cookie:` so it travels as a cookie.
43+
# A `tk_` API key works as-is.
44+
if not token.startswith(("tk_", "ory_st_", "cookie:")):
45+
token = f"cookie:{token}"
46+
47+
48+
async def main() -> None:
49+
env = TenkiEnvironment(
50+
api_key=token,
51+
workspace_id=os.environ.get("TENKI_WORKSPACE_ID") or cfg("current_workspace_id") or None,
52+
name="cookbook-verify",
53+
resources=TenkiResources(cpu_cores=1, memory_mb=1024),
54+
timeout=90,
55+
max_duration=600,
56+
)
57+
env.code_environment() # the CodeAdapter SandboxCodeTool consumes — python runner is python3
58+
59+
async with env: # the documented usage: tools share this factory for its whole scope
60+
async with env.open() as sb: # what SandboxShellTool does on every call
61+
r = await sb.exec(["python3", "-c", "print(6 * 7)"])
62+
assert r.exit_code == 0, f"exit {r.exit_code}: {r.output}"
63+
assert r.output == "42", f"got {r.output!r}"
64+
65+
# The factory caches by parameter set: files persist across tool calls.
66+
path = PurePosixPath("cookbook_probe.txt")
67+
await sb.put_file(path, b"persisted between tool calls")
68+
async with env.open() as sb2:
69+
assert sb2 is sb, "expected the cached sandbox on the second open()"
70+
r = await sb2.exec(["cat", str(path)])
71+
assert r.exit_code == 0 and r.output == "persisted between tool calls", r.output
72+
await sb.remove_file(path)
73+
# `async with env` exit -> aclose() terminated the sandbox and closed the client.
74+
75+
print("✓ ag2-tenki: TenkiEnvironment → live sandbox via the real integration → exec 42 → aclose teardown")
76+
77+
78+
try:
79+
asyncio.run(main())
80+
except Exception as e: # noqa
81+
print(f"✗ {type(e).__name__}: {e}")
82+
sys.exit(1)

0 commit comments

Comments
 (0)