ccx-native 0.0.1 → 0.2.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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +167 -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 +807 -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 +269 -0
  16. package/ccx/claude/host/session.py +5244 -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 +2535 -0
  26. package/ccx/claude/usage.py +796 -0
  27. package/ccx/claude/usage_control.py +204 -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/models.py +53 -0
  35. package/ccx/cli/commands/read.py +1104 -0
  36. package/ccx/cli/commands/turns.py +1081 -0
  37. package/ccx/cli/controller.py +757 -0
  38. package/ccx/cli/main.py +302 -0
  39. package/ccx/cli/parser.py +1439 -0
  40. package/ccx/cli/skills.py +215 -0
  41. package/ccx/cli/statusline.py +160 -0
  42. package/ccx/client.py +1904 -0
  43. package/ccx/codex/__init__.py +0 -0
  44. package/ccx/codex/attach_tty.py +267 -0
  45. package/ccx/codex/client.py +738 -0
  46. package/ccx/codex/completion.py +121 -0
  47. package/ccx/codex/control.py +288 -0
  48. package/ccx/codex/daemon/__init__.py +0 -0
  49. package/ccx/codex/daemon/__main__.py +4 -0
  50. package/ccx/codex/daemon/daemon.py +1558 -0
  51. package/ccx/codex/daemon/models.py +140 -0
  52. package/ccx/codex/daemon/native.py +1590 -0
  53. package/ccx/codex/daemon/release.py +367 -0
  54. package/ccx/codex/daemon/worker.py +2424 -0
  55. package/ccx/codex/delivery.py +758 -0
  56. package/ccx/codex/delivery_recovery.py +232 -0
  57. package/ccx/codex/evidence.py +827 -0
  58. package/ccx/codex/history.py +158 -0
  59. package/ccx/codex/history_store.py +131 -0
  60. package/ccx/codex/notify_history.py +140 -0
  61. package/ccx/codex/ops.py +666 -0
  62. package/ccx/codex/paths.py +70 -0
  63. package/ccx/codex/protocol.py +805 -0
  64. package/ccx/codex/provenance.py +105 -0
  65. package/ccx/codex/read.py +29 -0
  66. package/ccx/codex/records.py +51 -0
  67. package/ccx/codex/reports.py +177 -0
  68. package/ccx/codex/runtime.py +1454 -0
  69. package/ccx/codex/settle.py +363 -0
  70. package/ccx/codex/telemetry.py +792 -0
  71. package/ccx/codex/types.py +659 -0
  72. package/ccx/codex/worker_environment.py +25 -0
  73. package/ccx/constants.py +288 -0
  74. package/ccx/controllers/__init__.py +0 -0
  75. package/ccx/controllers/controller.py +1449 -0
  76. package/ccx/controllers/live_vars.py +1706 -0
  77. package/ccx/controllers/live_vars_runner.py +216 -0
  78. package/ccx/controllers/notify.py +213 -0
  79. package/ccx/controllers/orchestrator.py +193 -0
  80. package/ccx/controllers/policy.py +57 -0
  81. package/ccx/controllers/store.py +681 -0
  82. package/ccx/errors.py +634 -0
  83. package/ccx/models.py +225 -0
  84. package/ccx/notify/__init__.py +0 -0
  85. package/ccx/notify/__main__.py +5 -0
  86. package/ccx/notify/client.py +116 -0
  87. package/ccx/notify/daemon.py +480 -0
  88. package/ccx/notify/deliver.py +304 -0
  89. package/ccx/notify/paths.py +51 -0
  90. package/ccx/notify/registry.py +649 -0
  91. package/ccx/notify/select.py +750 -0
  92. package/ccx/notify/settle.py +684 -0
  93. package/ccx/ops/__init__.py +0 -0
  94. package/ccx/ops/background.py +115 -0
  95. package/ccx/ops/cancel.py +296 -0
  96. package/ccx/ops/codex_import.py +96 -0
  97. package/ccx/ops/common.py +397 -0
  98. package/ccx/ops/compact.py +337 -0
  99. package/ccx/ops/compact_ops.py +550 -0
  100. package/ccx/ops/context_events.py +48 -0
  101. package/ccx/ops/conversation.py +1593 -0
  102. package/ccx/ops/conversation_paging.py +1339 -0
  103. package/ccx/ops/create.py +56 -0
  104. package/ccx/ops/creation.py +952 -0
  105. package/ccx/ops/creation_steps.py +575 -0
  106. package/ccx/ops/event_feed.py +710 -0
  107. package/ccx/ops/forking.py +711 -0
  108. package/ccx/ops/history.py +1041 -0
  109. package/ccx/ops/input.py +556 -0
  110. package/ccx/ops/maintenance.py +295 -0
  111. package/ccx/ops/report.py +773 -0
  112. package/ccx/ops/semantics.py +490 -0
  113. package/ccx/ops/send.py +961 -0
  114. package/ccx/ops/steer_ops.py +867 -0
  115. package/ccx/ops/terminate.py +383 -0
  116. package/ccx/ops/turn_reports.py +348 -0
  117. package/ccx/ops/wait.py +762 -0
  118. package/ccx/ops/worker_events.py +640 -0
  119. package/ccx/rpc/__init__.py +0 -0
  120. package/ccx/rpc/local_rpc.py +1602 -0
  121. package/ccx/rpc/rpc.py +559 -0
  122. package/ccx/rpc/serve.py +374 -0
  123. package/ccx/rpc/transport.py +409 -0
  124. package/ccx/rpc/view.py +125 -0
  125. package/ccx/store/__init__.py +0 -0
  126. package/ccx/store/discovery.py +170 -0
  127. package/ccx/store/identity.py +105 -0
  128. package/ccx/store/journal_io.py +79 -0
  129. package/ccx/store/liveness.py +194 -0
  130. package/ccx/store/paths.py +250 -0
  131. package/ccx/store/record_cache.py +40 -0
  132. package/ccx/store/records.py +309 -0
  133. package/ccx/store/root.py +789 -0
  134. package/ccx/store/runtime_presence.py +137 -0
  135. package/ccx/store/store.py +2332 -0
  136. package/ccx/store/worker_state.py +86 -0
  137. package/ccx/system/__init__.py +0 -0
  138. package/ccx/system/bounded.py +296 -0
  139. package/ccx/system/cwd_paths.py +25 -0
  140. package/ccx/system/engine_binary.py +59 -0
  141. package/ccx/system/executable.py +50 -0
  142. package/ccx/system/oslayer.py +667 -0
  143. package/ccx/system/pipes.py +46 -0
  144. package/ccx/system/private_json.py +54 -0
  145. package/ccx/system/sanitize.py +43 -0
  146. package/ccx/types.py +809 -0
  147. package/ccx/version.py +29 -0
  148. package/ccx/workers.py +2093 -0
  149. package/ccx.py +9 -0
  150. package/package.json +39 -7
  151. package/pyproject.toml +87 -0
  152. package/skills/ccx/SKILL.md +166 -0
  153. package/skills/ccx/references/controllers.md +244 -0
  154. package/skills/ccx/references/history.md +50 -0
  155. package/skills/ccx/references/troubleshooting.md +94 -0
  156. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,137 @@
1
+ """Read-only presence of the runtime that writes a worker's event journal.
2
+
3
+ A runtime killed abruptly (SIGKILL, OOM) writes no final event, so a held
4
+ event read alone cannot tell a quiet worker from a dead one. Each runtime
5
+ already holds a kernel lifetime lease for its whole life: a Claude host
6
+ ``runtime-metadata/host-locks/<worker>.lease.lock`` (``host._host_process_lease``)
7
+ and codexd ``runtime-metadata/pid/codexd.lease.lock``
8
+ (``transport.daemon_process_lease``). The kernel drops it with the process.
9
+
10
+ This module only *looks* at those leases, through the kernel lock table
11
+ (``oslayer.held_flock_inodes``: ``/proc/locks`` on Linux; Darwin has none, so
12
+ ``runtime`` is always ``unknown`` there). It never
13
+ takes a lock: even a momentary non-blocking probe could make a starting
14
+ Claude host, whose lease acquisition is non-blocking, refuse to start. It
15
+ writes nothing, calls no runtime and starts nothing.
16
+
17
+ ``runtime`` values:
18
+
19
+ - ``live``: the lease of this root/generation/worker runtime is held.
20
+ - ``gone``: the lease has been free, and the recorded runtime process is not
21
+ alive, for a whole startup grace (the one ``worker.wait`` uses: 0.35 s, or
22
+ 5 s while booting or resuming) within this observation. It prompts an
23
+ authoritative bounded ``worker.wait``; it never proves that a turn ended.
24
+ - ``unknown``: anything else (no kernel lock table, an untrusted lease file,
25
+ a free lease whose recorded process is still alive, or a free lease not yet
26
+ observed for the grace).
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ import stat
32
+ import time
33
+ from typing import Callable
34
+
35
+ from ccx.system import oslayer
36
+ from ccx.store.liveness import owned_process
37
+
38
+ LIVE = "live"
39
+ GONE = "gone"
40
+ UNKNOWN = "unknown"
41
+
42
+ HOST_MISSING_GRACE_S = 0.35
43
+ BOOTING_GRACE_S = 5.0
44
+ _BOOTING = frozenset({"booting", "resuming", "starting"})
45
+
46
+
47
+ def _lease_file(store, engine: str, worker_id: str) -> os.stat_result | None | bool:
48
+ """The lease file's stat, None when it was never created, False if untrusted."""
49
+ parts, name = ((("runtime-metadata", "host-locks"), worker_id + ".lease.lock")
50
+ if engine == "claude" else
51
+ (("runtime-metadata", "pid"), "codexd.lease.lock"))
52
+ try:
53
+ with store._gen(*parts) as directory:
54
+ info = os.stat(name, dir_fd=directory, follow_symlinks=False)
55
+ except FileNotFoundError:
56
+ return None
57
+ if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
58
+ or info.st_mode & 0o077):
59
+ return False
60
+ return info
61
+
62
+
63
+ def _codexd_identity(store) -> tuple[object, object]:
64
+ """The recorded codexd pid/start of this exact root and generation."""
65
+ from ccx.store.root import _read_json
66
+ try:
67
+ with store._gen("runtime-metadata", "pid") as directory:
68
+ meta = _read_json(directory, "codexd.json")
69
+ except (FileNotFoundError, OSError, ValueError):
70
+ return None, None
71
+ if (not isinstance(meta, dict) or meta.get("generation") != store.generation
72
+ or meta.get("rootRealpath") != str(store.root)):
73
+ return None, None
74
+ return meta.get("pid"), meta.get("startToken")
75
+
76
+
77
+ def lease(store, engine: str, worker_id: str, record: dict | None) -> str:
78
+ """One sample: ``held``, ``free`` (and the recorded process is not alive) or ``unknown``."""
79
+ # Without a kernel lock table nothing can be observed, not even a free
80
+ # lease: a never-created lease file must not become "gone" here.
81
+ held = oslayer.held_flock_inodes()
82
+ if held is None:
83
+ return UNKNOWN
84
+ info = _lease_file(store, engine, worker_id)
85
+ if info is False:
86
+ return UNKNOWN
87
+ if (info is not None and
88
+ (os.major(info.st_dev), os.minor(info.st_dev), info.st_ino) in held):
89
+ return "held"
90
+ if engine == "claude":
91
+ lifecycle = (record or {}).get("lifecycle") or {}
92
+ pid, start = lifecycle.get("hostPid"), lifecycle.get("hostStart")
93
+ else:
94
+ pid, start = _codexd_identity(store)
95
+ # A free lease next to a recorded process that is still alive is a start
96
+ # before its lease, or a lease this reader cannot see: never "gone".
97
+ if pid is not None and owned_process(pid, start) != "dead":
98
+ return UNKNOWN
99
+ return "free"
100
+
101
+
102
+ class RuntimeObserver:
103
+ """Presence across one held read; no state outlives it or is shared."""
104
+
105
+ def __init__(self, store, target: str,
106
+ clock: Callable[[], float] = time.monotonic):
107
+ owner = store.resolve(target)
108
+ self._store, self._target = store, target
109
+ self._engine, self._worker_id = owner.engine, owner.worker_id
110
+ self._clock = clock
111
+ self._free_since: float | None = None
112
+ self.value: str = UNKNOWN
113
+ self.seen_live = False
114
+
115
+ def sample(self) -> str:
116
+ # The shared activation lease, as every other source read takes it.
117
+ with self._store.lease():
118
+ record = self._store.record(self._target)
119
+ observed = lease(self._store, self._engine, self._worker_id, record)
120
+ now = self._clock()
121
+ if observed == "held":
122
+ self._free_since = None
123
+ self.seen_live = True
124
+ self.value = LIVE
125
+ elif observed == "free":
126
+ if self._free_since is None:
127
+ self._free_since = now
128
+ native = ((record or {}).get("lifecycle") or {}).get("state")
129
+ grace = BOOTING_GRACE_S if native in _BOOTING else HOST_MISSING_GRACE_S
130
+ self.value = GONE if now - self._free_since >= grace else UNKNOWN
131
+ else:
132
+ self._free_since = None
133
+ self.value = UNKNOWN
134
+ return self.value
135
+
136
+
137
+ __all__ = ["GONE", "LIVE", "UNKNOWN", "RuntimeObserver", "lease"]