Repository navigation
Bug: Consumer-registered sandbox tools (LibreChat's create_file/edit_file) bypass code-session context — files invisible to subsequent bash_tool calls #274
Description
Bug: Consumer-registered sandbox tools (LibreChat's create_file/edit_file) bypass code-session context — files invisible to subsequent bash_tool calls
Summary
CODE_EXECUTION_TOOLS is a closed set defined in the SDK (src/common/enum.ts). It determines which tool calls get codeSessionContext populated (session_id + files) and which propagate their execution artifact's session_id back to Graph.sessions[EXECUTE_CODE]. Consumers (LibreChat) can register additional tools that route to the same remote sandbox /exec endpoint — but those tools are invisible to the 5 gates that key off CODE_EXECUTION_TOOLS.has(name), so they:
- Get
codeSessionContext = undefinedon the ToolCallRequest →session_idis not injected at invocation → the sandbox creates a new session. - Do not propagate their artifact's
session_idback toGraph.sessionsafter execution → the new session is lost.
Result: a file created by the consumer-registered tool in one turn is stored under session_id A, while the next bash_tool call (which IS in CODE_EXECUTION_TOOLS) reuses the original session_id B from Graph.sessions — and the file is not there. The sandbox reports "file not found".
Reproduction (via LibreChat 0.8.7 + LibreCodeInterpreter)
- Configure LibreChat 0.8.7 with an external Code Interpreter sandbox (e.g.
usnavy13/LibreCodeInterpreter) and an agent withexecute_codecapability +execute_codeintools. - LibreChat registers
create_fileandedit_filetools (packages/api/src/agents/initialize.ts:1096, PR 🗂️ feat: Add Agent File Authoring Tools LibreChat#13435) in addition tobash_tool+read_file. - Prompt the LLM to:
create_file('/mnt/data/mandelbrot.py', <content>)thenbash_tool('ls /mnt/data/'). - Observed:
create_filereturns "Created", butbash_toolreports the file does not exist.
The same file created via bash_tool heredoc (cat << 'EOF' > /mnt/data/file.py) DOES persist across subsequent bash_tool calls — because bash_tool is in CODE_EXECUTION_TOOLS and shares session_id via Graph.sessions[EXECUTE_CODE].
Root cause — 5 gates that exclude consumer-registered sandbox tools
CODE_EXECUTION_TOOLS (src/common/enum.ts:224-229):
export const CODE_EXECUTION_TOOLS: ReadonlySet<string> = new Set([
Constants.EXECUTE_CODE,
Constants.BASH_TOOL,
Constants.PROGRAMMATIC_TOOL_CALLING,
Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
]);Note: Constants.WRITE_FILE and Constants.EDIT_FILE exist (enum.ts:215-216) but belong to LOCAL_CODING_TOOL_NAMES (the local execution engine, PR #139) — they are NOT the same as LibreChat's remote-sandbox create_file/edit_file, and are not in CODE_EXECUTION_TOOLS.
The 5 gates:
getCodeSessionContext(src/stream.ts:180-189): returnsundefinedunlessCODE_EXECUTION_TOOLS.has(name) || name === SKILL_TOOL || name === READ_FILE. This populatestc.codeSessionContext(used atstream.ts:263).ToolNode.ts:976:if (CODE_EXECUTION_TOOLS.has(call.name))→ injectssession_idfromGraph.sessions[EXECUTE_CODE]intoinvokeParams. Skipped for consumer-registered sandbox tools → no session_id sent.ToolNode.ts:1780(eager path):if (!CODE_EXECUTION_TOOLS.has(request.name) && request.name !== SKILL_TOOL)→continue. Artifact'ssession_idnot propagated.ToolNode.ts:1833(standard path):if (this.sessions && CODE_EXECUTION_TOOLS.has(call.name))→updateCodeSession(this.sessions, execSessionId, artifact?.files). Skipped for consumer-registered tools → session_id lost.ToolNode.ts:2463-2477(planning path):codeSessionContext = CODE_EXECUTION_TOOLS.has(entry.call.name) || entry.call.name === SKILL_TOOL || entry.call.name === READ_FILE ? this.getCodeSessionContext() : undefined. Same exclusion.
For create_file, all 5 gates skip it → no session_id injected on invocation, no session_id propagated after execution.
Precedent: read_file was added as an exception
PR #99 (commit 4154c17b1a, 2026-04-14) "feat: add ReadFile tool definition for general-purpose file reading" added READ_FILE to the exception lists at stream.ts:185 and ToolNode.ts:2468. The same treatment should arguably apply to any consumer-registered tool that routes to the sandbox — but read_file was hardcoded as a single exception, not a general mechanism.
Proposed fix
The cleanest fix is to make the session-aware tool set extensible by the consumer. Two options:
Option A — Allow consumers to declare additional session-aware tool names
Add an optional additionalCodeSessionTools?: string[] (or Set<string>) to the graph/run config. Internally, build a combined set:
const SESSION_AWARE_TOOLS = new Set([
...CODE_EXECUTION_TOOLS,
...(config.additionalCodeSessionTools ?? []),
]);Replace the 5 CODE_EXECUTION_TOOLS.has(name) gates with SESSION_AWARE_TOOLS.has(name). LibreChat would pass ['create_file', 'edit_file'] (and any future consumer-registered sandbox tool).
Option B — Generalize the read_file exception pattern
If extending the config surface is too invasive, at minimum add create_file and edit_file to the exception lists alongside SKILL_TOOL and READ_FILE. Downside: requires an SDK release for every new consumer-registered sandbox tool.
Environment
@librechat/agents: 3.2.52 (latest tag at time of writing)- LibreChat: 0.8.7 (also reproduced in 0.8.7-rc1)
- Sandbox:
usnavy13/LibreCodeInterpreter(any sandbox that keys file persistence onsession_idwill exhibit the bug)
Workaround (consumer-side)
LibreChat can work around this in packages/api/src/agents/handlers.ts by reading session_id from graph.sessions[EXECUTE_CODE] before invoking create_file/edit_file, and writing the artifact's session_id back to graph.sessions[EXECUTE_CODE] after execution. This duplicates logic that the SDK already implements for bash_tool.
References
- SDK enum:
src/common/enum.ts:215-229 - SDK stream:
src/stream.ts:180-189,:263 - SDK ToolNode:
src/tools/ToolNode.ts:976,:1732-1752,:1778-1784,:1831-1841,:2463-2477 - LibreChat PR #13435 (introduced
create_file/edit_file): danny-avila/LibreChat@1da789bac - LibreChat handler:
packages/api/src/agents/handlers.ts:3189,3226-3234,3260-3265,3474-3497 - LibreChat file IO:
api/server/services/Files/Code/process.js:1059-1099 - Precedent for
read_fileexception: PR feat: add ReadFile tool definition for general-purpose file reading #99, commit4154c17b1a(2026-04-14)
Activity
danny-avila commentedon Jun 25, 2026
Use the official code interpreter: https://github.com/ClickHouse/code-interpreter
fabienbarbaud commentedon Jun 26, 2026
I followed your advice and ported to the official ClickHouse code-interpreter (https://github.com/ClickHouse/code-interpreter). The bug still reproduces — it is in the SDK, not the interpreter.
Proof the interpreter works (direct curl tests)
The ClickHouse API correctly persists files across separate
/v1/execcalls via its File Server (S3/MinIO):Step 1 — create file (sandbox auto-uploads to MinIO):
POST /v1/exec {"lang":"bash","code":"echo 'print(42)' > /mnt/data/test.py"} → 200 { "session_id": "7lbp...", "files": [{"id":"26Jbk...","name":"test.py","storage_session_id":"7lbp..."}], "stdout": "", "code": 0 }The sandbox auto-uploads files written to
/mnt/datato the File Server and returns them in thefilesarray.Step 2 — execute the file in a separate call (sandbox downloads from MinIO):
POST /v1/exec {"lang":"bash","code":"python3 /mnt/data/test.py", "files":[{"id":"26Jbk...","resource_id":"local-user", "storage_session_id":"7lbp...","name":"test.py","kind":"user"}]} → 200 {"stdout":"42\n","code":0,"wall_time":291}The sandbox downloads the file from the File Server into a fresh
/mnt/databefore NsJail starts. Works end-to-end.The bug still reproduces with the official interpreter
In LibreChat (0.8.7, default code tools, discussion mode — no agent):
create_file→ creates/mnt/data/fun_script.pybash_tool→python3 /mnt/data/fun_script.pypython3: can't open file '/mnt/data/fun_script.py': [Errno 2] No such file or directorySame exact symptom as the original report. The only difference: we now know the interpreter is not at fault.
Why it still fails — the SDK doesn't propagate
filesThe ClickHouse interpreter returns
files: [{id, name, storage_session_id}]in thecreate_fileresponse (Step 1 above). The SDK is supposed to:_injected_fileson the nextbash_toolcallfilesarray of thePOST /execbodyBut the 5 gates keying off
CODE_EXECUTION_TOOLS.has(name)skipcreate_file/edit_file(they are not in the set), so:ToolNode.ts:1833:updateCodeSession(sessions, execSessionId, artifact?.files)is skipped → files never stored_injected_filesstays empty forbash_tool→ nofilessent in the request body → sandbox starts with empty/mnt/data→ file not foundConclusion
The interpreter change was a red herring. The bug is in
@librechat/agents: consumer-registered sandbox tools (create_file/edit_file) are invisible to the 5CODE_EXECUTION_TOOLS.has(name)gates, so theirfilesartifacts are not propagated to subsequent calls.Could you reopen this issue? The proposed fix (Option A:
additionalCodeSessionToolsconfig param, or Option B: addcreate_file/edit_fileto the exception lists likeread_file) is still valid.fabienbarbaud commentedon Jul 6, 2026
@danny-avila ?