BoxliteBooter: 8 bugs — unimplemented available()/download_file(), upload protocol errors, response parsing mismatches (sandbox non-functional)
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
Hướng nghiên cứu
Start with astrbot/core/computer/booters/boxlite.py and compare BoxliteBooter with the cua/shipyard booter; inspect the base class methods and sandbox upload/download endpoints. Then read astrbot/core/tools/computer_tools/python.py and astrbot/core/computer/file_read_utils.py for response parsing. Reproduce the reported Boxlite failures and verify each fix against the expected behavior in the issue; the attached patch and end-to-end checks provide concrete references.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
BoxliteBooter: multiple unimplemented base-class methods + upload/download protocol bugs — sandbox broken in 8 places
AstrBot version: 4.28.1
Runtime: fnOS (Linux 6.x, btrfs), Python 3.12, computer_use_runtime = boxlite
Image: soulter/shipyard-ship (shipyard sandbox HTTP service inside microVM)
Summary
The Boxlite booter appears to be an unfinished port of the cua/shipyard booter. On a clean 4.28.1 install, the boxlite sandbox is non-functional in 8 distinct ways. Six are in BoxliteBooter itself (including two base-class methods that were never overridden and remain as ... no-ops), two are response-parsing mismatches in tool result handlers.
We fixed all 8 locally with patches; happy to turn them into PRs.
Repro & root causes
1. upload_file: doubled scheme in URL (HTTP 422/conn refused)
astrbot/core/computer/booters/boxlite.py MockShipyardSandboxClient.upload_file:
url = f"http://{self.sb_url}/upload" # sb_url already includes http://
→ http://http://127.0.0.1:PORT/upload. Fix: url = f"{self.sb_url}/upload".
2. upload_file: missing required X-SESSION-ID header (HTTP 422)
The sandbox /upload endpoint requires X-SESSION-ID (see its OpenAPI schema). All other client methods send it; upload_file doesn't. Fix: store session_id on the client and send the header.
3. upload_file: absolute target path rejected (HTTP 403)
The sandbox restricts writes to /home/ship_<session>/workspace. remote_path must be workspace-relative. Fix: strip the /home/ship_*/workspace/ prefix (and any leading /) before upload.
4. BoxliteBooter.available() never overridden → always falsy
ComputerBooter.available() is ... (returns None). get_booter() therefore considers every existing booter dead, shuts the VM down and re-boots it on every single tool call (files lost between calls, 2-3 s boot overhead per call; visible in logs as Shutting down Boxlite booter immediately followed by Initializing booter on each call). Fix: implement available() (process flag + box.info() + HTTP /health probe on self.mocked.sb_url).
5. BoxliteBooter.download_file() never overridden → silent fake success
Base class download_file is also .... The tool logs File ... downloaded from sandbox to <local> but no file is ever written; the subsequent send fails with File does not exist: (original input: ) / Errno 2. Fix: implement via the sandbox's own GET /download?file_path=<rel> endpoint (with X-SESSION-ID), streaming to local_path. Path normalization: /home/ship_<sid>/workspace/<rest> → <rest>.
6. Skills bundle exec: VM lacks unzip
The apply phase runs unzip which is not present in soulter/shipyard-ship (Python 3.13 is). Fix inside the generated script: use python3 -c "import zipfile; ...".
7. astrbot_execute_ipython always returns No output.
computer_tools/python.py handle_result reads result["data"]["output"]["text"] (nested layout used by other booters), but boxlite's /ipython/exec returns output/error at the top level. The data key doesn't exist → empty text → the "No output." fallback for every run, including errors and print. Fix: fall back to top-level keys when data is absent.
8. astrbot_file_read / grep: file probe failed: empty output
Same mismatch in computer/file_read_utils.py _exec_python_json (reads result["data"]["output"]). Fix identical.
Environment-specific note (not an AstrBot bug, but worth documenting)
BoxLite's shim script lost its exec bit when stored on btrfs (/vol2/@apphome/astrbot/.boxlite/...), which killed VM startup with VM failed to start. If other users run the data dir on btrfs, the booter should ensure the exec bit after materializing shims (os.chmod(path, 0o755)).
Patches
Self-contained unified diffs against 4.28.1: all-8-fixes-v4.28.1.patch (attached). All eight fixes are covered by end-to-end tests on our deployment (skills upload, exec, file read/write/grep, ipython output, cross-call VM reuse, sandbox→host→QQ file delivery of xlsx/docx/png/txt).
Expected behavior after fixes
- One VM per session, reused across tool calls (no per-call re-boot, no file loss between calls)
astrbot_download_fileactually writes the file to the host before reporting success- ipython/file_read/grep return real output instead of "No output."/"empty output"
Full patch (unified diff against 4.28.1)
--- /vol2/@apphome/astrbot/penv/lib/python3.12/site-packages/astrbot/core/computer/booters/boxlite.py.bak-20261104 2026-11-04 13:06:51.024403826 +0800
+++ /vol2/@apphome/astrbot/penv/lib/python3.12/site-packages/astrbot/core/computer/booters/boxlite.py 2026-11-04 21:09:31.932827315 +0800
@@ -16,8 +16,9 @@
class MockShipyardSandboxClient:
- def __init__(self, sb_url: str) -> None:
+ def __init__(self, sb_url: str, session_id: str = "default") -> None:
self.sb_url = sb_url.rstrip("/")
+ self.session_id = session_id
async def _exec_operation(
self,
@@ -43,7 +44,13 @@
async def upload_file(self, path: str, remote_path: str) -> dict:
"""Upload a file to the sandbox"""
- url = f"http://{self.sb_url}/upload"
+ url = f"{self.sb_url}/upload"
+ headers = {"X-SESSION-ID": self.session_id}
+ # sandbox upload service restricts writes to the session workspace;
+ # strip any absolute prefix so the file lands in the workspace.
+ remote_path = remote_path.lstrip("/")
+ if remote_path.startswith("home/"):
+ remote_path = "/".join(remote_path.split("/")[1:])
try:
# Read file content
@@ -63,7 +70,7 @@
timeout = aiohttp.ClientTimeout(total=120) # 2 minutes for file upload
async with aiohttp.ClientSession(timeout=timeout) as session:
- async with session.post(url, data=data) as response:
+ async with session.post(url, data=data, headers=headers) as response:
if response.status == 200:
logger.info(
"[Computer] File uploaded to Boxlite sandbox: %s",
@@ -130,6 +137,9 @@
class BoxliteBooter(ComputerBooter):
+ def __init__(self) -> None:
+ self._closed = False
+
async def boot(self, session_id: str) -> None:
logger.info(
f"Booting(Boxlite) for session: {session_id}, this may take a while..."
@@ -149,7 +159,7 @@
await self.box.start()
logger.info(f"Boxlite booter started for session: {session_id}")
self.mocked = MockShipyardSandboxClient(
- sb_url=f"http://127.0.0.1:{random_port}"
+ sb_url=f"http://127.0.0.1:{random_port}", session_id=session_id
)
self._python = ShipyardPythonComponent(
client=self.mocked, # type: ignore
@@ -172,7 +182,34 @@
await self.mocked.wait_healthy(self.box.id, session_id)
+ async def available(self) -> bool:
+ """Check whether the box VM and its sandbox HTTP service are still alive."""
+ if getattr(self, "_closed", False):
+ return False
+ box = getattr(self, "box", None)
+ if box is None:
+ return False
+ try:
+ info = await box.info()
+ except Exception:
+ return False
+ if isinstance(info, dict):
+ status = str(info.get("status", "running")).lower()
+ if status not in {"running", "started"} or not info.get("running", True):
+ return False
+ try:
+ import aiohttp
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{self.mocked.sb_url}/health", timeout=aiohttp.ClientTimeout(total=5)
+ ) as resp:
+ return resp.status == 200
+ except Exception:
+ return False
+
async def shutdown(self) -> None:
+ self._closed = True
logger.info(f"Shutting down Boxlite booter for ship: {self.box.id}")
self.box.shutdown()
logger.info(f"Boxlite booter for ship: {self.box.id} stopped")
@@ -189,6 +226,40 @@
def shell(self) -> ShellComponent:
return self._shell
+ async def download_file(self, remote_path: str, local_path: str) -> None:
+ """Download a file from the sandbox VM via its /download endpoint.
+
+ The sandbox upload/download service restricts reads to the session
+ workspace, so absolute sandbox paths (/home/ship_x/...) and relative
+ paths both need to be normalized to a workspace-relative path.
+ """
+ import os as _os
+ import aiohttp
+
+ rel = remote_path.strip()
+ if "/workspace/" in rel:
+ # /home/ship_<sid>/workspace/<rest> -> <rest>
+ rel = rel.split("/workspace/", 1)[1]
+ rel = rel.lstrip("/")
+
+ _os.makedirs(_os.path.dirname(local_path) or ".", exist_ok=True)
+ url = f"{self.mocked.sb_url}/download"
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ url,
+ params={"file_path": rel},
+ headers={"X-SESSION-ID": self.mocked.session_id},
+ timeout=aiohttp.ClientTimeout(total=300),
+ ) as resp:
+ if resp.status != 200:
+ detail = (await resp.text())[:200]
+ raise RuntimeError(
+ f"boxlite download failed: HTTP {resp.status} for {remote_path}: {detail}"
+ )
+ with open(local_path, "wb") as f:
+ async for chunk in resp.content.iter_chunked(1 << 16):
+ f.write(chunk)
+
async def upload_file(self, path: str, file_name: str) -> dict:
"""Upload file to sandbox"""
return await self.mocked.upload_file(path, file_name)
--- /vol2/@apphome/astrbot/penv/lib/python3.12/site-packages/astrbot/core/tools/computer_tools/python.py.bak-20261104 2026-11-04 19:01:02.933602593 +0800
+++ /vol2/@apphome/astrbot/penv/lib/python3.12/site-packages/astrbot/core/tools/computer_tools/python.py 2026-11-04 19:01:03.008252780 +0800
@@ -49,6 +49,9 @@
async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult:
data = result.get("data", {})
+ if not data and ("output" in result or "error" in result):
+ # boxlite sandbox /ipython/exec returns output/error at top level
+ data = result
output = data.get("output", {})
error = data.get("error", "")
images: list[dict] = output.get("images", [])
--- /vol2/@apphome/astrbot/penv/lib/python3.12/site-packages/astrbot/core/computer/file_read_utils.py.bak-20261104 2026-11-04 19:06:54.568849705 +0800
+++ /vol2/@apphome/astrbot/penv/lib/python3.12/site-packages/astrbot/core/computer/file_read_utils.py 2026-11-04 19:06:54.585550277 +0800
@@ -250,6 +250,9 @@
) -> dict:
result = await booter.python.exec(script)
data = result.get("data") if isinstance(result.get("data"), dict) else {}
+ if not data and isinstance(result.get("output"), dict):
+ # boxlite sandbox /ipython/exec returns output/error at top level
+ data = result
if not isinstance(data, dict):
raise RuntimeError(f"{action} failed: invalid result format")
output = data.get("output") if isinstance(data.get("output"), dict) else {}
- Ngôn ngữ chính
- Python
- Star
- 46.4k
- Fork
- 3.4k
- Merge trung bình
- 1 ngày 11 giờ
- Pull request đã merge (30 ngày)
- 120
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của AstrBotDevs/AstrBot
-
enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
AstrBotDevs/AstrBot#11038 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
AstrBotDevs/AstrBot#11034 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
AstrBotDevs/AstrBot#10999 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug] OpenAI-compatible Gemini 工具 schema const 导致 400Có thể đã có người làm @arccat-114 đã nhận 17 ngày trước. Đang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
AstrBotDevs/AstrBot#10720 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug] Discord slash command sync aborts with KeyError when a plugin handler is defined in a submoduleCó thể đã có người làm @yuexia5 đã nhận 22 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
AstrBotDevs/AstrBot#10626 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của AstrBotDevs/AstrBot
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
bytedance/agentkit-samples#277 ·
-
ai-driven-fuzzing bug claude Minor
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 83/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Edit: TVS Encarnacion [PY]Đang mởchannels:edit check:passed
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug] The blue-green mitigation strategy lists catalog names and views before the source tablesĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
aws-samples/sample-aws-genai-db-modernizer#2097 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày