syncmill 0.2.0__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 (56) hide show
  1. syncmill/__init__.py +11 -0
  2. syncmill/agents.py +2064 -0
  3. syncmill/artifact_io.py +201 -0
  4. syncmill/artifacts.py +1946 -0
  5. syncmill/board.py +2060 -0
  6. syncmill/breaker.py +327 -0
  7. syncmill/cli.py +4986 -0
  8. syncmill/command_errors.py +32 -0
  9. syncmill/config.py +1118 -0
  10. syncmill/dashboard.py +935 -0
  11. syncmill/gates.py +179 -0
  12. syncmill/gateway.py +234 -0
  13. syncmill/git_env.py +99 -0
  14. syncmill/isolated_session.py +431 -0
  15. syncmill/matcher.py +127 -0
  16. syncmill/memory.py +1067 -0
  17. syncmill/memory_backend.py +1907 -0
  18. syncmill/memory_migration.py +962 -0
  19. syncmill/model_routing.py +482 -0
  20. syncmill/models.py +244 -0
  21. syncmill/monitor.py +775 -0
  22. syncmill/phoenix_exporter.py +339 -0
  23. syncmill/planning.py +512 -0
  24. syncmill/platform_support.py +14 -0
  25. syncmill/preflight.py +1129 -0
  26. syncmill/project.py +1067 -0
  27. syncmill/report.py +502 -0
  28. syncmill/review_candidates.py +107 -0
  29. syncmill/room_compare.py +2380 -0
  30. syncmill/room_raw.py +484 -0
  31. syncmill/room_viewer.py +604 -0
  32. syncmill/rooms.py +4044 -0
  33. syncmill/session.py +444 -0
  34. syncmill/strategies/__init__.py +82 -0
  35. syncmill/strategies/base.py +910 -0
  36. syncmill/strategies/compete.py +233 -0
  37. syncmill/strategies/council.py +429 -0
  38. syncmill/strategies/decompose.py +570 -0
  39. syncmill/strategies/pipeline.py +221 -0
  40. syncmill/strategies/route.py +130 -0
  41. syncmill/supervisor.py +1423 -0
  42. syncmill/teamfile.py +571 -0
  43. syncmill/telemetry_hook.py +231 -0
  44. syncmill/terminal.py +245 -0
  45. syncmill/tracer.py +548 -0
  46. syncmill/tui.py +3537 -0
  47. syncmill/tui_collaboration.py +499 -0
  48. syncmill/tui_jobs.py +1407 -0
  49. syncmill/tui_render.py +156 -0
  50. syncmill/workflow.py +3746 -0
  51. syncmill/worktree.py +1045 -0
  52. syncmill-0.2.0.dist-info/METADATA +853 -0
  53. syncmill-0.2.0.dist-info/RECORD +56 -0
  54. syncmill-0.2.0.dist-info/WHEEL +4 -0
  55. syncmill-0.2.0.dist-info/entry_points.txt +2 -0
  56. syncmill-0.2.0.dist-info/licenses/LICENSE +191 -0
@@ -0,0 +1,201 @@
1
+ """Descriptor-relative artifact I/O beneath a trusted, canonical state root.
2
+
3
+ This prevents path traversal and symlink redirection, not arbitrary tampering by
4
+ another process with the same OS credentials. Directory handles live for one
5
+ operation and are checked against their names before use.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import errno
11
+ import os
12
+ import shutil
13
+ import stat
14
+ from pathlib import Path
15
+ from uuid import uuid4
16
+
17
+
18
+ Signature = tuple[int, int, int, int, int]
19
+
20
+
21
+ def file_signature(info: os.stat_result) -> Signature:
22
+ """Change signature of an artifact file: a rewrite through ``write``
23
+ lands as a new inode (temp + replace), an in-place edit moves size or
24
+ mtime, so an equal tuple means the bytes are the ones last parsed."""
25
+ return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
26
+
27
+
28
+ def validate_id(value: str) -> None:
29
+ if (
30
+ not isinstance(value, str)
31
+ or not value
32
+ or value in {".", ".."}
33
+ or any(c in value for c in ("/", "\\", "\0"))
34
+ ):
35
+ raise ValueError("artifact ID must be a single non-empty path component")
36
+
37
+
38
+ class ArtifactIO:
39
+ def __init__(self, base: Path):
40
+ self.base = base
41
+ self.fds: dict[Path, int] = {}
42
+ self.fds[base] = os.open(base, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
43
+
44
+ def close(self) -> None:
45
+ for fd in reversed(list(self.fds.values())):
46
+ os.close(fd)
47
+ self.fds.clear()
48
+
49
+ def release(self, path: Path) -> None:
50
+ """Bound descriptor use when listing many runs."""
51
+ for opened in reversed(list(self.fds)):
52
+ if opened == path or path in opened.parents:
53
+ os.close(self.fds.pop(opened))
54
+
55
+ def directory(self, path: Path, *, create: bool = False) -> int:
56
+ if path == self.base:
57
+ return self.fds[path]
58
+ # Reject lexical traversal before any OS operation (Path normalizes '.').
59
+ parts = path.relative_to(self.base).parts
60
+ for part in parts:
61
+ validate_id(part)
62
+ parent = self.directory(path.parent, create=create)
63
+ if path in self.fds:
64
+ current = os.stat(path.name, dir_fd=parent, follow_symlinks=False)
65
+ opened = os.fstat(self.fds[path])
66
+ if (current.st_dev, current.st_ino) != (opened.st_dev, opened.st_ino):
67
+ raise OSError(errno.ESTALE, "artifact directory changed", str(path))
68
+ return self.fds[path]
69
+ if create:
70
+ try:
71
+ os.mkdir(path.name, 0o700, dir_fd=parent)
72
+ except FileExistsError:
73
+ pass
74
+ fd = os.open(
75
+ path.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent
76
+ )
77
+ self.fds[path] = fd
78
+ return fd
79
+
80
+ def mkdir(self, path: Path, *, exist_ok: bool = True) -> None:
81
+ parent = self.directory(path.parent)
82
+ try:
83
+ os.mkdir(path.name, 0o700, dir_fd=parent)
84
+ except FileExistsError:
85
+ if not exist_ok:
86
+ raise
87
+ self.directory(path)
88
+
89
+ def open_file(self, path: Path, flags: int = os.O_RDONLY) -> int:
90
+ parent = self.directory(path.parent)
91
+ fd = os.open(
92
+ path.name, flags | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600, dir_fd=parent
93
+ )
94
+ try:
95
+ info = os.fstat(fd)
96
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
97
+ raise OSError(
98
+ errno.EINVAL,
99
+ "artifact file must be a singly linked regular file",
100
+ str(path),
101
+ )
102
+ except BaseException:
103
+ os.close(fd)
104
+ raise
105
+ return fd
106
+
107
+ def read_if_changed(
108
+ self, path: Path, signature: Signature | None, limit: int = -1
109
+ ) -> tuple[Signature, bytes | None]:
110
+ """Open once, fstat, and read the content ONLY if its signature
111
+ differs from ``signature`` (``None`` content means unchanged). Every
112
+ content read in this module goes through here, so one seam observes
113
+ them all (#168)."""
114
+ fd = self.open_file(path)
115
+ try:
116
+ current = file_signature(os.fstat(fd))
117
+ if signature is not None and current == signature:
118
+ os.close(fd)
119
+ return current, None
120
+ stream = os.fdopen(fd, "rb")
121
+ except BaseException:
122
+ os.close(fd)
123
+ raise
124
+ with stream:
125
+ return current, stream.read(limit)
126
+
127
+ def read(self, path: Path, limit: int = -1) -> bytes:
128
+ data = self.read_if_changed(path, None, limit)[1]
129
+ assert data is not None
130
+ return data
131
+
132
+ def exists(self, path: Path) -> bool:
133
+ try:
134
+ fd = self.open_file(path)
135
+ except FileNotFoundError:
136
+ return False
137
+ os.close(fd)
138
+ return True
139
+
140
+ def write(self, path: Path, data: bytes) -> None:
141
+ parent = self.directory(path.parent)
142
+ # Refuse existing links/special files even though rename would replace
143
+ # rather than follow the final component.
144
+ self.exists(path)
145
+ name = f".{path.name}.{uuid4().hex}.tmp"
146
+ fd = os.open(
147
+ name,
148
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
149
+ 0o600,
150
+ dir_fd=parent,
151
+ )
152
+ try:
153
+ try:
154
+ stream = os.fdopen(fd, "wb")
155
+ except BaseException:
156
+ os.close(fd)
157
+ raise
158
+ with stream:
159
+ stream.write(data)
160
+ stream.flush()
161
+ os.fsync(stream.fileno())
162
+ self.directory(path.parent) # detect a renamed/replaced run
163
+ os.replace(name, path.name, src_dir_fd=parent, dst_dir_fd=parent)
164
+ os.fsync(parent)
165
+ finally:
166
+ try:
167
+ os.unlink(name, dir_fd=parent)
168
+ except FileNotFoundError:
169
+ pass
170
+
171
+ def names(self, path: Path) -> list[str]:
172
+ return os.listdir(self.directory(path))
173
+
174
+ def entry_mode(self, path: Path) -> int:
175
+ """``st_mode`` of a directory entry without following a symlink — one
176
+ stat answers both the symlink and the directory question."""
177
+ return os.stat(
178
+ path.name, dir_fd=self.directory(path.parent), follow_symlinks=False
179
+ ).st_mode
180
+
181
+ def is_directory(self, path: Path) -> bool:
182
+ return stat.S_ISDIR(self.entry_mode(path))
183
+
184
+ def is_symlink(self, path: Path) -> bool:
185
+ return stat.S_ISLNK(self.entry_mode(path))
186
+
187
+ def unlink(self, path: Path) -> None:
188
+ parent = self.directory(path.parent)
189
+ try:
190
+ os.unlink(path.name, dir_fd=parent)
191
+ except FileNotFoundError:
192
+ pass
193
+
194
+ def rmtree(self, path: Path) -> None:
195
+ self.directory(path)
196
+ if not shutil.rmtree.avoids_symlink_attacks:
197
+ raise OSError("descriptor-relative directory removal unavailable")
198
+ try:
199
+ shutil.rmtree(path.name, dir_fd=self.directory(path.parent))
200
+ finally:
201
+ self.release(path)