openmodus 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. modus/__init__.py +13 -0
  2. modus/__main__.py +6 -0
  3. modus/_bundled/schemas/module-meta.schema.yaml +86 -0
  4. modus/activation_guidance.py +18 -0
  5. modus/artifacts_io.py +1066 -0
  6. modus/catalog.py +361 -0
  7. modus/cli.py +1785 -0
  8. modus/commands/__init__.py +17 -0
  9. modus/commands/context_cmd.py +50 -0
  10. modus/commands/help.py +131 -0
  11. modus/commands/init_project.py +503 -0
  12. modus/commands/install_state.py +212 -0
  13. modus/commands/maintenance_scope.py +69 -0
  14. modus/commands/permission_errors.py +26 -0
  15. modus/commands/reporting.py +113 -0
  16. modus/commands/self_update.py +577 -0
  17. modus/commands/status.py +52 -0
  18. modus/commands/uninstall.py +359 -0
  19. modus/commands/uninstall_report.py +183 -0
  20. modus/commands/uninstall_self.py +373 -0
  21. modus/commands/update.py +526 -0
  22. modus/commands/update_report.py +162 -0
  23. modus/commands/verify_anchors.py +230 -0
  24. modus/config.py +492 -0
  25. modus/git_hygiene.py +466 -0
  26. modus/hooks/__init__.py +21 -0
  27. modus/hooks/bootstrap.py +232 -0
  28. modus/hooks/context.py +29 -0
  29. modus/hooks/contracts.py +111 -0
  30. modus/hooks/environment.py +86 -0
  31. modus/hooks/protocol.py +156 -0
  32. modus/hooks/runtime.py +136 -0
  33. modus/init_analysis/README.md +72 -0
  34. modus/init_analysis/__init__.py +35 -0
  35. modus/init_analysis/api.py +360 -0
  36. modus/init_analysis/authoring_projection.py +192 -0
  37. modus/init_analysis/build_context.py +1326 -0
  38. modus/init_analysis/bundle.py +1092 -0
  39. modus/init_analysis/capability_evidence.py +585 -0
  40. modus/init_analysis/catalog.py +448 -0
  41. modus/init_analysis/compiler/__init__.py +1 -0
  42. modus/init_analysis/compiler/cache.py +63 -0
  43. modus/init_analysis/compiler/cpp.py +536 -0
  44. modus/init_analysis/compiler/go.py +158 -0
  45. modus/init_analysis/compiler/java.py +112 -0
  46. modus/init_analysis/compiler/merge.py +211 -0
  47. modus/init_analysis/compiler/models.py +88 -0
  48. modus/init_analysis/compiler/process.py +259 -0
  49. modus/init_analysis/compiler/python.py +430 -0
  50. modus/init_analysis/compiler/typescript.py +73 -0
  51. modus/init_analysis/contracts.py +283 -0
  52. modus/init_analysis/engine.py +230 -0
  53. modus/init_analysis/entry_contracts.py +155 -0
  54. modus/init_analysis/evidence_graph.py +133 -0
  55. modus/init_analysis/evidence_records.py +211 -0
  56. modus/init_analysis/extraction_cache.py +309 -0
  57. modus/init_analysis/fingerprint.py +38 -0
  58. modus/init_analysis/framework_facts.py +472 -0
  59. modus/init_analysis/frontend.py +1523 -0
  60. modus/init_analysis/generic.py +77 -0
  61. modus/init_analysis/ids.py +35 -0
  62. modus/init_analysis/input_snapshot.py +675 -0
  63. modus/init_analysis/language-registry.json +550 -0
  64. modus/init_analysis/language_capabilities.py +78 -0
  65. modus/init_analysis/native_binding.py +734 -0
  66. modus/init_analysis/native_parser.py +905 -0
  67. modus/init_analysis/navigation.py +184 -0
  68. modus/init_analysis/pipeline.py +504 -0
  69. modus/init_analysis/projection.py +409 -0
  70. modus/init_analysis/provider_assets/ModusJavaAnalyzer.java +350 -0
  71. modus/init_analysis/provider_assets/modus_go_analyzer.go +367 -0
  72. modus/init_analysis/provider_assets/modus_typescript_analyzer.cjs +163 -0
  73. modus/init_analysis/reference_outcomes.py +318 -0
  74. modus/init_analysis/resources.py +78 -0
  75. modus/init_analysis/scan.py +95 -0
  76. modus/init_analysis/scope_candidates.py +372 -0
  77. modus/init_analysis/search_metadata.py +32 -0
  78. modus/init_analysis/semantic_contracts.py +119 -0
  79. modus/init_analysis/semantic_ir.py +582 -0
  80. modus/init_analysis/semantic_providers.py +188 -0
  81. modus/init_analysis/semantic_reconcile.py +491 -0
  82. modus/init_analysis/source_contracts.py +127 -0
  83. modus/init_analysis/source_coordinates.py +80 -0
  84. modus/init_analysis/source_filters.py +184 -0
  85. modus/init_analysis/source_inventory.py +251 -0
  86. modus/init_analysis/source_roles.py +99 -0
  87. modus/init_analysis/source_snapshot.py +65 -0
  88. modus/init_analysis/structural.py +119 -0
  89. modus/init_analysis/topology.py +2060 -0
  90. modus/init_analysis/value_summary.py +463 -0
  91. modus/init_analysis/view_quarantine.py +606 -0
  92. modus/knowledge_identity.py +200 -0
  93. modus/knowledge_reader.py +220 -0
  94. modus/knowledge_seeds.py +56 -0
  95. modus/knowledge_update.py +212 -0
  96. modus/knowledge_write_session.py +241 -0
  97. modus/manifest.py +210 -0
  98. modus/orchestrator/README.md +67 -0
  99. modus/orchestrator/__init__.py +14 -0
  100. modus/orchestrator/context_resolver.py +142 -0
  101. modus/orchestrator/evidence_search.py +473 -0
  102. modus/orchestrator/fallback.py +83 -0
  103. modus/orchestrator/knowledge_sync.py +349 -0
  104. modus/orchestrator/models.py +82 -0
  105. modus/orchestrator/module_meta.py +194 -0
  106. modus/orchestrator/path_scope.py +38 -0
  107. modus/orchestrator/preflight.py +125 -0
  108. modus/orchestrator/query_terms.py +124 -0
  109. modus/orchestrator/search_policy.py +161 -0
  110. modus/paths.py +279 -0
  111. modus/platforms/__init__.py +45 -0
  112. modus/platforms/base.py +299 -0
  113. modus/platforms/claude.py +98 -0
  114. modus/platforms/codebuddy.py +51 -0
  115. modus/platforms/codex.py +41 -0
  116. modus/platforms/cursor.py +45 -0
  117. modus/platforms/hooks.py +188 -0
  118. modus/platforms/models.py +166 -0
  119. modus/platforms/registry.py +88 -0
  120. modus/platforms/rendering.py +77 -0
  121. modus/platforms/sections.py +100 -0
  122. modus/precedence.py +47 -0
  123. modus/project_activation.py +99 -0
  124. modus/retrieval/README.md +64 -0
  125. modus/retrieval/__init__.py +9 -0
  126. modus/retrieval/budget.py +50 -0
  127. modus/retrieval/delivery.py +226 -0
  128. modus/retrieval/index.py +92 -0
  129. modus/retrieval/pipeline.py +718 -0
  130. modus/retrieval/query.py +189 -0
  131. modus/retrieval/routing.py +237 -0
  132. modus/retrieval/runtime.py +39 -0
  133. modus/retrieval/settings.py +75 -0
  134. modus/retrieval/stages/__init__.py +7 -0
  135. modus/retrieval/stages/discovery.py +91 -0
  136. modus/retrieval/stages/documents.py +694 -0
  137. modus/retrieval/stages/lexical.py +116 -0
  138. modus/retrieval/stages/ranking.py +179 -0
  139. modus/retrieval/stages/sections.py +64 -0
  140. modus/retrieval/stages/selection.py +221 -0
  141. modus/retrieval/stages/settings.py +7 -0
  142. modus/scaffold/README.md +68 -0
  143. modus/scaffold/__init__.py +36 -0
  144. modus/scaffold/catalog.py +170 -0
  145. modus/scaffold/project.py +361 -0
  146. modus/scaffold/templates/README.md +37 -0
  147. modus/scaffold/templates/commands/init.md +19 -0
  148. modus/scaffold/templates/commands/reinit.md +18 -0
  149. modus/scaffold/templates/commands/update-knowledge.md +19 -0
  150. modus/scaffold/templates/knowledge/constraint/global-standard.md +18 -0
  151. modus/scaffold/templates/knowledge/constraint/team-standard.md +28 -0
  152. modus/scaffold/templates/knowledge/handbook/SKILL.md +21 -0
  153. modus/scaffold/templates/skills/modus-init/SKILL.md +52 -0
  154. modus/scaffold/templates/skills/modus-init/references/architecture-overview.md +23 -0
  155. modus/scaffold/templates/skills/modus-init/references/business-skill.md +65 -0
  156. modus/scaffold/templates/skills/modus-init/references/code-skill.md +190 -0
  157. modus/scaffold/templates/skills/modus-init/references/constraint-extraction.md +86 -0
  158. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/cpp.md +39 -0
  159. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/go.md +39 -0
  160. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/java.md +39 -0
  161. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/php.md +39 -0
  162. modus/scaffold/templates/skills/modus-init/references/constraint-profiles/web.md +50 -0
  163. modus/scaffold/templates/skills/modus-init/references/global-standard.md +9 -0
  164. modus/scaffold/templates/skills/modus-init/references/language-adaptation.md +19 -0
  165. modus/scaffold/templates/skills/modus-init/references/scope-discovery.md +86 -0
  166. modus/scaffold/templates/skills/modus-init/references/team-standard.md +52 -0
  167. modus/scaffold/templates/skills/modus-init/references/workspace-init.md +67 -0
  168. modus/scaffold/templates/skills/modus-init/references/zh-style.md +12 -0
  169. modus/scaffold/templates/skills/modus-reinit/SKILL.md +22 -0
  170. modus/scaffold/templates/skills/modus-sync-knowledge/SKILL.md +28 -0
  171. modus/scaffold/templates/skills/modus-update-knowledge/SKILL.md +22 -0
  172. modus/scaffold/templates/skills/using-modus/SKILL.md +31 -0
  173. modus/scaffold/templates/skills/using-modus/references/constraint-usage.md +28 -0
  174. modus/scaffold/templates/skills/using-modus/references/framework-update.md +15 -0
  175. modus/scaffold/templates/skills/using-modus/references/knowledge-update-lifecycle.md +68 -0
  176. modus/scaffold/templates/skills/using-modus/references/router-details.md +42 -0
  177. modus/scaffold/writer.py +101 -0
  178. modus/schema_resources.py +56 -0
  179. modus/scope_models.py +504 -0
  180. modus/scope_resolver.py +109 -0
  181. modus/sensitive_literals.py +64 -0
  182. modus/skillmd.py +144 -0
  183. modus/source_listing.py +82 -0
  184. modus/surface.py +65 -0
  185. modus/text_tree.py +64 -0
  186. modus/versioning.py +61 -0
  187. modus/workspace.py +881 -0
  188. openmodus-0.0.1.dist-info/METADATA +251 -0
  189. openmodus-0.0.1.dist-info/RECORD +192 -0
  190. openmodus-0.0.1.dist-info/WHEEL +4 -0
  191. openmodus-0.0.1.dist-info/entry_points.txt +2 -0
  192. openmodus-0.0.1.dist-info/licenses/LICENSE +21 -0
modus/artifacts_io.py ADDED
@@ -0,0 +1,1066 @@
1
+ """Corruption-tolerant loading of Modus-generated artifacts.
2
+
3
+ Modus-generated artifacts (evidence bundles, configuration, domain metadata,
4
+ and disposable caches) are machine-written YAML/JSON. A hand-edit, merge accident, or
5
+ interrupted write can leave them malformed. The human review surface is friendly
6
+ Markdown/YAML plus commands, so a malformed artifact must surface as a readable,
7
+ actionable error -- never a raw parser traceback. Disposable caches instead
8
+ self-heal (``tolerant=True``): a corrupt cache is treated as absent and rebuilt.
9
+
10
+ Two load modes:
11
+
12
+ * strict (default): malformed content raises :class:`CorruptArtifactError`, whose
13
+ message names the file, the failure, and how to recover. CLI gate commands
14
+ turn this into a clean non-zero exit instead of a traceback.
15
+ * tolerant: malformed content returns ``default`` so a regeneration/rebuild path
16
+ can overwrite the bad file instead of crashing on it.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ import secrets
24
+ import stat
25
+ import threading
26
+ from collections.abc import Iterable, Iterator
27
+ from contextlib import contextmanager
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import Any, Final
31
+
32
+ import yaml
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ _HELD_FILE_LOCK_DEPTHS = threading.local()
37
+ _OPEN_FILE_LOCKS: dict[int, object] = {}
38
+ _OPEN_FILE_LOCKS_GUARD = threading.Lock()
39
+ _FILE_LOCK_REGISTRY_PID = os.getpid()
40
+ _LOCK_BIND_ATTEMPTS: Final = 8
41
+
42
+
43
+ def _reset_file_lock_registry_after_pid_change() -> None:
44
+ """Drop copied ownership and close inherited lock descriptors in a fork child."""
45
+ global _FILE_LOCK_REGISTRY_PID, _OPEN_FILE_LOCKS, _OPEN_FILE_LOCKS_GUARD
46
+
47
+ inherited = set(_OPEN_FILE_LOCKS.values())
48
+ for descriptor in tuple(_OPEN_FILE_LOCKS):
49
+ try:
50
+ os.close(descriptor)
51
+ except OSError:
52
+ pass
53
+ _OPEN_FILE_LOCKS = {}
54
+ _OPEN_FILE_LOCKS_GUARD = threading.Lock()
55
+ _HELD_FILE_LOCK_DEPTHS.depths = {}
56
+ _HELD_FILE_LOCK_DEPTHS.required_depths = {}
57
+ _HELD_FILE_LOCK_DEPTHS.inherited_tokens = inherited
58
+ _FILE_LOCK_REGISTRY_PID = os.getpid()
59
+
60
+
61
+ def _ensure_file_lock_registry_pid() -> None:
62
+ if _FILE_LOCK_REGISTRY_PID != os.getpid():
63
+ _reset_file_lock_registry_after_pid_change()
64
+
65
+
66
+ if hasattr(os, "register_at_fork"):
67
+ os.register_at_fork(after_in_child=_reset_file_lock_registry_after_pid_change)
68
+
69
+
70
+ class CorruptArtifactError(Exception):
71
+ """A Modus-generated artifact exists but could not be parsed.
72
+
73
+ Carries enough structured context for the CLI to print a friendly, actionable
74
+ message (which file, what kind, how to recover).
75
+ """
76
+
77
+ def __init__(self, path: str | Path, kind: str, detail: str, hint: str | None = None) -> None:
78
+ self.path = str(path)
79
+ self.kind = kind
80
+ self.detail = detail
81
+ self.hint = hint
82
+ message = f"{kind} is malformed: {self.path}\n -> {detail}"
83
+ if hint:
84
+ message += f"\n -> {hint}"
85
+ super().__init__(message)
86
+
87
+
88
+ class FileLockIntegrityError(ValueError):
89
+ """A writer lock path could not be bound without following links."""
90
+
91
+
92
+ class RequiredFileLockUnavailable(RuntimeError):
93
+ """A correctness-critical writer lock has no safe platform backend."""
94
+
95
+
96
+ class AtomicWriteIntegrityError(OSError):
97
+ """An ``atomic_write_text`` target could not be safely bound or written.
98
+
99
+ Raised instead of silently writing through a symlink/special file, or
100
+ instead of completing a replace whose parent directory was swapped out
101
+ from under the write. A well-formed, non-adversarial target never raises
102
+ this -- only a malformed or actively-racing target does.
103
+ """
104
+
105
+
106
+ def _detail(exc: Exception) -> str:
107
+ return " ".join(str(exc).split())[:300]
108
+
109
+
110
+ def safe_load_yaml(
111
+ path: str | Path,
112
+ *,
113
+ kind: str,
114
+ default: Any = None,
115
+ tolerant: bool = False,
116
+ hint: str | None = None,
117
+ ) -> Any:
118
+ """Load a YAML artifact, mapping malformed content to a friendly outcome.
119
+
120
+ Returns ``default`` when the file is missing or empty. On malformed YAML:
121
+ raise :class:`CorruptArtifactError` (strict) or return ``default`` (tolerant).
122
+ """
123
+ p = Path(path)
124
+ if not p.is_file():
125
+ return default
126
+ try:
127
+ data = yaml.safe_load(p.read_text(encoding="utf-8"))
128
+ except yaml.YAMLError as exc:
129
+ if tolerant:
130
+ return default
131
+ raise CorruptArtifactError(p, kind, _detail(exc), hint) from exc
132
+ return default if data is None else data
133
+
134
+
135
+ def _lock_directory_flags() -> int:
136
+ no_follow = getattr(os, "O_NOFOLLOW", None)
137
+ directory = getattr(os, "O_DIRECTORY", None)
138
+ if no_follow is None or directory is None:
139
+ raise FileLockIntegrityError("safe writer locks require O_NOFOLLOW and O_DIRECTORY")
140
+ return os.O_RDONLY | no_follow | directory | getattr(os, "O_CLOEXEC", 0)
141
+
142
+
143
+ def _absolute_lock_path(lock_path: str | Path) -> Path:
144
+ """Return a lexical absolute path without resolving any filesystem link."""
145
+ return Path(os.path.abspath(os.fspath(lock_path)))
146
+
147
+
148
+ def _assert_lock_parent_binding(parent: Path, descriptor: int) -> None:
149
+ """Verify every named ancestor and the retained parent descriptor agree."""
150
+ anchor = Path(parent.anchor)
151
+ current = anchor
152
+ try:
153
+ info = os.stat(anchor, follow_symlinks=False)
154
+ if not stat.S_ISDIR(info.st_mode):
155
+ raise FileLockIntegrityError(f"writer lock anchor is not a directory: {anchor}")
156
+ for part in parent.parts[1:]:
157
+ current = current / part
158
+ info = os.stat(current, follow_symlinks=False)
159
+ if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
160
+ raise FileLockIntegrityError(
161
+ f"writer lock parent is not a real directory: {current}"
162
+ )
163
+ opened = os.fstat(descriptor)
164
+ except FileLockIntegrityError:
165
+ raise
166
+ except (OSError, TypeError, NotImplementedError) as exc:
167
+ raise FileLockIntegrityError(
168
+ f"writer lock parent is unsafe or unavailable: {current}: {exc}"
169
+ ) from exc
170
+ if (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino):
171
+ raise FileLockIntegrityError(f"writer lock parent path changed after opening: {parent}")
172
+
173
+
174
+ def _open_lock_parent(parent: Path) -> int:
175
+ """Create/open ``parent`` one component at a time without following links."""
176
+ flags = _lock_directory_flags()
177
+ descriptor: int | None = None
178
+ try:
179
+ descriptor = os.open(parent.anchor, flags)
180
+ for part in parent.parts[1:]:
181
+ if part in {"", ".", ".."}:
182
+ raise FileLockIntegrityError(f"writer lock parent is not canonical: {parent}")
183
+ try:
184
+ os.mkdir(part, mode=0o755, dir_fd=descriptor)
185
+ except FileExistsError:
186
+ pass
187
+ child = os.open(part, flags, dir_fd=descriptor)
188
+ os.close(descriptor)
189
+ descriptor = child
190
+ _assert_lock_parent_binding(parent, descriptor)
191
+ return descriptor
192
+ except FileLockIntegrityError:
193
+ if descriptor is not None:
194
+ os.close(descriptor)
195
+ raise
196
+ except (OSError, TypeError, NotImplementedError) as exc:
197
+ if descriptor is not None:
198
+ try:
199
+ os.close(descriptor)
200
+ except OSError:
201
+ pass
202
+ raise FileLockIntegrityError(
203
+ f"writer lock parent is unsafe or unavailable: {parent}: {exc}"
204
+ ) from exc
205
+
206
+
207
+ def _assert_lock_file_binding(parent_descriptor: int, name: str, descriptor: int) -> None:
208
+ """Require the lock name to remain bound to the opened regular-file inode."""
209
+ try:
210
+ opened = os.fstat(descriptor)
211
+ named = os.stat(name, dir_fd=parent_descriptor, follow_symlinks=False)
212
+ except (OSError, TypeError, NotImplementedError) as exc:
213
+ raise FileLockIntegrityError(
214
+ f"writer lock file is unsafe or unavailable: {name}: {exc}"
215
+ ) from exc
216
+ if not stat.S_ISREG(opened.st_mode) or not stat.S_ISREG(named.st_mode):
217
+ raise FileLockIntegrityError(f"writer lock must be a regular file: {name}")
218
+ if (opened.st_dev, opened.st_ino) != (named.st_dev, named.st_ino):
219
+ raise FileLockIntegrityError(f"writer lock path changed after opening: {name}")
220
+
221
+
222
+ def _open_lock_file(parent_descriptor: int, name: str) -> int:
223
+ no_follow = getattr(os, "O_NOFOLLOW", None)
224
+ non_block = getattr(os, "O_NONBLOCK", None)
225
+ if no_follow is None or non_block is None:
226
+ raise FileLockIntegrityError("safe writer locks require O_NOFOLLOW and O_NONBLOCK")
227
+ try:
228
+ descriptor = os.open(
229
+ name,
230
+ os.O_RDWR | os.O_CREAT | no_follow | non_block | getattr(os, "O_CLOEXEC", 0),
231
+ 0o600,
232
+ dir_fd=parent_descriptor,
233
+ )
234
+ except (OSError, TypeError, NotImplementedError) as exc:
235
+ raise FileLockIntegrityError(
236
+ f"writer lock file is unsafe or unavailable: {name}: {exc}"
237
+ ) from exc
238
+ try:
239
+ _assert_lock_file_binding(parent_descriptor, name, descriptor)
240
+ except Exception:
241
+ os.close(descriptor)
242
+ raise
243
+ return descriptor
244
+
245
+
246
+ def _register_open_file_lock(descriptor: int) -> object:
247
+ _ensure_file_lock_registry_pid()
248
+ token = object()
249
+ with _OPEN_FILE_LOCKS_GUARD:
250
+ _OPEN_FILE_LOCKS[descriptor] = token
251
+ return token
252
+
253
+
254
+ def _unregister_open_file_lock(descriptor: int, token: object) -> None:
255
+ _ensure_file_lock_registry_pid()
256
+ with _OPEN_FILE_LOCKS_GUARD:
257
+ if _OPEN_FILE_LOCKS.get(descriptor) is token:
258
+ _OPEN_FILE_LOCKS.pop(descriptor, None)
259
+
260
+
261
+ def _file_lock_token_was_inherited(token: object | None) -> bool:
262
+ if token is None:
263
+ return False
264
+ _ensure_file_lock_registry_pid()
265
+ inherited: set[object] = getattr(_HELD_FILE_LOCK_DEPTHS, "inherited_tokens", set())
266
+ return token in inherited
267
+
268
+
269
+ @dataclass(slots=True)
270
+ class _PosixLockHandle:
271
+ """Own every descriptor and registry token for one POSIX writer lock."""
272
+
273
+ parent_fd: int | None = None
274
+ lock_fd: int | None = None
275
+ parent_token: object | None = None
276
+ lock_token: object | None = None
277
+ locked: bool = False
278
+
279
+ def close(self, fcntl_module: Any, *, preserve_inherited: bool) -> None:
280
+ """Release registrations and descriptors in dependency order."""
281
+
282
+ try:
283
+ if self.lock_fd is not None:
284
+ inherited = preserve_inherited and _file_lock_token_was_inherited(self.lock_token)
285
+ if self.lock_token is not None:
286
+ _unregister_open_file_lock(self.lock_fd, self.lock_token)
287
+ if not inherited:
288
+ try:
289
+ if self.locked:
290
+ fcntl_module.flock(self.lock_fd, fcntl_module.LOCK_UN)
291
+ finally:
292
+ os.close(self.lock_fd)
293
+ finally:
294
+ if self.parent_fd is not None:
295
+ inherited = preserve_inherited and _file_lock_token_was_inherited(self.parent_token)
296
+ if self.parent_token is not None:
297
+ _unregister_open_file_lock(self.parent_fd, self.parent_token)
298
+ if not inherited:
299
+ os.close(self.parent_fd)
300
+
301
+
302
+ def _acquire_posix_lock(path: Path, fcntl_module: Any) -> _PosixLockHandle:
303
+ """Safely bind and acquire one logical lock, retrying concurrent first use."""
304
+
305
+ for attempt in range(_LOCK_BIND_ATTEMPTS):
306
+ handle = _PosixLockHandle()
307
+ try:
308
+ handle.parent_fd = _open_lock_parent(path.parent)
309
+ handle.parent_token = _register_open_file_lock(handle.parent_fd)
310
+ handle.lock_fd = _open_lock_file(handle.parent_fd, path.name)
311
+ handle.lock_token = _register_open_file_lock(handle.lock_fd)
312
+ fcntl_module.flock(handle.lock_fd, fcntl_module.LOCK_EX)
313
+ handle.locked = True
314
+ _assert_lock_parent_binding(path.parent, handle.parent_fd)
315
+ _assert_lock_file_binding(handle.parent_fd, path.name, handle.lock_fd)
316
+ return handle
317
+ except FileLockIntegrityError:
318
+ handle.close(fcntl_module, preserve_inherited=False)
319
+ if attempt == _LOCK_BIND_ATTEMPTS - 1:
320
+ raise
321
+ except BaseException:
322
+ handle.close(fcntl_module, preserve_inherited=False)
323
+ raise
324
+ raise AssertionError("unreachable lock acquisition state")
325
+
326
+
327
+ @contextmanager
328
+ def file_lock(lock_path: str | Path) -> Iterator[None]:
329
+ """Advisory exclusive lock around a critical section (POSIX ``flock``).
330
+
331
+ Serializes concurrent writers (e.g. two initialization-analysis builds, or two
332
+ ``promote`` runs feeding the same domain) so their read-modify-write never
333
+ interleaves. Degrades to a no-op where ``fcntl`` is unavailable (non-POSIX):
334
+ best-effort, never blocks the workflow. The lock file lives wherever the
335
+ caller points it -- by convention under the disposable cache, never inside an
336
+ output tree the lock protects.
337
+ """
338
+ _ensure_file_lock_registry_pid()
339
+ try:
340
+ import fcntl
341
+ except Exception: # noqa: BLE001 - non-POSIX: best-effort, no locking
342
+ yield
343
+ return
344
+
345
+ # Directory creation and the first lock open are intentionally outside any
346
+ # pre-existing mutex. Concurrent first-use writers can replace a just-made
347
+ # path component between openat/fstat checks. Retry the complete safe bind;
348
+ # every attempt still uses O_NOFOLLOW and never relaxes the symlink policy.
349
+ path = _absolute_lock_path(lock_path)
350
+ handle = _acquire_posix_lock(path, fcntl)
351
+ try:
352
+ yield
353
+ assert handle.parent_fd is not None and handle.lock_fd is not None
354
+ _assert_lock_parent_binding(path.parent, handle.parent_fd)
355
+ _assert_lock_file_binding(handle.parent_fd, path.name, handle.lock_fd)
356
+ finally:
357
+ handle.close(fcntl, preserve_inherited=True)
358
+
359
+
360
+ def _assert_portable_lock_binding(path: Path, descriptor: int) -> None:
361
+ """Validate the regular-file binding used by the native Windows backend."""
362
+ current = Path(path.anchor)
363
+ try:
364
+ for part in path.parent.parts[1:]:
365
+ current = current / part
366
+ info = os.stat(current, follow_symlinks=False)
367
+ if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
368
+ raise FileLockIntegrityError(
369
+ f"writer lock parent is not a real directory: {current}"
370
+ )
371
+ opened = os.fstat(descriptor)
372
+ named = os.stat(path, follow_symlinks=False)
373
+ except FileLockIntegrityError:
374
+ raise
375
+ except (OSError, TypeError, NotImplementedError) as exc:
376
+ raise FileLockIntegrityError(
377
+ f"writer lock is unsafe or unavailable: {path}: {exc}"
378
+ ) from exc
379
+ if stat.S_ISLNK(named.st_mode) or not stat.S_ISREG(named.st_mode):
380
+ raise FileLockIntegrityError(f"writer lock must be a regular file: {path}")
381
+ if not stat.S_ISREG(opened.st_mode) or (
382
+ opened.st_dev,
383
+ opened.st_ino,
384
+ ) != (named.st_dev, named.st_ino):
385
+ raise FileLockIntegrityError(f"writer lock path changed after opening: {path}")
386
+
387
+
388
+ @contextmanager
389
+ def _windows_required_file_lock(lock_path: str | Path, msvcrt_module: Any) -> Iterator[None]:
390
+ """Acquire a mandatory one-byte Windows file lock without shell tooling."""
391
+ path = _absolute_lock_path(lock_path)
392
+ path.parent.mkdir(parents=True, exist_ok=True)
393
+ flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOINHERIT", 0)
394
+ descriptor = os.open(path, flags, 0o600)
395
+ locked = False
396
+ try:
397
+ _assert_portable_lock_binding(path, descriptor)
398
+ if os.fstat(descriptor).st_size == 0:
399
+ os.write(descriptor, b"\0")
400
+ os.fsync(descriptor)
401
+ os.lseek(descriptor, 0, os.SEEK_SET)
402
+ msvcrt_module.locking(descriptor, msvcrt_module.LK_LOCK, 1)
403
+ locked = True
404
+ _assert_portable_lock_binding(path, descriptor)
405
+ yield
406
+ _assert_portable_lock_binding(path, descriptor)
407
+ finally:
408
+ if locked:
409
+ os.lseek(descriptor, 0, os.SEEK_SET)
410
+ msvcrt_module.locking(descriptor, msvcrt_module.LK_UNLCK, 1)
411
+ os.close(descriptor)
412
+
413
+
414
+ @contextmanager
415
+ def required_file_lock(lock_path: str | Path) -> Iterator[None]:
416
+ """Acquire a cross-platform writer lock or fail closed.
417
+
418
+ Use this for state transitions whose correctness depends on mutual exclusion
419
+ (for example knowledge and install writes). Read-only callers may continue to use
420
+ :func:`file_lock`'s best-effort behavior, but transactional writers must not
421
+ silently continue without a supported lock backend.
422
+ """
423
+ try:
424
+ import fcntl # noqa: F401 - capability probe; file_lock owns the handle
425
+ except Exception: # noqa: BLE001 - probe the native Windows backend next
426
+ try:
427
+ import msvcrt
428
+ except Exception as exc: # noqa: BLE001 - fail closed by contract
429
+ raise RequiredFileLockUnavailable(
430
+ "required writer locks need either POSIX fcntl or Windows msvcrt"
431
+ ) from exc
432
+ logger.debug("acquiring required Windows writer lock", extra={"path": str(lock_path)})
433
+ with _windows_required_file_lock(lock_path, msvcrt):
434
+ yield
435
+ return
436
+
437
+ logger.debug("acquiring required POSIX writer lock", extra={"path": str(lock_path)})
438
+ with file_lock(lock_path):
439
+ yield
440
+
441
+
442
+ @contextmanager
443
+ def required_reentrant_file_lock(lock_path: str | Path) -> Iterator[None]:
444
+ """Acquire one correctness-critical lock per thread and canonical path."""
445
+ identity = _file_lock_identity(lock_path)
446
+ depths = _held_file_lock_depths()
447
+ required_depths = _held_required_file_lock_depths()
448
+ if required_depths.get(identity, 0):
449
+ depths[identity] += 1
450
+ required_depths[identity] += 1
451
+ try:
452
+ yield
453
+ finally:
454
+ depths[identity] -= 1
455
+ required_depths[identity] -= 1
456
+ return
457
+
458
+ if depths.get(identity, 0):
459
+ # An advisory lock is correctness-equivalent only on POSIX, where
460
+ # ``file_lock`` necessarily acquired flock. On Windows that same API is
461
+ # deliberately a no-op, so a nested transaction must still acquire
462
+ # the native required backend instead of mistaking thread depth for an
463
+ # operating-system lock.
464
+ try:
465
+ import fcntl # noqa: F401 - capability/ownership distinction
466
+ except Exception: # noqa: BLE001 - native required backend acquired below
467
+ pass
468
+ else:
469
+ depths[identity] += 1
470
+ required_depths[identity] = 1
471
+ try:
472
+ yield
473
+ finally:
474
+ depths[identity] -= 1
475
+ required_depths.pop(identity, None)
476
+ return
477
+
478
+ with required_file_lock(lock_path):
479
+ previous_depth = depths.get(identity, 0)
480
+ depths[identity] = previous_depth + 1
481
+ required_depths[identity] = 1
482
+ try:
483
+ yield
484
+ finally:
485
+ required_depths.pop(identity, None)
486
+ if previous_depth:
487
+ depths[identity] = previous_depth
488
+ else:
489
+ depths.pop(identity, None)
490
+
491
+
492
+ @contextmanager
493
+ def reentrant_file_lock(lock_path: str | Path) -> Iterator[None]:
494
+ """Acquire one advisory writer lock once per current thread and canonical path."""
495
+ identity = _file_lock_identity(lock_path)
496
+ depths = _held_file_lock_depths()
497
+ if depths.get(identity, 0):
498
+ depths[identity] += 1
499
+ try:
500
+ yield
501
+ finally:
502
+ depths[identity] -= 1
503
+ return
504
+ with file_lock(lock_path):
505
+ depths[identity] = 1
506
+ try:
507
+ yield
508
+ finally:
509
+ depths.pop(identity, None)
510
+
511
+
512
+ def _file_lock_identity(lock_path: str | Path) -> str:
513
+ return os.path.normcase(os.fspath(_absolute_lock_path(lock_path)))
514
+
515
+
516
+ def _held_file_lock_depths() -> dict[str, int]:
517
+ _ensure_file_lock_registry_pid()
518
+ depths = getattr(_HELD_FILE_LOCK_DEPTHS, "depths", None)
519
+ if depths is None:
520
+ depths = {}
521
+ _HELD_FILE_LOCK_DEPTHS.depths = depths
522
+ return depths
523
+
524
+
525
+ def _held_required_file_lock_depths() -> dict[str, int]:
526
+ _ensure_file_lock_registry_pid()
527
+ depths = getattr(_HELD_FILE_LOCK_DEPTHS, "required_depths", None)
528
+ if depths is None:
529
+ depths = {}
530
+ _HELD_FILE_LOCK_DEPTHS.required_depths = depths
531
+ return depths
532
+
533
+
534
+ def _atomic_write_temp_name(name: str) -> str:
535
+ """A per-attempt temp name: PID *and* a random nonce.
536
+
537
+ PID alone collides across a fork boundary or under PID reuse (two
538
+ concurrent writers can legitimately share a PID). The nonce makes every
539
+ attempt's temp name unique regardless of PID, so 64 same-PID writers (or
540
+ genuinely different processes) never contend for one temp path.
541
+ """
542
+ return f".{name}.tmp-{os.getpid()}-{secrets.token_hex(8)}"
543
+
544
+
545
+ def _reject_unsafe_atomic_write_target(
546
+ parent_descriptor: int | None, name: str, path: Path
547
+ ) -> None:
548
+ """Fail closed if an existing target is a symlink or special file.
549
+
550
+ Checked via ``lstat`` (never following the final component), so this
551
+ guarantee holds even when ``O_NOFOLLOW`` is unavailable for the open calls
552
+ below -- the property does not depend on those flags.
553
+ """
554
+ try:
555
+ info = (
556
+ os.stat(name, dir_fd=parent_descriptor, follow_symlinks=False)
557
+ if parent_descriptor is not None
558
+ else path.lstat()
559
+ )
560
+ except FileNotFoundError:
561
+ return
562
+ except (OSError, TypeError, NotImplementedError) as exc:
563
+ raise AtomicWriteIntegrityError(
564
+ f"atomic write target is unsafe or unavailable: {path}: {exc}"
565
+ ) from exc
566
+ if _is_link_or_reparse(info) or not stat.S_ISREG(info.st_mode):
567
+ raise AtomicWriteIntegrityError(
568
+ f"atomic write target must be a regular file or absent, not a "
569
+ f"symlink/special file: {path}"
570
+ )
571
+
572
+
573
+ def _open_atomic_child(parent_fd: int, name: str, current: Path, flags: int) -> int:
574
+ """Create/open and inode-bind one directory below an already safe parent."""
575
+
576
+ try:
577
+ os.mkdir(name, mode=0o755, dir_fd=parent_fd)
578
+ except FileExistsError:
579
+ pass
580
+ child = os.open(name, flags, dir_fd=parent_fd)
581
+ try:
582
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
583
+ opened = os.fstat(child)
584
+ if (
585
+ _is_link_or_reparse(named)
586
+ or not stat.S_ISDIR(named.st_mode)
587
+ or not stat.S_ISDIR(opened.st_mode)
588
+ or (named.st_dev, named.st_ino) != (opened.st_dev, opened.st_ino)
589
+ ):
590
+ raise AtomicWriteIntegrityError(
591
+ f"atomic write parent is not a real directory: {current}"
592
+ )
593
+ return child
594
+ except BaseException:
595
+ os.close(child)
596
+ raise
597
+
598
+
599
+ def _open_atomic_write_parent(parent: Path) -> int:
600
+ """Create/open every ``parent`` component without following filesystem links.
601
+
602
+ A single ``os.open(parent, O_NOFOLLOW | ...)`` call only rejects a symlink
603
+ at the *final* path component -- the kernel still resolves every
604
+ intermediate component through ordinary path resolution, so a symlinked
605
+ grandparent (or higher) would otherwise be silently followed. Opening
606
+ component-by-component with ``dir_fd`` closes that gap. Missing directories
607
+ are created relative to an already-bound parent, avoiding the unsafe
608
+ ``Path.mkdir(parents=True)`` preflight that can traverse an ancestor link.
609
+ """
610
+ open_flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
611
+ no_follow = getattr(os, "O_NOFOLLOW", None)
612
+ if no_follow is not None:
613
+ open_flags |= no_follow
614
+ if hasattr(os, "O_DIRECTORY"):
615
+ open_flags |= os.O_DIRECTORY
616
+ descriptor: int | None = None
617
+ transferred = False
618
+ try:
619
+ descriptor = os.open(parent.anchor, open_flags)
620
+ current = Path(parent.anchor)
621
+ for part in parent.parts[1:]:
622
+ if part in {"", ".", ".."}:
623
+ raise AtomicWriteIntegrityError(f"atomic write parent is not canonical: {parent}")
624
+ current = current / part
625
+ child = _open_atomic_child(descriptor, part, current, open_flags)
626
+ os.close(descriptor)
627
+ descriptor = child
628
+ _assert_atomic_write_parent_binding(parent, descriptor)
629
+ transferred = True
630
+ return descriptor
631
+ except AtomicWriteIntegrityError:
632
+ raise
633
+ except (OSError, TypeError, NotImplementedError) as exc:
634
+ raise AtomicWriteIntegrityError(
635
+ f"atomic write parent is unsafe or unavailable: {parent}: {exc}"
636
+ ) from exc
637
+ finally:
638
+ if descriptor is not None and not transferred:
639
+ try:
640
+ os.close(descriptor)
641
+ except OSError:
642
+ pass
643
+
644
+
645
+ def _assert_atomic_write_parent_binding(parent: Path, descriptor: int) -> None:
646
+ """Verify every named ancestor and the open parent descriptor agree.
647
+
648
+ Mirrors ``file_lock``'s ``_assert_lock_parent_binding``: walks the full
649
+ ancestor chain (not just the immediate parent) with an unresolved
650
+ ``stat``, then compares the innermost component's identity against the
651
+ descriptor actually held open. A parent-directory swap anywhere in the
652
+ chain -- not only the last component -- is therefore detected.
653
+ """
654
+ anchor = Path(parent.anchor)
655
+ current = anchor
656
+ try:
657
+ info = os.stat(anchor, follow_symlinks=False)
658
+ if not stat.S_ISDIR(info.st_mode):
659
+ raise AtomicWriteIntegrityError(f"atomic write anchor is not a directory: {anchor}")
660
+ for part in parent.parts[1:]:
661
+ current = current / part
662
+ info = os.stat(current, follow_symlinks=False)
663
+ if _is_link_or_reparse(info) or not stat.S_ISDIR(info.st_mode):
664
+ raise AtomicWriteIntegrityError(
665
+ f"atomic write parent is not a real directory: {current}"
666
+ )
667
+ opened = os.fstat(descriptor)
668
+ except AtomicWriteIntegrityError:
669
+ raise
670
+ except (OSError, TypeError, NotImplementedError) as exc:
671
+ raise AtomicWriteIntegrityError(
672
+ f"atomic write parent is unsafe or unavailable: {current}: {exc}"
673
+ ) from exc
674
+ if (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino):
675
+ raise AtomicWriteIntegrityError(f"atomic write parent path changed after opening: {parent}")
676
+
677
+
678
+ def _is_link_or_reparse(info: os.stat_result) -> bool:
679
+ """Recognize POSIX symlinks and Windows reparse-point indirections."""
680
+ reparse_flag = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x0400)
681
+ attributes = getattr(info, "st_file_attributes", 0)
682
+ return stat.S_ISLNK(info.st_mode) or bool(attributes & reparse_flag)
683
+
684
+
685
+ def _portable_lstat(path: Path) -> os.stat_result:
686
+ """Return unresolved metadata on both POSIX and native Windows Python."""
687
+ try:
688
+ return os.stat(path, follow_symlinks=False)
689
+ except (TypeError, NotImplementedError): # pragma: no cover - old Windows Python
690
+ return path.lstat()
691
+
692
+
693
+ def _portable_parent_snapshot(parent: Path) -> tuple[tuple[int, int], ...]:
694
+ """Validate an atomic-write ancestor chain and return stable identities.
695
+
696
+ This is the strongest portable fallback available where ``openat`` and
697
+ ``O_DIRECTORY`` are absent (notably native Windows). Reparse points are
698
+ rejected explicitly; callers compare the snapshot again immediately before
699
+ publication to catch ordinary directory replacement races.
700
+ """
701
+ absolute = _absolute_lock_path(parent)
702
+ anchor = Path(absolute.anchor)
703
+ if not absolute.anchor:
704
+ raise AtomicWriteIntegrityError(f"atomic write parent must be an absolute path: {parent}")
705
+ current = anchor
706
+ identities: list[tuple[int, int]] = []
707
+ try:
708
+ info = _portable_lstat(current)
709
+ if _is_link_or_reparse(info) or not stat.S_ISDIR(info.st_mode):
710
+ raise AtomicWriteIntegrityError(
711
+ f"atomic write parent is not a real directory: {current}"
712
+ )
713
+ identities.append((info.st_dev, info.st_ino))
714
+ for part in absolute.parts[1:]:
715
+ if part in {"", ".", ".."}:
716
+ raise AtomicWriteIntegrityError(f"atomic write parent is not canonical: {absolute}")
717
+ current = current / part
718
+ info = _portable_lstat(current)
719
+ if _is_link_or_reparse(info) or not stat.S_ISDIR(info.st_mode):
720
+ raise AtomicWriteIntegrityError(
721
+ f"atomic write parent is not a real directory: {current}"
722
+ )
723
+ identities.append((info.st_dev, info.st_ino))
724
+ except (AtomicWriteIntegrityError, FileNotFoundError):
725
+ raise
726
+ except (OSError, TypeError, NotImplementedError) as exc:
727
+ raise AtomicWriteIntegrityError(
728
+ f"atomic write parent is unsafe or unavailable: {current}: {exc}"
729
+ ) from exc
730
+ return tuple(identities)
731
+
732
+
733
+ def _assert_portable_atomic_parent(
734
+ parent: Path, expected: tuple[tuple[int, int], ...] | None = None
735
+ ) -> tuple[tuple[int, int], ...]:
736
+ """Validate portable ancestors and optionally compare their identities."""
737
+ try:
738
+ actual = _portable_parent_snapshot(parent)
739
+ except FileNotFoundError as exc:
740
+ raise AtomicWriteIntegrityError(
741
+ f"atomic write parent is unsafe or unavailable: {parent}: {exc}"
742
+ ) from exc
743
+ if expected is not None and actual != expected:
744
+ raise AtomicWriteIntegrityError(
745
+ f"atomic write parent path changed during publication: {parent}"
746
+ )
747
+ return actual
748
+
749
+
750
+ def _ensure_portable_atomic_parent(parent: Path) -> tuple[tuple[int, int], ...]:
751
+ """Create missing fallback directories without traversing a static link."""
752
+ absolute = _absolute_lock_path(parent)
753
+ anchor = Path(absolute.anchor)
754
+ if not absolute.anchor:
755
+ raise AtomicWriteIntegrityError(f"atomic write parent must be an absolute path: {parent}")
756
+ current = anchor
757
+ try:
758
+ anchor_info = _portable_lstat(anchor)
759
+ if _is_link_or_reparse(anchor_info) or not stat.S_ISDIR(anchor_info.st_mode):
760
+ raise AtomicWriteIntegrityError(
761
+ f"atomic write anchor is not a real directory: {anchor}"
762
+ )
763
+ for part in absolute.parts[1:]:
764
+ if part in {"", ".", ".."}:
765
+ raise AtomicWriteIntegrityError(f"atomic write parent is not canonical: {absolute}")
766
+ current = current / part
767
+ try:
768
+ info = _portable_lstat(current)
769
+ except FileNotFoundError:
770
+ # Re-check every existing ancestor immediately before the
771
+ # path-based mkdir. Native Windows has no dir_fd equivalent,
772
+ # so this is intentionally fail-closed against static links
773
+ # and best-effort against an active swap race.
774
+ _assert_portable_atomic_parent(current.parent)
775
+ try:
776
+ os.mkdir(current, mode=0o755)
777
+ except FileExistsError:
778
+ # A concurrent benign writer may have won directory
779
+ # creation; the unresolved metadata check below decides
780
+ # whether that winner is safe.
781
+ pass
782
+ info = _portable_lstat(current)
783
+ if _is_link_or_reparse(info) or not stat.S_ISDIR(info.st_mode):
784
+ raise AtomicWriteIntegrityError(
785
+ f"atomic write parent is not a real directory: {current}"
786
+ )
787
+ except AtomicWriteIntegrityError:
788
+ raise
789
+ except (OSError, TypeError, NotImplementedError) as exc:
790
+ raise AtomicWriteIntegrityError(
791
+ f"atomic write parent is unsafe or unavailable: {current}: {exc}"
792
+ ) from exc
793
+ return _assert_portable_atomic_parent(absolute)
794
+
795
+
796
+ def _atomic_write_posix(parent: Path, name: str, data: bytes | Iterable[bytes], path: Path) -> None:
797
+ """Bind the parent directory, then create-fsync-rename-fsync under it.
798
+
799
+ The parent is opened one path component at a time (mirrors ``file_lock``'s
800
+ ``_open_lock_parent``/``_assert_lock_parent_binding``), so a symlink
801
+ anywhere in the ancestor chain -- not just the immediate parent -- is
802
+ rejected; a single multi-component ``os.open(parent, O_NOFOLLOW)`` call
803
+ would only protect the final component. Before the final rename the whole
804
+ chain is re-checked against a fresh, unresolved ``stat``, so a parent-
805
+ directory swap anywhere in that chain between temp-file creation and
806
+ replace is detected and fails closed instead of silently renaming into the
807
+ wrong directory.
808
+ """
809
+ no_follow = getattr(os, "O_NOFOLLOW", None)
810
+ parent_descriptor = _open_atomic_write_parent(parent)
811
+ try:
812
+ parent_stat = os.fstat(parent_descriptor)
813
+ if not stat.S_ISDIR(parent_stat.st_mode):
814
+ raise AtomicWriteIntegrityError(f"atomic write parent is not a directory: {parent}")
815
+ _reject_unsafe_atomic_write_target(parent_descriptor, name, path)
816
+
817
+ tmp_name = _atomic_write_temp_name(name)
818
+ create_flags = (
819
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) | (no_follow or 0)
820
+ )
821
+ fd = os.open(tmp_name, create_flags, 0o600, dir_fd=parent_descriptor)
822
+ try:
823
+ with os.fdopen(fd, "wb") as handle:
824
+ for chunk in (data,) if isinstance(data, bytes) else data:
825
+ handle.write(chunk)
826
+ handle.flush()
827
+ os.fsync(handle.fileno())
828
+
829
+ _assert_atomic_write_parent_binding(parent, parent_descriptor)
830
+ _reject_unsafe_atomic_write_target(parent_descriptor, name, path)
831
+
832
+ if os.rename in os.supports_dir_fd:
833
+ os.rename(
834
+ tmp_name, name, src_dir_fd=parent_descriptor, dst_dir_fd=parent_descriptor
835
+ )
836
+ else: # pragma: no cover - exercised only on platforms without renameat
837
+ os.replace(parent / tmp_name, path)
838
+ except BaseException:
839
+ try:
840
+ if os.unlink in os.supports_dir_fd:
841
+ os.unlink(tmp_name, dir_fd=parent_descriptor)
842
+ else: # pragma: no cover - exercised only without unlinkat
843
+ (parent / tmp_name).unlink()
844
+ except OSError:
845
+ pass
846
+ raise
847
+
848
+ try:
849
+ os.fsync(parent_descriptor)
850
+ except OSError: # pragma: no cover - best-effort durability only
851
+ pass
852
+
853
+ # Post-write binding validation: confirm the name we just published
854
+ # still resolves to a real, regular file (never a symlink/special
855
+ # file) without following it. A concurrent writer may have already
856
+ # published its own complete content over ours by the time this
857
+ # check runs -- that is correct atomic-write interleaving, not
858
+ # corruption, so this only re-checks *shape*, never our own bytes.
859
+ verified = os.stat(name, dir_fd=parent_descriptor, follow_symlinks=False)
860
+ if not stat.S_ISREG(verified.st_mode):
861
+ raise AtomicWriteIntegrityError(
862
+ f"atomic write target failed post-write binding validation: {path}"
863
+ )
864
+ finally:
865
+ os.close(parent_descriptor)
866
+
867
+
868
+ def _atomic_write_fallback(path: Path, data: bytes | Iterable[bytes]) -> None:
869
+ """Atomic write fallback with portable ancestor/reparse-point checks.
870
+
871
+ Native Windows lacks the POSIX ``dir_fd`` publication primitive. It can
872
+ still reject every static ancestor reparse point and compare directory
873
+ identities before and after publication instead of blindly following a
874
+ linked parent.
875
+ """
876
+ path = _absolute_lock_path(path)
877
+ parent_snapshot = _ensure_portable_atomic_parent(path.parent)
878
+ _reject_unsafe_atomic_write_target(None, path.name, path)
879
+ tmp = path.with_name(_atomic_write_temp_name(path.name))
880
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0)
881
+ fd = os.open(tmp, flags, 0o600)
882
+ try:
883
+ with os.fdopen(fd, "wb") as handle:
884
+ for chunk in (data,) if isinstance(data, bytes) else data:
885
+ handle.write(chunk)
886
+ handle.flush()
887
+ try:
888
+ os.fsync(handle.fileno())
889
+ except OSError: # pragma: no cover - best-effort durability only
890
+ pass
891
+ _assert_portable_atomic_parent(path.parent, parent_snapshot)
892
+ _reject_unsafe_atomic_write_target(None, path.name, path)
893
+ os.replace(tmp, path)
894
+ _assert_portable_atomic_parent(path.parent, parent_snapshot)
895
+ _reject_unsafe_atomic_write_target(None, path.name, path)
896
+ except BaseException:
897
+ try:
898
+ tmp.unlink()
899
+ except OSError:
900
+ pass
901
+ raise
902
+
903
+
904
+ def atomic_write_text(path: str | Path, text: str, *, encoding: str = "utf-8") -> Path:
905
+ """Write ``text`` to ``path`` atomically (temp file + fsync + rename + fsync).
906
+
907
+ A reader either sees the old complete file or the new complete file, never a
908
+ half-written one -- so a crashed/concurrent write can't leave a torn cache.
909
+ Hardened beyond plain temp+replace: the temp name carries a random nonce (not
910
+ just the writer's PID, which can collide across a fork boundary or PID
911
+ reuse), the temp file is fsynced before the rename and the parent directory
912
+ is fsynced after it (durability across a crash), a symlink or special file
913
+ already at the target is rejected rather than written through, and a parent-
914
+ directory swap between temp-file creation and the final rename is detected
915
+ and fails closed. None of this changes behavior for a well-formed,
916
+ non-adversarial write to a regular file or absent path.
917
+ """
918
+ p = Path(path)
919
+ absolute = _absolute_lock_path(p)
920
+ data = text.encode(encoding)
921
+ if hasattr(os, "O_DIRECTORY") and hasattr(os, "fdopen"):
922
+ _atomic_write_posix(absolute.parent, absolute.name, data, absolute)
923
+ else: # pragma: no cover - exercised only on platforms without O_DIRECTORY
924
+ _atomic_write_fallback(absolute, data)
925
+ return p
926
+
927
+
928
+ def atomic_write_chunks(path: str | Path, chunks: Iterable[str]) -> Path:
929
+ """Stream a large UTF-8 export through the same bound atomic publication.
930
+
931
+ Iterator failure leaves the previous complete export untouched. The input
932
+ need not be copied into a second object or serialized into one giant string.
933
+ """
934
+ p = Path(path)
935
+ absolute = _absolute_lock_path(p)
936
+ data = (chunk.encode("utf-8") for chunk in chunks)
937
+ if hasattr(os, "O_DIRECTORY") and hasattr(os, "fdopen"):
938
+ _atomic_write_posix(absolute.parent, absolute.name, data, absolute)
939
+ else:
940
+ _atomic_write_fallback(absolute, data)
941
+ return p
942
+
943
+
944
+ def _atomic_create_posix(parent: Path, name: str, data: bytes, path: Path) -> None:
945
+ """Publish one fully durable regular file without replacing an existing name."""
946
+ parent_descriptor = _open_atomic_write_parent(parent)
947
+ tmp_name = _atomic_write_temp_name(name)
948
+ temp_exists = False
949
+ try:
950
+ _reject_unsafe_atomic_write_target(parent_descriptor, name, path)
951
+ flags = (
952
+ os.O_WRONLY
953
+ | os.O_CREAT
954
+ | os.O_EXCL
955
+ | getattr(os, "O_CLOEXEC", 0)
956
+ | getattr(os, "O_NOFOLLOW", 0)
957
+ )
958
+ descriptor = os.open(tmp_name, flags, 0o600, dir_fd=parent_descriptor)
959
+ temp_exists = True
960
+ with os.fdopen(descriptor, "wb") as handle:
961
+ handle.write(data)
962
+ handle.flush()
963
+ os.fsync(handle.fileno())
964
+
965
+ _assert_atomic_write_parent_binding(parent, parent_descriptor)
966
+ # Hard-link publication is atomic and has no-clobber semantics on both
967
+ # POSIX and Windows filesystems. The state commit may therefore refer to
968
+ # either the complete immutable event or no event, never a torn event.
969
+ if os.link in os.supports_dir_fd:
970
+ os.link(
971
+ tmp_name,
972
+ name,
973
+ src_dir_fd=parent_descriptor,
974
+ dst_dir_fd=parent_descriptor,
975
+ )
976
+ else: # pragma: no cover - only platforms without linkat
977
+ os.link(parent / tmp_name, path)
978
+
979
+ if os.unlink in os.supports_dir_fd:
980
+ os.unlink(tmp_name, dir_fd=parent_descriptor)
981
+ else: # pragma: no cover - only platforms without unlinkat
982
+ (parent / tmp_name).unlink()
983
+ temp_exists = False
984
+ try:
985
+ os.fsync(parent_descriptor)
986
+ except OSError: # pragma: no cover - best-effort directory durability
987
+ pass
988
+ published = os.stat(name, dir_fd=parent_descriptor, follow_symlinks=False)
989
+ if not stat.S_ISREG(published.st_mode):
990
+ raise AtomicWriteIntegrityError(
991
+ f"atomic create target failed binding validation: {path}"
992
+ )
993
+ finally:
994
+ if temp_exists:
995
+ try:
996
+ if os.unlink in os.supports_dir_fd:
997
+ os.unlink(tmp_name, dir_fd=parent_descriptor)
998
+ else: # pragma: no cover - only platforms without unlinkat
999
+ (parent / tmp_name).unlink()
1000
+ except OSError:
1001
+ pass
1002
+ os.close(parent_descriptor)
1003
+
1004
+
1005
+ def _atomic_create_fallback(path: Path, data: bytes) -> None:
1006
+ """No-clobber publication with portable ancestor/reparse-point checks."""
1007
+ path = _absolute_lock_path(path)
1008
+ parent_snapshot = _ensure_portable_atomic_parent(path.parent)
1009
+ _reject_unsafe_atomic_write_target(None, path.name, path)
1010
+ tmp = path.with_name(_atomic_write_temp_name(path.name))
1011
+ descriptor = os.open(
1012
+ tmp,
1013
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0),
1014
+ 0o600,
1015
+ )
1016
+ try:
1017
+ with os.fdopen(descriptor, "wb") as handle:
1018
+ handle.write(data)
1019
+ handle.flush()
1020
+ try:
1021
+ os.fsync(handle.fileno())
1022
+ except OSError: # pragma: no cover - best-effort durability
1023
+ pass
1024
+ _assert_portable_atomic_parent(path.parent, parent_snapshot)
1025
+ _reject_unsafe_atomic_write_target(None, path.name, path)
1026
+ os.link(tmp, path)
1027
+ _assert_portable_atomic_parent(path.parent, parent_snapshot)
1028
+ _reject_unsafe_atomic_write_target(None, path.name, path)
1029
+ finally:
1030
+ try:
1031
+ tmp.unlink()
1032
+ except OSError:
1033
+ pass
1034
+
1035
+
1036
+ def atomic_create_text(path: str | Path, text: str, *, encoding: str = "utf-8") -> Path:
1037
+ """Atomically create an immutable text file and fail if it already exists."""
1038
+ target = Path(path)
1039
+ absolute = _absolute_lock_path(target)
1040
+ data = text.encode(encoding)
1041
+ if hasattr(os, "O_DIRECTORY") and hasattr(os, "fdopen"):
1042
+ _atomic_create_posix(
1043
+ absolute.parent,
1044
+ absolute.name,
1045
+ data,
1046
+ absolute,
1047
+ )
1048
+ else: # pragma: no cover - exercised on native Windows CI
1049
+ _atomic_create_fallback(absolute, data)
1050
+ return target
1051
+
1052
+
1053
+ __all__ = [
1054
+ "AtomicWriteIntegrityError",
1055
+ "CorruptArtifactError",
1056
+ "FileLockIntegrityError",
1057
+ "RequiredFileLockUnavailable",
1058
+ "atomic_create_text",
1059
+ "atomic_write_chunks",
1060
+ "atomic_write_text",
1061
+ "file_lock",
1062
+ "reentrant_file_lock",
1063
+ "required_file_lock",
1064
+ "required_reentrant_file_lock",
1065
+ "safe_load_yaml",
1066
+ ]