Commit 60d46a5ab for llama.cpp

commit 60d46a5abe4f13fa54af61c488047549c5840bbe
Author: Xuan-Son Nguyen <son@huggingface.co>
Date:   Thu Nov 12 09:34:16 2026 +0100

    server: when --tools-runtime is specified, ignore x-tool-runtime (#32119)

diff --git a/tools/server/README-dev.md b/tools/server/README-dev.md
index 17b39f0aa..01d1a65cf 100644
--- a/tools/server/README-dev.md
+++ b/tools/server/README-dev.md
@@ -201,7 +201,7 @@ Invoke a tool call, request body is a JSON object with:

 Headers:
 - `x-tool-cwd`: optional; if set, use as the CWD for tool; this is not part of tool's params because it's meant to be set by the runtime, not the LLM itself
-- `x-tool-runtime`: optional; if set, run the tool inside this isolate instead of on the host. Either `docker-container:<id>` or `podman-container:<id>`, using an already-running container, or `ssh:<target>`, running the tool on a remote host
+- `x-tool-runtime`: optional; if set, run the tool inside this isolate instead of on the host. Either `docker-container:<id>` or `podman-container:<id>`, using an already-running container, or `ssh:<target>`, running the tool on a remote host. Refused with 403 when the server is started with `--tools-runtime`, so clients cannot move tools out of that sandbox

 Returns JSON object. There are two response formats (MCP tools use the same two formats: their result content is concatenated into `plain_text_response`, and RPC or tool errors are surfaced as the `error` string):

diff --git a/tools/server/server-tools.cpp b/tools/server/server-tools.cpp
index bde774b37..8070e14b4 100644
--- a/tools/server/server-tools.cpp
+++ b/tools/server/server-tools.cpp
@@ -2093,15 +2093,20 @@ void server_tools::setup(const std::vector<std::string> & enabled_tools,
             }

             // accept x-tool-runtime header to route tool I/O through an isolate, e.g. "docker-container:<id>";
-            // falls back to the --tools-runtime isolate, if configured
+            // --tools-runtime wins: it is the sandbox, the client must not move tools out of it
             if (params.contains("runtime")) {
                 params.erase("runtime");
             }
             auto runtime_header = get_header(req.headers, "x-tool-runtime");
-            if (!runtime_header.empty()) {
-                params["runtime"] = runtime_header;
-            } else if (runtime) {
+            if (runtime) {
+                if (!runtime_header.empty()) {
+                    res->status = 403;
+                    res->data   = safe_json_to_str(format_error_response("x-tool-runtime is not allowed when --tools-runtime is set", ERROR_TYPE_PERMISSION));
+                    return res;
+                }
                 params["runtime"] = runtime->spec();
+            } else if (!runtime_header.empty()) {
+                params["runtime"] = runtime_header;
             }

             // x-resp-type header is only used by read_file for now
diff --git a/tools/server/tests/unit/test_tools_builtin.py b/tools/server/tests/unit/test_tools_builtin.py
index d4ebd28d1..b3af73604 100755
--- a/tools/server/tests/unit/test_tools_builtin.py
+++ b/tools/server/tests/unit/test_tools_builtin.py
@@ -206,6 +206,21 @@ def test_tools_builtin_runtime_header(container_engine: str, container_id: str):
     assert "hello container" in exec_res["plain_text_response"]


+def test_tools_builtin_runtime_header_refused_with_configured_runtime(container_engine: str, container_id: str):
+    global server
+    server.server_tools_runtime = f"{container_engine}-container:{container_id}"
+    server.start()
+
+    res = call_tool("exec_shell_command", {"command": "echo sandboxed"})
+    assert "sandboxed" in res["plain_text_response"]
+
+    for runtime in ["ssh:user@internal-host", f"{container_engine}-container:other", server.server_tools_runtime]:
+        res = server.make_request("POST", "/tools",
+                                  data={"tool": "exec_shell_command", "params": {"command": "echo hi"}},
+                                  headers={"x-tool-runtime": runtime})
+        assert res.status_code == 403, res.body
+
+
 def test_tools_builtin_runtime_header_unknown_scheme():
     global server
     server.start()