Skip to content

Bug: Consumer-registered sandbox tools (LibreChat's create_file/edit_file) bypass code-session context — files invisible to subsequent bash_tool calls #274

Closed as not planned

Description

@fabienbarbaud

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:

  1. Get codeSessionContext = undefined on the ToolCallRequest → session_id is not injected at invocation → the sandbox creates a new session.
  2. Do not propagate their artifact's session_id back to Graph.sessions after 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)

  1. Configure LibreChat 0.8.7 with an external Code Interpreter sandbox (e.g. usnavy13/LibreCodeInterpreter) and an agent with execute_code capability + execute_code in tools.
  2. LibreChat registers create_file and edit_file tools (packages/api/src/agents/initialize.ts:1096, PR 🗂️ feat: Add Agent File Authoring Tools LibreChat#13435) in addition to bash_tool + read_file.
  3. Prompt the LLM to: create_file('/mnt/data/mandelbrot.py', <content>) then bash_tool('ls /mnt/data/').
  4. Observed: create_file returns "Created", but bash_tool reports 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:

  1. getCodeSessionContext (src/stream.ts:180-189): returns undefined unless CODE_EXECUTION_TOOLS.has(name) || name === SKILL_TOOL || name === READ_FILE. This populates tc.codeSessionContext (used at stream.ts:263).
  2. ToolNode.ts:976: if (CODE_EXECUTION_TOOLS.has(call.name)) → injects session_id from Graph.sessions[EXECUTE_CODE] into invokeParams. Skipped for consumer-registered sandbox tools → no session_id sent.
  3. ToolNode.ts:1780 (eager path): if (!CODE_EXECUTION_TOOLS.has(request.name) && request.name !== SKILL_TOOL) → continue. Artifact's session_id not propagated.
  4. 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.
  5. 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 on session_id will 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_file exception: PR feat: add ReadFile tool definition for general-purpose file reading #99, commit 4154c17b1a (2026-04-14)

Activity

  1. danny-avila commented on Jun 25, 2026

    @danny-avila
    Collaborator

    Configure LibreChat 0.8.7 with an external Code Interpreter sandbox (e.g. usnavy13/LibreCodeInterpreter) and an agent with execute_code capability + execute_code in tools.

    Use the official code interpreter: https://github.com/ClickHouse/code-interpreter

  2. fabienbarbaud commented on Jun 26, 2026

    @fabienbarbaud
    Author

    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/exec calls 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/data to the File Server and returns them in the files array.

    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/data before 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):

    1. LLM calls create_file → creates /mnt/data/fun_script.py
    2. LLM calls bash_tool → python3 /mnt/data/fun_script.py
    3. Error: python3: can't open file '/mnt/data/fun_script.py': [Errno 2] No such file or directory

    Same 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 files

    The ClickHouse interpreter returns files: [{id, name, storage_session_id}] in the create_file response (Step 1 above). The SDK is supposed to:

    1. Store those files in the artifact
    2. Inject them as _injected_files on the next bash_tool call
    3. Send them in the files array of the POST /exec body

    But the 5 gates keying off CODE_EXECUTION_TOOLS.has(name) skip create_file/edit_file (they are not in the set), so:

    • ToolNode.ts:1833: updateCodeSession(sessions, execSessionId, artifact?.files) is skipped → files never stored
    • _injected_files stays empty for bash_tool → no files sent in the request body → sandbox starts with empty /mnt/data → file not found

    Conclusion

    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 5 CODE_EXECUTION_TOOLS.has(name) gates, so their files artifacts are not propagated to subsequent calls.

    Could you reopen this issue? The proposed fix (Option A: additionalCodeSessionTools config param, or Option B: add create_file/edit_file to the exception lists like read_file) is still valid.

  3. fabienbarbaud commented on Jul 6, 2026

    @fabienbarbaud
    Author
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions