{"record":{"id":"66ac6241b57e4404","repo":"bytedance/deer-flow","slug":"only-files-in-mnt-user-data-outputs-can-be-edited","errorCode":null,"errorMessage":"Only files in /mnt/user-data/outputs can be edited","messagePattern":"Only files in /mnt/user-data/outputs can be edited","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/app/gateway/routers/artifacts.py","lineNumber":71,"sourceCode":"    size: int\n\n\n@asynccontextmanager\nasync def reserve_artifact_write(request: Request, thread_id: str, *, user_id: str) -> AsyncIterator[None]:\n    \"\"\"Serialize an artifact edit against runs and other thread mutations.\"\"\"\n    run_manager = get_run_manager(request)\n    async with run_manager.reserve_thread_operation(\n        thread_id,\n        kind=ThreadOperationKind.artifact_write,\n        user_id=user_id,\n    ):\n        yield\n\n\ndef _normalize_editable_artifact_path(path: str) -> str:\n    stripped = path.lstrip(\"/\")\n    if not stripped.startswith(_EDITABLE_OUTPUTS_PREFIX):\n        raise HTTPException(status_code=400, detail=\"Only files in /mnt/user-data/outputs can be edited\")\n    if \".skill/\" in stripped or stripped.endswith(\".skill\"):\n        raise HTTPException(status_code=415, detail=\"Skill archives cannot be edited in the artifacts panel\")\n    return f\"/{stripped}\"\n\n\ndef _load_editable_artifact(actual_path: Path, path: str, expected_sha256: str) -> tuple[bytes, os.stat_result]:\n    try:\n        file_stat = os.lstat(actual_path)\n    except FileNotFoundError:\n        raise HTTPException(status_code=404, detail=f\"Artifact not found: {path}\") from None\n    if stat.S_ISLNK(file_stat.st_mode):\n        raise HTTPException(status_code=415, detail=\"Symlinked artifacts cannot be edited\")\n    if not stat.S_ISREG(file_stat.st_mode):\n        raise HTTPException(status_code=400, detail=f\"Path is not a file: {path}\")\n    if file_stat.st_size > MAX_EDITABLE_ARTIFACT_BYTES:\n        raise HTTPException(status_code=413, detail=\"Artifact is too large to edit\")\n\n    current = actual_path.read_bytes()","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/artifacts.py#L53-L89","documentation":"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.","triggerScenarios":"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/.","commonSituations":"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.","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"],"exampleFix":"// before\nawait client.put(f\"/api/threads/{tid}/artifacts\", json={\"path\": \"reports/final.md\", ...})\n\n// after\nawait client.put(f\"/api/threads/{tid}/artifacts\", json={\"path\": \"mnt/user-data/outputs/reports/final.md\", ...})","handlingStrategy":"validation","validationCode":"EDITABLE_PREFIX = \"mnt/user-data/outputs/\"\n\ndef is_editable_artifact_path(path: str) -> bool:\n    return path.lstrip(\"/\").startswith(EDITABLE_PREFIX)","typeGuard":"def is_editable_artifact_path(path: str) -> bool:\n    \"\"\"True if the path passes the Gateway's editable-prefix check.\"\"\"\n    EDITABLE_PREFIX = \"mnt/user-data/outputs/\"\n    stripped = path.lstrip(\"/\")\n    return stripped.startswith(EDITABLE_PREFIX) and \".skill/\" not in stripped and not stripped.endswith(\".skill\")","tryCatchPattern":"if resp.status_code == 400 and \"Only files in /mnt/user-data/outputs\" in resp.text:\n    # path escaped the outputs sandbox; re-fetch canonical path from listing\n    artifacts = await client.get(f\"/api/threads/{tid}/artifacts\")","preventionTips":["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"],"tags":["api","artifacts","path-validation","http-400","sandbox"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}