bytedance/deer-flow · error · HTTPException
Only files in /mnt/user-data/outputs can be edited
Error message
Only files in /mnt/user-data/outputs can be edited
What it means
HTTP 400 from the artifact-edit endpoints: _normalize_editable_artifact_path strips leading slashes and requires the remainder to start with 'mnt/user-data/outputs/' (_EDITABLE_OUTPUTS_PREFIX, artifacts.py:41). Any path outside the sandbox outputs directory is rejected before the filesystem is touched — the editor can only modify files the sandbox writes as outputs.
Source
Thrown at backend/app/gateway/routers/artifacts.py:71
size: int
@asynccontextmanager
async def reserve_artifact_write(request: Request, thread_id: str, *, user_id: str) -> AsyncIterator[None]:
"""Serialize an artifact edit against runs and other thread mutations."""
run_manager = get_run_manager(request)
async with run_manager.reserve_thread_operation(
thread_id,
kind=ThreadOperationKind.artifact_write,
user_id=user_id,
):
yield
def _normalize_editable_artifact_path(path: str) -> str:
stripped = path.lstrip("/")
if not stripped.startswith(_EDITABLE_OUTPUTS_PREFIX):
raise HTTPException(status_code=400, detail="Only files in /mnt/user-data/outputs can be edited")
if ".skill/" in stripped or stripped.endswith(".skill"):
raise HTTPException(status_code=415, detail="Skill archives cannot be edited in the artifacts panel")
return f"/{stripped}"
def _load_editable_artifact(actual_path: Path, path: str, expected_sha256: str) -> tuple[bytes, os.stat_result]:
try:
file_stat = os.lstat(actual_path)
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Artifact not found: {path}") from None
if stat.S_ISLNK(file_stat.st_mode):
raise HTTPException(status_code=415, detail="Symlinked artifacts cannot be edited")
if not stat.S_ISREG(file_stat.st_mode):
raise HTTPException(status_code=400, detail=f"Path is not a file: {path}")
if file_stat.st_size > MAX_EDITABLE_ARTIFACT_BYTES:
raise HTTPException(status_code=413, detail="Artifact is too large to edit")
current = actual_path.read_bytes()View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Send the artifact path exactly as returned by the artifacts listing/get endpoint (it is already under /mnt/user-data/outputs)
- If you need the file editable, have the agent copy it into the outputs directory first (e.g. via the sandbox write/cp tool)
- Check for a missing or doubled path segment: 'user-data/outputs/x' without the 'mnt/' prefix also fails
Example fix
// before
await client.put(f"/api/threads/{tid}/artifacts", json={"path": "reports/final.md", ...})
// after
await client.put(f"/api/threads/{tid}/artifacts", json={"path": "mnt/user-data/outputs/reports/final.md", ...}) Defensive patterns
Strategy: validation
Validate before calling
EDITABLE_PREFIX = "mnt/user-data/outputs/"
def is_editable_artifact_path(path: str) -> bool:
return path.lstrip("/").startswith(EDITABLE_PREFIX) Type guard
def is_editable_artifact_path(path: str) -> bool:
"""True if the path passes the Gateway's editable-prefix check."""
EDITABLE_PREFIX = "mnt/user-data/outputs/"
stripped = path.lstrip("/")
return stripped.startswith(EDITABLE_PREFIX) and ".skill/" not in stripped and not stripped.endswith(".skill") Try / catch
if resp.status_code == 400 and "Only files in /mnt/user-data/outputs" in resp.text:
# path escaped the outputs sandbox; re-fetch canonical path from listing
artifacts = await client.get(f"/api/threads/{tid}/artifacts") Prevention
- Always use paths returned by the artifacts listing rather than constructing them by hand
- Remember inputs/uploads are read-only; only sandbox outputs are editable
- Centralize the 'mnt/user-data/outputs/' prefix as a shared constant in client code so it cannot drift
When it happens
Trigger: PUT /api/threads/{thread_id}/artifacts (edit artifact) with a path like 'mnt/user-data/input/foo.txt', 'etc/passwd', 'home/user/x.md', or any path whose normalized form does not begin with mnt/user-data/outputs/.
Common situations: Frontend artifact panels sending the display path instead of the canonical outputs path, attempts to edit uploaded inputs (which live under uploads/, not outputs/), or hardcoded paths from a different deployment layout.
Related errors
- Path is not a file: {path}
- Path is not a file: {skill_file_path}
- Skill archives cannot be edited in the artifacts panel
- Artifact not found: {path}
- Symlinked artifacts cannot be edited
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/66ac6241b57e4404.
Report an issue: GitHub.