ccx-native 0.0.1 → 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 -4
  3. package/bin/ccx +55 -2
  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 +39 -7
  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,152 @@
1
+ """Promptless Claude Code usage control protocol; no credential or transcript reads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import select
8
+ import subprocess
9
+ import time
10
+ import uuid
11
+
12
+ from ccx.constants import CLAUDE_BIN
13
+
14
+ # None of these flags is --bare: --bare disables native OAuth/Keychain auth.
15
+ _PROBE_ARGS = (
16
+ "-p", "--input-format", "stream-json", "--output-format", "stream-json",
17
+ "--verbose", "--safe-mode", "--strict-mcp-config",
18
+ "--no-session-persistence", "--disable-slash-commands", "--no-chrome",
19
+ "--tools", "",
20
+ )
21
+ _MAX_OUTPUT_BYTES = 2 * 1024 * 1024
22
+ _MAX_LINE_BYTES = 1 * 1024 * 1024
23
+
24
+
25
+ def request_usage(timeout: float = 8.0) -> dict:
26
+ """Return {kind, body?, raw?, error?} from one bounded get_usage exchange.
27
+
28
+ The native CLI owns auth. The only input consists of initialize/get_usage
29
+ control requests; no model prompt, hooks, MCP server or transcript scan.
30
+ """
31
+ timeout = max(0.1, float(timeout))
32
+ start = time.monotonic()
33
+ deadline = start + timeout
34
+ init_id, usage_id = uuid.uuid4().hex, uuid.uuid4().hex
35
+ child = None
36
+ reply = None
37
+ invalid_line = None
38
+ total_bytes = 0
39
+ buffer = b""
40
+ problem = None
41
+ try:
42
+ child = subprocess.Popen(
43
+ [CLAUDE_BIN, *_PROBE_ARGS], stdin=subprocess.PIPE,
44
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
45
+ )
46
+ for rid, request in (
47
+ (init_id, {"subtype": "initialize"}),
48
+ (usage_id, {"subtype": "get_usage", "skip_behaviors": True}),
49
+ ):
50
+ message = {"type": "control_request", "request_id": rid, "request": request}
51
+ child.stdin.write((json.dumps(message, separators=(",", ":")) + "\n").encode())
52
+ child.stdin.flush()
53
+
54
+ while time.monotonic() < deadline and reply is None and problem is None:
55
+ readable, _, _ = select.select(
56
+ [child.stdout], [], [], min(0.2, max(0, deadline - time.monotonic()))
57
+ )
58
+ if not readable:
59
+ if child.poll() is not None:
60
+ break
61
+ continue
62
+ chunk = os.read(child.stdout.fileno(), 32768)
63
+ if not chunk:
64
+ break
65
+ total_bytes += len(chunk)
66
+ if total_bytes > _MAX_OUTPUT_BYTES:
67
+ problem = "Claude usage control output too large"
68
+ break
69
+ buffer += chunk
70
+ if len(buffer) > _MAX_LINE_BYTES and b"\n" not in buffer:
71
+ problem = "Claude usage control line too large"
72
+ break
73
+ while b"\n" in buffer:
74
+ line, buffer = buffer.split(b"\n", 1)
75
+ if len(line) > _MAX_LINE_BYTES:
76
+ problem = "Claude usage control line too large"
77
+ break
78
+ try:
79
+ message = json.loads(line)
80
+ except (ValueError, UnicodeDecodeError):
81
+ invalid_line = line[:1000].decode("utf-8", "replace")
82
+ continue
83
+ if not isinstance(message, dict) or message.get("type") != "control_response":
84
+ continue
85
+ response = message.get("response")
86
+ if not isinstance(response, dict):
87
+ continue
88
+ if response.get("request_id") == init_id and response.get("subtype") == "error":
89
+ problem = "Claude usage initialize control request failed"
90
+ break
91
+ if response.get("request_id") == usage_id:
92
+ reply = message
93
+ break
94
+ # Always close stdin after the matching response, or on a bounded failure.
95
+ child.stdin.close()
96
+ child.stdin = None
97
+ remaining = max(0.0, deadline - time.monotonic())
98
+ try:
99
+ exit_code = child.wait(timeout=remaining)
100
+ except subprocess.TimeoutExpired:
101
+ problem = problem or "Claude usage control timed out"
102
+ exit_code = None
103
+ if reply is None:
104
+ if problem is None:
105
+ problem = "Claude usage control returned no matching response"
106
+ if invalid_line is not None:
107
+ return {"kind": "unparsed", "raw": {"stdout_text": invalid_line},
108
+ "error": problem}
109
+ return {"kind": "error", "error": problem}
110
+ if problem is not None or exit_code not in (0, None):
111
+ return {"kind": "error", "error": problem or "Claude usage control exited unsuccessfully"}
112
+ response = reply["response"]
113
+ if response.get("subtype") == "error":
114
+ return {"kind": "error", "error": "Claude usage control response was an error"}
115
+ if response.get("subtype") != "success":
116
+ return {"kind": "unparsed", "raw": reply,
117
+ "error": "Claude usage control returned an unknown response subtype"}
118
+ body = response.get("response")
119
+ if not isinstance(body, dict):
120
+ return {"kind": "unparsed", "raw": reply,
121
+ "error": "Claude usage control returned an unknown JSON body"}
122
+ if body.get("rate_limits_available") is False and body.get("rate_limits") is None:
123
+ return {"kind": "unavailable", "error": "Claude plan rate limits unavailable"}
124
+ if body.get("rate_limits_available") is True and body.get("rate_limits") is None:
125
+ return {"kind": "held", "error": "Claude plan usage is held by the provider"}
126
+ rate_limits = body.get("rate_limits")
127
+ if isinstance(rate_limits, dict) and any(k in rate_limits for k in (
128
+ "five_hour", "seven_day", "limits", "model_scoped", "extra_usage",
129
+ "seven_day_opus", "seven_day_sonnet",
130
+ )):
131
+ return {"kind": "success", "body": rate_limits}
132
+ return {"kind": "unparsed", "raw": reply,
133
+ "error": "Claude usage control returned an unrecognized rate_limits field"}
134
+ except (OSError, ValueError, BrokenPipeError) as exc:
135
+ return {"kind": "error", "error": f"Claude usage control unavailable: {type(exc).__name__}"}
136
+ finally:
137
+ if child is not None:
138
+ if child.stdin is not None and not child.stdin.closed:
139
+ child.stdin.close()
140
+ if child.poll() is None:
141
+ # No early SIGKILL: wait for the bounded deadline to expire first.
142
+ try:
143
+ child.wait(timeout=max(0.0, deadline - time.monotonic()))
144
+ except subprocess.TimeoutExpired:
145
+ child.terminate()
146
+ try:
147
+ child.wait(timeout=1.0)
148
+ except subprocess.TimeoutExpired:
149
+ child.kill()
150
+ child.wait(timeout=1.0)
151
+ if child.stdout is not None:
152
+ child.stdout.close()
File without changes