Skip to content

Commit 6e53b60

Browse files
author
Francisco
committed
feat: add viewer support for shell sessions and enhance file harvest logic
- Introduced `viewer` role in `v1.py` for read-only WebSocket connections. - Improved file harvest logic to scan multiple directories and broadcast completion events. - Updated sandbox warning handling and refined cleanup of uploaded files.
1 parent 1ad1948 commit 6e53b60

3 files changed

Lines changed: 67 additions & 20 deletions

File tree

src/api/entities_api/orchestration/mixins/shell_execution_mixin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,13 @@ async def handle_shell_action(
217217
}
218218
)
219219

220+
# Filter out known benign sandbox startup warnings
221+
chunk_lower = chunk.lower()
222+
eval_chunk = chunk_lower.replace("bash: /root/.bashrc: permission denied", "")
223+
eval_chunk = eval_chunk.replace("warning: an existing sandbox was detected", "")
224+
220225
if any(
221-
marker in chunk.lower()
226+
marker in eval_chunk
222227
for marker in [
223228
"not found",
224229
"permission denied",

src/api/sandbox/routers/v1.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ async def websocket_endpoint(
118118
room: str = Query(..., description="The Thread/Room ID"),
119119
elevated: bool = Query(False),
120120
token: str = Query(..., description="Signed JWT from Main API"),
121+
role: str = Query("executor", description="Role of the client (viewer or executor)"),
121122
):
122123
"""
123124
SECURED: Interactive Shell Session.
@@ -133,7 +134,6 @@ async def websocket_endpoint(
133134
return
134135

135136
# 2. Validate Room Access (Multi-Tenancy Security)
136-
# The token MUST contain a "room" claim matching the requested room
137137
allowed_room = payload.get("room")
138138
user_id = payload.get("sub")
139139

@@ -144,8 +144,20 @@ async def websocket_endpoint(
144144
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
145145
return
146146

147-
logging_utility.info(f"Shell session started for User: {user_id} in Room: {room}")
147+
# 3. Handle Frontend UI (Viewer)
148+
if role == "viewer":
149+
logging_utility.info(f"Viewer UI connected to Room: {room}")
150+
await room_manager.connect(room, websocket)
151+
try:
152+
# Keep the connection alive and listen for the client disconnecting
153+
while True:
154+
await websocket.receive_text()
155+
except WebSocketDisconnect:
156+
await room_manager.disconnect(room, websocket)
157+
logging_utility.info(f"Viewer UI disconnected from Room: {room}")
158+
return
148159

149-
# 3. Start Session
160+
# 4. Handle AI SDK (Executor)
161+
logging_utility.info(f"Shell session started for User: {user_id} in Room: {room}")
150162
session = PersistentShellSession(websocket, room, room_manager, elevated=elevated)
151163
await session.start()

src/api/sandbox/services/shell_session.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -272,14 +272,30 @@ async def _message_loop(self) -> None:
272272
await self.websocket.send_json({"type": "pong"})
273273

274274
elif action == "harvest_files":
275+
275276
# Explicit mid-session harvest requested by the assistant.
277+
276278
# Does NOT terminate the session — shell stays alive.
279+
277280
self._reset_idle_timer()
281+
278282
await self._harvest_and_upload_files(
279283
context="explicit_harvest",
280284
wipe_after=True,
281285
)
282286

287+
# FIX: Broadcast command_complete so the SDK knows the harvest is done!
288+
289+
asyncio.create_task(
290+
self.room_manager.broadcast(
291+
self.room,
292+
{
293+
"type": "command_complete",
294+
"thread_id": self.room,
295+
},
296+
)
297+
)
298+
283299
elif action == "disconnect":
284300
break
285301

@@ -429,25 +445,38 @@ async def _harvest_and_upload_files(
429445
Delete successfully uploaded files after upload. Always True in
430446
normal operation; False only for debugging.
431447
"""
432-
if not os.path.isdir(self.session_dir):
433-
return
448+
# 1. Scan BOTH the room's session directory AND the global generated_files directory
449+
directories_to_check = [self.session_dir, "/app/generated_files"]
434450

435-
# Collect harvestable files, skipping oversized ones
436451
candidates: list[tuple[str, str]] = []
437-
for fname in os.listdir(self.session_dir):
438-
fpath = os.path.join(self.session_dir, fname)
439-
if not os.path.isfile(fpath):
440-
continue
441-
size_mb = os.path.getsize(fpath) / (1024 * 1024)
442-
if size_mb > MAX_HARVEST_FILE_SIZE_MB:
443-
logger.warning(
444-
"Skipping %s (%.1f MB exceeds %d MB limit)",
445-
fname,
446-
size_mb,
447-
MAX_HARVEST_FILE_SIZE_MB,
448-
)
452+
seen_paths = set()
453+
454+
for target_dir in directories_to_check:
455+
if not os.path.isdir(target_dir):
449456
continue
450-
candidates.append((fname, fpath))
457+
458+
for fname in os.listdir(target_dir):
459+
fpath = os.path.join(target_dir, fname)
460+
461+
# Prevent double-processing just in case paths overlap
462+
abs_path = os.path.abspath(fpath)
463+
if abs_path in seen_paths:
464+
continue
465+
seen_paths.add(abs_path)
466+
467+
if not os.path.isfile(fpath):
468+
continue
469+
470+
size_mb = os.path.getsize(fpath) / (1024 * 1024)
471+
if size_mb > MAX_HARVEST_FILE_SIZE_MB:
472+
logger.warning(
473+
"Skipping %s (%.1f MB exceeds %d MB limit)",
474+
fname,
475+
size_mb,
476+
MAX_HARVEST_FILE_SIZE_MB,
477+
)
478+
continue
479+
candidates.append((fname, fpath))
451480

452481
if not candidates:
453482
logger.info("Harvest [%s] room=%s: no files found.", context, self.room)
@@ -534,6 +563,7 @@ async def _upload_one(fname: str, fpath: str) -> Optional[dict]:
534563
if wipe_after:
535564
for fpath in uploaded_paths:
536565
try:
566+
# We only delete the specific files we successfully uploaded!
537567
os.remove(fpath)
538568
except Exception:
539569
pass

0 commit comments

Comments
 (0)