Skip to content

run_code as a user instead of root #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions .changeset/five-jobs-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@e2b/code-interpreter-template': patch
'@e2b/code-interpreter-python': patch
'@e2b/code-interpreter': patch
---

adds the ability to set the user for new contexts
7 changes: 7 additions & 0 deletions js/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export interface CreateCodeContextOpts {
* @default python
*/
language?: string,
/**
* User for the context.
*
* @default user
*/
user?: "user" | "root",
/**
* Timeout for the request in **milliseconds**.
*
Expand Down Expand Up @@ -269,6 +275,7 @@ export class Sandbox extends BaseSandbox {
body: JSON.stringify({
language: opts?.language,
cwd: opts?.cwd,
user: opts?.user,
}),
keepalive: true,
signal: this.connectionConfig.getSignal(opts?.requestTimeoutMs),
Expand Down
4 changes: 4 additions & 0 deletions python/e2b_code_interpreter/code_interpreter_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,15 @@ async def create_code_context(
self,
cwd: Optional[str] = None,
language: Optional[str] = None,
user: Optional[Literal["user", "root"]] = None,
request_timeout: Optional[float] = None,
) -> Context:
"""
Creates a new context to run code in.

:param cwd: Set the current working directory for the context, defaults to `/home/user`
:param language: Language of the context. If not specified, defaults to Python
:param user: User of the context. If not specified, defaults to `user`
:param request_timeout: Timeout for the request in **milliseconds**

:return: Context object
Expand All @@ -249,6 +251,8 @@ async def create_code_context(
data["language"] = language
if cwd:
data["cwd"] = cwd
if user:
data["user"] = user

try:
response = await self._client.post(
Expand Down
23 changes: 14 additions & 9 deletions template/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
FROM python:3.10.14

ENV HOME=/home/user

RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends \
build-essential curl git util-linux jq sudo fonts-noto-cjk

# Install Node.js 20.x from NodeSource
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs

RUN mkdir -p $HOME/.jupyter $HOME/.ipython $HOME/.server

ENV PIP_DEFAULT_TIMEOUT=100 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
JUPYTER_CONFIG_PATH="/root/.jupyter" \
IPYTHON_CONFIG_PATH="/root/.ipython" \
SERVER_PATH="/root/.server" \
JUPYTER_CONFIG_PATH="$HOME/.jupyter" \
IPYTHON_CONFIG_PATH="$HOME/.ipython" \
SERVER_PATH="$HOME/.server" \
R_VERSION=4.4.2

ENV R_HOME=/opt/R/${R_VERSION} \
JAVA_HOME=/opt/java/openjdk

# Install Jupyter
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt && ipython kernel install --name "python3" --user
RUN pip install --no-cache-dir -r requirements.txt && ipython kernel install --name "python3"

# R Kernel
RUN curl -O https://cdn.rstudio.com/r/debian-12/pkgs/r-${R_VERSION}_1_amd64.deb && sudo apt-get update && sudo apt-get install -y ./r-${R_VERSION}_1_amd64.deb && ln -s ${R_HOME}/bin/R /usr/bin/R
Expand All @@ -38,8 +42,11 @@ COPY .ts.swcrc $SERVER_PATH/.ts.swcrc
# Deno Kernel
COPY --from=denoland/deno:bin-2.0.4 /deno /usr/bin/deno
RUN chmod +x /usr/bin/deno
RUN deno jupyter --unstable --install
COPY ./deno.json /root/.local/share/jupyter/kernels/deno/kernel.json
RUN deno jupyter --unstable --install && \
mkdir -p /usr/local/share/jupyter/kernels/deno && \
mv $HOME/.local/share/jupyter/kernels/deno/* /usr/local/share/jupyter/kernels/deno/ && \
rmdir $HOME/.local/share/jupyter/kernels/deno
COPY ./deno.json /usr/local/share/jupyter/kernels/deno/kernel.json

# Bash Kernel
RUN pip install bash_kernel
Expand All @@ -49,13 +56,12 @@ RUN python -m bash_kernel.install
RUN python -m venv $SERVER_PATH/.venv

# Copy server and its requirements
RUN mkdir -p $SERVER_PATH/
COPY ./server/requirements.txt $SERVER_PATH
RUN $SERVER_PATH/.venv/bin/pip install --no-cache-dir -r $SERVER_PATH/requirements.txt
COPY ./server $SERVER_PATH

# Copy matplotlibrc
COPY matplotlibrc /root/.config/matplotlib/.matplotlibrc
COPY matplotlibrc $HOME/.config/matplotlib/matplotlibrc

# Copy Jupyter configuration
COPY ./start-up.sh $JUPYTER_CONFIG_PATH/
Expand All @@ -69,7 +75,6 @@ COPY ipython_kernel_config.py $IPYTHON_CONFIG_PATH/profile_default/
RUN mkdir -p $IPYTHON_CONFIG_PATH/profile_default/startup
COPY startup_scripts/* $IPYTHON_CONFIG_PATH/profile_default/startup


COPY --from=eclipse-temurin:11-jdk $JAVA_HOME $JAVA_HOME
RUN ln -s ${JAVA_HOME}/bin/java /usr/bin/java

Expand Down
16 changes: 8 additions & 8 deletions template/e2b.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# This is a config for E2B sandbox template.
# You can use template ID (nlhz8vlwyupq845jsdg9) or template name (code-interpreter-v1) to create a sandbox:
# You can use template ID (n1vc02i7rx9xg0lao9nx) or template name (code-interpreter-v1beta1) to create a sandbox:

# Python SDK
# from e2b import Sandbox, AsyncSandbox
# sandbox = Sandbox("code-interpreter-v1") # Sync sandbox
# sandbox = await AsyncSandbox.create("code-interpreter-v1") # Async sandbox
# sandbox = Sandbox("code-interpreter-v1beta1") # Sync sandbox
# sandbox = await AsyncSandbox.create("code-interpreter-v1beta1") # Async sandbox

# JS SDK
# import { Sandbox } from 'e2b'
# const sandbox = await Sandbox.create('code-interpreter-v1')
# const sandbox = await Sandbox.create('code-interpreter-v1beta1')

team_id = "460355b3-4f64-48f9-9a16-4442817f79f5"
memory_mb = 1_024
start_cmd = "/root/.jupyter/start-up.sh"
dockerfile = "e2b.Dockerfile"
template_name = "code-interpreter-v1"
template_id = "nlhz8vlwyupq845jsdg9"
start_cmd = "sudo -u user /home/user/.jupyter/start-up.sh"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing: this is a breaking change, should I put a script in /root/.jupyter/start-up.sh that just calls the actual start-up.sh with sudo -u user?

dockerfile = "Dockerfile"
template_name = "code-interpreter-v1beta1"
template_id = "n1vc02i7rx9xg0lao9nx"
1 change: 1 addition & 0 deletions template/server/api/models/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Context(BaseModel):
id: StrictStr = Field(description="Context ID")
language: StrictStr = Field(description="Language of the context")
cwd: StrictStr = Field(description="Current working directory of the context")
user: StrictStr = Field(description="User of the context")

def __hash__(self):
return hash(self.id)
4 changes: 4 additions & 0 deletions template/server/api/models/create_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@


class CreateContext(BaseModel):
user: Optional[StrictStr] = Field(
default="user",
description="User to run the context",
)
cwd: Optional[StrictStr] = Field(
default="/home/user",
description="Current working directory",
Expand Down
28 changes: 19 additions & 9 deletions template/server/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@

logger = logging.Logger(__name__)

def get_kernel_for_language(language: str) -> str:
if language == "typescript":
return "javascript"

return language
def get_user_cwd(user: str, cwd: Optional[str]) -> str:
if not cwd:
if user == "root":
return "/root"
return "/home/user"
return cwd

def normalize_language(language: Optional[str]) -> str:
if not language:
Expand All @@ -32,14 +33,23 @@ def normalize_language(language: Optional[str]) -> str:
return language


async def create_context(client, websockets: dict, language: str, cwd: str) -> Context:
def get_kernel_name(language: str, user: str) -> str:
if language == "typescript":
language = "javascript"

if user == "root":
return language+"_root"
return language


async def create_context(client, websockets: dict, language: str, cwd: str, user: str) -> Context:
data = {
"path": str(uuid.uuid4()),
"kernel": {"name": get_kernel_for_language(language)},
"kernel": {"name": get_kernel_name(language, user)}, # replace with root kernel when user is root
"type": "notebook",
"name": str(uuid.uuid4()),
}
logger.debug(f"Creating new {language} context")
logger.debug(f"Creating new {language} context for user {user}")

response = await client.post(f"{JUPYTER_BASE_URL}/api/sessions", json=data)

Expand Down Expand Up @@ -67,4 +77,4 @@ async def create_context(client, websockets: dict, language: str, cwd: str) -> C
status_code=500,
)

return Context(language=language, id=context_id, cwd=cwd)
return Context(language=language, id=context_id, cwd=cwd, user=user)
11 changes: 6 additions & 5 deletions template/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from api.models.create_context import CreateContext
from api.models.execution_request import ExecutionRequest
from consts import JUPYTER_BASE_URL
from contexts import create_context, normalize_language
from contexts import create_context, normalize_language, get_user_cwd
from messaging import ContextWebSocket
from stream import StreamingListJsonResponse
from utils.locks import LockedMap
Expand All @@ -34,7 +34,7 @@ async def lifespan(app: FastAPI):
global client
client = httpx.AsyncClient()

with open("/root/.jupyter/kernel_id") as file:
with open("/home/user/.jupyter/kernel_id") as file:
default_context_id = file.read().strip()

default_ws = ContextWebSocket(
Expand Down Expand Up @@ -91,7 +91,7 @@ async def post_execute(request: ExecutionRequest):
if not context_id:
try:
context = await create_context(
client, websockets, language, "/home/user"
client, websockets, language, "/home/user", "user"
)
except Exception as e:
return PlainTextResponse(str(e), status_code=500)
Expand Down Expand Up @@ -126,10 +126,11 @@ async def post_contexts(request: CreateContext) -> Context:
logger.info(f"Creating a new context")

language = normalize_language(request.language)
cwd = request.cwd or "/home/user"
user = request.user or "user"
cwd = get_user_cwd(user, request.cwd)

try:
return await create_context(client, websockets, language, cwd)
return await create_context(client, websockets, language, cwd, user)
except Exception as e:
return PlainTextResponse(str(e), status_code=500)

Expand Down
39 changes: 32 additions & 7 deletions template/start-up.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
#!/bin/bash

function create_root_kernels() {
# Get all installed kernels
kernels=$(jupyter kernelspec list --json | jq -r '.kernelspecs | keys[]')

for kernel in $kernels; do
# Get the kernel directory
kernel_dir=$(jupyter kernelspec list --json | jq -r ".kernelspecs[\"$kernel\"].resource_dir")

# Create directory for root kernel if it doesn't exist
root_kernel_dir="/usr/local/share/jupyter/kernels/${kernel}_root"
sudo mkdir -p "$root_kernel_dir"

# Copy all files from original kernel first
sudo cp -r "$kernel_dir"/* "$root_kernel_dir/" 2>/dev/null || true

# Create and write the modified kernel.json
cat "$kernel_dir/kernel.json" | jq '.argv = ["sudo"] + .argv | .display_name = .display_name + " (root)"' | sudo tee "$root_kernel_dir/kernel.json" > /dev/null

echo "Created root version of kernel: ${kernel}_root"
done
}

function start_jupyter_server() {
counter=0
response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8888/api/status")
Expand All @@ -13,22 +35,25 @@ function start_jupyter_server() {
response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8888/api/status")
done

response=$(curl -s -X POST "localhost:8888/api/sessions" -H "Content-Type: application/json" -d '{"path": "/home/user", "kernel": {"name": "python3"}, "type": "notebook", "name": "default"}')
response=$(curl -s -X POST "localhost:8888/api/sessions" -H "Content-Type: application/json" -d '{"path": "'$HOME'", "kernel": {"name": "python3"}, "type": "notebook", "name": "default"}')
status=$(echo "${response}" | jq -r '.kernel.execution_state')
if [[ ${status} != "starting" ]]; then
echo "Error creating kernel: ${response} ${status}"
exit 1
fi

sudo mkdir -p /root/.jupyter
mkdir -p $HOME/.jupyter
kernel_id=$(echo "${response}" | jq -r '.kernel.id')
sudo echo "${kernel_id}" | sudo tee /root/.jupyter/kernel_id >/dev/null
sudo echo "${response}" | sudo tee /root/.jupyter/.session_info >/dev/null
echo "${kernel_id}" > $HOME/.jupyter/kernel_id
echo "${response}" > $HOME/.jupyter/.session_info

cd /root/.server/
/root/.server/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 49999 --workers 1 --no-access-log --no-use-colors --timeout-keep-alive 640
cd $HOME/.server/
$HOME/.server/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 49999 --workers 1 --no-access-log --no-use-colors --timeout-keep-alive 640
}

echo "Creating root versions of kernels..."
create_root_kernels

echo "Starting Code Interpreter server..."
start_jupyter_server &
MATPLOTLIBRC=/root/.config/matplotlib/.matplotlibrc jupyter server --IdentityProvider.token="" >/dev/null 2>&1
MATPLOTLIBRC=$HOME/.config/matplotlib/.matplotlibrc jupyter server --IdentityProvider.token=""
Loading
Loading