Skip to content

Commit 8e5b4d2

Browse files
authored
Merge pull request #39 from XSpoonAi/fix/chat_attachment
Fix: Harden sandbox chat attachments, session history, and stream lifecycle
2 parents cfe0838 + a33ac13 commit 8e5b4d2

File tree

9 files changed

+1028
-41
lines changed

9 files changed

+1028
-41
lines changed

spoon_bot/agent/context.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from pathlib import Path
99
from typing import Any
1010

11+
from loguru import logger
12+
1113

1214
class ContextBuilder:
1315
"""
@@ -18,6 +20,7 @@ class ContextBuilder:
1820
"""
1921

2022
BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"]
23+
SANDBOX_WORKSPACE_ROOT = "/workspace"
2124

2225
def __init__(self, workspace: Path):
2326
"""
@@ -199,24 +202,61 @@ def _build_user_content(
199202
return text
200203

201204
images = []
205+
skipped: list[str] = []
202206
for path in media:
203-
p = Path(path)
207+
p = self._resolve_media_path(path)
204208
mime, _ = mimetypes.guess_type(path)
205-
if not p.is_file() or not mime or not mime.startswith("image/"):
209+
if p is None or not p.is_file():
210+
skipped.append(f"{path}: missing file")
211+
continue
212+
if not mime or not mime.startswith("image/"):
213+
skipped.append(f"{path}: unsupported mime {mime or 'unknown'}")
206214
continue
207215
try:
208216
b64 = base64.b64encode(p.read_bytes()).decode()
209217
images.append({
210218
"type": "image_url",
211219
"image_url": {"url": f"data:{mime};base64,{b64}"}
212220
})
213-
except Exception:
214-
pass
221+
except Exception as exc:
222+
skipped.append(f"{path}: {exc}")
223+
224+
if skipped:
225+
logger.warning(
226+
"Skipped media inputs while building multimodal content: {}",
227+
"; ".join(skipped),
228+
)
215229

216230
if not images:
217231
return text
218232
return images + [{"type": "text", "text": text}]
219233

234+
def _resolve_media_path(self, path: str) -> Path | None:
235+
"""Resolve runtime media refs, including /workspace aliases and workspace-relative paths."""
236+
candidate = str(path or "").strip()
237+
if not candidate:
238+
return None
239+
240+
sandbox_root = self.SANDBOX_WORKSPACE_ROOT.rstrip("/")
241+
workspace_root_str = self.workspace.as_posix().rstrip("/")
242+
try:
243+
if candidate.startswith("/"):
244+
normalized = Path(candidate).as_posix()
245+
if normalized == sandbox_root or normalized.startswith(sandbox_root + "/"):
246+
relative = normalized[len(sandbox_root):].lstrip("/")
247+
resolved = (self.workspace / relative).resolve(strict=True)
248+
elif normalized == workspace_root_str or normalized.startswith(workspace_root_str + "/"):
249+
relative = normalized[len(workspace_root_str):].lstrip("/")
250+
resolved = (self.workspace / relative).resolve(strict=True)
251+
else:
252+
resolved = Path(candidate).expanduser().resolve(strict=True)
253+
else:
254+
resolved = (self.workspace / candidate).resolve(strict=True)
255+
except (FileNotFoundError, OSError):
256+
return None
257+
258+
return resolved if resolved.is_file() else None
259+
220260
def add_tool_result(
221261
self,
222262
messages: list[dict[str, Any]],

0 commit comments

Comments
 (0)