Skip to content

LangChain Integration Guide

中文

Run a LangChain agent that calls a Python tool inside a CubeSandbox MicroVM. Because Cube exposes an E2B-compatible API, migrating a LangChain app from E2B to Cube usually means changing a few environment variables, while giving every line of agent-generated code KVM-level isolation. This guide ships a runnable examples/langchain-integration sample project.

Two major LangChain versions (0.x and 1.x)

This guide provides two variants that differ only in the LangChain Agent API and the required Python version. Both share the same run_python tool (built on the official cubesandbox SDK) and the same sandbox template.

VariantLangChainlangchain-openaiPythonAgent APIExample path
0.x (legacy)0.3.x0.3.x3.9+AgentExecutor + create_react_agentexamples/langchain-integration/0.x
1.x (modern)1.x1.x3.10+langchain.agents.create_agent + @toolexamples/langchain-integration/1.x

If you are on Python 3.9, or already have LangChain 0.3.x code → use 0.x. If you are on Python 3.10+ and starting fresh (or already on LangChain 1.x) → use 1.x.

Components and versions

Component0.x1.x
LangChainlangchain==0.3.23langchain>=1.3.14,<2.0
langchain-openailangchain-openai==0.3.12langchain-openai>=1.0,<2.0
LangGraph (create_agent runtime)— (not used; AgentExecutor + create_react_agent have no LangGraph dep)langgraph>=0.2 (declared in 1.x/requirements.txt so the sample is self-contained)
cubesandbox SDK (main driver)cubesandbox>=0.6.0cubesandbox>=0.6.0
CubeSandbox base imageghcr.io/tencentcloud/cubesandbox-base:2026.16same
CubeSandbox platform>= 0.3.0 (core); higher for optional features below>= 0.3.0 (core); higher for optional features below

Per-feature platform minimums (the SDK floor above covers the base workflow; raise cubesandbox/platform only when you use the feature):

FeatureMinimum platformMinimum SDK
Base workflow (commands.run / files.write over the envd process API on 49983, context-manager teardown)>= 0.3.0>= 0.6.0
CubeEgress secret injection (advanced usage, default-deny egress)>= 0.4.0>= 0.3.0
Volume mounts (volume_mounts=, Volume API)>= 0.6.0>= 0.6.0

Prerequisites

  • CubeSandbox is deployed and CubeAPI is reachable at http://<node>:3000.
  • cubemastercli is on your $PATH and connected to the cluster.
  • The build machine has Docker and the registry is pullable by Cube nodes.
  • Python 3.10+ (for the 1.x variant). The 0.x variant also runs on Python 3.9+.
  • Reachability to the Cube cluster node. The cubesandbox SDK reaches CubeAPI via CUBE_API_URL and the data plane via CUBE_PROXY_NODE_IP (so *.cube.app resolves without DNS); if the proxy does not listen on port 80, set CUBE_PROXY_PORT_HTTP.
  • An OpenAI-compatible LLM endpoint (the sample uses TokenHub; any OpenAI-compatible endpoint works via OPENAI_BASE_URL / OPENAI_API_KEY).

Why run the LangChain agent in a sandbox

LangChain agents often expose a code-execution tool (data analysis, file conversion, shell calls). Running that tool on the host mixes the agent's blast radius with your dev machine. Inside CubeSandbox you get:

ConcernWhat CubeSandbox provides
IsolationOne KVM MicroVM per session, with its own guest kernel — agent code cannot touch the host
ReproducibilityEvery session boots from the same template snapshot
Fast startupCold start under 60ms, so running many agents in parallel is cheap
Long taskssandbox.pause() snapshots the VM + root filesystem for later resume
Secret hygieneCubeEgress can inject LLM auth headers at the network layer — the real secret is never visible inside the VM
Egress auditEvery outbound request is logged to the egress audit log

Integration steps

1. Build the template image

Layer a Python data-science stack on top of cubesandbox-base (envd listens on :49983).

dockerfile
# examples/langchain-integration/Dockerfile
ARG CUBE_BASE_IMAGE=ghcr.io/tencentcloud/cubesandbox-base:2026.16
FROM ${CUBE_BASE_IMAGE}

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        python3 python3-pip ca-certificates git \
    && rm -rf /var/lib/apt/lists/*

# Pin versions for reproducible builds; upgrade as needed
RUN python3 -m pip install --no-cache-dir --upgrade pip \
    && python3 -m pip install --no-cache-dir --break-system-packages \
        pandas==2.2.3 numpy==1.26.4 matplotlib==3.9.3 scikit-learn==1.6.1

WORKDIR /workspace

# Seed a demo dataset so the sample agent runs without external input
COPY sales.csv /workspace/sales.csv

EXPOSE 49983

Build and push:

bash
docker build --platform linux/amd64 \
  -t <your-registry>/langchain-cube:latest \
  examples/langchain-integration
docker push <your-registry>/langchain-cube:latest

2. Register as a Cube template

bash
cubemastercli tpl create-from-image \
  --image <your-registry>/langchain-cube:latest \
  --writable-layer-size 2G \
  --expose-port 49983 \
  --probe       49983 \
  --probe-path  /health

create-from-image blocks and watches the job to completion by default; pass --detach if you want to submit-and-exit and poll later with cubemastercli tpl watch --job-id <job_id>.

Once the job reaches READY, note the template_id — you must pass it to every Sandbox.create(). The 2G writable layer suits medium analysis tasks; raise it to 4G+ if the agent installs large packages at runtime.

3. Install dependencies and configure env vars

bash
cd examples/langchain-integration/1.x      # or 0.x, depending on your LangChain version
cp ../.env.example .env
# Fill in CUBE_API_URL, CUBE_API_KEY, CUBE_TEMPLATE_ID, CUBE_PROXY_NODE_IP, and your LLM key
pip install -r requirements.txt
VariableFlows toNotes
CUBE_API_URLcubesandbox SDKCubeAPI address (http://<node>:3000)
CUBE_API_KEYcubesandbox SDK(optional) X-API-Key header; only set it for an auth-enabled CubeAPI backend — the SDK sends no auth header when unset
CUBE_TEMPLATE_IDSandbox.create(template=...)From step 2
CUBE_PROXY_NODE_IPcubesandbox SDKCubeProxy node IP so *.cube.app resolves without DNS
CUBE_PROXY_PORT_HTTPcubesandbox SDKProxy HTTP port (default 80; set 8081 if the proxy listens on 8081)
OPENAI_API_KEY / TOKENHUB_API_KEYLLM clientOpenAI-compatible key
OPENAI_BASE_URLLLM cliente.g. https://tokenhub.tencentmaas.com/v1
CHAT_MODELLLM cliente.g. deepseek-v3
CUBE_SSL_CERT_FILEdemo(optional) self-signed CA cert path for HTTPS CubeAPI. The demo exports it as SSL_CERT_FILE (process-global) so the gRPC client trusts it — the bundle should include public root CAs or the LLM endpoint is affected too

4. Wire the code-execution tool to Cube

The agent logic is identical across versions; only the tool wrapper changes from a local REPL to the Cube sandbox. The snippet below is the shared run_python tool for both 0.x and 1.x — built on the official cubesandbox Python SDK (from cubesandbox import Sandbox), using sandbox.files.write to upload code and sandbox.commands.run to execute, so no raw HTTP is needed.

python
# run_python tool — official cubesandbox SDK (used by the snippets below;
# the shipped demos inline equivalent logic inside build_agent)
import itertools
from cubesandbox import Sandbox

def make_run_python(sandbox: Sandbox):
    """Return a run_python tool bound to the given created `sandbox`."""
    script_counter = itertools.count()
    def run_python(code: str) -> str:
        """Execute Python inside the Cube Sandbox MicroVM; return stdout, with
        stderr delimited below it when present.

        The image preinstalls pandas / numpy / matplotlib / scikit-learn. Each call
        writes the snippet to a unique /workspace/_agent_<n>.py and runs it, so
        concurrent tool calls don't overwrite each other. Charts can be saved under
        /workspace (e.g. /workspace/revenue.png).
        """
        script = f"/workspace/_agent_{next(script_counter)}.py"
        sandbox.files.write(script, code)
        result = sandbox.commands.run(f"python3 {script}", timeout=120, cwd="/workspace")
        out = result.stdout
        # Keep stderr delimited from stdout so library warnings (exit_code 0)
        # don't blur the real output seen by the LLM.
        if result.stderr:
            out += "\n--- stderr ---\n" + result.stderr
        if result.exit_code != 0:
            out += f"\n[non-zero exit code: {result.exit_code}]"
        return out
    return run_python

One sandbox for the whole run, auto-torn-down. The entire agent loop creates a single MicroVM (Sandbox.create(template=...)) and the context manager calls sandbox.kill() (DELETE /sandboxes/:sandboxID) when the with block exits, so no sandbox leaks. The bundled sample uses exactly this pattern.

5. Run the agent

Both variants build the same ChatOpenAI client and reuse the shared run_python tool; they differ only in how the agent is constructed. Create one Sandbox for the whole run and bind the tool with make_run_python(sandbox):

1.x (modern — langchain.agents.create_agent)

python
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain_core.tools import tool
from cubesandbox import Sandbox

load_dotenv()
_llm_key = os.getenv("OPENAI_API_KEY") or os.getenv("TOKENHUB_API_KEY")
if not _llm_key:
    raise SystemExit("Missing LLM API key (set OPENAI_API_KEY or TOKENHUB_API_KEY)")

llm = ChatOpenAI(
    model=os.getenv("CHAT_MODEL") or "deepseek-v3",
    api_key=_llm_key,
    base_url=os.getenv("OPENAI_BASE_URL") or "https://tokenhub.tencentmaas.com/v1",
    timeout=60,
    max_retries=2,
    temperature=0,
)

SANDBOX_CONTEXT = (
    "You are a data analyst. You can execute Python inside a Cube Sandbox "
    "MicroVM via the run_python tool. Environment facts:\n"
    "- Working directory: /workspace\n"
    "- Demo dataset: /workspace/sales.csv with columns month,product,units,price\n"
    "  (6 rows: 3 months x 2 products; revenue is defined as units * price)\n"
    "- Preinstalled: pandas, numpy, matplotlib, scikit-learn\n"
    "- Save any charts/artifacts under /workspace\n"
    "When the user mentions 'the dataset' without a path, use /workspace/sales.csv.\n"
    "Modeling conventions for this tiny demo dataset (follow them unless the "
    "user explicitly specifies otherwise):\n"
    "- Regression/forecast target: monthly TOTAL revenue. Aggregate to one row "
    "per month, then use a numeric month index (0, 1, 2, ...) as the only "
    "feature.\n"
    "- Never use the target itself or its direct components (units, price) as "
    "features when predicting revenue - that is data leakage and yields a "
    "meaningless RMSE of 0.\n"
    "- The dataset is too small for a train/test split; fit and evaluate on "
    "all rows and explicitly state that the metric is in-sample.\n"
    "- Report only numbers actually printed by the executed code; never "
    "invent or estimate metric values."
)

with Sandbox.create(template=os.environ["CUBE_TEMPLATE_ID"], timeout=600) as sandbox:
    run_python = tool(make_run_python(sandbox))
    agent = create_agent(llm, [run_python], system_prompt=SANDBOX_CONTEXT)
    result = agent.invoke({"messages": [{"role": "user", "content":
        "Load sales.csv from /workspace, compute total revenue per month, "
        "and report the month -> revenue numbers in your final answer."}]})
    for msg in reversed(result["messages"]):
        if msg.content:
            print(msg.content)
            break
    else:
        print("(no final answer in messages)")

0.x (legacy — AgentExecutor + create_react_agent)

python
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import Tool
from langchain_core.prompts import PromptTemplate
from cubesandbox import Sandbox

load_dotenv()
_llm_key = os.getenv("OPENAI_API_KEY") or os.getenv("TOKENHUB_API_KEY")
if not _llm_key:
    raise SystemExit("Missing LLM API key (set OPENAI_API_KEY or TOKENHUB_API_KEY)")

llm = ChatOpenAI(  # same params as above
    model=os.getenv("CHAT_MODEL") or "deepseek-v3",
    api_key=_llm_key,
    base_url=os.getenv("OPENAI_BASE_URL") or "https://tokenhub.tencentmaas.com/v1",
    timeout=60, max_retries=2, temperature=0,
)

with Sandbox.create(template=os.environ["CUBE_TEMPLATE_ID"], timeout=600) as sandbox:
    run_python = make_run_python(sandbox)
    tools = [Tool(name="run_python", func=run_python,
                  description="Execute Python code in a Cube Sandbox MicroVM.")]
    agent = create_react_agent(llm, tools, PromptTemplate.from_template(
        "You are a data analyst. Answer the question using tools.\n\n"
        "Environment facts:\n"
        "- Demo dataset: /workspace/sales.csv with columns month,product,units,price\n"
        "  (6 rows: 3 months x 2 products; revenue is defined as units * price)\n"
        "- Preinstalled: pandas, numpy, matplotlib, scikit-learn\n"
        "- Save any charts/artifacts under /workspace\n"
        "When the user mentions 'the dataset' without a path, use /workspace/sales.csv.\n"
        "Modeling conventions (unless the user specifies otherwise):\n"
        "- Regression target: monthly TOTAL revenue; aggregate per month and use "
        "a numeric month index (0, 1, 2, ...) as the only feature.\n"
        "- Never use the target or its components (units, price) as features - "
        "that is data leakage and yields a meaningless RMSE of 0.\n"
        "- Too small for a train/test split; evaluate in-sample and say so.\n"
        "- Report only numbers actually printed by the executed code.\n\n"
        "Tools: {tools}\n\n"
        "Use the following format:\n\n"
        "Question: the input question you must answer\n"
        "Thought: you should always think about what to do\n"
        "Action: the action to take, should be one of [{tool_names}]\n"
        "Action Input: the input to the action\n"
        "Observation: the result of the action\n"
        "... (this Thought/Action/Action Input/Observation can repeat N times)\n"
        "Thought: I now know the final answer\n"
        "Final Answer: the final answer to the original input question\n\n"
        "Begin!\n\n"
        "Question: {input}\n\nThought: {agent_scratchpad}"))
    executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
    result = executor.invoke({"input":
        "Load sales.csv from /workspace, compute total revenue per month, "
        "and report the month -> revenue numbers in your final answer."})
    print(result["output"])

Run (from the chosen variant directory):

bash
python langchain_agent_demo.py
# Custom question:
python langchain_agent_demo.py "Train a linear model on the dataset and report the RMSE."

Expected output

With the default prompt, the agent's final answer printed on the host includes the per-month revenue numbers (2780.0 / 3375.5 / 3872.0). (The 1.x create_agent is not verbose by default; the 0.x AgentExecutor(verbose=True) also prints the full reasoning trace to the console.)

Teardown is best-effort: if kill() fails (e.g. the sandbox already expired), the SDK's context manager silently swallows the error — no warning is printed. If you need to detect teardown failures, call sandbox.kill() explicitly and handle CubeSandboxError yourself.

Key snippets

Migrate an E2B app to Cube

If your LangChain app already drives an E2B sandbox, the code is almost identical — just swap the import for the cubesandbox SDK and point it at CubeAPI:

diff
- from e2b_code_interpreter import Sandbox
- # E2B cloud (managed)
- export E2B_API_KEY="e2b_xxx"
- export E2B_API_URL="https://api.e2b.dev"   # default, usually omitted
+ from cubesandbox import Sandbox
+ # Cube Sandbox (self-hosted, MicroVM isolation)
+ export CUBE_API_URL="http://<your-cube-host>:3000"
+ # export CUBE_API_KEY="<your-api-key>"  # only for auth-enabled backends
+ export CUBE_TEMPLATE_ID="<your-cube-template-id>"
+ export CUBE_PROXY_NODE_IP="<your-cube-host>"

The Sandbox API (Sandbox.create, commands.run, files.write, kill) keeps the same shape, so the rest of your tool code needs no changes. The sample above uses exactly this SDK.

Local baseline (the part being replaced)

python
# Before: code runs on the host via langchain_experimental
from langchain_experimental.tools import PythonREPLTool
tools = [PythonREPLTool()]

Swap PythonREPLTool for the run_python Cube tool above to get isolation for free.

Caveats

  • envd user. The cubesandbox SDK's files.write / commands.run run as root by default, so no extra config is needed. (The base image also provisions a non-root uid=1000 user; if you switch the SDK to run as that user, file-permission behavior changes.)
  • Cleanup safety. with Sandbox.create(...) as sandbox: calls sandbox.kill() (DELETE /sandboxes/:sandboxID, no /kill subpath) on exit, so no sandbox leaks even if the agent raises.
  • One sandbox for the whole run, reused across calls. The entire agent loop creates a single MicroVM and reuses it between run_python calls, so you pay the lifecycle cost only once.
  • State does not persist across turns. Each commands.run is a fresh python process; variables/imports do not survive between tool calls. Inline everything the snippet needs, or write intermediate state to /workspace and read it back.
  • The template must contain the required stack. Preinstall pandas/numpy/matplotlib into the image; under a default-deny egress policy, a runtime pip install will fail.
  • Timeouts. Set both the sandbox timeout (platform reclaim) and the per-command timeout; a long agent loop can exhaust either.

Advanced usage

Network isolation + secret injection (native SDK)

On a shared cluster, prefer the native cubesandbox SDK together with default-deny egress and network-layer secret injection, so the LLM secret never enters the VM:

python
import os
from cubesandbox import Sandbox, Rule, Match, Action, Inject

rules = [
    Rule(
        name="allow_llm",
        match=Match(scheme="https", sni="api.openai.com", host="api.openai.com"),
        action=Action(allow=True, audit="metadata", inject=[
            Inject(header="Authorization",
                   secret=os.environ["OPENAI_API_KEY"],
                   format="Bearer ${SECRET}"),
        ]),
    ),
]
with Sandbox.create(
    template=os.environ["CUBE_TEMPLATE_ID"],
    timeout=600,
    allow_internet_access=False,           # default-deny; hosts in rules are allowed
    network={"rules": rules},
) as sandbox:
    run_agent(sandbox)                     # sandbox.kill() runs on block exit

File mounts

Sandbox.create() has no mounts parameter. Use the Cube-specific APIs instead:

  • Host directory mounts go through metadata["host-mount"] (a JSON-encoded list of {hostPath, mountPath, readOnly} descriptors), so the agent reads/writes shared host data directly without round-tripping through files.write. Note that hostPath must live under an allowed prefix (by default /data/shared/; extend allowed_host_mount_prefixes in CubeMaster's config to use other paths):
python
import json
from cubesandbox import Sandbox

sandbox = Sandbox.create(
    template=os.environ["CUBE_TEMPLATE_ID"],
    metadata={
        "host-mount": json.dumps([
            {"hostPath": "/data/shared/datasets", "mountPath": "/workspace/datasets",
             "readOnly": True},
        ]),
    },
)
  • Volume mounts use volume_mounts={mount_path: volume_id} (see examples/volume):
python
from cubesandbox import Sandbox, Volume

vol = Volume.create("my-workspace")
sandbox = Sandbox.create(
    template=os.environ["CUBE_TEMPLATE_ID"],
    volume_mounts={"/workspace": vol},
)

Long-task pause / resume

pause() returns None — the sandbox ID never changes. Call Sandbox.connect() on the same ID to resume:

python
sandbox = Sandbox.create(template=os.environ["CUBE_TEMPLATE_ID"], timeout=1800)
try:
    turn_1(sandbox)
    sandbox.pause()
    sandbox = Sandbox.connect(sandbox.sandbox_id)   # /workspace intact after resume
    turn_2(sandbox)
finally:
    sandbox.kill()

Troubleshooting

SymptomPossible causeFix
File op permission deniedenvd user mismatchThe cubesandbox SDK's files.write / commands.run run as root by default. If using the E2B SDK, set e2b.envd.rpc.default_username = "root".
command not found: pythonTemplate missing PythonRebuild the image with python3
ModuleNotFoundError: pandasStack not in imageAdd pandas/numpy/matplotlib in the Dockerfile
403 Forbidden - CubeEgressDefault-deny with no allow ruleAllow the LLM host (and any other needed hosts)
CubeAPI Connection refusedWrong CUBE_API_URLSet http://<node>:3000
Template stuck in PULLINGCluster cannot reach the registryPush to a registry the cluster can reach
run_python returns nothingScript has no stdoutHave the agent explicitly print() results

References