This document provides an in-depth explanation of the code-interpreter service architecture, execution environments, and security model.
- Overview
- Architecture
- Execution Environments
- Security Model
- Last-Line Interactive Mode
- File Management
The code-interpreter service is a secure FastAPI-based platform for executing untrusted Python code in isolated environments. It uses a pluggable executor architecture that supports different isolation backends (Docker, Kubernetes) while maintaining consistent security guarantees.
The service follows a clean separation of concerns across four layers:
┌─────────────────────────────────────────────────────┐
│ API Layer (FastAPI) │
│ - Request validation (Pydantic) │
│ - HTTP routing │
│ - Error handling │
└──────────────────┬──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ Service Layer │
│ - Executor factory (backend selection) │
│ - File management │
│ - Business logic │
└──────────────────┬──────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────┐
│ Executor Abstraction Layer │
│ - BaseExecutor (abstract interface) │
│ - ExecutorProtocol (type interface) │
│ - Common utilities (output truncation, etc.) │
└──────────────────┬──────────────────────────────────┘
│
┌──────────┴──────────┐
│ │
┌───────▼───────┐ ┌────────▼────────┐
│ DockerExecutor│ │KubernetesExecutor│
│ │ │ │
└───────────────┘ └──────────────────┘
Layer Responsibilities:
- API Layer: Validates requests, enforces API contracts, handles HTTP concerns
- Service Layer: Orchestrates business logic, manages executor lifecycle
- Executor Layer: Provides unified interface for different execution backends
- Backend Implementations: Handle environment-specific isolation and execution
- Request Reception: FastAPI receives POST request at
/v1/execute - Validation: Pydantic models validate code, files, and execution parameters
- Executor Selection: Factory pattern selects backend based on
EXECUTOR_BACKENDenv var - File Preparation: Files encoded as base64 are decoded and prepared for injection
- Code Wrapping: If
last_line_interactive=true, code is wrapped to auto-print last expression - Environment Creation: Ephemeral isolated environment is created
- Code Execution: Python code runs in isolation with resource limits enforced
- Output Collection: stdout, stderr, exit code, and execution time are captured
- Workspace Extraction: Generated files are extracted from the workspace
- Cleanup: Execution environment is destroyed
- Response: Results are returned to client
The Docker executor (executor_docker.py) provides strong isolation using Linux containers.
Container Lifecycle:
1. Create ephemeral container (--rm, --network none)
2. Start with sleep command (detached mode)
3. Inject code + files via tar archive
4. Execute Python as unprivileged user (uid:gid 65532:65532)
5. Extract workspace artifacts
6. Kill and cleanup container
Security Controls:
| Control | Implementation | Purpose |
|---|---|---|
| Network Isolation | --network none |
No network access (prevents data exfiltration) |
| Capability Dropping | --cap-drop ALL, --cap-add CHOWN |
Minimal privileges (CHOWN only for workspace setup) |
| No New Privileges | --security-opt no-new-privileges |
Prevents privilege escalation |
| Process Limits | --pids-limit 64 |
Prevents fork bombs |
| Unprivileged Execution | Run as user 65532:65532 | Non-root execution |
| Read-Only Root | Workspace as tmpfs | Prevents filesystem tampering |
| Ephemeral Containers | --rm flag |
Automatic cleanup |
| Memory Limits | --memory, --memory-swap |
Prevents memory exhaustion |
| CPU Limits | --ulimit cpu |
Prevents CPU exhaustion |
File System Isolation:
Container Filesystem Layout:
/ (read-only root filesystem)
├── tmp/ (tmpfs, 64MB, writable)
│ └── matplotlib/ (matplotlib cache dir)
├── workspace/ (tmpfs, 100MB, owned by 65532:65532)
│ ├── __main__.py (injected user code)
│ └── <user-files> (injected via tar)
└── opt/
└── executor-venv/ (pre-installed Python packages)
- Workspace Injection: Code and files are injected via tar archive streaming
- Ownership: All workspace files owned by unprivileged user (65532:65532)
- Isolation: Workspace is ephemeral tmpfs (memory-backed, no disk persistence)
Resource Limits:
# Memory (configurable via MEMORY_LIMIT_MB)
--memory 256m --memory-swap 256m # Hard limit, no swap
# CPU Time (configurable via CPU_TIME_LIMIT_SEC)
--ulimit cpu=5:5 # SIGKILL after 5 seconds of CPU time
# Process Count
--pids-limit 64 # Maximum 64 processes
# Filesystem
--tmpfs /tmp:size=64m # 64MB temp storage
--tmpfs /workspace:size=100m # 100MB workspaceExecution Model:
- Container starts with
sleepcommand (keeps container alive) - Tar archive with code + files streamed via stdin to
docker exec tar -x - Python execution via
docker exec -u 65532:65532 python __main__.py - Timeout enforcement via
subprocess.communicate(timeout=...) - On timeout:
pkill -9 pythoninside container, then kill container
The recommended deployment mode mounts the host's Docker socket:
docker run --rm -it \
--user root \
-p 8000:8000 \
-v /var/run/docker.sock:/var/run/docker.sock \
code-interpreterSecurity Considerations:
- Privilege Requirement: Requires root inside API container to access Docker socket. Note
that the container actually running arbitrary python, does not use the root user. Also does
not require
--privileged. - Isolation Trade-off: Executor containers run on host Docker daemon.
- Attack Vector: Compromised API container could spawn malicious containers
- Mitigation: API container itself should be isolated (network policies, resource limits)
Advantages:
- Simpler deployment (no Docker-in-Docker)
- Better performance (no nested virtualization)
- Standard Docker security controls apply
Alternatively, enable nested Docker:
docker build -f code-interpreter/Dockerfile .
docker run --privileged code-interpreterTrade-offs:
⚠️ Requires--privilegedflag- Stronger isolation between API and executor containers
- More complex setup, potential stability issues
The Kubernetes executor (executor_kubernetes.py) provides cloud-native, scalable isolation using Kubernetes Pods.
Pod Lifecycle:
1. Create Pod manifest with security constraints
2. Submit Pod to Kubernetes API
3. Wait for Pod to reach Running state
4. Inject code + files via kubectl exec tar -x
5. Execute Python via kubectl exec python
6. Extract workspace artifacts via kubectl exec tar -c
7. Delete Pod (grace period 0)
Security Controls:
| Control | Implementation | Purpose |
|---|---|---|
| RunAsNonRoot | securityContext.runAsNonRoot: true |
Enforces non-root execution |
| User/Group | runAsUser: 65532, runAsGroup: 65532 |
Unprivileged execution |
| No Privilege Escalation | allowPrivilegeEscalation: false |
Prevents setuid/setgid |
| Capability Dropping | drop: ["ALL"] |
Zero Linux capabilities |
| Network Policy | (Cluster-configurable) | Can restrict network access |
| Resource Limits | limits.memory, limits.cpu |
Prevents resource exhaustion |
| Ephemeral Storage | emptyDir volumes |
No persistent storage |
| ServiceAccount | Minimal or none | Restricts Kubernetes API access |
Pod Security Context:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]Resource Limits:
resources:
limits:
memory: "256Mi" # Configurable via MEMORY_LIMIT_MB
cpu: "5" # Configurable via CPU_TIME_LIMIT_SEC
requests:
memory: "64Mi" # Request 25% of limit
cpu: "100m" # Request minimal CPUFile System Layout:
Pod Filesystem:
/ (read-only container filesystem)
├── tmp/ (emptyDir, 64Mi limit)
│ └── matplotlib/
├── workspace/ (emptyDir, 100Mi limit, uid:gid 65532:65532)
│ ├── __main__.py
│ └── <user-files>
└── opt/
└── executor-venv/
Execution Model:
- Pod created with
sleep 3600command - Wait up to 30 seconds for Pod to reach Running state
- Stream tar archive via
kubectl exec -i tar -x - Execute Python via
kubectl exec python __main__.py - Read output via WebSocket streams (stdout, stderr, error channel)
- Timeout via client-side timer (kill Python process with
pkill -9on timeout) - Extract files via
kubectl exec tar -c - Delete Pod with
grace_period_seconds=0
Kubernetes-Specific Security:
- Pod Security Standards: Can enforce restricted, baseline, or privileged policies
- Network Policies: Can isolate Pods from cluster network
- Resource Quotas: Cluster-level limits on compute resources
- RBAC: ServiceAccount with minimal permissions
- Admission Controllers: Can enforce additional security constraints (e.g., PSP, OPA)
Advantages Over Docker:
- Native cloud orchestration
- Multi-tenancy support (namespaces)
- Built-in resource management
- Audit logging (Kubernetes API server)
- Integration with cloud-native security tools
Trade-offs:
- More complex deployment (requires Kubernetes cluster)
- Higher latency (Pod creation overhead ~1-5 seconds)
- Requires cluster permissions (create/delete Pods)
User Namespace Isolation:
- All code execution happens as UID/GID 65532 (unprivileged user)
- User
65532is the "nobody" user with zero permissions - No ability to interact with host processes
PID Namespace Isolation:
- Processes cannot see host processes
- Docker: Enforced by container runtime
- Kubernetes: Enforced by container runtime + Pod isolation
Docker:
--network none: Complete network stack removal- No loopback except localhost
- No external connectivity (blocks data exfiltration)
Kubernetes:
- Default: Pod has network access (cluster networking)
- Recommended: Apply NetworkPolicy to deny all egress
- Can allow specific destinations if needed (e.g., package registries)
Example NetworkPolicy for zero network access:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: code-interpreter-deny-all
spec:
podSelector:
matchLabels:
component: executor
policyTypes:
- Ingress
- Egress
# Empty ingress/egress = deny allRead-Only Root Filesystem:
- Container base filesystem is read-only (Docker default)
- Prevents tampering with Python interpreter, libraries
Ephemeral Storage:
- Workspace is tmpfs/emptyDir (memory-backed)
- Data deleted on container/Pod termination
- No persistence = no cross-execution contamination
Path Validation:
def _validate_relative_path(self, path_str: str) -> Path:
# Prevents path traversal attacks
# Blocks: absolute paths, "..", empty paths
# Ensures files stay within workspaceDocker:
--memory 256m --memory-swap 256m- Hard limit enforced by cgroups
- OOM killer terminates process if exceeded
- No swap = prevents disk thrashing
Kubernetes:
resources:
limits:
memory: "256Mi"- Hard limit enforced by kubelet
- OOM killed if exceeded
Docker:
--ulimit cpu=5:5- SIGKILL sent after 5 seconds of CPU time
- Measures actual CPU consumption (not wall time)
- Prevents infinite loops, crypto mining
Kubernetes:
resources:
limits:
cpu: "5"- Throttling applied via CFS (Completely Fair Scheduler)
- Less strict than Docker ulimit (throttling, not killing)
- Recommendation: Combine with client-side timeout
Wall Clock Timeout:
timeout_ms = 60_000 # Default 60 seconds
proc.communicate(timeout=timeout_ms / 1000.0)- Enforced by parent process
- On timeout:
SIGKILLsent to Python process - Protects against sleep, network waits, blocking I/O
Combined Strategy:
- Wall clock timeout: Protects against blocking operations
- CPU time limit: Protects against infinite loops
- Memory limit: Protects against memory bombs
--pids-limit 64 # Docker only- Prevents fork bombs
- Maximum 64 processes/threads per container
- Kubernetes: Enforced by PID cgroup controller (if enabled)
MAX_OUTPUT_BYTES = 1_000_000 # 1 MB default- Prevents memory exhaustion in API server
- Truncates stdout/stderr at limit
- Suffix:
\n...[truncated]
Protection:
- Code is written to file (
__main__.py), not passed via command-line - No shell execution (
shell=Falsein subprocess) - No string interpolation into commands
File Injection:
# Files validated before injection
validated_path = self._validate_relative_path(file_path)
# Injected via tar archive (binary safe)
tar.addfile(file_info, io.BytesIO(content))Docker:
--security-opt no-new-privileges: Blocks setuid/setgid--cap-drop ALL: No Linux capabilities- User 65532: No sudo, no setuid binaries
Kubernetes:
allowPrivilegeEscalation: false: Blocks setuid/setgidcapabilities.drop: ["ALL"]: No Linux capabilitiesrunAsNonRoot: true: Enforced by Kubernetes
Multi-Layer Defense:
| Attack Vector | Defense |
|---|---|
| Fork Bomb | --pids-limit 64 |
| Memory Bomb | --memory 256m |
| CPU Exhaustion | --ulimit cpu=5 |
| Disk Fill | tmpfs with size limits |
| Infinite Loop | Wall clock timeout + CPU limit |
| Output Flood | Output truncation (1MB) |
Docker:
- User namespace isolation (UID 65532 inside = UID 65532 outside)
- No capabilities (can't mount, modify networking, etc.)
- No new privileges (can't escalate)
- Kernel exploit still possible (use Docker-in-Docker for defense-in-depth)
Kubernetes:
- Pod Security Standards (enforce restricted profile)
- Runtime hardening (e.g., gVisor, Kata Containers)
- Node isolation (dedicated node pools for untrusted workloads)
Docker:
--network none: Complete network isolation- No DNS, no HTTP, no external connectivity
Kubernetes:
- NetworkPolicy: Deny all egress (recommended)
- Without NetworkPolicy: Risk of data exfiltration via network
Recommendation for Production:
Always apply NetworkPolicy in Kubernetes:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: executor-deny-egress
spec:
podSelector:
matchLabels:
component: executor
policyTypes:
- Egress
egress: [] # Deny all egressThe service supports a Jupyter-like REPL behavior where the last expression's value is automatically printed.
Code Wrapping:
def wrap_last_line_interactive(code: str) -> str:
"""
Wraps user code to execute the last line in Python's 'single' mode.
This mimics Jupyter notebook behavior: bare expressions print their value.
"""
# Parses AST, executes all but last line normally
# Last line: if it's an expression, compile with mode='single' and exec
# Python's 'single' mode auto-prints expression valuesExample:
# User code
x = 10
y = 20
x + y # Last line is an expression
# Wrapped code (simplified)
tree = ast.parse(code)
for node in tree.body[:-1]:
exec(compile(node)) # Execute normally
last_node = tree.body[-1]
if isinstance(last_node, ast.Expr):
exec(compile(last_node, mode='single')) # Auto-prints valueResult:
stdout: "30\n"
Behavior:
- Only the last line is affected
- Earlier expressions are not printed
- Statements (assignments, imports, etc.) don't print anything
- Matches Jupyter notebook UX
Control:
{
"last_line_interactive": true // Enable (default)
}Set to false for traditional script behavior (no auto-printing).
Request Format:
{
"code": "import pandas as pd\ndf = pd.read_csv('data.csv')\ndf.head()",
"files": [
{
"path": "data.csv",
"content": "base64-encoded-content"
}
]
}Process:
- Validation: Path validated (no
.., no absolute paths, no__main__.py) - Directory Creation: Parent directories created in tar archive
- Tar Injection: Files added to tar with correct ownership (65532:65532)
- Extraction: Tar streamed into container/Pod workspace
- Execution: Code can access files via relative paths
After execution, any files created in /workspace (except __main__.py) are extracted.
Process:
- Tar Creation:
tar -c --exclude=__main__.py -C /workspace . - Streaming: Tar archive streamed out via stdout
- Extraction: Files extracted from tar, content captured
- Response: Files returned as
WorkspaceEntry[]with base64-encoded content
Response Format:
{
"files": [
{
"path": "output.png",
"kind": "file",
"content": "base64-encoded-image"
},
{
"path": "results/",
"kind": "directory",
"content": null
}
]
}The service also provides a file storage API for managing uploaded files.
Upload:
POST /v1/files
Content-Type: multipart/form-dataStorage:
- Files stored in
FILE_STORAGE_DIR(default:/tmp/code-interpreter-files) - UUIDs used as file identifiers
- TTL-based cleanup (default: 3600 seconds)
- Size limits enforced (default: 100MB per file)
Usage in Execution:
{
"code": "import pandas as pd\ndf = pd.read_csv('data.csv')",
"files": [
{
"path": "data.csv",
"file_id": "uuid-from-upload"
}
]
}Security:
- Path validation prevents directory traversal
- Size limits prevent disk exhaustion
- TTL prevents unbounded storage growth
- Files isolated per request (no cross-request access)
The code-interpreter service provides secure, isolated Python execution through:
- Strong Isolation: Docker/Kubernetes containers with restricted capabilities
- Resource Limits: Memory, CPU, process, and output limits prevent exhaustion
- Minimal Privileges: All code runs as unprivileged user (UID 65532)
- Network Isolation: No network access by default (Docker) or via NetworkPolicy (Kubernetes)
- Ephemeral Storage: No persistent data, preventing cross-execution contamination
- Defense in Depth: Multiple overlapping security controls at every layer
Recommended Deployment:
- Development: Docker executor with Docker-out-of-Docker
- Production: Kubernetes executor with:
- NetworkPolicy (deny all egress)
- Pod Security Standards (restricted profile)
- Dedicated node pools for untrusted workloads
- Resource quotas and limits
- Monitoring and alerting on execution metrics
This architecture balances security, performance, and operational simplicity while providing strong guarantees against malicious or buggy code.