Skip to content

feat(sandbox): reuse e2b container across requests when metadata.session_id is set - #31688

Merged
krrish-berri-2 merged 2 commits into
litellm_internal_stagingfrom
litellm_sticky_session_sandbox
Jul 1, 2026
Merged

feat(sandbox): reuse e2b container across requests when metadata.session_id is set#31688
krrish-berri-2 merged 2 commits into
litellm_internal_stagingfrom
litellm_sticky_session_sandbox

Conversation

@krrish-berri-2

@krrish-berri-2 krrish-berri-2 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting `@greptileai` and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

See QA runbook in `qa_sticky_session.sh`. Run the proxy with:

set -a && source .env && set +a
python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --port 4000 2>&1 | tee litellm.log &

Then run the script:

./qa_sticky_session.sh

The script verifies that: (1) a variable set in one request is visible in the next when session_id is stable, (2) changing session_id starts a fresh sandbox, (3) omitting session_id still works as an ephemeral per-request sandbox.

Type

🆕 New Feature

Changes

Adds sticky session support to the code interpreter interception layer: when a client passes metadata.session_id, the same e2b sandbox container is reused across sequential HTTP requests for that session instead of being torn down after each agentic loop.

Two security properties are enforced:

Cross-tenant isolation. The session_id supplied by the client is combined with the server-minted user_api_key_hash to form the process-wide cache key ({hash}:{session_id} when authenticated via proxy, bare session_id for non-proxy use). Two API keys that happen to send the same session_id therefore get separate sandboxes.

Bounded allocation per identity. Each API key is capped at 10 live session-scoped containers (_SESSION_SCOPED_PER_IDENTITY_CAP). When a new session would exceed the cap, the least-recently-used session for that identity is evicted and its sandbox deleted. This prevents a caller from rotating session IDs to accumulate an unbounded number of live e2b containers.

Implementation details:

  • async_pre_call_deployment_hook: when session_id is present, sets _SANDBOX_KEY = "{identity}:{session_id}" and marks _SESSION_SCOPED_KEY = True; otherwise mints a random UUID as before
  • _get_or_create_container: accepts an optional identity arg; refreshes last_accessed on cache hit (so active sessions do not expire); calls _evict_lru_session_if_over_cap before inserting a new session-scoped entry
  • _evict_lru_session_if_over_cap: filters cache entries by identity (stored as the 4th element of the cache tuple), evicts the one with the oldest last_accessed
  • Cleanup and post-loop hooks skip deletion when is_session_scoped is set in the plan metadata
  • TTL pruning (_prune_expired_cache) resets on each access, so an actively-used session does not expire mid-conversation
  • _SESSION_SCOPED_KEY is stripped from the upstream LLM request via agentic_loop_internal_litellm_params and from the inbound proxy request body

…ion_id is set

When a client passes `metadata.session_id` in a /chat/completions request
alongside a code_interpreter tool, the proxy now routes all requests sharing
that session_id to the same sandbox container. State (variables, imports,
installed packages) persists across requests within the session.

Without a session_id the existing ephemeral behavior is unchanged: one
container per agentic loop, deleted immediately after.

The sandbox key is derived from session_id rather than a per-request UUID.
The cleanup and post-loop hooks skip deletion for session-scoped containers.
TTL-based pruning (15 min idle) still applies and refreshes on every use,
so an active session never expires mid-use. The session_id-scoped key is
registered in all_litellm_params and the proxy strip-list so it never
leaks to the upstream LLM provider.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds sticky E2B code-interpreter sandboxes keyed by metadata.session_id. The main changes are:

  • Reuses a sandbox container across requests with the same session ID
  • Keeps non-session requests on the existing per-request sandbox behavior
  • Refreshes sandbox cache TTL on reuse
  • Adds internal parameter stripping for the new session-scoped flag
  • Adds unit tests and a manual QA script for sticky-session behavior

Confidence Score: 4/5

The sticky-session implementation needs tenant-scoped cache keys before it is safe to merge.

The changed code is small and covered by targeted tests, but the session cache key currently omits caller context, allowing unrelated callers with the same session identifier to share one sandbox.

litellm/integrations/code_interpreter_interception/handler.py

T-Rex T-Rex Logs

What T-Rex did

  • I executed a focused pytest harness with two simulated tenants sharing the same metadata.session_id, and both tenants received the same raw cache key demo-A and reused sandbox-container-1.
  • I logged the container lifecycle across session A and B, showing that before A2 used container-2 and could not read x, while after the head reused container-1 for both A requests; session B used container-2 and could not read x, and no-session requests used container-3/container-4 which were deleted.
  • I inspected the TTL test results, noting that ttl-refresh-01-before.txt shows the cache hit reused the same container with timestamp_after_hit=100.0; ttl-refresh-02-after.txt shows timestamp_after_hit=200.0; pruning at 1101 reports cache_present_after_prune=True and deleted_count=0, demonstrating pruning respects the refreshed timestamp.
  • I compared the before and after internal flag-strip results and observed that the before state had agentic_loop_internal_membership=false, all_litellm_params_membership=false, source_contains_flag_in_proxy_file=false, and would_leak_upstream_as_non_default=true, while the head run showed agentic_loop_internal_membership=true, all_litellm_params_membership=true, handler_session_key=_code_interpreter_interception_session_scoped, source_contains_flag_in_proxy_file=true, and would_leak_upstream_as_non_default=false.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "feat(sandbox): reuse e2b container acros..." | Re-trigger Greptile

Comment on lines +207 to +208
kwargs[_SANDBOX_KEY] = session_id
kwargs[_SESSION_SCOPED_KEY] = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Scope session cache keys
Using the raw client-supplied metadata.session_id as the process-wide sandbox cache key lets any caller who guesses or reuses the same string attach to another caller's E2B container. The cache at self._container_cache is shared for the logger instance, and there is no user/team/key component in this key, so requests with session_id="demo-A" from different tenants share code state and files.

Artifacts

Repro: focused pytest harness for cross-tenant session cache reuse

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: verbose pytest output showing shared raw cache key and shared container

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

kwargs[_SANDBOX_KEY] = uuid.uuid4().hex
session_id = _extract_session_id(kwargs)
if session_id:
kwargs[_SANDBOX_KEY] = session_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

High: Cross-user sandbox reuse

metadata.session_id is client-controlled, but it becomes the process-wide sandbox cache key without being scoped to the authenticated key, user, or team. A logged-in user who knows or collides with another caller's session ID can run code in that caller's long-lived sandbox and read files, variables, or other state left there; derive the cache key from server-side identity plus the session ID instead of using the raw metadata value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 659c9c9. The cache key is now f"{user_api_key_hash}:{session_id}" when a proxy API key is present (_extract_identity reads kwargs["user_api_key_hash"], which is the server-minted hash set by proxy auth middleware). Two tenants supplying the same session_id therefore get separate cache entries and separate sandboxes. For non-proxy use where no hash is available, the bare session_id is used as before (single-tenant context, no cross-user risk). A dedicated test test_sandbox_key_scoped_to_api_key_hash_isolates_users verifies that two callers with different hashes but the same session_id get distinct sandbox keys.

Comment thread litellm/integrations/code_interpreter_interception/handler.py
@veria-ai

veria-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request adds session-aware reuse of E2B code interpreter sandboxes, allowing requests with metadata.session_id to share a long-lived container instead of always creating a fresh one. The touched handler code manages sandbox cache lookup, creation, and lifecycle behavior for these session-scoped executions.

There are still two open security issues in the session-scoped sandbox caching logic. The most significant concern is that a client-controlled session ID is used as the cache key without server-side identity scoping, which could let one authenticated caller attach to another caller’s reused sandbox if the session ID is known or collides. A second open issue allows concurrent requests for the same new session to bypass the intended per-identity sandbox cap and leave overwritten containers untracked. One issue has already been addressed, but these remaining items leave the PR with a clear cross-user isolation risk.

Open issues (2)

Fixed/addressed: 1 · PR risk: 7/10

…identity LRU cap

Two security issues addressed:

1. Cross-user sandbox isolation: the session_id supplied by the client is now
   combined with the server-minted user_api_key_hash to form the cache key
   (format: "{hash}:{session_id}" when authenticated, bare session_id for
   non-proxy use). Two tenants sharing the same session_id no longer share a
   sandbox.

2. Bounded session allocation: each API key identity is capped at
   _SESSION_SCOPED_PER_IDENTITY_CAP (10) live session-scoped containers. When
   a new session is opened beyond the cap, the least-recently-used entry for
   that identity is evicted and its sandbox deleted, preventing unbounded
   accumulation via rotating session IDs.

The container cache tuple gains a fourth element (identity: str | None) so
eviction can filter by identity without parsing key formats. Tests added for
both properties.
@codspeed-hq

codspeed-hq Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing litellm_sticky_session_sandbox (659c9c9) with litellm_internal_staging (be4d0d8)

Open in CodSpeed

@krrish-berri-2

Copy link
Copy Markdown
Contributor Author

@veria-ai

1 similar comment
@krrish-berri-2

Copy link
Copy Markdown
Contributor Author

@veria-ai

@@ -153,6 +153,7 @@ def parse_cache_control(cache_control):
"_code_interpreter_interception_active",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we not group these variables code_interpreter_variables that way we can re-use the enum across diff places

self._container_cache[cache_key] = (cached[0], cached[1], time.time(), cached[3])
return cached[0], cached[1]

container, params = await self._create_container()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

High: Concurrent sandbox allocation bypasses the session cap

metadata.session_id is client-controlled, and concurrent requests for the same new session can all pass the cache miss check before any of them stores an entry here. Each request creates a sandbox, later writes over the same cache key, and because session-scoped plans skip both cleanup hooks, the overwritten containers are no longer tracked or deleted; an authenticated caller can repeat this to run far more sandboxes than _SESSION_SCOPED_PER_IDENTITY_CAP. Add a per-cache-key creation lock or reserve the key before awaiting _create_container(), and make sure any displaced container is explicitly deleted.

@krrish-berri-2
krrish-berri-2 merged commit 6c21029 into litellm_internal_staging Jul 1, 2026
124 of 125 checks passed
@krrish-berri-2
krrish-berri-2 deleted the litellm_sticky_session_sandbox branch July 1, 2026 01:58
krrish-berri-2 added a commit to BerriAI/litellm-docs that referenced this pull request Jul 1, 2026
Covers the new sticky-session support in the code interpreter interceptor
(BerriAI/litellm#31688): curl + OpenAI SDK examples, per-key hash
isolation, 10-session LRU cap, TTL reset on access, and a Notes update
so the "one sandbox per request" line no longer contradicts the feature.

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
duanhongyi pushed a commit to duanhongyi/litellm that referenced this pull request Jul 2, 2026
…ion_id is set (BerriAI#31688)

* feat(sandbox): reuse e2b container across requests when metadata.session_id is set

When a client passes `metadata.session_id` in a /chat/completions request
alongside a code_interpreter tool, the proxy now routes all requests sharing
that session_id to the same sandbox container. State (variables, imports,
installed packages) persists across requests within the session.

Without a session_id the existing ephemeral behavior is unchanged: one
container per agentic loop, deleted immediately after.

The sandbox key is derived from session_id rather than a per-request UUID.
The cleanup and post-loop hooks skip deletion for session-scoped containers.
TTL-based pruning (15 min idle) still applies and refreshes on every use,
so an active session never expires mid-use. The session_id-scoped key is
registered in all_litellm_params and the proxy strip-list so it never
leaks to the upstream LLM provider.

* fix(sandbox): scope session sandbox key to API key identity; add per-identity LRU cap

Two security issues addressed:

1. Cross-user sandbox isolation: the session_id supplied by the client is now
   combined with the server-minted user_api_key_hash to form the cache key
   (format: "{hash}:{session_id}" when authenticated, bare session_id for
   non-proxy use). Two tenants sharing the same session_id no longer share a
   sandbox.

2. Bounded session allocation: each API key identity is capped at
   _SESSION_SCOPED_PER_IDENTITY_CAP (10) live session-scoped containers. When
   a new session is opened beyond the cap, the least-recently-used entry for
   that identity is evicted and its sandbox deleted, preventing unbounded
   accumulation via rotating session IDs.

The container cache tuple gains a fourth element (identity: str | None) so
eviction can filter by identity without parsing key formats. Tests added for
both properties.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants