Skip to content

Commit 4892181

Browse files
committed
fix: recognize kind field for skill priming, add storage_session_id
LibreChat's batchUploadCodeEnvFiles sends kind/id/version form fields (not entity_id) and expects storage_session_id in the response. Two bugs in upload_files_batch caused skill priming to always fail: - is_agent_file checked only entity_id, so kind=skill uploads were treated as user files and .xsd schemas were rejected by the whitelist - response returned session_id only; LibreChat throws if storage_session_id is absent Add kind=skill/agent recognition alongside the existing entity_id path. Add storage_session_id as an alias for session_id in the response. Both changes are backward compatible.
1 parent df4ecc3 commit 4892181

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

src/api/files.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,13 @@ async def upload_files_batch(
249249
entity_id: Optional[str] = (
250250
entity_id_raw if isinstance(entity_id_raw, str) and entity_id_raw else None
251251
)
252-
is_agent_file = entity_id is not None
252+
# LibreChat sends kind=skill/agent (not entity_id) for skill-priming uploads.
253+
# Treat these as agent files so skill bundles bypass the user-facing extension
254+
# whitelist and are correctly tagged read-only in the sandbox.
255+
kind_raw = form.get("kind")
256+
is_agent_file = entity_id is not None or (
257+
isinstance(kind_raw, str) and kind_raw in ("skill", "agent")
258+
)
253259

254260
read_only_raw = form.get("read_only")
255261
is_read_only = isinstance(read_only_raw, str) and read_only_raw.lower() in (
@@ -340,6 +346,7 @@ async def upload_files_batch(
340346
return {
341347
"message": message,
342348
"session_id": session_id,
349+
"storage_session_id": session_id, # LibreChat alias for session_id
343350
"files": results,
344351
"succeeded": succeeded,
345352
"failed": failed,

tests/integration/test_librechat_compat.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,46 @@ def test_nested_filename_preserved_in_response(
20382038
# The stored filename also preserves the path so S3/sandbox round-trip works.
20392039
assert "skills/weather_lookup/SKILL.md" in setup_mocks["stored"]
20402040

2041+
def test_kind_skill_marks_files_as_agent(self, client, auth_headers, setup_mocks):
2042+
"""LibreChat sends kind=skill (not entity_id) for skill-priming uploads.
2043+
2044+
appendCodeEnvFileIdentity() in LibreChat appends kind/id/version fields
2045+
to the multipart form — entity_id is never sent. The endpoint must
2046+
recognise kind=skill as an agent-file upload so that skill bundle files
2047+
with non-standard extensions (.xsd schemas, .toml configs, etc.) bypass
2048+
the user-facing extension whitelist and are tagged read-only in the sandbox.
2049+
"""
2050+
files = [("file", ("schema.xsd", io.BytesIO(b"<xs:schema/>"), "application/xml"))]
2051+
data = {"kind": "skill", "id": "skill_abc123", "version": "3"}
2052+
response = client.post(
2053+
"/upload/batch", files=files, data=data, headers=auth_headers
2054+
)
2055+
2056+
assert response.status_code == 200
2057+
store = setup_mocks["file_service"].store_uploaded_file
2058+
assert store.await_count == 1
2059+
kwargs = store.await_args.kwargs
2060+
assert kwargs["is_agent_file"] is True
2061+
2062+
def test_batch_response_includes_storage_session_id(
2063+
self, client, auth_headers, setup_mocks
2064+
):
2065+
"""LibreChat's batchUploadCodeEnvFiles validates storage_session_id in the response.
2066+
2067+
crud.js throws if the field is absent:
2068+
if (!result.storage_session_id || !Array.isArray(result.files)) {
2069+
throw new Error(`Unexpected batch upload response: ...`)
2070+
}
2071+
The field must equal session_id (same underlying value, different name).
2072+
"""
2073+
files = [("file", ("data.csv", io.BytesIO(b"a,b"), "text/csv"))]
2074+
response = client.post("/upload/batch", files=files, headers=auth_headers)
2075+
2076+
assert response.status_code == 200
2077+
body = response.json()
2078+
assert "storage_session_id" in body
2079+
assert body["storage_session_id"] == body["session_id"]
2080+
20412081

20422082
# =============================================================================
20432083
# GET /sessions/{session_id}/objects/{file_id} — liveness probe

0 commit comments

Comments
 (0)