ccx-native 0.0.0-stage → 0.1.0

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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -2
  3. package/bin/ccx +56 -0
  4. package/ccx/__init__.py +163 -0
  5. package/ccx/__main__.py +28 -0
  6. package/ccx/claude/__init__.py +0 -0
  7. package/ccx/claude/analytics.py +466 -0
  8. package/ccx/claude/attach.py +762 -0
  9. package/ccx/claude/context.py +465 -0
  10. package/ccx/claude/delivery.py +806 -0
  11. package/ccx/claude/external.py +253 -0
  12. package/ccx/claude/host/__init__.py +0 -0
  13. package/ccx/claude/host/events.py +297 -0
  14. package/ccx/claude/host/hooks.py +438 -0
  15. package/ccx/claude/host/pty.py +250 -0
  16. package/ccx/claude/host/session.py +5184 -0
  17. package/ccx/claude/host/subagents.py +169 -0
  18. package/ccx/claude/host/successor.py +267 -0
  19. package/ccx/claude/managed_turns.py +240 -0
  20. package/ccx/claude/oauth.py +176 -0
  21. package/ccx/claude/settings.py +339 -0
  22. package/ccx/claude/spawn.py +557 -0
  23. package/ccx/claude/tasks.py +72 -0
  24. package/ccx/claude/telemetry.py +282 -0
  25. package/ccx/claude/transcript.py +2281 -0
  26. package/ccx/claude/usage.py +837 -0
  27. package/ccx/claude/usage_control.py +152 -0
  28. package/ccx/cli/__init__.py +0 -0
  29. package/ccx/cli/codex.py +1009 -0
  30. package/ccx/cli/commands/__init__.py +0 -0
  31. package/ccx/cli/commands/_common.py +407 -0
  32. package/ccx/cli/commands/control.py +161 -0
  33. package/ccx/cli/commands/create.py +669 -0
  34. package/ccx/cli/commands/read.py +1020 -0
  35. package/ccx/cli/commands/turns.py +1081 -0
  36. package/ccx/cli/controller.py +757 -0
  37. package/ccx/cli/main.py +301 -0
  38. package/ccx/cli/parser.py +1324 -0
  39. package/ccx/cli/skills.py +215 -0
  40. package/ccx/cli/statusline.py +160 -0
  41. package/ccx/client.py +1900 -0
  42. package/ccx/codex/__init__.py +0 -0
  43. package/ccx/codex/attach_tty.py +267 -0
  44. package/ccx/codex/client.py +732 -0
  45. package/ccx/codex/completion.py +121 -0
  46. package/ccx/codex/control.py +288 -0
  47. package/ccx/codex/daemon/__init__.py +0 -0
  48. package/ccx/codex/daemon/__main__.py +4 -0
  49. package/ccx/codex/daemon/daemon.py +1556 -0
  50. package/ccx/codex/daemon/models.py +140 -0
  51. package/ccx/codex/daemon/native.py +1590 -0
  52. package/ccx/codex/daemon/release.py +367 -0
  53. package/ccx/codex/daemon/worker.py +2396 -0
  54. package/ccx/codex/delivery.py +758 -0
  55. package/ccx/codex/delivery_recovery.py +232 -0
  56. package/ccx/codex/evidence.py +827 -0
  57. package/ccx/codex/history.py +158 -0
  58. package/ccx/codex/notify_history.py +140 -0
  59. package/ccx/codex/ops.py +666 -0
  60. package/ccx/codex/paths.py +70 -0
  61. package/ccx/codex/protocol.py +805 -0
  62. package/ccx/codex/provenance.py +105 -0
  63. package/ccx/codex/read.py +29 -0
  64. package/ccx/codex/records.py +51 -0
  65. package/ccx/codex/reports.py +177 -0
  66. package/ccx/codex/runtime.py +1412 -0
  67. package/ccx/codex/settle.py +363 -0
  68. package/ccx/codex/telemetry.py +792 -0
  69. package/ccx/codex/types.py +659 -0
  70. package/ccx/codex/worker_environment.py +25 -0
  71. package/ccx/constants.py +288 -0
  72. package/ccx/controllers/__init__.py +0 -0
  73. package/ccx/controllers/controller.py +1449 -0
  74. package/ccx/controllers/live_vars.py +1706 -0
  75. package/ccx/controllers/live_vars_runner.py +216 -0
  76. package/ccx/controllers/notify.py +213 -0
  77. package/ccx/controllers/orchestrator.py +193 -0
  78. package/ccx/controllers/policy.py +57 -0
  79. package/ccx/controllers/store.py +681 -0
  80. package/ccx/errors.py +634 -0
  81. package/ccx/notify/__init__.py +0 -0
  82. package/ccx/notify/__main__.py +5 -0
  83. package/ccx/notify/client.py +116 -0
  84. package/ccx/notify/daemon.py +480 -0
  85. package/ccx/notify/deliver.py +304 -0
  86. package/ccx/notify/paths.py +51 -0
  87. package/ccx/notify/registry.py +649 -0
  88. package/ccx/notify/select.py +750 -0
  89. package/ccx/notify/settle.py +684 -0
  90. package/ccx/ops/__init__.py +0 -0
  91. package/ccx/ops/background.py +115 -0
  92. package/ccx/ops/cancel.py +289 -0
  93. package/ccx/ops/codex_import.py +96 -0
  94. package/ccx/ops/common.py +397 -0
  95. package/ccx/ops/compact.py +337 -0
  96. package/ccx/ops/compact_ops.py +550 -0
  97. package/ccx/ops/context_events.py +48 -0
  98. package/ccx/ops/conversation.py +1540 -0
  99. package/ccx/ops/conversation_paging.py +1338 -0
  100. package/ccx/ops/create.py +56 -0
  101. package/ccx/ops/creation.py +952 -0
  102. package/ccx/ops/creation_steps.py +579 -0
  103. package/ccx/ops/event_feed.py +710 -0
  104. package/ccx/ops/forking.py +711 -0
  105. package/ccx/ops/input.py +556 -0
  106. package/ccx/ops/maintenance.py +295 -0
  107. package/ccx/ops/report.py +770 -0
  108. package/ccx/ops/semantics.py +490 -0
  109. package/ccx/ops/send.py +961 -0
  110. package/ccx/ops/steer_ops.py +867 -0
  111. package/ccx/ops/terminate.py +383 -0
  112. package/ccx/ops/turn_reports.py +306 -0
  113. package/ccx/ops/wait.py +762 -0
  114. package/ccx/ops/worker_events.py +636 -0
  115. package/ccx/rpc/__init__.py +0 -0
  116. package/ccx/rpc/local_rpc.py +1602 -0
  117. package/ccx/rpc/rpc.py +559 -0
  118. package/ccx/rpc/serve.py +374 -0
  119. package/ccx/rpc/transport.py +409 -0
  120. package/ccx/rpc/view.py +125 -0
  121. package/ccx/store/__init__.py +0 -0
  122. package/ccx/store/discovery.py +170 -0
  123. package/ccx/store/identity.py +105 -0
  124. package/ccx/store/journal_io.py +79 -0
  125. package/ccx/store/liveness.py +194 -0
  126. package/ccx/store/paths.py +254 -0
  127. package/ccx/store/record_cache.py +40 -0
  128. package/ccx/store/records.py +309 -0
  129. package/ccx/store/root.py +804 -0
  130. package/ccx/store/runtime_presence.py +137 -0
  131. package/ccx/store/store.py +2332 -0
  132. package/ccx/store/worker_state.py +86 -0
  133. package/ccx/system/__init__.py +0 -0
  134. package/ccx/system/bounded.py +296 -0
  135. package/ccx/system/cwd_paths.py +25 -0
  136. package/ccx/system/engine_binary.py +59 -0
  137. package/ccx/system/executable.py +50 -0
  138. package/ccx/system/oslayer.py +667 -0
  139. package/ccx/system/pipes.py +46 -0
  140. package/ccx/system/sanitize.py +43 -0
  141. package/ccx/types.py +705 -0
  142. package/ccx/version.py +29 -0
  143. package/ccx/workers.py +2081 -0
  144. package/ccx.py +9 -0
  145. package/package.json +40 -4
  146. package/pyproject.toml +87 -0
  147. package/skills/ccx/SKILL.md +155 -0
  148. package/skills/ccx/references/controllers.md +244 -0
  149. package/skills/ccx/references/troubleshooting.md +94 -0
  150. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,667 @@
1
+ """Internal Linux and Darwin process, filesystem and socket observations.
2
+
3
+ The boundary retains kernel /proc and kill(0) facts; callers make lifecycle,
4
+ identity and ownership decisions instead of treating PID presence as proof.
5
+ Only this module dispatches by OS. A Darwin fact that cannot be verified
6
+ remains unprovable; it never falls back to a Linux PID or pathname.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from contextlib import contextmanager
12
+ import ctypes
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone
15
+ import errno
16
+ import fcntl
17
+ from functools import lru_cache
18
+ import os
19
+ from pathlib import Path
20
+ import re
21
+ import select
22
+ import signal
23
+ import socket
24
+ import stat
25
+ import struct
26
+ import sys
27
+ import tempfile
28
+ import threading
29
+ import time
30
+ from typing import Iterator, Literal
31
+
32
+
33
+ Presence = Literal["alive", "dead", "unprovable"]
34
+ PidProbe = Literal["exists", "missing", "denied"]
35
+
36
+
37
+ class UnsupportedPlatformError(RuntimeError):
38
+ """No verified implementation of this OS fact is available."""
39
+
40
+
41
+ class PeerIdentityUnavailable(OSError):
42
+ """The kernel did not provide usable credentials on this connection."""
43
+
44
+
45
+ class RootOpenError(OSError):
46
+ """An explicit root ancestor or leaf could not be opened safely."""
47
+
48
+
49
+ if sys.platform not in ("linux", "darwin"):
50
+ raise UnsupportedPlatformError(f"unsupported operating system: {sys.platform}")
51
+ UNIX_SUN_PATH_BYTES = 108 if sys.platform == "linux" else 104
52
+
53
+
54
+ def claude_project_cwd_is_nfc() -> bool:
55
+ """Claude normalizes the native project cwd on Darwin, not on Linux."""
56
+ return sys.platform == "darwin"
57
+
58
+
59
+ def direct_oauth_usage_supported() -> bool:
60
+ """Direct credential-file usage is available only on Linux for now.
61
+
62
+ Claude Code's own promptless get_usage path supports macOS separately.
63
+ """
64
+ return sys.platform == "linux"
65
+
66
+
67
+ def stdout_reader_gone(fd: int) -> bool:
68
+ """Prove that a pipe's reader has closed, without masking other EPIPEs.
69
+
70
+ Linux poll reports POLLERR on the abandoned write end. macOS has no
71
+ select.poll; EVFILT_WRITE reports EV_EOF for the same abandoned pipe.
72
+ A missing/inconclusive kernel event must not hide an unrelated socket
73
+ error or alter the CLI's exit status.
74
+ """
75
+ try:
76
+ if sys.platform == "linux":
77
+ poller = select.poll()
78
+ poller.register(fd, select.POLLOUT)
79
+ return any(event & select.POLLERR for _, event in poller.poll(0))
80
+ queue = select.kqueue()
81
+ try:
82
+ change = select.kevent(
83
+ fd, filter=select.KQ_FILTER_WRITE,
84
+ flags=select.KQ_EV_ADD | select.KQ_EV_ENABLE,
85
+ )
86
+ return any(
87
+ event.flags & select.KQ_EV_EOF
88
+ for event in queue.control([change], 1, 0)
89
+ )
90
+ finally:
91
+ queue.close()
92
+ except (AttributeError, OSError, ValueError):
93
+ return False
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class ProcessInfo:
98
+ pid: int | None
99
+ presence: Presence
100
+ ppid: int | None
101
+ state: str | None
102
+ start_token: str | None
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class PeerCredentials:
107
+ uid: int
108
+ pid: int
109
+ gid: int | None
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class RuntimeChoice:
114
+ """Darwin's read-only base selection; only callers may create a safe leaf."""
115
+
116
+ path: Path
117
+ may_create: bool = False
118
+
119
+
120
+ @contextmanager
121
+ def open_root_dir(path: Path) -> Iterator[tuple[Path, int]]:
122
+ """Pin each root ancestor without following links, even if execute-only."""
123
+ flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
124
+ path_flags = os.O_PATH | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
125
+ absolute = Path(os.path.abspath(os.fspath(path)))
126
+ parts = absolute.parts[1:]
127
+
128
+ def pin(name: str, mode: int, reason: str, **kwargs) -> int:
129
+ try:
130
+ return os.open(name, mode, **kwargs)
131
+ except OSError as exc:
132
+ raise RootOpenError(reason) from exc
133
+
134
+ fd = pin(absolute.anchor, path_flags if parts else flags, "unsafe explicit root anchor")
135
+ try:
136
+ for index, part in enumerate(parts):
137
+ next_fd = pin(part, flags if index == len(parts) - 1 else path_flags,
138
+ "unsafe explicit root component (symlink or non-directory)", dir_fd=fd)
139
+ previous, fd = fd, next_fd
140
+ os.close(previous)
141
+ yield absolute, fd
142
+ finally:
143
+ os.close(fd)
144
+
145
+
146
+ def peer_credentials(conn: socket.socket) -> PeerCredentials:
147
+ option = getattr(socket, "SO_PEERCRED", None)
148
+ if option is None:
149
+ raise PeerIdentityUnavailable("Linux SO_PEERCRED unavailable")
150
+ try:
151
+ raw = conn.getsockopt(socket.SOL_SOCKET, option, struct.calcsize("3i"))
152
+ pid, uid, gid = struct.unpack("3i", raw)
153
+ except (OSError, struct.error) as exc:
154
+ raise PeerIdentityUnavailable(str(exc)) from exc
155
+ return PeerCredentials(uid=uid, pid=pid, gid=gid)
156
+
157
+
158
+ def probe_pid(pid: object) -> PidProbe:
159
+ try:
160
+ os.kill(pid, 0)
161
+ return "exists"
162
+ except (ProcessLookupError, OverflowError, ValueError, TypeError):
163
+ return "missing"
164
+ except PermissionError:
165
+ return "denied"
166
+
167
+
168
+ def _linux_parse_stat(pid: int, raw: bytes) -> ProcessInfo:
169
+ try:
170
+ # comm may contain spaces, ')' and non-UTF-8 bytes. Split after the
171
+ # final ')' before interpreting the ASCII fields (including start #22).
172
+ tail = raw.rsplit(b")", 1)[1].split()
173
+ state = tail[0].decode("ascii")
174
+ except (ValueError, IndexError, TypeError):
175
+ return ProcessInfo(pid, "unprovable", None, None, None)
176
+ def optional(index: int, convert):
177
+ try:
178
+ return convert(tail[index])
179
+ except (ValueError, IndexError, TypeError):
180
+ return None
181
+
182
+ ppid = optional(1, int) # field 4
183
+ start = optional(19, lambda text: text.decode("ascii")) # field 22 clock ticks
184
+ dead = state in ("Z", "X")
185
+ presence = (
186
+ "dead" if dead else
187
+ "alive" if start is not None else "unprovable"
188
+ )
189
+ return ProcessInfo(pid, presence, ppid, state, None if dead else start)
190
+
191
+
192
+ def process_info(pid: int | None) -> ProcessInfo:
193
+ if not pid:
194
+ return ProcessInfo(pid, "dead", None, None, None)
195
+ try:
196
+ raw = Path(f"/proc/{int(pid)}/stat").read_bytes()
197
+ except (OSError, ValueError, TypeError) as exc:
198
+ presence = "dead" if isinstance(exc, FileNotFoundError) else "unprovable"
199
+ return ProcessInfo(pid, presence, None, None, None)
200
+ return _linux_parse_stat(pid, raw)
201
+
202
+
203
+ def read_argv(pid: int | None) -> bytes | None:
204
+ if not pid:
205
+ return None
206
+ try:
207
+ return Path(f"/proc/{pid}/cmdline").read_bytes()
208
+ except OSError:
209
+ return None
210
+
211
+
212
+ PROC_LOCKS = "/proc/locks"
213
+
214
+
215
+ def _linux_held_flock_inodes() -> frozenset[tuple[int, int, int]] | None:
216
+ """Every (major, minor, inode) with a granted flock, read without locking.
217
+
218
+ None when the kernel table is unreadable. Never takes a lock itself: a
219
+ probe lock could make a concurrent non-blocking lease acquisition fail.
220
+ """
221
+ try:
222
+ with open(PROC_LOCKS, encoding="ascii", errors="replace") as stream:
223
+ lines = stream.read().splitlines()
224
+ except OSError:
225
+ return None
226
+ held = set()
227
+ for line in lines:
228
+ fields = line.split()
229
+ # A granted lock: "<n>: FLOCK ADVISORY WRITE <pid> MAJ:MIN:INO 0 EOF".
230
+ # A blocked waiter's line has "->" after the index and is skipped.
231
+ if len(fields) < 6 or fields[1] != "FLOCK":
232
+ continue
233
+ parts = fields[5].split(":")
234
+ if len(parts) != 3:
235
+ continue
236
+ try:
237
+ held.add((int(parts[0], 16), int(parts[1], 16), int(parts[2])))
238
+ except ValueError:
239
+ continue
240
+ return frozenset(held)
241
+
242
+
243
+ held_flock_inodes = _linux_held_flock_inodes
244
+
245
+
246
+ def _darwin_held_flock_inodes() -> None:
247
+ """Darwin has no lock-free, dependency-free view of other processes'
248
+ flocks (``lsof`` is an external spawn; ``F_GETLK`` sees POSIX locks only;
249
+ a probe ``flock`` would contend). Lease presence is unobservable here."""
250
+ return None
251
+
252
+
253
+ def fits_unix_path(path: Path | str) -> bool:
254
+ return len(os.fsencode(path)) + 1 <= UNIX_SUN_PATH_BYTES
255
+
256
+
257
+ def pinned_fd_path(fd: int) -> str:
258
+ """Reopen a still-held Linux file descriptor, never its mutable pathname."""
259
+ return f"/proc/self/fd/{fd}"
260
+
261
+
262
+ def arm_parent_death(parent_pid: int) -> None:
263
+ """Fail closed when a read-only child cannot arm Linux parent death."""
264
+ try:
265
+ libc = ctypes.CDLL(None, use_errno=True)
266
+ result = libc.prctl(1, signal.SIGKILL, 0, 0, 0)
267
+ except (OSError, AttributeError, TypeError, ctypes.ArgumentError):
268
+ os._exit(0)
269
+ if result != 0 or os.getppid() != parent_pid:
270
+ os._exit(0)
271
+
272
+
273
+ def runtime_base(
274
+ *, purpose: Literal["transport", "live_vars"], suffix_bytes: int = 0,
275
+ ) -> RuntimeChoice | None:
276
+ """Choose an existing private XDG base without writing to the filesystem.
277
+
278
+ An inherited XDG_RUNTIME_DIR may belong to a different user (for example
279
+ when using su without -); transport refuses it, while live-vars can use
280
+ the per-user directory under the system temporary base.
281
+ """
282
+ private = None
283
+ text = os.environ.get("XDG_RUNTIME_DIR")
284
+ if text and os.path.isabs(text):
285
+ try:
286
+ st = os.lstat(text)
287
+ except OSError:
288
+ pass
289
+ else:
290
+ if (stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid()
291
+ and not st.st_mode & 0o077):
292
+ private = Path(text)
293
+ if purpose == "transport":
294
+ if (private is not None
295
+ and len(os.fsencode(private)) + suffix_bytes <= UNIX_SUN_PATH_BYTES):
296
+ return RuntimeChoice(private)
297
+ return _short_private_runtime_base(Path("/tmp"), suffix_bytes)
298
+ if purpose == "live_vars":
299
+ return RuntimeChoice(private or Path(tempfile.gettempdir()))
300
+ raise ValueError(f"unsupported runtime purpose: {purpose}")
301
+
302
+
303
+ def _short_private_runtime_base(parent: Path, suffix_bytes: int) -> RuntimeChoice:
304
+ """Guard the OS's short sticky tmp ancestor and owner-only UID leaf.
305
+
306
+ Selection never creates anything. Transport/live-vars create a missing leaf
307
+ later, under an ACTIVE generation and a no-follow parent descriptor.
308
+ """
309
+ st = os.lstat(parent)
310
+ if (not stat.S_ISDIR(st.st_mode) or st.st_uid != 0
311
+ or not st.st_mode & stat.S_ISVTX or not st.st_mode & 0o002):
312
+ raise OSError(f"short runtime ancestor must be root-owned sticky {parent}")
313
+ base = parent / f"ccx-u{os.getuid()}"
314
+ if len(os.fsencode(base)) + suffix_bytes > UNIX_SUN_PATH_BYTES:
315
+ raise OSError("private runtime pathname exceeds AF_UNIX budget")
316
+ try:
317
+ existing = os.lstat(base)
318
+ except FileNotFoundError:
319
+ return RuntimeChoice(base, may_create=True)
320
+ if (not stat.S_ISDIR(existing.st_mode) or existing.st_uid != os.getuid()
321
+ or existing.st_mode & 0o077):
322
+ raise OSError("short runtime base is not private and user-owned")
323
+ return RuntimeChoice(base)
324
+
325
+
326
+ _ENGLISH_WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
327
+ _ENGLISH_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
328
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
329
+ _DARWIN_LSTART = re.compile(
330
+ r"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) "
331
+ r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) "
332
+ r"\s?([0-3]?\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$",
333
+ re.ASCII,
334
+ )
335
+
336
+
337
+ def provider_start_matches(
338
+ provider_start: object, kernel_start: str | None,
339
+ provider_domain: object = None,
340
+ ) -> bool:
341
+ """Does Claude's provider start denote this kernel-observed process start?
342
+
343
+ Linux provider metadata is the exact decimal /proc field-22 token. Darwin
344
+ records a C-locale, UTC one-second lstart timestamp; its kernel token
345
+ retains the finer microseconds, which must not be compared as a string.
346
+ Parse English names explicitly, independent of the host's LC_TIME locale.
347
+ """
348
+ if not isinstance(kernel_start, str):
349
+ return False
350
+ # Claude's Linux pidDomain includes machine and namespace details, e.g.
351
+ # linux:<machine-id>:pid:[<namespace-inode>]. Only its OS prefix is
352
+ # relevant here: kernel PID/start ownership is proved independently.
353
+ if (provider_domain is not None
354
+ and (not isinstance(provider_domain, str)
355
+ or provider_domain.split(":", 1)[0] != sys.platform)):
356
+ return False
357
+ if sys.platform == "linux":
358
+ return (type(provider_start) in (str, int)
359
+ and str(provider_start).isascii()
360
+ and str(provider_start).isdecimal()
361
+ and str(provider_start) == kernel_start)
362
+ if sys.platform != "darwin" or not isinstance(provider_start, str):
363
+ return False
364
+ match = _DARWIN_LSTART.fullmatch(provider_start)
365
+ token = re.fullmatch(r"darwin:(\d+):(\d{6})", kernel_start, re.ASCII)
366
+ if match is None or token is None:
367
+ return False
368
+ try:
369
+ weekday, month, day, hour, minute, second, year = match.groups()
370
+ parsed = datetime(int(year), _ENGLISH_MONTHS.index(month) + 1,
371
+ int(day), int(hour), int(minute), int(second),
372
+ tzinfo=timezone.utc)
373
+ return (parsed.weekday() == _ENGLISH_WEEKDAYS.index(weekday)
374
+ and int(parsed.timestamp()) == int(token[1])
375
+ and int(token[2]) < 1_000_000)
376
+ except (ValueError, OverflowError):
377
+ return False
378
+
379
+
380
+ def valid_kernel_start_token(token: object) -> bool:
381
+ """Recognize only this OS's original process-start identity encoding."""
382
+ if not isinstance(token, str) or not token:
383
+ return False
384
+ if sys.platform == "linux":
385
+ return token.isascii() and token.isdecimal()
386
+ if sys.platform == "darwin":
387
+ match = re.fullmatch(r"darwin:(\d+):(\d{6})", token, re.ASCII)
388
+ return bool(match and int(match[1]) > 0 and int(match[2]) < 1_000_000)
389
+ return False
390
+
391
+
392
+ # Darwin struct proc_bsdinfo from sys/proc_info.h (MAXCOMLEN=16). These
393
+ # layouts and the imported constants must be checked against the installed
394
+ # Monterey SDK before treating a real Mac run as release validation.
395
+ _DARWIN_PROC_PIDTBSDINFO = 3
396
+ _DARWIN_SZOMB = 5
397
+ _DARWIN_SOL_LOCAL = 0
398
+ _DARWIN_LOCAL_PEERCRED = 1
399
+ _DARWIN_LOCAL_PEERPID = 2
400
+ _DARWIN_XUCRED_VERSION = 0
401
+
402
+
403
+ class _DarwinBSDInfo(ctypes.Structure):
404
+ _fields_ = [
405
+ ("pbi_flags", ctypes.c_uint32), ("pbi_status", ctypes.c_uint32),
406
+ ("pbi_xstatus", ctypes.c_uint32), ("pbi_pid", ctypes.c_uint32),
407
+ ("pbi_ppid", ctypes.c_uint32), ("pbi_uid", ctypes.c_uint32),
408
+ ("pbi_gid", ctypes.c_uint32), ("pbi_ruid", ctypes.c_uint32),
409
+ ("pbi_rgid", ctypes.c_uint32), ("pbi_svuid", ctypes.c_uint32),
410
+ ("pbi_svgid", ctypes.c_uint32), ("rfu_1", ctypes.c_uint32),
411
+ ("pbi_comm", ctypes.c_char * 16), ("pbi_name", ctypes.c_char * 32),
412
+ ("pbi_nfiles", ctypes.c_uint32), ("pbi_pgid", ctypes.c_uint32),
413
+ ("pbi_pjobc", ctypes.c_uint32), ("e_tdev", ctypes.c_uint32),
414
+ ("e_tpgid", ctypes.c_uint32), ("pbi_nice", ctypes.c_int32),
415
+ ("pbi_start_tvsec", ctypes.c_uint64),
416
+ ("pbi_start_tvusec", ctypes.c_uint64),
417
+ ]
418
+
419
+
420
+ class _DarwinXucred(ctypes.Structure):
421
+ _fields_ = [
422
+ ("cr_version", ctypes.c_uint32), ("cr_uid", ctypes.c_uint32),
423
+ ("cr_ngroups", ctypes.c_int16),
424
+ ("cr_groups", ctypes.c_uint32 * 16),
425
+ ]
426
+
427
+
428
+ @lru_cache(maxsize=1)
429
+ def _darwin_proc_pidinfo():
430
+ """Load libproc and configure its ABI once, lazily on first Darwin use."""
431
+ libproc = ctypes.CDLL("/usr/lib/libproc.dylib", use_errno=True)
432
+ call = libproc.proc_pidinfo
433
+ call.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_uint64,
434
+ ctypes.c_void_p, ctypes.c_int]
435
+ call.restype = ctypes.c_int
436
+ return call
437
+
438
+
439
+ def _darwin_pidinfo_call(pid: int, info: _DarwinBSDInfo) -> tuple[int, int]:
440
+ """One SDK call; injectable on Linux for exact ABI/error tests."""
441
+ call = _darwin_proc_pidinfo()
442
+ ctypes.set_errno(0)
443
+ result = call(pid, _DARWIN_PROC_PIDTBSDINFO, 0,
444
+ ctypes.byref(info), ctypes.sizeof(info))
445
+ return result, ctypes.get_errno()
446
+
447
+
448
+ def _darwin_process_info(pid: int | None) -> ProcessInfo:
449
+ if not pid:
450
+ return ProcessInfo(pid, "dead", None, None, None)
451
+ if type(pid) is not int or pid <= 0:
452
+ return ProcessInfo(pid, "unprovable", None, None, None)
453
+ info = _DarwinBSDInfo()
454
+ try:
455
+ size, error = _darwin_pidinfo_call(pid, info)
456
+ except (OSError, AttributeError, TypeError, ValueError):
457
+ return ProcessInfo(pid, "unprovable", None, None, None)
458
+ if size != ctypes.sizeof(info):
459
+ # ESRCH establishes a missing process. A short result, EPERM,
460
+ # race or ambiguous zero must not establish absence/ownership.
461
+ return ProcessInfo(pid, "dead" if size <= 0 and error == errno.ESRCH
462
+ else "unprovable", None, None, None)
463
+ if (info.pbi_pid != pid or info.pbi_status not in (1, 2, 3, 4, 5)
464
+ or info.pbi_uid == 0xffffffff):
465
+ return ProcessInfo(pid, "unprovable", None, None, None)
466
+ state = {1: "I", 2: "R", 3: "S", 4: "T", 5: "Z"}[info.pbi_status]
467
+ if info.pbi_status == _DARWIN_SZOMB:
468
+ return ProcessInfo(pid, "dead", info.pbi_ppid, state, None)
469
+ sec, usec = info.pbi_start_tvsec, info.pbi_start_tvusec
470
+ if sec <= 0 or usec >= 1_000_000:
471
+ return ProcessInfo(pid, "unprovable", info.pbi_ppid, state, None)
472
+ return ProcessInfo(pid, "alive", info.pbi_ppid, state,
473
+ f"darwin:{sec}:{usec:06d}")
474
+
475
+
476
+ def _darwin_peer_credentials(conn: socket.socket) -> PeerCredentials:
477
+ """Both credentials are from this same accepted AF_UNIX connection."""
478
+ try:
479
+ credentials = conn.getsockopt(
480
+ _DARWIN_SOL_LOCAL, _DARWIN_LOCAL_PEERCRED, ctypes.sizeof(_DarwinXucred))
481
+ peer_pid = conn.getsockopt(
482
+ _DARWIN_SOL_LOCAL, _DARWIN_LOCAL_PEERPID, ctypes.sizeof(ctypes.c_int32))
483
+ if (not isinstance(credentials, bytes) or
484
+ len(credentials) != ctypes.sizeof(_DarwinXucred)
485
+ or not isinstance(peer_pid, bytes)
486
+ or len(peer_pid) != ctypes.sizeof(ctypes.c_int32)):
487
+ raise ValueError("incomplete Darwin kernel peer credentials")
488
+ identity = _DarwinXucred.from_buffer_copy(credentials)
489
+ pid = ctypes.c_int32.from_buffer_copy(peer_pid).value
490
+ if (identity.cr_version != _DARWIN_XUCRED_VERSION or
491
+ identity.cr_uid == 0xffffffff or
492
+ not 0 <= identity.cr_ngroups <= 16 or pid <= 0):
493
+ raise ValueError("invalid Darwin xucred version, UID, group count or peer PID")
494
+ except (OSError, ValueError, TypeError) as exc:
495
+ raise PeerIdentityUnavailable(f"Darwin socket peer identity unavailable: {exc}") from exc
496
+ gid = identity.cr_groups[0] if identity.cr_ngroups else None
497
+ return PeerCredentials(uid=identity.cr_uid, pid=pid, gid=gid)
498
+
499
+
500
+ @contextmanager
501
+ def _darwin_open_root_dir(path: Path) -> Iterator[tuple[Path, int]]:
502
+ """Pin no-follow ancestors and leaf, including search-only ancestors.
503
+
504
+ macOS 27 offers O_SEARCH for traverse-only directories. Older Darwin
505
+ kernels may reject it; retain their established O_RDONLY behavior.
506
+ """
507
+ flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
508
+ search = getattr(os, "O_SEARCH", 0)
509
+ absolute = Path(os.path.abspath(os.fspath(path)))
510
+ fd: int | None = None
511
+
512
+ def pinned_open(component: str, *, parent: int | None, leaf: bool) -> int:
513
+ options = {} if parent is None else {"dir_fd": parent}
514
+ if not leaf and search:
515
+ try:
516
+ return os.open(component, flags | search, **options)
517
+ except OSError as exc:
518
+ if exc.errno not in (errno.EINVAL, errno.ENOTSUP, errno.EOPNOTSUPP,
519
+ errno.ENOSYS):
520
+ raise
521
+ return os.open(component, flags, **options)
522
+
523
+ try:
524
+ parts = absolute.parts[1:]
525
+ fd = pinned_open(absolute.anchor, parent=None, leaf=not parts)
526
+ for index, part in enumerate(parts):
527
+ next_fd = pinned_open(part, parent=fd, leaf=index == len(parts) - 1)
528
+ os.close(fd)
529
+ fd = next_fd
530
+ except OSError as exc:
531
+ if fd is not None:
532
+ os.close(fd)
533
+ raise RootOpenError("unsafe/unreadable Darwin root ancestor or leaf") from exc
534
+ try:
535
+ # Exceptions raised by the context body retain their original type.
536
+ yield absolute, fd
537
+ finally:
538
+ os.close(fd)
539
+
540
+
541
+ def _darwin_runtime_base(
542
+ *, purpose: Literal["transport", "live_vars"], suffix_bytes: int = 0,
543
+ ) -> RuntimeChoice:
544
+ """Select a short private base without any filesystem writes."""
545
+ if purpose not in ("transport", "live_vars") or suffix_bytes < 0:
546
+ raise ValueError(f"unsupported Darwin runtime selection: {purpose}")
547
+ xdg = os.environ.get("XDG_RUNTIME_DIR")
548
+ if xdg and os.path.isabs(xdg):
549
+ try:
550
+ st = os.lstat(xdg)
551
+ except OSError:
552
+ pass
553
+ else:
554
+ if (stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid()
555
+ and not st.st_mode & 0o077
556
+ and len(os.fsencode(xdg)) + suffix_bytes <= 104):
557
+ return RuntimeChoice(Path(xdg))
558
+ # No reliance on macOS's often very long /var/folders/... TMPDIR.
559
+ return _short_private_runtime_base(Path("/private/tmp"), suffix_bytes)
560
+
561
+
562
+ def _darwin_pinned_fd_path(fd: int) -> str:
563
+ """Probe /dev/fd reopen on this exact held inode, never its pathname."""
564
+ original = os.fstat(fd)
565
+ if not stat.S_ISREG(original.st_mode):
566
+ raise OSError("cannot reopen nonregular Darwin source")
567
+ old_offset = os.lseek(fd, 0, os.SEEK_CUR)
568
+ path = f"/dev/fd/{fd}"
569
+ reopened = os.open(path, os.O_RDONLY | os.O_CLOEXEC)
570
+ try:
571
+ observed = os.fstat(reopened)
572
+ if ((observed.st_dev, observed.st_ino, observed.st_mode) !=
573
+ (original.st_dev, original.st_ino, original.st_mode)
574
+ or observed.st_size != original.st_size):
575
+ raise OSError("Darwin /dev/fd did not reopen the original file")
576
+ saved = os.lseek(reopened, 0, os.SEEK_CUR)
577
+ os.lseek(reopened, 0, os.SEEK_SET)
578
+ if os.lseek(fd, 0, os.SEEK_CUR) != old_offset:
579
+ raise OSError("Darwin /dev/fd reopen shares mutable source offset")
580
+ os.lseek(reopened, saved, os.SEEK_SET)
581
+ if os.lseek(fd, 0, os.SEEK_CUR) != old_offset:
582
+ raise OSError("Darwin /dev/fd verification changed source offset")
583
+ finally:
584
+ os.close(reopened)
585
+ if os.lseek(fd, 0, os.SEEK_CUR) != old_offset:
586
+ os.lseek(fd, old_offset, os.SEEK_SET)
587
+ return path
588
+
589
+
590
+ def _darwin_read_argv(_pid: int | None) -> None:
591
+ """Darwin argv is not an ownership fact; no KERN_PROCARGS2 parser."""
592
+ return None
593
+
594
+
595
+ def physical_directory_spelling(resolved: Path) -> str:
596
+ """Linux's physical symlink resolution keeps Path.resolve() spelling."""
597
+ return str(resolved)
598
+
599
+
600
+ def _darwin_physical_directory_spelling(resolved: Path) -> str:
601
+ """Read the real APFS directory case via a pinned descriptor/F_GETPATH.
602
+
603
+ Creation opens the resolved directory (not O_NOFOLLOW: symlinks are valid).
604
+ For an untrusted path that was deleted after its ledger write, read the
605
+ longest existing directory ancestor and append only the missing suffix.
606
+ """
607
+ ancestor = resolved
608
+ suffix: list[str] = []
609
+ while True:
610
+ try:
611
+ fd = os.open(ancestor, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
612
+ except OSError as exc:
613
+ if exc.errno not in (errno.ENOENT, errno.ENOTDIR) or ancestor == ancestor.parent:
614
+ raise
615
+ suffix.insert(0, ancestor.name)
616
+ ancestor = ancestor.parent
617
+ continue
618
+ try:
619
+ # F_GETPATH is Darwin fcntl command 50; some Python builds lack
620
+ # the symbolic name even though the kernel supplies the operation.
621
+ raw = fcntl.fcntl(fd, getattr(fcntl, "F_GETPATH", 50), bytes(1024))
622
+ finally:
623
+ os.close(fd)
624
+ if not isinstance(raw, bytes) or b"\0" not in raw or raw.startswith(b"\0"):
625
+ raise OSError(errno.EIO, "Darwin F_GETPATH returned an invalid directory path")
626
+ actual = Path(os.fsdecode(raw.split(b"\0", 1)[0]))
627
+ if not actual.is_absolute():
628
+ raise OSError(errno.EIO, "Darwin F_GETPATH returned a relative directory path")
629
+ return str(actual.joinpath(*suffix))
630
+
631
+
632
+ def _darwin_parent_watch(parent_pid: int) -> None:
633
+ while True:
634
+ try:
635
+ if os.getppid() != parent_pid:
636
+ os._exit(0)
637
+ time.sleep(0.3)
638
+ except Exception:
639
+ os._exit(0)
640
+
641
+
642
+ def _darwin_arm_parent_death(parent_pid: int) -> None:
643
+ """Read-only RPC children die on lost parent even during blocked I/O."""
644
+ try:
645
+ if parent_pid <= 0 or os.getppid() != parent_pid:
646
+ os._exit(0)
647
+ watcher = threading.Thread(target=_darwin_parent_watch,
648
+ args=(parent_pid,), daemon=True,
649
+ name="ccx-read-only-parent-watch")
650
+ watcher.start()
651
+ if not watcher.is_alive() or os.getppid() != parent_pid:
652
+ os._exit(0)
653
+ except Exception:
654
+ os._exit(0)
655
+
656
+
657
+ # The sole OS dispatch in CCX; callers consume facts, not platform branches.
658
+ if sys.platform == "darwin":
659
+ process_info = _darwin_process_info
660
+ read_argv = _darwin_read_argv
661
+ peer_credentials = _darwin_peer_credentials
662
+ open_root_dir = _darwin_open_root_dir
663
+ runtime_base = _darwin_runtime_base
664
+ pinned_fd_path = _darwin_pinned_fd_path
665
+ arm_parent_death = _darwin_arm_parent_death
666
+ physical_directory_spelling = _darwin_physical_directory_spelling
667
+ held_flock_inodes = _darwin_held_flock_inodes