Skip to content

Commit f593603

Browse files
committed
feat: Enhance file upload and session management with agent file support
- Updated the `upload_file` function to create sessions for file uploads, enabling session reuse for referenced files. - Introduced `is_agent_file` flag to distinguish between user-uploaded files and agent-assigned files, enforcing read-only restrictions on agent files. - Modified `FileService` to handle the `is_agent_file` attribute in file metadata, ensuring proper storage and retrieval. - Enhanced `ExecutionOrchestrator` to prevent modifications to files associated with different sessions and agent files. - Added integration tests to verify the read-only behavior of agent files and the editability of user files.
1 parent d1c0bd0 commit f593603

4 files changed

Lines changed: 222 additions & 58 deletions

File tree

src/api/files.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414

1515
# Local application imports
1616
from ..config import settings
17-
from ..dependencies import FileServiceDep
17+
from ..dependencies import FileServiceDep, SessionServiceDep
18+
from ..models import SessionCreate
1819
from ..services.execution.output import OutputProcessor
19-
from ..utils.id_generator import generate_session_id
2020

2121
logger = structlog.get_logger(__name__)
2222
router = APIRouter()
@@ -55,6 +55,7 @@ async def upload_file(
5555
files: Optional[List[UploadFile]] = File(None),
5656
entity_id: Optional[str] = Form(None),
5757
file_service: FileServiceDep = None,
58+
session_service: SessionServiceDep = None,
5859
):
5960
"""Upload files with multipart form handling - LibreChat compatible.
6061
@@ -112,8 +113,17 @@ async def upload_file(
112113

113114
uploaded_files = []
114115

115-
# Create a session ID for this upload
116-
session_id = generate_session_id()
116+
# Create a real session for file uploads
117+
# This enables session reuse when files are referenced in /exec
118+
metadata = {}
119+
if entity_id:
120+
metadata["entity_id"] = entity_id
121+
session = await session_service.create_session(SessionCreate(metadata=metadata))
122+
session_id = session.session_id
123+
124+
# Determine if this is an agent file (uploaded with entity_id)
125+
# Agent files are read-only and cannot be modified by user code
126+
is_agent_file = entity_id is not None and len(entity_id) > 0
117127

118128
for file in upload_files:
119129
# Read file content
@@ -125,6 +135,7 @@ async def upload_file(
125135
filename=file.filename,
126136
content=content,
127137
content_type=file.content_type,
138+
is_agent_file=is_agent_file,
128139
)
129140

130141
# Sanitize filename to match what will be used in container

src/services/file.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,20 @@ async def store_uploaded_file(
541541
filename: str,
542542
content: bytes,
543543
content_type: Optional[str] = None,
544+
is_agent_file: bool = False,
544545
) -> str:
545-
"""Store an uploaded file directly."""
546+
"""Store an uploaded file directly.
547+
548+
Args:
549+
session_id: Session identifier
550+
filename: Original filename
551+
content: File content as bytes
552+
content_type: MIME type of the file
553+
is_agent_file: If True, marks the file as read-only (agent-assigned)
554+
555+
Returns:
556+
The generated file_id
557+
"""
546558
await self._ensure_bucket_exists()
547559

548560
# Generate unique file ID
@@ -579,6 +591,7 @@ async def store_uploaded_file(
579591
"size": len(content),
580592
"path": f"/{filename}",
581593
"type": "upload", # Mark as uploaded file
594+
"is_agent_file": "1" if is_agent_file else "0", # Read-only if agent file
582595
}
583596

584597
await self._store_file_metadata(session_id, file_id, metadata)

src/services/orchestrator.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,10 @@ async def _update_mounted_files_content(self, ctx: ExecutionContext) -> None:
564564
This ensures in-place edits to mounted files persist after execution.
565565
Called after execution completes, reads current content from container
566566
and updates the file in MinIO storage.
567+
568+
SECURITY: Only updates files that belong to the current session.
569+
Files referenced from other sessions are read-only to prevent
570+
cross-session/cross-user data modification.
567571
"""
568572
if not ctx.mounted_files or not ctx.container:
569573
return
@@ -574,9 +578,33 @@ async def _update_mounted_files_content(self, ctx: ExecutionContext) -> None:
574578
try:
575579
filename = file_info.get("filename")
576580
file_id = file_info.get("file_id")
577-
session_id = file_info.get("session_id")
581+
file_session_id = file_info.get("session_id")
582+
583+
if not all([filename, file_id, file_session_id]):
584+
continue
585+
586+
# SECURITY: Only update files from the current session
587+
# Files from other sessions are read-only
588+
if file_session_id != ctx.session_id:
589+
logger.debug(
590+
"Skipping update for cross-session file",
591+
filename=filename,
592+
file_session=file_session_id[:12] if file_session_id else None,
593+
exec_session=ctx.session_id[:12] if ctx.session_id else None,
594+
)
595+
continue
578596

579-
if not all([filename, file_id, session_id]):
597+
# SECURITY: Skip agent-assigned files (uploaded with entity_id)
598+
# Agent files are read-only and cannot be modified by user code
599+
file_metadata = await self.file_service._get_file_metadata(
600+
file_session_id, file_id
601+
)
602+
if file_metadata and file_metadata.get("is_agent_file") == "1":
603+
logger.debug(
604+
"Skipping update for agent-assigned file (read-only)",
605+
filename=filename,
606+
file_id=file_id,
607+
)
580608
continue
581609

582610
# Read current content from container
@@ -595,7 +623,7 @@ async def _update_mounted_files_content(self, ctx: ExecutionContext) -> None:
595623

596624
# Update file in storage
597625
await self.file_service.update_file_content(
598-
session_id=session_id,
626+
session_id=file_session_id,
599627
file_id=file_id,
600628
content=content,
601629
state_hash=ctx.new_state_hash,

0 commit comments

Comments
 (0)