Skip to content

Commit 698c370

Browse files
feat: isolate Jupyter behind a private Unix socket (#343)
* feat: isolate Jupyter behind a private Unix socket Co-Authored-By: mish@e2b.dev <mish@e2b.dev> * docs: omit internal Jupyter transport details Co-Authored-By: mish@e2b.dev <mish@e2b.dev> * ci: run interpreter tests in a separate workflow Co-Authored-By: mish@e2b.dev <mish@e2b.dev> * fix: probe Jupyter over its private socket in debug logs Co-Authored-By: mish@e2b.dev <mish@e2b.dev> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev>
1 parent 11d8a0f commit 698c370

12 files changed

Lines changed: 165 additions & 13 deletions

File tree

‎.github/workflows/server_tests.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Interpreter Server Tests
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
test:
11+
name: Interpreter Server Tests
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 10
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
17+
18+
- name: Parse .tool-versions
19+
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
20+
with:
21+
filename: '.tool-versions'
22+
uppercase: 'true'
23+
prefix: 'tool_version_'
24+
25+
- name: Install uv
26+
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
27+
with:
28+
version: '${{ env.TOOL_VERSION_UV }}'
29+
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
30+
enable-cache: true
31+
32+
- name: Test interpreter server
33+
run: uv run --with-requirements template/requirements-test.txt pytest template/tests

‎template/debug_logs.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"systemctl --no-pager status code-interpreter || true",
1616
"journalctl --no-pager -u jupyter || true",
1717
"journalctl --no-pager -u code-interpreter || true",
18-
"curl -s --max-time 3 -o /dev/null -w 'jupyter :8888 -> %{http_code}\\n' http://localhost:8888/api/status || true",
18+
"curl --unix-socket /run/e2b-jupyter/server.sock -s --max-time 3 -o /dev/null -w 'jupyter socket -> %{http_code}\\n' http://localhost/api/status || true",
1919
"curl -s --max-time 3 -o /dev/null -w 'server :49999 -> %{http_code}\\n' http://localhost:49999/health || true",
2020
]
2121

‎template/jupyter-healthcheck.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ MAX_RETRIES=50
66
RETRY_INTERVAL=0.2
77

88
for i in $(seq 1 $MAX_RETRIES); do
9-
status_code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8888/api/status")
9+
status_code=$(curl --unix-socket /run/e2b-jupyter/server.sock -s -o /dev/null -w "%{http_code}" "http://localhost/api/status")
1010

1111
if [ "$status_code" -eq 200 ]; then
1212
echo "Jupyter Server is healthy"

‎template/jupyter_server_config.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
c = get_config() # noqa
44

5+
c.ServerApp.sock = "/run/e2b-jupyter/server.sock"
6+
c.ServerApp.sock_mode = "0600"
7+
58

69
# Pin the contents root directory.
710
#
@@ -22,7 +25,7 @@
2225
#
2326
# Takes precedence over allow_origin_pat.
2427
# Default: ''
25-
c.ServerApp.allow_origin = "*"
28+
c.ServerApp.allow_origin = ""
2629

2730

2831
# Allow requests where the Host header doesn't point to a local server
@@ -38,7 +41,7 @@
3841
# Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local,
3942
# along with hostnames configured in local_hostnames.
4043
# Default: False
41-
c.ServerApp.allow_remote_access = True
44+
c.ServerApp.allow_remote_access = False
4245

4346
# Disable cross-site-request-forgery protection
4447
#

‎template/pytest.ini‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
pythonpath = server
3+
asyncio_mode = auto
4+
testpaths = tests

‎template/requirements-test.txt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-r server/requirements.txt
2+
pytest==9.0.3
3+
pytest-asyncio==1.3.0
4+
jupyter-server==2.20.0
5+
ipykernel==6.31.0

‎template/server/consts.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
JUPYTER_BASE_URL = "http://localhost:8888"
1+
JUPYTER_SOCKET_PATH = "/run/e2b-jupyter/server.sock"
2+
JUPYTER_BASE_URL = "http://localhost"

‎template/server/main.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from api.models.context import Context
1313
from api.models.create_context import CreateContext
1414
from api.models.execution_request import ExecutionRequest
15-
from consts import JUPYTER_BASE_URL
15+
from consts import JUPYTER_BASE_URL, JUPYTER_SOCKET_PATH
1616
from contexts import create_context, normalize_language
1717
from messaging import ContextWebSocket
1818
from stream import StreamingListJsonResponse
@@ -32,7 +32,9 @@
3232
@asynccontextmanager
3333
async def lifespan(app: FastAPI):
3434
global client
35-
client = httpx.AsyncClient()
35+
client = httpx.AsyncClient(
36+
transport=httpx.AsyncHTTPTransport(uds=JUPYTER_SOCKET_PATH), trust_env=False
37+
)
3638

3739
try:
3840
python_context = await create_context(

‎template/server/messaging.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
Union,
1414
)
1515
from pydantic import StrictStr
16-
from websockets.client import WebSocketClientProtocol, connect
16+
from websockets.client import WebSocketClientProtocol, unix_connect
1717
from websockets.exceptions import (
1818
ConnectionClosedError,
1919
WebSocketException,
@@ -28,7 +28,7 @@
2828
OutputType,
2929
UnexpectedEndOfExecution,
3030
)
31-
from consts import JUPYTER_BASE_URL
31+
from consts import JUPYTER_BASE_URL, JUPYTER_SOCKET_PATH
3232
from errors import ExecutionError
3333
from envs import get_envs
3434

@@ -67,7 +67,7 @@ def __init__(self, context_id: str, session_id: str, language: str, cwd: str):
6767
self.language = language
6868
self.cwd = cwd
6969
self.context_id = context_id
70-
self.url = f"ws://localhost:8888/api/kernels/{context_id}/channels"
70+
self.url = f"ws://localhost/api/kernels/{context_id}/channels"
7171
self.session_id = session_id
7272
self._executions: Dict[str, Execution] = {}
7373
self._lock = asyncio.Lock()
@@ -87,8 +87,9 @@ async def connect(self):
8787
ws_logger = logger.getChild("websockets.client")
8888
ws_logger.setLevel(logging.ERROR)
8989

90-
self._ws = await connect(
91-
self.url,
90+
self._ws = await unix_connect(
91+
JUPYTER_SOCKET_PATH,
92+
uri=self.url,
9293
ping_timeout=PING_TIMEOUT,
9394
max_size=None,
9495
max_queue=None,
@@ -104,7 +105,10 @@ async def connect(self):
104105
async def interrupt(self):
105106
"""Interrupt the current kernel execution via the Jupyter REST API."""
106107
try:
107-
async with httpx.AsyncClient() as client:
108+
async with httpx.AsyncClient(
109+
transport=httpx.AsyncHTTPTransport(uds=JUPYTER_SOCKET_PATH),
110+
trust_env=False,
111+
) as client:
108112
response = await client.post(
109113
f"{JUPYTER_BASE_URL}/api/kernels/{self.context_id}/interrupt"
110114
)

‎template/start-up.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/bin/bash
22

3+
install -d -m 0700 /run/e2b-jupyter
4+
35
function start_code_interpreter() {
46
/root/.jupyter/jupyter-healthcheck.sh
57
if [ $? -ne 0 ]; then

0 commit comments

Comments
 (0)