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,46 @@
1
+ """Quiet exit when the reader of stdout goes away (`ccx logs | head`).
2
+
3
+ Standard CLI tools stop silently when the downstream reader closes the pipe.
4
+ Python instead raises ``BrokenPipeError`` on the next write or on the final
5
+ flush at shutdown, which prints a traceback. Entry points wrap their work in
6
+ :func:`quiet_broken_stdout`; only a broken *stdout* is silenced, so an EPIPE
7
+ from a worker socket still surfaces as an error.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import sys
13
+ from contextlib import contextmanager
14
+ from typing import Iterator
15
+
16
+ from ccx.system import oslayer
17
+
18
+
19
+ def stdout_reader_gone() -> bool:
20
+ """True when stdout is a pipe or socket whose reading end has closed."""
21
+ try:
22
+ return oslayer.stdout_reader_gone(sys.stdout.fileno())
23
+ except (AttributeError, OSError, ValueError):
24
+ return False
25
+
26
+
27
+ @contextmanager
28
+ def quiet_broken_stdout() -> Iterator[None]:
29
+ """Exit 0 without a traceback when stdout's reader has gone away."""
30
+ try:
31
+ try:
32
+ yield
33
+ finally:
34
+ sys.stdout.flush()
35
+ except BrokenPipeError:
36
+ if not stdout_reader_gone():
37
+ raise
38
+ # Point stdout at /dev/null so the interpreter's own shutdown flush of
39
+ # whatever is still buffered cannot fail again.
40
+ devnull = os.open(os.devnull, os.O_WRONLY)
41
+ os.dup2(devnull, sys.stdout.fileno())
42
+ os.close(devnull)
43
+ raise SystemExit(0)
44
+
45
+
46
+ __all__ = ["quiet_broken_stdout", "stdout_reader_gone"]
@@ -0,0 +1,54 @@
1
+ """Private JSON files shared by account-level caches and settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import stat
10
+ import tempfile
11
+
12
+
13
+ def _open_private_file(path: Path) -> int:
14
+ """Do not trust a symlink, another owner's file, or a readable private record."""
15
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
16
+ st = os.fstat(fd)
17
+ if not stat.S_ISREG(st.st_mode) or st.st_uid != os.getuid() or st.st_mode & 0o077:
18
+ os.close(fd)
19
+ raise PermissionError(f"not a private regular file owned by this user: {path}")
20
+ return fd
21
+
22
+
23
+ def _read_private_json(path: Path) -> dict | None:
24
+ with os.fdopen(_open_private_file(path), "rb") as handle:
25
+ value = json.load(handle)
26
+ return value if isinstance(value, dict) else None
27
+
28
+
29
+ def _atomic_private_json(path: Path, value: dict) -> None:
30
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
31
+ tmp = None
32
+ try:
33
+ fd, tmp = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
34
+ with os.fdopen(fd, "w", encoding="utf-8") as out:
35
+ json.dump(value, out, separators=(",", ":"), allow_nan=False)
36
+ out.flush()
37
+ os.fsync(out.fileno())
38
+ # Never overwrite a planted symlink/foreign file at the final path.
39
+ try:
40
+ existing = _open_private_file(path)
41
+ except FileNotFoundError:
42
+ pass
43
+ except OSError as exc:
44
+ # Replacing a planted symlink itself is safe: os.replace never
45
+ # follows the destination. Its target is neither read nor written.
46
+ if exc.errno != errno.ELOOP:
47
+ raise
48
+ else:
49
+ os.close(existing)
50
+ os.replace(tmp, path)
51
+ tmp = None
52
+ finally:
53
+ if tmp is not None:
54
+ os.unlink(tmp)
@@ -0,0 +1,43 @@
1
+ """One sanitizer for every native payload CCX preserves or persists.
2
+
3
+ Hidden reasoning is the thing CCX must never write down. Both places that keep
4
+ native payloads — the normalized conversation document and the Codex evidence
5
+ journal — have to strip it exactly the same way, so the rule lives here, in a
6
+ module low enough that either can import it without importing the other.
7
+
8
+ The rule is structural rather than a list of known types: any mapping whose
9
+ ``type`` mentions reasoning loses its raw ``content``, at every depth. A future
10
+ item type that nests a reasoning-shaped payload inside itself is covered
11
+ without being anticipated, while the engine's own surfaced ``summary`` — which
12
+ is not hidden — survives.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+
20
+ def is_reasoning_shaped(value: Any) -> bool:
21
+ """Does this mapping look like it carries chain-of-thought?"""
22
+ return (
23
+ isinstance(value, dict)
24
+ and "reasoning" in str(value.get("type", "")).lower()
25
+ )
26
+
27
+
28
+ def safe_native(value: Any) -> Any:
29
+ """A copy of one native payload that is safe to persist verbatim."""
30
+ if isinstance(value, list):
31
+ return [safe_native(entry) for entry in value]
32
+ if not isinstance(value, dict):
33
+ return value
34
+ hide = is_reasoning_shaped(value)
35
+ return {
36
+ key: safe_native(entry)
37
+ for key, entry in value.items()
38
+ if not key.startswith("_ccx_")
39
+ and not (hide and key == "content")
40
+ }
41
+
42
+
43
+ __all__ = ["is_reasoning_shaped", "safe_native"]