internal/agent/tool/code_exec_sandbox_design.md
Status: decision recorded, implementation pending. This file captures the trade-offs so the choice can be revisited without re-deriving it.
The Python agent's code_exec delegates to a provider-based
sandbox subsystem under agent/sandbox/. It is NOT a single
SDK and NOT a local subprocess — it is a thin abstraction over
several execution backends.
agent/sandbox/providers/base.py)class SandboxProvider(ABC):
def initialize(self, config) -> bool
def create_instance(self, template: str) -> SandboxInstance
def execute_code(self, instance_id, code, language,
timeout=10, arguments=None) -> ExecutionResult
def destroy_instance(self, instance_id) -> bool
def health_check(self) -> bool
def get_supported_languages(self) -> List[str]
| Provider | File | Backend |
|---|---|---|
SelfManagedProvider | self_managed.py | HTTP at localhost:9385 (the executor_manager, which runs a Docker pool with gVisor) |
AliyunCodeInterpreterProvider | aliyun_codeinterpreter.py | Alibaba Cloud sandbox (uses agentrun SDK / Function Compute) |
E2BProvider | e2b.py | e2b cloud sandbox (SaaS) |
ProviderManager (manager.py) selects one provider at startup
based on configuration; the CodeExec tool talks only to the
manager, never to a specific provider.
A CodeExec call goes through agent/sandbox/client.execute_code(...)
which is the public entry point the CodeExec component uses
(agent/tools/code_exec.py:365):
from agent.sandbox.client import execute_code as sandbox_execute_code
result = sandbox_execute_code(
code=code, language=language,
timeout=timeout_seconds, arguments=arguments,
)
That function:
ProviderManager (which reads
SystemSettingsService.get_by_name("sandbox.provider_type") —
i.e. the choice is driven by the system admin panel, not the
caller).provider.create_instance(template=language) →
provider.execute_code(...) → provider.destroy_instance(...).So the CodeExec component does support all three providers —
the provider choice is invisible to it. If the provider system
is not configured, the CodeExec component falls back to a direct
HTTP POST to http://{SANDBOX_HOST}:9385/run (the executor_manager
endpoint) for backward compatibility. Both paths land in the
same _process_execution_result handler.
Go spawns python3 -c "..." that:
agent.sandbox.providers.ProviderManagerPros : Reuses the full provider surface (self-managed + Aliyun + e2b). A single Python subprocess call covers all three. : Plan §2.11.4 ("don't rewrite the sandbox") honored literally. : Operators that already deploy Python RAGFlow have the provider configuration in place; Go inherits it for free.
Cons : Per-call latency = Python interpreter startup + provider import
Read the three provider implementations and write Go equivalents:
SelfManagedProvider → Go HTTP client to localhost:9385
(the executor_manager). Smallest of the three.AliyunCodeInterpreterProvider → Go reimplementation of the
agentrun SDK client. ~Vendor SDK surface to maintain.E2BProvider → Go reimplementation of the e2b SDK client.Pros : No Python dependency on the Go side. : Lower per-call latency. : Clean integration with the rest of the Go agent runtime.
Cons
: Three SDK / API surfaces to maintain in parallel with the
Python ones. Every vendor release requires Go updates.
: Plan §2.11.4's intent is to avoid duplicating sandbox logic;
reimplementing three providers arguably violates the spirit
even if it doesn't violate the letter.
: The agentrun and e2b SDKs include auth, retry, pagination,
and connection management — real ongoing work.
Option A (shell out to a Python subprocess that uses
ProviderManager). Reasoning:
The Python subprocess must go through ProviderManager, not
directly to any one provider, so configuration stays in one place.
"Shell out to system python3 directly" (without the agentrun
SDK or any sandbox) is NOT a valid implementation — it would
execute user-supplied code with the agent process's privileges,
violating the security model.
Add a PythonProviderManagerClient implementing
SandboxClient (tool/code_exec_client.go).
The subprocess command mirrors the Python CodeExec flow:
cmd := exec.CommandContext(ctx, "python3", "-c", `
import json, sys
from agent.sandbox.client import execute_code
result = execute_code(
code=sys.argv[1],
language=sys.argv[2],
timeout=int(sys.argv[3]),
arguments=json.loads(sys.argv[4]) if sys.argv[4] else None,
)
json.dump({
"stdout": result.stdout,
"stderr": result.stderr,
"returned": "", # provider returns stdout; no REPL value
"artifacts": (result.metadata or {}).get("artifacts", []),
}, sys.stdout)
`)
cmd.Args = append(cmd.Args, code, language,
strconv.Itoa(timeout), argsJSON)
Parse the JSON response and map to SandboxResponse.
Add config knobs:
code_exec_python_bin (default python3)code_exec_provider_type (read by Python — let the admin
panel set sandbox.provider_type as today)Capability parity note: by going through
agent.sandbox.client.execute_code, the Go CodeExec tool inherits
all three providers (self_managed / aliyun_codeinterpreter /
e2b) for the cost of one Python subprocess call. The provider
choice happens inside Python based on SystemSettingsService,
invisible to the Go side. This matches what the Python
agent/tools/code_exec.py does today (lines 358-381 of that
file).
This is not an implementation task. It records the agreed-upon
direction so any future contributor (or this file's author, six
months from now) doesn't accidentally land a "shell out to
system python3" stub that bypasses the sandbox. If you find
yourself writing exec.CommandContext("python3", "-c", ...)
without going through ProviderManager, stop — you're
working against the plan and the security model.