diff --git a/.changeset/five-jobs-cry.md b/.changeset/five-jobs-cry.md new file mode 100644 index 00000000..f576343f --- /dev/null +++ b/.changeset/five-jobs-cry.md @@ -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 diff --git a/js/src/sandbox.ts b/js/src/sandbox.ts index 74db4c9e..024c5855 100644 --- a/js/src/sandbox.ts +++ b/js/src/sandbox.ts @@ -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**. * @@ -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), diff --git a/python/e2b_code_interpreter/code_interpreter_async.py b/python/e2b_code_interpreter/code_interpreter_async.py index c521af83..79e7a809 100644 --- a/python/e2b_code_interpreter/code_interpreter_async.py +++ b/python/e2b_code_interpreter/code_interpreter_async.py @@ -231,6 +231,7 @@ 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: """ @@ -238,6 +239,7 @@ async def create_code_context( :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 @@ -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( diff --git a/template/Dockerfile b/template/Dockerfile index 199a5cb0..5add3018 100644 --- a/template/Dockerfile +++ b/template/Dockerfile @@ -1,5 +1,7 @@ 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 @@ -7,12 +9,14 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-ins 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} \ @@ -20,7 +24,7 @@ ENV R_HOME=/opt/R/${R_VERSION} \ # 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 @@ -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 @@ -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/ @@ -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 diff --git a/template/e2b.toml b/template/e2b.toml index a488e5d9..0ab84f55 100644 --- a/template/e2b.toml +++ b/template/e2b.toml @@ -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" +dockerfile = "Dockerfile" +template_name = "code-interpreter-v1beta1" +template_id = "n1vc02i7rx9xg0lao9nx" diff --git a/template/server/api/models/context.py b/template/server/api/models/context.py index 5efb850e..c3e6a25f 100644 --- a/template/server/api/models/context.py +++ b/template/server/api/models/context.py @@ -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) diff --git a/template/server/api/models/create_context.py b/template/server/api/models/create_context.py index 1e1fefb7..26808bac 100644 --- a/template/server/api/models/create_context.py +++ b/template/server/api/models/create_context.py @@ -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", diff --git a/template/server/contexts.py b/template/server/contexts.py index d078dc6e..b3051f2b 100644 --- a/template/server/contexts.py +++ b/template/server/contexts.py @@ -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: @@ -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) @@ -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) diff --git a/template/server/main.py b/template/server/main.py index fa7760a9..d3264026 100644 --- a/template/server/main.py +++ b/template/server/main.py @@ -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 @@ -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( @@ -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) @@ -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) diff --git a/template/start-up.sh b/template/start-up.sh index 735e5811..2bf2eb21 100644 --- a/template/start-up.sh +++ b/template/start-up.sh @@ -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") @@ -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="" diff --git a/template/test.Dockerfile b/template/test.Dockerfile index f9302081..919fd1f6 100644 --- a/template/test.Dockerfile +++ b/template/test.Dockerfile @@ -1,5 +1,7 @@ FROM python:3.10.14 +ENV HOME=/home/user + ENV JAVA_HOME=/opt/java/openjdk COPY --from=eclipse-temurin:11-jdk $JAVA_HOME $JAVA_HOME ENV PATH="${JAVA_HOME}/bin:${PATH}" @@ -7,6 +9,12 @@ ENV PATH="${JAVA_HOME}/bin:${PATH}" 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 +# Create new user with root privileges while keeping root user +RUN useradd -m -s /bin/bash user && \ + echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ + echo 'user:password' | chpasswd && \ + usermod -aG sudo user + # Install Node.js 20.x from NodeSource RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get install -y nodejs @@ -14,13 +22,13 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ 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" # Install Jupyter COPY ./template/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" # Javascript Kernel RUN npm install -g --unsafe-perm ijavascript @@ -33,8 +41,12 @@ COPY ./template/.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 ./template/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 ./template/deno.json /usr/local/share/jupyter/kernels/deno/kernel.json # Create separate virtual environment for server RUN python -m venv $SERVER_PATH/.venv @@ -46,7 +58,7 @@ RUN $SERVER_PATH/.venv/bin/pip install --no-cache-dir -r $SERVER_PATH/requiremen COPY ./template/server $SERVER_PATH # Copy matplotlibrc -COPY ./template/matplotlibrc /root/.config/matplotlib/matplotlibrc +COPY ./template/matplotlibrc $HOME/.config/matplotlib/matplotlibrc # Copy Jupyter configuration COPY ./template/start-up.sh $JUPYTER_CONFIG_PATH/ @@ -61,7 +73,12 @@ RUN mkdir -p $IPYTHON_CONFIG_PATH/profile_default/startup COPY ./template/startup_scripts/* $IPYTHON_CONFIG_PATH/profile_default/startup # Setup entrypoint for local development -WORKDIR /home/user +WORKDIR $HOME COPY ./chart_data_extractor ./chart_data_extractor RUN pip install -e ./chart_data_extractor + +# Change ownership of all files to user +RUN chown -R user:user $HOME + +USER user ENTRYPOINT $JUPYTER_CONFIG_PATH/start-up.sh