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,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
|
+
)
|