{"record":{"id":"ff2a19e94a691174","repo":"agentscope-ai/agentscope","slug":"host-cache-dir-was-replaced-before-execution","errorCode":null,"errorMessage":"host_cache_dir was replaced before execution.","messagePattern":"host_cache_dir was replaced before execution\\.","errorType":"error_code","errorClass":"RuntimeError","httpStatus":null,"severity":"critical","filePath":"src/agentscope/workspace/_bubblewrap/_bubblewrap_backend.py","lineNumber":495,"sourceCode":"                ) from exc\n            if identity != expected_identity:\n                raise RuntimeError(\n                    f\"{label} was replaced before execution.\",\n                )\n\n        if self._host_cache_dir is None:\n            return\n        try:\n            identity = self._directory_identity(\n                self._host_cache_dir,\n                label=\"host_cache_dir\",\n            )\n        except ValueError as exc:\n            raise RuntimeError(\n                \"host_cache_dir was removed or replaced before execution.\",\n            ) from exc\n        if identity != self._host_cache_identity:\n            raise RuntimeError(\n                \"host_cache_dir was replaced before execution.\",\n            )\n\n    @staticmethod\n    def _directory_identity(\n        path: str,\n        *,\n        label: str,\n    ) -> tuple[int, int]:\n        \"\"\"Return a stable identity for a real directory mount source.\"\"\"\n        if os.path.islink(path) or not os.path.isdir(path):\n            raise ValueError(f\"{label} must be a real directory: {path}\")\n        stat_result = os.stat(path, follow_symlinks=False)\n        return stat_result.st_dev, stat_result.st_ino\n\n    @staticmethod\n    def _paths_overlap(left: str, right: str) -> bool:\n        \"\"\"Return whether either real path contains the other.\"\"\"","sourceCodeStart":477,"sourceCodeEnd":513,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/workspace/_bubblewrap/_bubblewrap_backend.py#L477-L513","documentation":"Raised when host_cache_dir still exists but its (st_dev, st_ino) no longer matches the identity captured in __init__. The directory was deleted and recreated (or swapped to a different filesystem object), so binding it again could mount attacker-controlled or unrelated content.","triggerScenarios":"rm -rf + mkdir of the cache directory between backend construction and any sandboxed command execution.","commonSituations":"Recovery routines that 'reset the cache' by recreating it; container image updates replacing the volume underneath a long-lived process.","solutions":["Treat the workspace as invalid: close it and construct a new BubblewrapWorkspace with the same paths","Use in-place cache invalidation (delete contents, keep the directory) instead of recreating the directory","Put the cache on a dedicated volume not subject to replacement"],"exampleFix":"# before\n# cache reset that breaks identity\nshutil.rmtree(cache_dir); os.makedirs(cache_dir)\n# after\n# in-place clear keeps the directory (and its inode) intact\nfor entry in os.listdir(cache_dir):\n    shutil.rmtree(os.path.join(cache_dir, entry))","handlingStrategy":"fallback","validationCode":"null","typeGuard":"null","tryCatchPattern":"except RuntimeError as e:\n    if 'host_cache_dir was replaced' in str(e):\n        await close_and_recreate_workspace()","preventionTips":["Clear cache contents in place rather than recreating the directory","Use dedicated volumes for cache dirs"],"tags":["bubblewrap","cache","toctou","inode"],"backgroundTag":"mount-source-disappeared","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}