{"record":{"id":"daea5b1c5ae1dc8f","repo":"zylon-ai/private-gpt","slug":"path-path-is-in-a-read-only-mount-prefix","errorCode":null,"errorMessage":"Path '{path}' is in a read-only mount ('{prefix}').","messagePattern":"Path '(.+?)' is in a read-only mount \\('(.+?)'\\)\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"private_gpt/components/sandbox/local.py","lineNumber":76,"sourceCode":"    ) -> None:\n        \"\"\"Register a new host-path mount after session creation.\"\"\"\n        self._translator.register(canonical, host_path, writable)\n        if not writable and canonical not in self._readonly:\n            self._readonly.append(canonical)\n\n    def remove_local_mount(self, canonical: str) -> None:\n        \"\"\"Unregister a mount — does not delete files from host storage.\"\"\"\n        self._translator.unregister(canonical)\n        self._readonly = [p for p in self._readonly if p != canonical]\n\n    async def remove_mount(self, canonical_path: str) -> None:\n        \"\"\"Unregister a local mount without touching host-backed storage files.\"\"\"\n        self.remove_local_mount(canonical_path)\n\n    def _assert_writable(self, path: str) -> None:\n        for prefix in self._readonly:\n            if path.startswith(prefix):\n                raise ValueError(f\"Path '{path}' is in a read-only mount ('{prefix}').\")\n\n    async def exec(\n        self, command: str, opts: SandboxExecOptions | None = None\n    ) -> SandboxExecutionResult:\n        cwd = self._translator.to_real(\n            (opts.cwd if opts else None) or self._default_cwd\n        )\n        cmd = self._translator.rewrite_command(command)\n        result = await self._executor.run(\n            cmd, cwd=cwd, timeout=opts.timeout if opts else None\n        )\n        return SandboxExecutionResult(\n            success=result.success,\n            stdout=self._translator.scrub_output(result.stdout),\n            stderr=self._translator.scrub_output(result.stderr),\n            exit_code=result.exit_code,\n            execution_time_ms=result.execution_time_ms,\n        )","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/components/sandbox/local.py#L58-L94","documentation":"The local sandbox registers mounts as writable or read-only; _assert_writable walks the read-only prefix list and raises ValueError if a target path starts with any read-only mount prefix. It protects host-backed storage that was mounted with read-only semantics from being mutated by sandbox operations (file writes, renames, deletions).","triggerScenarios":"Any sandbox operation that calls _assert_writable(path) where path falls under a canonical prefix registered via add_local_mount(..., readonly=True) (or default read-only mounts) — e.g. writing generated files into a mounted knowledge/corpus directory.","commonSituations":"Agents/tools attempting to write outputs into a mounted source-data directory; mismatch between the canonical path used to register the mount and the path used at write time (prefix string comparison); forgetting that skills/corpus mounts are read-only by design.","solutions":["Write to a writable location (scratch/workdir mount) instead of the read-only prefix.","If mutation is genuinely required, re-register the mount without the read-only flag (a deployment decision — read-only is usually deliberate for source data).","Check the exact prefix strings: the check is path.startswith(prefix), so trailing slashes and case differences matter; normalize paths before comparing.","Copy the file out of the read-only mount, modify the copy, then use it."],"exampleFix":"# before\nawait sandbox.write_file(\"/mnt/corpus/report.txt\", data)  # ValueError: read-only mount\n\n# after\nawait sandbox.write_file(\"/mnt/scratch/report.txt\", data)","handlingStrategy":"validation","validationCode":"def ensure_writable(sandbox, path: str) -> bool:\n    try:\n        sandbox._assert_writable(path)\n        return True\n    except ValueError:\n        return False\n\n# or track mount flags yourself\nWRITABLE_ROOTS = [\"/mnt/scratch\"]\ndef writable_path(p: str) -> str:\n    return p if any(p.startswith(r, 0) for r in WRITABLE_ROOTS) else \"/mnt/scratch/out\"","typeGuard":null,"tryCatchPattern":"try:\n    await sandbox.write_file(path, data)\nexcept ValueError as e:\n    if \"read-only mount\" in str(e):\n        path = \"/mnt/scratch/\" + Path(path).name\n        await sandbox.write_file(path, data)\n    else:\n        raise","preventionTips":["Route all agent writes to a dedicated scratch mount by convention.","Normalize and canonicalize paths before comparing against mount prefixes.","Surface mount read-only flags to tools/agents so they choose writable targets."],"tags":["sandbox","filesystem","read-only","mounts","validation"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}