Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Run a self-hosted agent platform's sandboxes on Tenki.
| Example | Platform |
| --- | --- |
| [DeerFlow](examples/deerflow-tenki/) | Community sandbox provider for ByteDance's SuperAgent harness |
| [RAGFlow](examples/ragflow-tenki/) | Ephemeral microVMs for the RAG engine's code components |

### Migrating from another provider

Expand Down Expand Up @@ -113,7 +114,7 @@ These open-source projects ship a Tenki provider or backend out of the box:

| Project | Tenki integration |
| --- | --- |
| [RAGFlow](https://github.com/infiniflow/ragflow) | Sandbox provider for the RAG engine's code executor |
| [RAGFlow](https://github.com/infiniflow/ragflow) | Sandbox provider for the RAG engine's code executor — [example](examples/ragflow-tenki/) |
| [DeerFlow](https://github.com/bytedance/deer-flow) | Sandbox provider for ByteDance's SuperAgent harness — [example](examples/deerflow-tenki/) |
| [AgentBox](https://github.com/madarco/agentbox) | Provider for running parallel agents in sandboxed VMs — [example](examples/agentbox-tenki/) |
| [ComputeSDK](https://github.com/computesdk/computesdk) | Tenki provider for the multi-provider compute toolkit — [example](examples/computesdk-tenki/) |
Expand Down
45 changes: 45 additions & 0 deletions examples/ragflow-tenki/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# RAGFlow's code executor on Tenki

[RAGFlow](https://github.com/infiniflow/ragflow) is an open-source RAG engine whose agent workflows can include **code components** — Python or JavaScript snippets executed through pluggable sandbox providers. Since [ragflow#17305](https://github.com/infiniflow/ragflow/pull/17305), `tenki` is one of those providers (implemented in both Python and Go): each execution runs in a fresh [Tenki](https://tenki.cloud) microVM that is destroyed afterwards. Because it's cloud-hosted, there is nothing to operate locally — no gVisor, no Docker base images, no executor-manager service. Just an API key.

## Enable it

The `tenki-sandbox` SDK is an optional dependency (it needs `protobuf>=6.31`, which differs from RAGFlow's default gRPC stack), so install it into the RAGFlow runtime first:

```bash
pip install tenki-sandbox
```

Then configure the provider in **Admin > Sandbox Settings**:

| Setting | Notes |
| --- | --- |
| `api_key` (required) | create one at [app.tenki.cloud](https://app.tenki.cloud) under **API Keys** |
| `project_id` (required) | required by RAGFlow's schema; current Tenki no longer scopes sandboxes by project, so any identifier satisfies it |
| `image` | leave empty for the Tenki default image, which ships both `python3` and `node` |
| `allow_outbound` | **defaults to `false`** — sandboxed code gets no network unless you opt in (e.g. to install packages) |
| `timeout` / `max_lifetime` | per-execution limit (default 30 s) / server-side sandbox cap (default 3600 s) |
| `cpu_cores`, `memory_mb`, `disk_size_gb`, output & artifact limits | tunable, sensible defaults |

## What the provider does

- **Ephemeral by design**: create → execute → destroy, once per run. No volumes, no snapshots, nothing persists between executions.
- **Two languages**: Python (`python3`) and JavaScript (`node`), with wrapper scripts that inject the component's arguments.
- **Artifacts come back** (Python provider): anything the code writes to `artifacts/` in its working directory is collected over Tenki's file API — extension-whitelisted (`.csv .html .jpeg .json .pdf .png .svg`), size-capped, symlinks rejected. The Go port runs code with the same wrapping protocol but, like the Go e2b provider, does not collect artifacts.
- **Structured results**: stdout, stderr, exit code, and timing map to RAGFlow's `ExecutionResult`; timeouts and API failures map to clear error messages.
- **`max_duration` is a server-side cap**, so a sandbox self-terminates even if RAGFlow crashes mid-run — no leaked billing.

## Verify

```bash
uv venv && uv pip install -r requirements.txt
node verify.mjs # or: .venv/bin/python verify.py
```

[`verify.py`](verify.py) proves the Tenki surface the provider is built on without running RAGFlow: the **`who_am_i()`** health check → an ephemeral sandbox with **outbound network off** (RAGFlow's security default) → the **Python** and **JavaScript** execution paths (script staged over `fs`, run with a timeout) → the **`artifacts/` read-back** transport → terminate. The provider's own upstream test suites (Python and Go) cover the RAGFlow-side contract.

## Notes

- **Auth:** the Python SDK wants a `tk_` API key (`export TENKI_API_KEY=tk_…`). A `tenki login` browser session token works too, but must be prefixed `cookie:` — `verify.py` does this for you.
- With `allow_outbound: false`, `pip install` inside components will fail by design; flip it on per RAGFlow's settings page when needed.
- Full setup docs: [sandbox quickstart](https://github.com/infiniflow/ragflow/blob/main/docs/guides/agent/agent_quickstarts/sandbox_quickstart.md) in the RAGFlow repo.
1 change: 1 addition & 0 deletions examples/ragflow-tenki/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tenki-sandbox>=0.4.0
11 changes: 11 additions & 0 deletions examples/ragflow-tenki/verify.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Shim so the cookbook's Node verify harness can run this Python example.
* Runs verify.py with the example's local venv if present, else `python3`.
* Setup (Python 3.10+): `uv venv && uv pip install -r requirements.txt` (see README).
*/
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";

const py = existsSync(".venv/bin/python") ? ".venv/bin/python" : process.env.PYTHON || "python3";
const r = spawnSync(py, ["verify.py"], { stdio: "inherit" });
process.exit(r.status ?? 1);
78 changes: 78 additions & 0 deletions examples/ragflow-tenki/verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
Proves the Tenki-facing surface RAGFlow's `tenki` sandbox provider
(agent/sandbox/providers/tenki.py) is built on, without running RAGFlow:
Client + who_am_i health check, an ephemeral sandbox with outbound network
off (RAGFlow's security default), both supported languages (python3 and
node), the artifacts/ read-back path, and terminate.

Needs Python 3.10+ and `tenki-sandbox` (requirements.txt). Token/workspace
from env (CI) or ~/.config/tenki/config.yaml (local `tenki login`).
"""
import os
import sys

from tenki_sandbox import Client


def cfg(key):
try:
with open(os.path.expanduser("~/.config/tenki/config.yaml")) as f:
for line in f:
if line.startswith(key + ":"):
return line.split(":", 1)[1].strip()
except Exception:
pass
return ""


token = os.environ.get("TENKI_AUTH_TOKEN") or os.environ.get("TENKI_API_KEY") or cfg("auth_token")
if not token:
print("No token. Set TENKI_AUTH_TOKEN, or run `tenki login`.")
sys.exit(1)
# A `tk_` API key works as-is; a `tenki login` browser session token must go
# over as a cookie — the SDK does that when the token is prefixed `cookie:`.
if not token.startswith(("tk_", "ory_st_", "cookie:")):
token = f"cookie:{token}"

create_opts = {"cpu_cores": 1, "memory_mb": 1024, "allow_outbound": False, "max_duration": 3600}
workspace_id = os.environ.get("TENKI_WORKSPACE_ID") or cfg("current_workspace_id")
if workspace_id:
create_opts["workspace_id"] = workspace_id

client = Client(auth_token=token)
sb = None
try:
# 1) the provider's health check
client.who_am_i()

# 2) an ephemeral sandbox, network off — RAGFlow's per-execution lifecycle
sb = client.create(**create_opts)

# 3) python path: stage the script over fs, run it (the provider's wrapper pattern)
sb.fs.write_text("/home/tenki/main.py", "print(6 * 7)\n")
r = sb.exec("python3", "main.py", cwd="/home/tenki", timeout=30)
if not (r.ok and r.stdout_text.strip() == "42"):
raise AssertionError(f"python: ok={r.ok}, stdout={r.stdout_text!r}, stderr={r.stderr_text!r}")

# 4) javascript path — the default image ships node too
sb.fs.write_text("/home/tenki/main.js", 'console.log("hello from node")\n')
r = sb.exec("node", "main.js", cwd="/home/tenki", timeout=30)
if not (r.ok and r.stdout_text.strip() == "hello from node"):
raise AssertionError(f"node: ok={r.ok}, stdout={r.stdout_text!r}, stderr={r.stderr_text!r}")

# 5) artifact collection transport: code writes artifacts/, provider reads them back
sb.exec("python3", "-c", "import os,json; os.makedirs('artifacts',exist_ok=True); json.dump({'answer':42},open('artifacts/result.json','w'))", cwd="/home/tenki", timeout=30)
if b'"answer": 42' not in sb.fs.read_bytes("/home/tenki/artifacts/result.json"):
raise AssertionError("artifact read-back mismatch")

print("✓ ragflow-tenki: who_am_i → outbound-off sandbox → python (42) → node → artifacts/ read-back → terminate")
except Exception as e: # noqa: BLE001
print(f"✗ {type(e).__name__}: {e}")
sys.exit(1)
finally:
if sb is not None:
try:
sb.terminate()
except Exception: # noqa: BLE001
pass # self-reaps via max_duration
client.close()
Loading