From 5316b568b85375e107c7494f8f8d3f47d758ea23 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 4 Aug 2026 02:10:36 +0500 Subject: [PATCH 1/3] fix: use atomic writes for composed preset command files Three write_text() calls for composed command files were not atomic. A crash mid-write leaves a partial .md file which can cause errors when the command is later read. Now uses a shared _atomic_write_text helper with tempfile.mkstemp + os.replace. --- src/specify_cli/presets/__init__.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index cc5308f3fc..30a7c90f7b 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -62,6 +62,23 @@ def _content_sha256(content: bytes) -> str: return hashlib.sha256(content).hexdigest() +def _atomic_write_text(path: Path, content: str) -> None: + """Write *content* to *path* atomically via mkstemp + os.replace.""" + fd, tmp = tempfile.mkstemp( + dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp" + ) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(content) + os.replace(tmp, path) + except BaseException: + try: + os.unlink(tmp) + except OSError: + pass + raise + + def _constitution_is_generated( project_root: Path, memory_constitution: Path, @@ -848,7 +865,7 @@ def _register_commands( composed_dir = preset_dir / ".composed" composed_dir.mkdir(parents=True, exist_ok=True) composed_file = composed_dir / f"{cmd['name']}.md" - composed_file.write_text(composed, encoding="utf-8") + _atomic_write_text(composed_file, composed) commands_to_register.append({ **cmd, "file": f".composed/{cmd['name']}.md", @@ -1838,7 +1855,7 @@ def record_written(written: Dict[str, List[str]]) -> None: composed_dir = pack_dir / ".composed" composed_dir.mkdir(parents=True, exist_ok=True) composed_file = composed_dir / f"{cmd_name}.md" - composed_file.write_text(composed, encoding="utf-8") + _atomic_write_text(composed_file, composed) written = self._register_for_non_skill_agents( registrar, [{**tmpl, "file": f".composed/{cmd_name}.md"}], @@ -1858,7 +1875,7 @@ def record_written(written: Dict[str, List[str]]) -> None: shared_composed = self.presets_dir / ".composed" shared_composed.mkdir(parents=True, exist_ok=True) composed_file = shared_composed / f"{cmd_name}.md" - composed_file.write_text(composed, encoding="utf-8") + _atomic_write_text(composed_file, composed) source = layers[0]["source"] if source.startswith("extension:"): source_id = source.split(":", 1)[1].split(" ", 1)[0] From 076800fcfb2c8b827ae622fe09b1bb7abb40ddb5 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Sun, 16 Aug 2026 01:29:51 +0500 Subject: [PATCH 2/3] fix: set file mode to 0644 after mkstemp in _atomic_write_text mkstemp() creates files with mode 0600 (owner-only). The original write_text() used the default umask (typically 0644). Restore the expected permissions so other users/processes can read the file. --- src/specify_cli/presets/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index 30a7c90f7b..a4c0a5ae21 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -67,6 +67,7 @@ def _atomic_write_text(path: Path, content: str) -> None: fd, tmp = tempfile.mkstemp( dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp" ) + os.chmod(tmp, 0o644) try: with os.fdopen(fd, "w", encoding="utf-8") as f: f.write(content) From 39f57509d08e201bba12d4f69c3d36c6bbc739af Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Sun, 16 Aug 2026 01:37:14 +0500 Subject: [PATCH 3/3] fix: preserve original file mode instead of hardcoding 0644 Use os.fchmod() to copy the destination file's permission bits to the staged temp file when the destination exists. Falls back to mkstemp's default 0600 when the destination is new. --- src/specify_cli/presets/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index a4c0a5ae21..cbc645f7fb 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -67,8 +67,9 @@ def _atomic_write_text(path: Path, content: str) -> None: fd, tmp = tempfile.mkstemp( dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp" ) - os.chmod(tmp, 0o644) try: + if path.exists() and hasattr(os, "fchmod"): + os.fchmod(fd, path.stat(follow_symlinks=False).st_mode & 0o7777) with os.fdopen(fd, "w", encoding="utf-8") as f: f.write(content) os.replace(tmp, path)