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.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/bin/ccx +56 -0
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +40 -4
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- 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
|