worktree-controller 1.30.4__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 (61) hide show
  1. worktree_controller/__init__.py +3 -0
  2. worktree_controller/activation_layout.py +483 -0
  3. worktree_controller/branch_retirement.py +1865 -0
  4. worktree_controller/canonical_refresh.py +150 -0
  5. worktree_controller/canonical_transition.py +324 -0
  6. worktree_controller/cleanup_attempts.py +442 -0
  7. worktree_controller/cleanup_evidence.py +75 -0
  8. worktree_controller/cleanup_recovery.py +912 -0
  9. worktree_controller/cli.py +1907 -0
  10. worktree_controller/controller.py +1974 -0
  11. worktree_controller/convergence.py +893 -0
  12. worktree_controller/delivered_closeout.py +928 -0
  13. worktree_controller/disposable.py +677 -0
  14. worktree_controller/environment.py +1038 -0
  15. worktree_controller/frozen_recovery.py +291 -0
  16. worktree_controller/git_backend.py +682 -0
  17. worktree_controller/governance.py +365 -0
  18. worktree_controller/hardlinks.py +294 -0
  19. worktree_controller/integration_cleanup.py +92 -0
  20. worktree_controller/integration_conflict.py +117 -0
  21. worktree_controller/integration_retry.py +526 -0
  22. worktree_controller/keyed_document_scope.py +162 -0
  23. worktree_controller/lifecycle.py +8992 -0
  24. worktree_controller/lifecycle_history.py +358 -0
  25. worktree_controller/lifecycle_observation.py +234 -0
  26. worktree_controller/metadata_codec.py +1694 -0
  27. worktree_controller/metadata_relocation.py +131 -0
  28. worktree_controller/models.py +546 -0
  29. worktree_controller/mount_diagnostics.py +324 -0
  30. worktree_controller/multiple_cleanup_recovery.py +730 -0
  31. worktree_controller/namespace_migration.py +1412 -0
  32. worktree_controller/native_session_recovery.py +171 -0
  33. worktree_controller/owner_actions.py +34 -0
  34. worktree_controller/policy.py +333 -0
  35. worktree_controller/preservation.py +983 -0
  36. worktree_controller/preserved_cleanup.py +849 -0
  37. worktree_controller/publication_execution.py +361 -0
  38. worktree_controller/reconciler.py +430 -0
  39. worktree_controller/recovery_guidance.py +205 -0
  40. worktree_controller/registration_backfill.py +166 -0
  41. worktree_controller/registration_prune.py +391 -0
  42. worktree_controller/rendering.py +179 -0
  43. worktree_controller/resource_admission.py +1736 -0
  44. worktree_controller/retirement_adapters.py +130 -0
  45. worktree_controller/retirement_execution.py +234 -0
  46. worktree_controller/semantics.py +459 -0
  47. worktree_controller/session.py +505 -0
  48. worktree_controller/session_admission.py +265 -0
  49. worktree_controller/source_cleanup.py +515 -0
  50. worktree_controller/source_preservation.py +692 -0
  51. worktree_controller/source_recovery.py +592 -0
  52. worktree_controller/store.py +1228 -0
  53. worktree_controller/tool_cli_contract.json +1791 -0
  54. worktree_controller/transaction_scope.py +876 -0
  55. worktree_controller/writer_lease.py +662 -0
  56. worktree_controller/writer_session.py +887 -0
  57. worktree_controller-1.30.4.dist-info/METADATA +3149 -0
  58. worktree_controller-1.30.4.dist-info/RECORD +61 -0
  59. worktree_controller-1.30.4.dist-info/WHEEL +4 -0
  60. worktree_controller-1.30.4.dist-info/entry_points.txt +5 -0
  61. worktree_controller-1.30.4.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,324 @@
1
+ """Read-only mount and lock accessibility observations for doctor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno
6
+ import os
7
+ import stat
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .models import ControllerError, RepositoryInventory
12
+ from .store import MetadataStore
13
+
14
+ MOUNT_CODES = {
15
+ "repo": "REPOSITORY_MOUNT_READ_ONLY",
16
+ "git_entry": "GIT_ENTRY_MOUNT_READ_ONLY",
17
+ "git_common_dir": "GIT_COMMON_MOUNT_READ_ONLY",
18
+ "lock_mount": "REPOSITORY_LOCK_MOUNT_READ_ONLY",
19
+ "lock_parent_mount": "REPOSITORY_LOCK_PARENT_MOUNT_READ_ONLY",
20
+ }
21
+ ACCESS_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS}
22
+
23
+
24
+ def _mount_state(path: Path) -> str:
25
+ try:
26
+ flags = os.statvfs(path).f_flag
27
+ except OSError:
28
+ return "UNKNOWN"
29
+ return "READ_ONLY" if flags & os.ST_RDONLY else "RW"
30
+
31
+
32
+ def _mount_access_errno(path: Path) -> str | None:
33
+ """Keep a failed statvfs reason without changing the v1 state helper."""
34
+ try:
35
+ os.statvfs(path)
36
+ except OSError as exc:
37
+ return errno.errorcode.get(exc.errno, "UNKNOWN")
38
+ return None
39
+
40
+
41
+ def _lock_probe(path: Path) -> dict[str, str | None]:
42
+ """Inspect the existing lock without creating it or changing its contents."""
43
+ try:
44
+ mode = os.lstat(path).st_mode
45
+ except FileNotFoundError:
46
+ return {"shape": "MISSING", "writable": "UNKNOWN", "access_errno": None}
47
+ except OSError as exc:
48
+ return {
49
+ "shape": "UNKNOWN",
50
+ "writable": "UNKNOWN",
51
+ "access_errno": errno.errorcode.get(exc.errno, "UNKNOWN"),
52
+ }
53
+ if stat.S_ISLNK(mode):
54
+ return {"shape": "SYMLINK", "writable": "UNWRITABLE", "access_errno": None}
55
+ if not stat.S_ISREG(mode):
56
+ return {"shape": "NON_REGULAR", "writable": "UNWRITABLE", "access_errno": None}
57
+ try:
58
+ descriptor = os.open(path, os.O_RDWR | os.O_NOFOLLOW | os.O_CLOEXEC)
59
+ except OSError as exc:
60
+ if exc.errno in {errno.ELOOP, errno.EISDIR}:
61
+ shape = "SYMLINK" if exc.errno == errno.ELOOP else "NON_REGULAR"
62
+ return {"shape": shape, "writable": "UNWRITABLE", "access_errno": None}
63
+ return {
64
+ "shape": "REGULAR",
65
+ "writable": "UNWRITABLE" if exc.errno in ACCESS_ERRNOS else "UNKNOWN",
66
+ "access_errno": errno.errorcode.get(exc.errno, "UNKNOWN"),
67
+ }
68
+ try:
69
+ try:
70
+ observed_mode = os.fstat(descriptor).st_mode
71
+ except OSError as exc:
72
+ return {
73
+ "shape": "UNKNOWN",
74
+ "writable": "UNKNOWN",
75
+ "access_errno": errno.errorcode.get(exc.errno, "UNKNOWN"),
76
+ }
77
+ if not stat.S_ISREG(observed_mode):
78
+ return {"shape": "NON_REGULAR", "writable": "UNWRITABLE", "access_errno": None}
79
+ finally:
80
+ os.close(descriptor)
81
+ return {"shape": "REGULAR", "writable": "WRITABLE", "access_errno": None}
82
+
83
+
84
+ def _lock_state(path: Path) -> str:
85
+ """Preserve the v1 probe helper for callers that only need writability."""
86
+ return str(_lock_probe(path)["writable"])
87
+
88
+
89
+ def _namespace_id(pid: int | str) -> str | None:
90
+ try:
91
+ return os.readlink(f"/proc/{pid}/ns/mnt")
92
+ except OSError:
93
+ return None
94
+
95
+
96
+ def _effective_lock_mount(states: dict[str, Any]) -> str:
97
+ if states["lock_shape"] in {"MISSING", "SYMLINK", "NON_REGULAR"}:
98
+ return str(states["lock_parent_mount"])
99
+ return str(states["lock_mount"])
100
+
101
+
102
+ def classify_environment(diagnostics: dict[str, Any]) -> dict[str, Any]:
103
+ """Classify observable access without attributing a namespace to a sandbox."""
104
+ current = diagnostics["current"]
105
+ namespace = diagnostics["namespace"]
106
+ peer = namespace["peer"]
107
+ observers = [("current", current)] + ([("peer", peer)] if peer is not None else [])
108
+ facts: list[str] = []
109
+
110
+ if namespace["comparison"] == "DIFFERENT" and peer is not None:
111
+ for name in ("repo", "git_entry", "git_common_dir", "lock_mount", "lock_parent_mount"):
112
+ current_state = (
113
+ _effective_lock_mount(current) if name == "lock_mount" else current[name]
114
+ )
115
+ peer_state = _effective_lock_mount(peer) if name == "lock_mount" else peer[name]
116
+ if (
117
+ current_state in {"RW", "READ_ONLY"}
118
+ and peer_state in {"RW", "READ_ONLY"}
119
+ and current_state != peer_state
120
+ ):
121
+ facts.append(f"{name.upper()}_MOUNT_DIFFERS")
122
+ if current_state == "READ_ONLY":
123
+ facts.append(f"CURRENT_{MOUNT_CODES[name]}")
124
+ if peer_state == "READ_ONLY":
125
+ facts.append(f"PEER_{MOUNT_CODES[name]}")
126
+ if facts:
127
+ return {"code": "MOUNT_NAMESPACE_MISMATCH", "facts": facts}
128
+
129
+ for observer, states in observers:
130
+ for name, code in MOUNT_CODES.items():
131
+ if name == "lock_mount" and states["lock_shape"] in {
132
+ "MISSING",
133
+ "SYMLINK",
134
+ "NON_REGULAR",
135
+ }:
136
+ continue
137
+ if states[name] == "READ_ONLY":
138
+ facts.append(f"{observer.upper()}_{code}")
139
+ if observer == "current":
140
+ for name, access_errno in states["mount_access_errno"].items():
141
+ if access_errno in {"EACCES", "EPERM", "EROFS"}:
142
+ facts.append(f"CURRENT_{name.upper()}_MOUNT_ACCESS_{access_errno}")
143
+ if states["lock_access_errno"] in {"EACCES", "EPERM", "EROFS"}:
144
+ facts.append(f"CURRENT_LOCK_ACCESS_{states['lock_access_errno']}")
145
+ if facts:
146
+ return {"code": "FILESYSTEM_PERMISSION", "facts": facts}
147
+
148
+ for observer, states in observers:
149
+ for name in ("repo", "git_entry", "git_common_dir", "lock_parent_mount"):
150
+ if states[name] == "UNKNOWN":
151
+ access_errno = states["mount_access_errno"][name] or "UNKNOWN"
152
+ facts.append(f"{observer.upper()}_{name.upper()}_MOUNT_ACCESS_{access_errno}")
153
+ if _effective_lock_mount(states) == "UNKNOWN":
154
+ access_errno = states["mount_access_errno"]["lock_mount"] or "UNKNOWN"
155
+ facts.append(f"{observer.upper()}_LOCK_MOUNT_ACCESS_{access_errno}")
156
+ if states["lock_shape"] == "UNKNOWN":
157
+ facts.append(f"{observer.upper()}_LOCK_SHAPE_UNKNOWN")
158
+ if states["lock_shape"] == "REGULAR" and states["lock_writable"] == "UNKNOWN":
159
+ facts.append(f"{observer.upper()}_LOCK_ACCESS_UNKNOWN")
160
+ if observer == "peer" and states["lock_access_errno"] is not None:
161
+ facts.append(f"PEER_LOCK_ACCESS_{states['lock_access_errno']}")
162
+ if namespace["pid"] is not None and namespace["comparison"] == "UNKNOWN":
163
+ facts.append("NAMESPACE_COMPARISON_UNKNOWN")
164
+ return {"code": "UNKNOWN" if facts else "HEALTHY", "facts": facts}
165
+
166
+
167
+ def diagnose_mounts(
168
+ repository: RepositoryInventory, *, namespace_pid: int | None = None
169
+ ) -> dict[str, Any]:
170
+ """Observe the caller and, when supplied, a process's mount namespace.
171
+
172
+ Opening an existing lock O_RDWR tests access without creating or modifying it.
173
+ A missing lock uses its parent mount for environment classification; an
174
+ inaccessible mount remains UNKNOWN. Lock shape is reported separately.
175
+ """
176
+ if namespace_pid is not None and namespace_pid <= 0:
177
+ raise ControllerError("INVALID_NAMESPACE_PID", "namespace_pid must be positive")
178
+ store = MetadataStore(repository.common_dir)
179
+ paths = {
180
+ "repo": repository.requested_path,
181
+ "git_entry": repository.requested_path / ".git",
182
+ "git_common_dir": repository.common_dir,
183
+ "lock_path": store.lock_file,
184
+ "lock_parent": store.lock_file.parent,
185
+ }
186
+
187
+ def probe(root: Path | None) -> dict[str, Any]:
188
+ observed = {
189
+ name: root / path.relative_to("/") if root else path for name, path in paths.items()
190
+ }
191
+ lock = _lock_probe(observed["lock_path"])
192
+ mount_paths = {
193
+ "repo": observed["repo"],
194
+ "git_entry": observed["git_entry"],
195
+ "git_common_dir": observed["git_common_dir"],
196
+ "lock_mount": observed["lock_path"],
197
+ "lock_parent_mount": observed["lock_parent"],
198
+ }
199
+ mounts = {name: _mount_state(path) for name, path in mount_paths.items()}
200
+ return {
201
+ **mounts,
202
+ "mount_access_errno": {
203
+ name: _mount_access_errno(path) if mounts[name] == "UNKNOWN" else None
204
+ for name, path in mount_paths.items()
205
+ },
206
+ "lock_writable": lock["writable"],
207
+ "lock_shape": lock["shape"],
208
+ "lock_access_errno": lock["access_errno"],
209
+ }
210
+
211
+ current = probe(None)
212
+ self_ns = _namespace_id("self")
213
+ peer_ns = _namespace_id(namespace_pid) if namespace_pid is not None else None
214
+ comparison = (
215
+ "UNKNOWN"
216
+ if namespace_pid is None or self_ns is None or peer_ns is None
217
+ else "SAME"
218
+ if self_ns == peer_ns
219
+ else "DIFFERENT"
220
+ )
221
+ peer = probe(Path(f"/proc/{namespace_pid}/root")) if peer_ns is not None else None
222
+ diagnostics = {
223
+ "paths": {name: str(path) for name, path in paths.items()},
224
+ "current": current,
225
+ "namespace": {
226
+ "pid": namespace_pid,
227
+ "current_id": self_ns,
228
+ "peer_id": peer_ns,
229
+ "comparison": comparison,
230
+ "peer": peer,
231
+ },
232
+ }
233
+ diagnostics["environment"] = classify_environment(diagnostics)
234
+ return diagnostics
235
+
236
+
237
+ def mount_warnings(diagnostics: dict[str, Any]) -> list[dict[str, str]]:
238
+ warnings: list[dict[str, str]] = []
239
+ observed = [("current", diagnostics["current"])]
240
+ peer = diagnostics["namespace"]["peer"]
241
+ if peer is not None:
242
+ observed.append(("peer", peer))
243
+ for observer, states in observed:
244
+ for name, code in MOUNT_CODES.items():
245
+ if name == "lock_mount" and states["lock_shape"] in {
246
+ "MISSING",
247
+ "SYMLINK",
248
+ "NON_REGULAR",
249
+ }:
250
+ continue
251
+ if states[name] == "READ_ONLY":
252
+ warnings.append(
253
+ {
254
+ "code": code,
255
+ "message": f"{observer} {name} mount is read-only",
256
+ "severity": "blocking",
257
+ }
258
+ )
259
+ elif states[name] == "UNKNOWN":
260
+ access_errno = states["mount_access_errno"][name]
261
+ current_permission_denied = observer == "current" and access_errno in {
262
+ "EACCES",
263
+ "EPERM",
264
+ "EROFS",
265
+ }
266
+ warnings.append(
267
+ {
268
+ "code": "FILESYSTEM_PERMISSION"
269
+ if current_permission_denied
270
+ else "MOUNT_AUTHORITY_UNKNOWN",
271
+ "message": f"{observer} {name} mount could not be checked"
272
+ + (f" ({access_errno})" if access_errno else ""),
273
+ "severity": "blocking" if current_permission_denied else "degraded",
274
+ }
275
+ )
276
+ if states["lock_shape"] in {"SYMLINK", "NON_REGULAR"}:
277
+ warnings.append(
278
+ {
279
+ "code": "REPOSITORY_LOCK_SHAPE_INVALID",
280
+ "message": f"{observer} lock path is not a regular file",
281
+ "severity": "blocking",
282
+ }
283
+ )
284
+ elif states["lock_writable"] == "UNWRITABLE" and observer == "peer":
285
+ warnings.append(
286
+ {
287
+ "code": "LOCK_WRITABILITY_UNKNOWN",
288
+ "message": "peer lock path access could not establish writability",
289
+ "severity": "degraded",
290
+ }
291
+ )
292
+ elif states["lock_writable"] == "UNWRITABLE":
293
+ warnings.append(
294
+ {
295
+ "code": "REPOSITORY_LOCK_UNWRITABLE",
296
+ "message": f"{observer} lock path cannot be opened for writing",
297
+ "severity": "blocking",
298
+ }
299
+ )
300
+ elif states["lock_writable"] == "UNKNOWN" and states["lock_shape"] != "MISSING":
301
+ warnings.append(
302
+ {
303
+ "code": "LOCK_WRITABILITY_UNKNOWN",
304
+ "message": f"{observer} lock path writability could not be checked",
305
+ "severity": "degraded",
306
+ }
307
+ )
308
+ if diagnostics["environment"]["code"] == "MOUNT_NAMESPACE_MISMATCH":
309
+ warnings.append(
310
+ {
311
+ "code": "MOUNT_NAMESPACE_MISMATCH",
312
+ "message": "caller and selected process observe different mount access",
313
+ "severity": "blocking",
314
+ }
315
+ )
316
+ if diagnostics["namespace"]["comparison"] == "DIFFERENT":
317
+ warnings.append(
318
+ {
319
+ "code": "MOUNT_NAMESPACE_DIFFERENT",
320
+ "message": "caller and selected process use different mount namespaces",
321
+ "severity": "info",
322
+ }
323
+ )
324
+ return warnings