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,579 @@
1
+ """Store-side creation steps: validate, prepare, activate, dispatch fence, reconcile.
2
+
3
+ Used by creation.create (also for forks) with caller-supplied native submissions.
4
+ Durable fences retain one owned full UUID and independent provider-native binding;
5
+ compatible exact-ID retries preserve the original effect without replay.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from pathlib import Path
11
+ from dataclasses import dataclass
12
+ from typing import Mapping
13
+
14
+ from ccx import constants
15
+ from ccx.errors import InvalidRequest
16
+ from ccx.store.records import sanitize_name
17
+ from ccx.system.cwd_paths import canonical_cwd
18
+ from ccx.store.root import CreationConflict, CreationIndeterminate
19
+ from ccx.store.store import GenerationStore, fingerprint_v2
20
+ from ccx.store.identity import canonical_worker_id, mint_worker_id
21
+
22
+
23
+ _ENGINES = {"claude", "codex"}
24
+
25
+
26
+ def _reject_model_as_agent(agent: str | None) -> None:
27
+ """Reject a model alias used instead of a Claude subagent name."""
28
+ if agent and agent.strip().lower() in constants.MODEL_ALIASES:
29
+ raise InvalidRequest(
30
+ f"agent {agent!r} is a model, not an agent — pass it as model={agent!r} "
31
+ f"(CLI: `--model {agent}`). An agent names a subagent, and `claude` "
32
+ f"would exit at boot with \"--agent '{agent}' not found\"."
33
+ )
34
+
35
+
36
+ def validated_create_spec(*, engine: str, cwd: str, model: str | None, effort: str | None,
37
+ idle_timeout: int, permissions: str | None, sandbox: str | None,
38
+ approval_policy: str | None, agent: str | None,
39
+ append_system_prompt: str | None, allowed_tools: str | None,
40
+ disallowed_tools: str | None, add_dir: list[str] | None,
41
+ permission_mode: str | None) -> tuple[str, dict]:
42
+ if engine not in _ENGINES:
43
+ raise InvalidRequest(f"unsupported engine: {engine!r}")
44
+ directory = canonical_cwd(cwd)
45
+ if not Path(directory).is_dir():
46
+ raise InvalidRequest(f"cwd is not a directory: {directory}")
47
+ if type(idle_timeout) is not int or idle_timeout < 0:
48
+ raise InvalidRequest("idle_timeout must be a non-negative integer")
49
+ if permissions is not None and permissions not in {"full", "restricted"}:
50
+ raise InvalidRequest("permissions must be full or restricted")
51
+ if engine == "claude":
52
+ _reject_model_as_agent(agent)
53
+ if sandbox is not None or approval_policy is not None:
54
+ from ccx.errors import CapabilityUnsupported
55
+ raise CapabilityUnsupported(
56
+ "Claude does not accept Codex sandbox/approval policy",
57
+ engine="claude", capability="create_sandbox_or_approval_policy",
58
+ )
59
+ args = {
60
+ "model": model, "effort": effort, "agent": agent,
61
+ "append_system_prompt": append_system_prompt,
62
+ "allowed_tools": allowed_tools, "disallowed_tools": disallowed_tools,
63
+ "add_dir": [canonical_cwd(d) for d in (add_dir or [])],
64
+ "permission_mode": permission_mode,
65
+ "skip_permissions": permissions != "restricted",
66
+ "idle_timeout_s": idle_timeout,
67
+ }
68
+ else:
69
+ if any(value is not None for value in (
70
+ agent, append_system_prompt, allowed_tools, disallowed_tools,
71
+ permission_mode, add_dir,
72
+ )):
73
+ raise InvalidRequest("Codex does not accept Claude launch options")
74
+ if permissions is not None and (sandbox is not None or approval_policy is not None):
75
+ raise InvalidRequest("permissions conflicts with sandbox/approval_policy")
76
+ if permissions == "restricted":
77
+ sandbox, approval_policy = "workspace-write", "on-request"
78
+ elif permissions == "full":
79
+ sandbox, approval_policy = "danger-full-access", "never"
80
+ sandbox = sandbox or "danger-full-access"
81
+ approval_policy = approval_policy or "never"
82
+ if sandbox not in {"read-only", "workspace-write", "danger-full-access"}:
83
+ raise InvalidRequest(f"unsupported Codex sandbox: {sandbox}")
84
+ if approval_policy not in {"untrusted", "on-request", "never"}:
85
+ raise InvalidRequest(f"unsupported Codex approval policy: {approval_policy}")
86
+ args = {
87
+ "model": model, "effort": effort, "idleTimeout": idle_timeout,
88
+ "sandbox": sandbox, "approvalPolicy": approval_policy,
89
+ }
90
+ return directory, args
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class PreparedWorker:
95
+ worker_id: str
96
+ engine: str
97
+ created: bool
98
+ reconciled: bool
99
+ activation_state: str
100
+ native_source: str
101
+ record: dict
102
+
103
+ @property
104
+ def worker(self) -> str:
105
+ return f"{self.engine}:{self.worker_id}"
106
+
107
+ @property
108
+ def short(self) -> str:
109
+ return self.worker_id[:8]
110
+
111
+ def as_dict(self) -> dict:
112
+ return {
113
+ "worker": self.worker, "worker_id": self.worker_id,
114
+ "engine": self.engine, "short": self.short,
115
+ "created": self.created, "reconciled": self.reconciled,
116
+ "record_committed": True, "activation_state": self.activation_state,
117
+ "native_ready": self.activation_state == "confirmed",
118
+ "native_source": self.native_source,
119
+ "created_at": self.record["createdAt"],
120
+ }
121
+
122
+
123
+ def prepare_worker(
124
+ store: GenerationStore,
125
+ *,
126
+ engine: str, cwd: str, worker_id: str | None = None,
127
+ name: str | None = None, model: str | None = None,
128
+ effort: str | None = None, idle_timeout: int = 900,
129
+ permissions: str | None = None, sandbox: str | None = None,
130
+ approval_policy: str | None = None, agent: str | None = None,
131
+ append_system_prompt: str | None = None,
132
+ allowed_tools: str | None = None,
133
+ disallowed_tools: str | None = None,
134
+ add_dir: list[str] | None = None,
135
+ permission_mode: str | None = None,
136
+ created_via: str = "python-api",
137
+ import_session_id: str | None = None,
138
+ imported_from: Mapping | None = None,
139
+ fork_anchor: Mapping | None = None,
140
+ spawned_by: Mapping | None = None,
141
+ _failpoint=None,
142
+ ) -> PreparedWorker:
143
+ """Durable immutable owner/claim/record, no native start or false success.
144
+
145
+ The original first native Claude session UUID is minted independently
146
+ of the CCX full worker ID and fsynced in the owner intent. Exact-ID
147
+ recovery reuses it even after crash before claim/record publication.
148
+ The record's actual timestamps and mutable fields are never overwritten
149
+ by a retry. Unknown original native fate cannot imply new startup.
150
+ """
151
+ from ccx.errors import InvalidRequest
152
+ from uuid import UUID
153
+ importing = import_session_id is not None
154
+ if importing != (imported_from is not None) or (importing and engine != "claude"):
155
+ raise InvalidRequest("Import requires the session ID and its saved source details")
156
+ if importing:
157
+ try:
158
+ if str(UUID(import_session_id)) != import_session_id:
159
+ raise ValueError("noncanonical native session")
160
+ except (ValueError, TypeError, AttributeError) as exc:
161
+ raise InvalidRequest("Import requires a complete Claude session ID") from exc
162
+ if imported_from.get("sessionId") != import_session_id:
163
+ raise InvalidRequest("Claude import session ID does not match its source")
164
+ origin = "ccx-import" if importing else "ccx-new"
165
+ provenance = dict(imported_from) if importing else None
166
+ if not isinstance(created_via, str) or not created_via or len(created_via) > 128:
167
+ raise InvalidRequest("created_via must be bounded, nonempty provenance")
168
+ if name is not None and (not isinstance(name, str) or not sanitize_name(name)):
169
+ raise InvalidRequest("display name must be nonempty or absent")
170
+ if not isinstance(cwd, str):
171
+ raise InvalidRequest("create cwd must be a path")
172
+ directory, args = validated_create_spec(
173
+ engine=engine, cwd=cwd, model=model, effort=effort,
174
+ idle_timeout=idle_timeout, permissions=permissions,
175
+ sandbox=sandbox, approval_policy=approval_policy,
176
+ agent=agent, append_system_prompt=append_system_prompt,
177
+ allowed_tools=allowed_tools, disallowed_tools=disallowed_tools,
178
+ add_dir=add_dir, permission_mode=permission_mode,
179
+ )
180
+ chosen = canonical_worker_id(worker_id) if worker_id is not None else mint_worker_id()
181
+ anchor = dict(fork_anchor) if isinstance(fork_anchor, Mapping) else None
182
+ if fork_anchor is not None and engine == "codex":
183
+ # Freeze the exact native source/terminal turn, whether its source is
184
+ # a CCX worker or an external native Codex thread.
185
+ if importing or anchor is None:
186
+ raise CreationIndeterminate("Codex fork point is not confirmed; inspect source worker")
187
+ from ccx.store.store import codex_fork_lineage
188
+ codex_fork_lineage(anchor, child_id=chosen)
189
+ seed = anchor.get("seed")
190
+ seed_stats = seed.get("stats") if isinstance(seed, dict) else None
191
+ turns = seed_stats.get("turns") if isinstance(seed_stats, dict) else None
192
+ last_at = seed.get("lastTurnAt") if isinstance(seed, dict) else None
193
+ last = seed.get("lastTurn") if isinstance(seed, dict) else None
194
+ overrides = anchor.get("overrides")
195
+ if (anchor.get("engine") != "codex"
196
+ or any(not isinstance(anchor.get(k), str)
197
+ or not 0 < len(anchor[k]) <= 256 or "\x00" in anchor[k]
198
+ for k in ("sourceNativeThreadId", "lastTurnId"))
199
+ or anchor.get("cwd") != directory or anchor.get("createArgs") != args
200
+ or type(anchor.get("forkedAtMs")) is not int or anchor["forkedAtMs"] <= 0
201
+ or not isinstance(seed, dict)
202
+ or set(seed) not in ({"stats", "lastTurnAt"},
203
+ {"stats", "lastTurnAt", "lastTurn"})
204
+ or not isinstance(seed_stats, dict) or set(seed_stats) != {"turns"}
205
+ or type(turns) is not int or not 0 <= turns <= 1_000_000_000
206
+ or (last_at is not None and (type(last_at) is not int or last_at < 0))
207
+ or ("lastTurn" in seed and (not isinstance(last, dict)
208
+ or last.get("turnId") != anchor["lastTurnId"]))
209
+ or not isinstance(overrides, dict)
210
+ or set(overrides) != {"model", "effort"}
211
+ or any(value is not None and (not isinstance(value, str)
212
+ or not value.strip())
213
+ for value in overrides.values())
214
+ or any(value is not None and args.get(key) != value
215
+ for key, value in overrides.items())):
216
+ raise CreationIndeterminate("Codex fork settings changed; inspect source worker")
217
+ if fork_anchor is not None and engine != "codex":
218
+ # A Claude fork requires a complete frozen source point, never an
219
+ # arbitrary unverified native UUID or a caller's changed launch spec.
220
+ from ccx.store.identity import InvalidTarget
221
+ if engine != "claude" or importing or anchor is None:
222
+ raise CreationIndeterminate("Claude fork point is not confirmed; inspect source worker")
223
+ try:
224
+ source_id = canonical_worker_id(anchor.get("sourceWorkerId"))
225
+ source_sid = anchor.get("sourceNativeSessionId")
226
+ # Native Claude SIDs (including exact imported sessions) need a
227
+ # canonical UUID, not CCX's stricter worker-ID UUIDv4 restriction.
228
+ if not isinstance(source_sid, str) or str(UUID(source_sid)) != source_sid:
229
+ raise ValueError("noncanonical native SID")
230
+ except (InvalidTarget, ValueError, TypeError, AttributeError) as exc:
231
+ raise CreationIndeterminate("native Claude fork point has invalid source identity") from exc
232
+ seed = anchor.get("seed")
233
+ overrides = anchor.get("overrides")
234
+ seed_stats = seed.get("stats") if isinstance(seed, dict) else None
235
+ turns = seed_stats.get("turns") if isinstance(seed_stats, dict) else None
236
+ last_at = seed.get("lastTurnAt") if isinstance(seed, dict) else None
237
+ if (set(anchor) != {"engine", "sourceWorkerId", "sourceNativeSessionId",
238
+ "resumeAt", "transcript", "cwd", "createArgs", "seed",
239
+ "overrides", "forkedAtMs"}
240
+ or anchor.get("engine") != "claude"
241
+ or source_id in {chosen, source_sid} or source_sid == chosen
242
+ or anchor.get("cwd") != directory or anchor.get("createArgs") != args
243
+ or not isinstance(anchor.get("resumeAt"), str)
244
+ or not 0 < len(anchor["resumeAt"]) <= 256
245
+ or not isinstance(anchor.get("transcript"), str)
246
+ or not anchor["transcript"].startswith("/")
247
+ or type(anchor.get("forkedAtMs")) is not int or anchor["forkedAtMs"] <= 0
248
+ or not isinstance(seed, dict) or set(seed) != {"stats", "lastTurnAt"}
249
+ or not isinstance(seed.get("stats"), dict)
250
+ or set(seed["stats"]) != {"turns"}
251
+ or type(turns) is not int or not 0 <= turns <= 1_000_000_000
252
+ or (last_at is not None and (type(last_at) is not int or last_at < 0))
253
+ or not isinstance(overrides, dict)
254
+ or set(overrides) != {"model", "effort"}
255
+ or any(value is not None and (not isinstance(value, str) or not value.strip())
256
+ for value in overrides.values())
257
+ or any(value is not None and args.get(key) != value
258
+ for key, value in overrides.items())):
259
+ raise CreationIndeterminate("Claude fork settings changed; inspect source worker")
260
+ digest = fingerprint_v2(
261
+ engine=engine, cwd=directory, create_args=args, fork_anchor=anchor,
262
+ )
263
+ original = store.creation_binding(
264
+ chosen, engine=engine, fingerprint=digest,
265
+ provenance=origin, imported_from=provenance)
266
+ native = (original if original is not None else
267
+ {"sessionId": import_session_id or mint_worker_id()}
268
+ if engine == "claude" else {})
269
+ if engine == "claude":
270
+ session_id = native.get("sessionId")
271
+ if importing:
272
+ if session_id != import_session_id:
273
+ raise CreationConflict("import retry changed original native SID")
274
+ else:
275
+ canonical_worker_id(session_id)
276
+ if anchor is not None and session_id in {
277
+ chosen, anchor["sourceWorkerId"], anchor["sourceNativeSessionId"]}:
278
+ raise CreationIndeterminate("fork child native SID must be independent")
279
+ native_source = f"claude-session:{session_id}"
280
+ else:
281
+ if native:
282
+ raise CreationIndeterminate("Codex native thread bound before first activation")
283
+ native_source = f"ccx-worker:{chosen}"
284
+ try:
285
+ if importing:
286
+ reservation = store.reserve_import_creation(
287
+ chosen, session_id=import_session_id, fingerprint=digest,
288
+ native_binding=native, imported_from=provenance)
289
+ else:
290
+ reservation = store.reserve_creation(
291
+ chosen, engine=engine, fingerprint=digest, native_binding=native,
292
+ fork_anchor=anchor, spawned_by=spawned_by, _failpoint=_failpoint,
293
+ )
294
+ except CreationConflict:
295
+ if importing:
296
+ raise # No replacement CCX worker/native ID after an import conflict.
297
+ # Two independent processes can race on the same caller-provided
298
+ # worker UUID before either sees the first durable owner intent.
299
+ # Claude would otherwise mint TWO distinct native sessionIds. The
300
+ # winner's immutable owner intent is authoritative: reread it under
301
+ # ACTIVE, prove engine+original spec, and retry only that exact
302
+ # native binding. Never mint a substitute CCX UUID.
303
+ previous = store.creation_binding(
304
+ chosen, engine=engine, fingerprint=digest,
305
+ provenance=origin, imported_from=provenance,
306
+ )
307
+ if previous is None or previous == native:
308
+ raise
309
+ native = previous
310
+ if engine == "claude":
311
+ session_id = native.get("sessionId")
312
+ canonical_worker_id(session_id)
313
+ if anchor is not None and session_id in {
314
+ chosen, anchor["sourceWorkerId"], anchor["sourceNativeSessionId"]}:
315
+ raise CreationIndeterminate("fork retry native SID differs from frozen source")
316
+ native_source = f"claude-session:{session_id}"
317
+ elif native:
318
+ raise CreationIndeterminate("Codex original native identity unexpectedly prebound")
319
+ reservation = store.reserve_creation(
320
+ chosen, engine=engine, fingerprint=digest, native_binding=native,
321
+ fork_anchor=anchor, spawned_by=spawned_by, _failpoint=_failpoint,
322
+ )
323
+ if reservation.activation_state == "unprepared":
324
+ # A visible first record after a crash is NOT necessarily an active
325
+ # owner yet; reconcile immutable bytes and promote the reservation
326
+ # even if reserve_creation reported record_committed=True.
327
+ initial_record_exists = store.recover_first_record(reservation)
328
+ else:
329
+ # After original prepared/submitted/confirmed native intent, NO
330
+ # replay/rebuild of first-record contents is safe. Only a previously
331
+ # promoted complete worker may be read back.
332
+ if not reservation.record_committed:
333
+ raise CreationIndeterminate(
334
+ "native activation intent exists without a complete first record"
335
+ )
336
+ initial_record_exists = True
337
+ if initial_record_exists:
338
+ record = store.record(f"{engine}:{chosen}")
339
+ else:
340
+ ts = int(time.time() * 1000)
341
+ actual_name = sanitize_name(name) or (
342
+ ("ccx-" if engine == "claude" else "codex-") + chosen[:8]
343
+ )
344
+ record = {
345
+ "schemaVersion": 2, "workerId": chosen, "engine": engine,
346
+ "short": chosen[:8], "name": actual_name,
347
+ "cwd": directory, "createdAt": ts,
348
+ "lastTurnAt": anchor["seed"]["lastTurnAt"] if anchor else None,
349
+ "createdVia": created_via,
350
+ "spawnedBy": reservation.spawned_by,
351
+ **({"origin": "ccx-import", "importedFrom": provenance} if importing else {}),
352
+ "createSha256": digest, "fingerprintVersion": 2,
353
+ "forkAnchor": anchor, "createArgs": args,
354
+ **({"forkedFrom": (
355
+ codex_fork_lineage(anchor, child_id=chosen) if engine == "codex"
356
+ else {key: anchor[key] for key in (
357
+ "engine", "sourceWorkerId", "sourceNativeSessionId",
358
+ "resumeAt", "forkedAtMs")}
359
+ )} if anchor else {}),
360
+ **({"lastTurn": dict(anchor["seed"]["lastTurn"])}
361
+ if anchor and engine == "codex" and "lastTurn" in anchor["seed"] else {}),
362
+ "native": {
363
+ **native, "nativeState": (
364
+ "import-resume-pending" if importing else "not-started-proven"),
365
+ **({"threadId": None} if engine == "codex" else {}),
366
+ },
367
+ "terminalOnly": False,
368
+ "lifecycle": {
369
+ "state": "prepared", "since": ts,
370
+ "activeTurnId": None, "hostPid": None,
371
+ "workerPid": None, "lastError": None,
372
+ },
373
+ "stats": {
374
+ "turns": anchor["seed"]["stats"]["turns"] if anchor else 0,
375
+ },
376
+ "recordRevision": 0,
377
+ }
378
+ if engine == "claude":
379
+ from ccx.store.paths import transcript_path_for
380
+ record["transcript"] = str(transcript_path_for(directory, session_id))
381
+ record["transcriptJson"] = str(
382
+ store.pointer.generation_root / "workers" /
383
+ "claude" / chosen / "transcript.json"
384
+ )
385
+ else:
386
+ record.update(
387
+ nativeArchive={"state": "active", "attempts": 0},
388
+ pendingApprovals=[], lastReport=None, context=None,
389
+ activeCompact=None, lastCompact=None,
390
+ )
391
+ store.publish_first_record(reservation, record, _failpoint=_failpoint)
392
+ latest = store.reserve_creation(
393
+ chosen, engine=engine, fingerprint=digest, native_binding=native,
394
+ fork_anchor=anchor, provenance=origin, imported_from=provenance,
395
+ )
396
+ return PreparedWorker(
397
+ worker_id=chosen, engine=engine, created=not reservation.reconciled,
398
+ reconciled=reservation.reconciled,
399
+ activation_state=latest.activation_state,
400
+ native_source=native_source,
401
+ record=record,
402
+ )
403
+
404
+
405
+
406
+ def activate_prepared_worker(
407
+ store: GenerationStore, prepared: PreparedWorker, *,
408
+ bounded_native_submit,
409
+ dispatch_budget_s: float = 1.0,
410
+ _failpoint=None,
411
+ ) -> PreparedWorker:
412
+ """Run one native *submission* through reviewed test/injected adapter.
413
+
414
+ This function is INTERNAL: the actual host/codexd adapter still must
415
+ verify SO_PEERCRED, PID/start, owner, native source and real timeout before
416
+ production wiring. A caller-controlled result or hash alone is NOT
417
+ sufficient; the injected low-level transport must return its original
418
+ source/receipt, and unknown native outcomes cannot authorize a retry.
419
+ Tests use fake native adapters only. No default provider is imported.
420
+ """
421
+ from ccx.store.root import CreationIndeterminate, NativeEffectIndeterminate
422
+ if not callable(bounded_native_submit):
423
+ raise CreationIndeterminate("Engine is unavailable; check its executable and retry")
424
+ reservation = store.reserve_creation(
425
+ prepared.worker_id, engine=prepared.engine,
426
+ fingerprint=prepared.record["createSha256"],
427
+ native_binding=(
428
+ {"sessionId": prepared.record["native"]["sessionId"]}
429
+ if prepared.engine == "claude" else {}
430
+ ),
431
+ provenance=("ccx-import" if prepared.record.get("origin") == "ccx-import"
432
+ else "ccx-new"),
433
+ imported_from=prepared.record.get("importedFrom"),
434
+ fork_anchor=prepared.record.get("forkAnchor"),
435
+ )
436
+ if reservation.activation_state == "confirmed":
437
+ record = store.bind_confirmed_native(prepared.worker)
438
+ return PreparedWorker(
439
+ prepared.worker_id, prepared.engine, False, True, "confirmed",
440
+ prepared.native_source, record,
441
+ )
442
+ if reservation.activation_state == "submitted-unknown":
443
+ raise NativeEffectIndeterminate(
444
+ "Worker startup was submitted but not confirmed; "
445
+ "check the existing worker instead of creating it twice"
446
+ )
447
+ original = store.prepare_native_activation(
448
+ reservation, native_source=prepared.native_source,
449
+ )
450
+ receipt = store.native_activation_dispatch(
451
+ original, bounded_submit=bounded_native_submit,
452
+ dispatch_budget_s=dispatch_budget_s, _failpoint=_failpoint,
453
+ )
454
+ if (receipt.get("accepted") is not True
455
+ or receipt.get("workerId") != prepared.worker_id
456
+ or receipt.get("engine") != prepared.engine
457
+ or receipt.get("source") != original.source
458
+ or receipt.get("submissionId") != original.submission_id
459
+ or not isinstance(receipt.get("nativeEvidenceSha256"), str)):
460
+ raise NativeEffectIndeterminate(
461
+ "Engine did not confirm startup; "
462
+ "check this worker before retrying"
463
+ )
464
+ binding = receipt.get("nativeBinding")
465
+ if not isinstance(binding, dict):
466
+ raise NativeEffectIndeterminate(
467
+ "Engine start response does not match this worker; inspect status"
468
+ )
469
+ # The native adapter must independently attest the source and original
470
+ # immutable client submission ID from the provider's actual event/receipt.
471
+ store.confirm_native_activation(
472
+ original, native_evidence_sha256=receipt["nativeEvidenceSha256"],
473
+ native_binding=binding,
474
+ )
475
+ updated = store.bind_confirmed_native(prepared.worker)
476
+ return PreparedWorker(
477
+ prepared.worker_id, prepared.engine, False, True, "confirmed",
478
+ prepared.native_source, updated,
479
+ )
480
+
481
+
482
+
483
+ def dispatch_prepared_codex_native(
484
+ store: GenerationStore, prepared: PreparedWorker, *,
485
+ bounded_native_submit, dispatch_budget_s: float = 2.0,
486
+ ) -> PreparedWorker:
487
+ """Admit ONE original Codex thread/start; do not await native completion.
488
+
489
+ A positive daemon admission is not proof of a provider thread. Retain the
490
+ original submitted-unknown creation fence until the daemon's *observe-only*
491
+ candidate/thread-read evidence is verified by reconcile_submitted_native.
492
+ This is the existing global creation/activation store, not a second owner.
493
+ """
494
+ from ccx.store.root import CreationIndeterminate, NativeEffectIndeterminate
495
+ if prepared.engine != "codex" or not callable(bounded_native_submit):
496
+ raise CreationIndeterminate("Codex startup has not been submitted; retry creation")
497
+ reservation = store.reserve_creation(
498
+ prepared.worker_id, engine="codex",
499
+ fingerprint=prepared.record["createSha256"], native_binding={},
500
+ fork_anchor=prepared.record.get("forkAnchor"),
501
+ )
502
+ if reservation.activation_state == "confirmed":
503
+ bound = store.bind_confirmed_native(prepared.worker)
504
+ return PreparedWorker(prepared.worker_id, "codex", False, True,
505
+ "confirmed", prepared.native_source, bound)
506
+ if reservation.activation_state == "submitted-unknown":
507
+ # Retrying this path never calls the provider. The caller must observe.
508
+ return PreparedWorker(prepared.worker_id, "codex", False, True,
509
+ "submitted-unknown", prepared.native_source,
510
+ store.record(prepared.worker))
511
+ original = store.prepare_native_activation(
512
+ reservation, native_source=prepared.native_source,
513
+ )
514
+ receipt = store.native_activation_dispatch(
515
+ original, bounded_submit=bounded_native_submit,
516
+ dispatch_budget_s=dispatch_budget_s,
517
+ )
518
+ if (receipt.get("ok") is not True or receipt.get("queued") is not True
519
+ or receipt.get("workerId") != prepared.worker_id
520
+ or receipt.get("engine") != "codex"
521
+ or receipt.get("source") != original.source
522
+ or receipt.get("submissionId") != original.submission_id):
523
+ # Preserve daemon's actual refusal reason even when marker fsync or
524
+ # claim CAS failed. The message is a PROVISIONAL diagnostic, never
525
+ # proof that missing evidence allows re-dispatch of this UUID.
526
+ reason = receipt.get("error")
527
+ if not isinstance(reason, str) or not reason:
528
+ reason = "admission unproven"
529
+ raise NativeEffectIndeterminate(
530
+ "Codex startup was refused or not confirmed; check its worker: " + reason[:450]
531
+ + "; submitted-unknown fence retained unless original proof seals"
532
+ )
533
+ return PreparedWorker(prepared.worker_id, "codex", False, True,
534
+ "submitted-unknown", prepared.native_source,
535
+ store.record(prepared.worker))
536
+
537
+
538
+ def reconcile_submitted_native(
539
+ store: GenerationStore, prepared: PreparedWorker, *,
540
+ original_receipt: Mapping,
541
+ ) -> PreparedWorker:
542
+ """Forensic-only reconcile one original submitted native effect.
543
+
544
+ Caller must obtain the original positive native source/receipt from a
545
+ reviewed provider-evidence reader; a missing response must NOT be replayed.
546
+ The durable existing submissionId anchors the evidence so another
547
+ thread/session ID cannot be installed opportunistically.
548
+ """
549
+ from ccx.store.root import CreationIndeterminate, NativeEffectIndeterminate
550
+ from ccx.store.store import NativeActivation
551
+ if not isinstance(original_receipt, Mapping):
552
+ raise NativeEffectIndeterminate("No engine response was recorded; inspect worker status")
553
+ activation = store.native_activation(prepared.worker)
554
+ if activation.get("state") not in ("submitted-unknown", "confirmed"):
555
+ raise CreationIndeterminate("Engine start was not submitted; retry creation")
556
+ if (original_receipt.get("accepted") is not True
557
+ or original_receipt.get("workerId") != prepared.worker_id
558
+ or original_receipt.get("engine") != prepared.engine
559
+ or original_receipt.get("source") != prepared.native_source
560
+ or original_receipt.get("submissionId") != activation.get("submissionId")
561
+ or not isinstance(original_receipt.get("nativeBinding"), dict)
562
+ or not isinstance(original_receipt.get("nativeEvidenceSha256"), str)):
563
+ raise NativeEffectIndeterminate(
564
+ "Engine result does not match this worker; inspect status"
565
+ )
566
+ original = NativeActivation(
567
+ prepared.worker_id, prepared.engine, prepared.native_source,
568
+ activation["submissionId"], "submitted-unknown", store.generation,
569
+ )
570
+ store.confirm_native_activation(
571
+ original,
572
+ native_evidence_sha256=original_receipt["nativeEvidenceSha256"],
573
+ native_binding=original_receipt["nativeBinding"],
574
+ )
575
+ bound = store.bind_confirmed_native(prepared.worker)
576
+ return PreparedWorker(
577
+ prepared.worker_id, prepared.engine, False, True, "confirmed",
578
+ prepared.native_source, bound,
579
+ )