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,288 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import pathlib
5
+ import shutil
6
+
7
+ from ccx.store.root import canonical_root
8
+
9
+ # Roots are made absolute once, here: the managed host chdirs into the worker
10
+ # cwd before its first path write, and the hooks relay runs in that cwd too, so
11
+ # a relative root would name a different directory in each process.
12
+ # Canonicalize ancestors only; a linked root leaf must fail the no-follow walk.
13
+ _ORIGINAL_CCX_ROOT_ENV = os.environ.get("CCX_ROOT")
14
+ CCX_ROOT = canonical_root(os.environ.get("CCX_ROOT") or "~/.ccx")
15
+
16
+
17
+ def select_root(path: pathlib.Path | str) -> pathlib.Path:
18
+ """Select an explicit process root and pin the same identity for children."""
19
+ global CCX_ROOT
20
+ CCX_ROOT = canonical_root(path)
21
+ os.environ["CCX_ROOT"] = str(CCX_ROOT)
22
+ return CCX_ROOT
23
+
24
+ _CLAUDE_CONFIG_OVERRIDE = os.environ.get("CLAUDE_CONFIG_DIR")
25
+ CLAUDE_HOME = pathlib.Path(os.path.realpath(os.path.expanduser(_CLAUDE_CONFIG_OVERRIDE or "~/.claude")))
26
+ # Always pin the canonical root for detached hosts, daemon and hook children,
27
+ # even when the caller used the default rather than exporting CCX_ROOT.
28
+ os.environ["CCX_ROOT"] = str(CCX_ROOT)
29
+ if _CLAUDE_CONFIG_OVERRIDE:
30
+ os.environ["CLAUDE_CONFIG_DIR"] = str(CLAUDE_HOME)
31
+ # Claude 2.1.293 reads global metadata (including per-cwd folder trust) from
32
+ # the effective native profile. With a custom config directory, native Claude
33
+ # reads <CLAUDE_CONFIG_DIR>/.claude.json, NOT HOME/.claude.json. Both initial
34
+ # worker creation and respawn call settings.ensure_trusted_paths.
35
+ CLAUDE_DOTFILE = (
36
+ CLAUDE_HOME / ".claude.json"
37
+ if _CLAUDE_CONFIG_OVERRIDE
38
+ else pathlib.Path(os.path.expanduser("~/.claude.json"))
39
+ )
40
+ def _default_claude_bin() -> str:
41
+ # The native installer's location first; npm/Homebrew installs put claude
42
+ # elsewhere on PATH. Keep the native path when neither exists so error
43
+ # messages name the conventional location.
44
+ native = os.path.expanduser("~/.local/bin/claude")
45
+ if os.path.exists(native):
46
+ return native
47
+ return shutil.which("claude") or native
48
+
49
+
50
+ CLAUDE_BIN = os.environ.get("CCX_CLAUDE_BIN") or _default_claude_bin()
51
+
52
+ SCHEMA_VERSION = 1
53
+ # Long-lived clients and daemons can span package upgrades. Fence requests and
54
+ # replies in both directions; bump when required fields or operation meanings change.
55
+ CODEXD_RPC_GENERATION = 21
56
+ # Inspection and teardown answer any client, whatever generation it speaks:
57
+ # they are how an operator sees an incompatible daemon and gets rid of it.
58
+ # Every other operation is fenced in both directions before it runs.
59
+ CODEXD_RECOVERY_OPS = frozenset({
60
+ "status", "compact-observe", "kill", "interrupt", "daemon-status", "daemon-stop",
61
+ })
62
+ # Notification supervisors also outlive package upgrades.
63
+ NOTIFYD_PROTOCOL = 19
64
+ # notifyd is a codexd client, so a codexd RPC bump must also retire a running
65
+ # notifyd (otherwise it keeps old code and its Codex parent wakes are deferred).
66
+ NOTIFYD_GENERATION = NOTIFYD_PROTOCOL * 100 + CODEXD_RPC_GENERATION
67
+ # Exact managed caller full identity replaces old short-routing authority.
68
+ SELF_WORKER_ID_ENV = "CCX_SELF_WORKER_ID"
69
+ SELF_ENGINE_ENV = "CCX_SELF_ENGINE"
70
+ SELF_SHORT_ENV = "CCX_SELF_SHORT" # historical, refused by new runtime
71
+ DEFAULT_IDLE_TIMEOUT_S = 30 * 60 # 30 minutes
72
+ # Boot watchdog: a worker leaves S.BOOTING only on a SessionStart hook. If Claude
73
+ # blocks before that (e.g. an un-pre-seeded --add-dir trust dialog or onboarding
74
+ # gate) no hook ever fires and the idle-killer — gated on S.IDLE — never reaps it,
75
+ # so the boot hangs indefinitely. Healthy boots fire SessionStart within seconds;
76
+ # 90s is generously above observed boot times. Env-overridable for tests/tuning.
77
+ BOOT_TIMEOUT_S = float(os.environ.get("CCX_BOOT_TIMEOUT_S", 90))
78
+ DEFAULT_PTY_COLS = 120
79
+ DEFAULT_PTY_ROWS = 40
80
+ WARMUP_S = 4.0 # post-IDLE settle before allowing sends (after a turn)
81
+ PTY_QUIESCENCE_S = 1.5 # PTY must be quiet this long after IDLE-entry → Ink-ready
82
+ PTY_WRITE_TIMEOUT_S = 2.0 # bounded drain for a complete write to the non-blocking PTY
83
+ PASTE_SUBMIT_DELAY_S = 0.01 # let Ink apply a completed bracketed paste before Enter
84
+ # Ink turns large/multiline bracketed pastes into a collapsed attachment
85
+ # asynchronously. An Enter sent during that conversion is silently ignored.
86
+ # Only the single-paste fallback (see PASTE_INLINE_*) collapses: it stages large
87
+ # IDLE sends and delays submission in proportion to their UTF-8 size while the
88
+ # host continues draining PTY output.
89
+ PASTE_STAGED_THRESHOLD_BYTES = 8 * 1024
90
+ PASTE_STAGED_BYTES_PER_S = 2 * 1024
91
+ PASTE_STAGED_MAX_DELAY_S = 30.0
92
+ # Claude collapses a bracketed paste longer than 800 UTF-16 units or with more
93
+ # than min(rows - 10, 2) line breaks into a `[Pasted text]` attachment, and
94
+ # submits attachments wrapped in <pasted_content> tags that its system prompt
95
+ # tells the model to treat as possibly not user-authored. Smaller pastes are
96
+ # inserted as typed text, so prompts are framed as a run of pastes under both
97
+ # limits (2.1.294: FY=800, S8=min(rows-10,2)). The unit cap keeps a margin.
98
+ PASTE_INLINE_MAX_UNITS = 640
99
+ PASTE_INLINE_ROWS_RESERVED = 10
100
+ PASTE_INLINE_MAX_NEWLINES = 2
101
+ SEND_FIRST_ACK_S = 5.0 # if no UserPromptSubmit within this of first paste, retry once
102
+ SEND_RETRY_ACK_S = 30.0 # if no UPS within this of retry paste, CRASHED
103
+ CLEAR_INPUT_MIN_GAP_S = 1.5 # min gap between input-clearing Ctrl-C presses: Claude
104
+ # exits on a second Ctrl-C within 800ms of one that
105
+ # found the editor empty
106
+ CLEAR_KILL_REPEAT = 48 # Ctrl-U presses in the /compact residue clear — drains a multi-line
107
+ # input buffer line-by-line (Ctrl-U = delete-to-line-start,
108
+ # not whole-buffer); no-op once empty. See drain_bytes().
109
+ POST_CLEAR_SETTLE_S = 0.3 # min gap after a retry clear before re-pasting, so the
110
+ # buffer-empty has rendered and isn't raced by the paste
111
+ COMPACT_WAIT_DEFAULT_S = 5 * 60 # max wait for native /compact completion
112
+ COMPACT_POLL_S = 0.2 # transcript poll cadence while compacting
113
+ COMPACT_SUBMIT_DELAY_S = 1.0 # gap between pasting "/compact" and the Enter that runs it —
114
+ # pasting a slash command opens Ink's autocomplete menu, and a
115
+ # \r in the same write is swallowed by the menu instead of
116
+ # submitting (LIVE-verified on Claude 2.1.207)
117
+ COMPACT_SUBMIT_RETRY_S = 3.0 # re-press Enter at this cadence until the transcript records
118
+ # the /compact command as submitted
119
+ POST_COMPACT_HOLD_GRACE_S = 10.0 # a compact that settles while a real turn is already running
120
+ # holds BUSY for that turn instead of releasing to IDLE. If the
121
+ # inference was wrong the worker would be pinned BUSY forever,
122
+ # so the hold is bounded: this much combined hook AND PTY
123
+ # silence releases it. A live turn renders its spinner
124
+ # continuously, so it never approaches the grace.
125
+ BOUNDARY_SEND_ACK_S = 4.0 # hook relay RPC timeout headroom; singleton steer uses its
126
+ # own short boundary ACK budget below
127
+ BOUNDARY_SEND_POLL_S = 0.02 # native singleton steer enqueue-ack poll cadence
128
+ HOOK_RELAY_RETRY_S = 30.0 # a parent hook relay retries a busy host (full listen backlog) this long
129
+ HOOK_RELAY_RETRY_POLL_S = 0.05 # pause between those connect attempts
130
+ # Durable steering (`ccx steer`). A steer is journaled before it is written to
131
+ # the PTY, so absence of quick evidence is never a reason to write it again.
132
+ STEER_BOUNDARY_ACK_S = 1.0 # max PostToolUse/SubagentStop hold waiting for the native
133
+ # enqueue of a durable steer. Short on purpose: the delivery
134
+ # is already journaled, so a slow enqueue is reconciled off
135
+ # the hook path instead of blocking Claude's tool loop.
136
+ STEER_EVIDENCE_GRACE_S = 300.0 # a delivery pasted with no native evidence at all for this long
137
+ # becomes durable `unknown` (never a silent re-paste)
138
+ STEER_OBSERVE_DEFAULT_S = 120.0 # caller-side observation window for stronger placement evidence
139
+ STEER_OBSERVE_POLL_S = 0.5 # caller-side poll cadence while observing
140
+ # Native status is a liveness fallback only; elapsed time never proves terminality.
141
+ INTERRUPT_NATIVE_POLL_S = 0.10
142
+ INTERRUPT_CONTINUED_ACTIVITY_GRACE_S = 0.60
143
+ SEMANTIC_EVIDENCE_GRACE_S = 0.60
144
+ PASSIVE_NATIVE_IDLE_GRACE_S = 2.0 # avoid outpacing normal Stop and blocked UPS hooks
145
+ LATE_HOOK_BACKSTOP_S = 1.60
146
+ LATE_HOOK_PROMPT_ID_S = 60.0 # exact old prompt IDs outlive the heuristic Stop grace
147
+
148
+ INTERRUPT_CONFIRM_S = 12.0 # after an `interrupt` op, how long the host watches the
149
+ # transcript for Claude's "[Request interrupted by user"
150
+ # marker before presuming the Escape was a no-op (a
151
+ # cancelled turn fires no Stop hook, so this watcher is
152
+ # what settles the turn to IDLE)
153
+ RESPAWN_IDLE_DEADLINE_S = 180 # _respawn waits this long for state=IDLE post-host-spawn
154
+ # There is deliberately no default turn-wait bound. A turn is model work with no
155
+ # natural length, so every verb that starts or resumes one (new/send/steer's
156
+ # delivery/answer/approve/reject/wait --next-turn) waits unbounded by default and
157
+ # ends on the worker's own lifecycle. Callers who want a deadline pass an explicit
158
+ # --timeout / timeout=. Operational waits (compact, kill, RPC acks) keep theirs.
159
+ # Optional managed `send` payload bound (UTF-8 bytes of the prompt text).
160
+ # Unlimited by default: CLI and Python callers must be able to relay complete
161
+ # worker output without a CCX-owned length rejection. Operators can opt into a
162
+ # defensive PTY-paste guard with CCX_SEND_MAX_BYTES=N; <= 0 stays unlimited.
163
+ # The PTY transport drains short writes, but operators may still choose a bound
164
+ # for pathological payloads that cannot be delivered within PTY_WRITE_TIMEOUT_S.
165
+ SEND_TEXT_MAX_BYTES = int(os.environ.get("CCX_SEND_MAX_BYTES", "0"))
166
+ HOST_SOCK_WAIT_NEW_S = 5.0 # detach launcher: max wait for a fresh host's socket
167
+ HOST_SOCK_WAIT_RESPAWN_S = 10.0 # detach launcher: max wait for a revived host's socket
168
+ SPAWN_LAUNCHER_GRACE_S = 20.0 # headroom over sock-wait for the launcher subprocess itself
169
+ # (interpreter start + record read + double-fork)
170
+ NB_TERMINAL_CONFIRM_S = 30 # confirm a real user turn lands after terminal-bg queuedPrompt+respawn
171
+ PERMISSION_HOOK_TIMEOUT_S = float(os.environ.get("CCX_PERMISSION_HOOK_TIMEOUT_S", 6 * 60 * 60))
172
+ PERMISSION_POLL_INTERVAL_S = 0.25
173
+
174
+ # --- generic CCX event stream ----------------------------------------------
175
+ # Every event a host broadcasts (state changes, tool boundaries, subagent
176
+ # finishes, …) is stamped with an envelope (v/type/short/seq/epoch/ts) and tee'd
177
+ # to a per-session sidecar NDJSON so dashboards/orchestrators can read history
178
+ # for dead sessions and replay-then-subscribe for live ones. Event streaming enables this by
179
+ # default with no config knobs; persistence is bounded by amortized rotation.
180
+ EVENT_SCHEMA_VERSION = 1 # bumped if the envelope/event shapes change
181
+ CCX_EVENTS_MAX = 5000 # rotate the sidecar once it exceeds this many lines
182
+ CCX_EVENTS_KEEP = 4000 # lines retained (most recent) after a rotation
183
+ CCX_EVENTS_ROTATE_CHECK_INTERVAL = 256 # events between rotation-size checks (amortization)
184
+ EVENT_PREVIEW_LIMIT = 500 # max chars of a bounded input/result/message preview
185
+ BACKGROUND_TASK_SURFACE_LIMIT = 10 # max task objects exposed on peek/report/wait surfaces
186
+
187
+ # --- usage / budget caps ---------------------------------------------------
188
+ # Claude plan usage defaults to a promptless Claude Code control request.
189
+ # Direct OAuth is an explicit per-CCX_ROOT opt-in, never the default.
190
+ USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
191
+ USAGE_BETA = "oauth-2025-04-20"
192
+ # Per-user and private by default: a fixed /tmp name lets other local users
193
+ # read the account's usage or plant data that cap enforcement trusts. Set
194
+ # CCX_USAGE_CACHE lets external *readers* consume one CCX-owned cache.
195
+ def usage_cache_path() -> pathlib.Path:
196
+ """A current-process root path, not a snapshot captured before select_root."""
197
+ return pathlib.Path(os.path.abspath(os.path.expanduser(
198
+ os.environ.get("CCX_USAGE_CACHE") or str(CCX_ROOT / "usage-cache.json")
199
+ )))
200
+
201
+
202
+ def usage_source_path() -> pathlib.Path:
203
+ return CCX_ROOT / "usage-source.json"
204
+ USAGE_MIN_FETCH_INTERVAL_S = 60.0 # shared minimum, even with force/max_age=0
205
+ CAP_POLL_INTERVAL_S = 120.0 # capped hosts check/refresh at most every 120s
206
+ USAGE_STALE_LIMIT_S = 120.0 # caps fail open when the last good fetch is older
207
+ # Direct OAuth measured 2026-10-26: about 5 reads per 10 minutes, then HTTP 429.
208
+ # Space its reads evenly instead and let caps accept the correspondingly older sample.
209
+ OAUTH_USAGE_MIN_FETCH_INTERVAL_S = 120.0
210
+ OAUTH_USAGE_STALE_LIMIT_S = 300.0
211
+ BUDGET_EXIT = 7 # exit code from blocking new/send when a cap hard-stops a run
212
+ # Cap flag key → legacy field name in the usage payload. Per-model cap
213
+ # lookup also follows the dynamic ``limits`` catalog when the legacy field is
214
+ # absent/null.
215
+ CAP_METRICS = {"5h": "five_hour", "7d": "seven_day", "opus": "seven_day_opus"}
216
+
217
+ # --- opt-in gates for unofficial provider internals --------------------------
218
+ # Paths that MUTATE undocumented Anthropic/Claude Code state stay off unless the
219
+ # operator opts in explicitly. Read-only usage and native-bg discovery are not
220
+ # gated. Read at call time (not import time) so tests and callers can toggle them.
221
+ UNOFFICIAL_RESET_ENV = "CCX_ENABLE_UNOFFICIAL_RESET" # 5-hour reset POST
222
+
223
+
224
+ def opt_in_enabled(env_name: str) -> bool:
225
+ """True only for an explicit truthy opt-in (1/true/yes/on)."""
226
+ return os.environ.get(env_name, "").strip().lower() in ("1", "true", "yes", "on")
227
+
228
+
229
+ # Model family aliases understood by `claude --model`. This is NOT an allowlist
230
+ # for --model, which stays a free-form passthrough (new model ids ship on
231
+ # Claude Code's cadence, not ccx's). Its only job is to catch the `--agent
232
+ # <model>` mixup at `ccx new`: `--agent` takes a *subagent* name, and a worker
233
+ # launched with `--agent opus` dies at boot with "--agent 'opus' not found",
234
+ # which used to surface only as an opaque state=crashed.
235
+ MODEL_ALIASES = ("fable", "opus", "sonnet", "haiku")
236
+
237
+ # --- CLI exit codes --------------------------------------------------------
238
+ # The integer codes every `ccx` and `ccx codex` command exits with. Both engines
239
+ # use one meaning per code, and each typed Python error carries the same code as
240
+ # `exit_code` (see ccx/errors.py and EXIT_CODE_TO_ERROR):
241
+ # 1 EXIT_ERROR bad input or usage (argparse errors included), I/O
242
+ # error, general failure
243
+ # 2 EXIT_NOT_FOUND no such worker, or no transcript / reply yet
244
+ # 3 EXIT_UNAVAILABLE CCX cannot operate on the worker: host or daemon not
245
+ # alive, read-only external session, failed respawn,
246
+ # a recorded active turn nothing serves any more
247
+ # 4 EXIT_WAITING the worker is blocked on input (answer / approve /
248
+ # reject / respond)
249
+ # 5 EXIT_REJECTED the host or daemon refused the request in the
250
+ # worker's current state (busy, no active turn,
251
+ # something in flight); nothing was changed
252
+ # 6 EXIT_TURN_INCOMPLETE did not complete cleanly: a wait or turn timed out,
253
+ # or the awaited turn failed / was interrupted
254
+ # 7 BUDGET_EXIT a usage cap hard-stopped the run (defined above)
255
+ # 8 EXIT_RATE_LIMITED Anthropic rate-limited the account (429). Distinct from
256
+ # BUDGET_EXIT, which is CCX's OWN cap: this one is imposed
257
+ # upstream and clears on its own reset, so a caller that
258
+ # lumps the two together cannot tell "I set this limit"
259
+ # from "wait until the quota returns"
260
+ # 9 EXIT_API_TRANSIENT Anthropic could not serve the request right now, but
261
+ # the identical call could succeed later — a 529
262
+ # (capacity, and free of quota), a 5xx, or a stream that
263
+ # died mid-response. Split from EXIT_RATE_LIMITED because
264
+ # the remedy differs: this clears on its own in minutes,
265
+ # where a rate limit waits for a reset. Both are split
266
+ # from EXIT_UNAVAILABLE, which stays the permanent-failure
267
+ # code (auth, billing, bad request) where retrying is
268
+ # pointless
269
+ EXIT_ERROR = 1
270
+ EXIT_NOT_FOUND = 2
271
+ EXIT_UNAVAILABLE = 3
272
+ EXIT_WAITING = 4
273
+ EXIT_REJECTED = 5
274
+ EXIT_TURN_INCOMPLETE = 6
275
+ EXIT_RATE_LIMITED = 8
276
+ EXIT_API_TRANSIENT = 9
277
+
278
+ # Lifecycle states
279
+ class S:
280
+ BOOTING = "booting"
281
+ IDLE = "idle"
282
+ BUSY = "busy"
283
+ WAITING = "waiting"
284
+ IDLE_KILLED = "idle-killed"
285
+ RESUMING = "resuming"
286
+ CRASHED = "crashed"
287
+ DEAD = "dead"
288
+ BUDGET_STOPPED = "budget-stopped" # hard-stopped because a usage cap was reached
File without changes