ccx-native 0.0.1 → 0.2.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 +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -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 +807 -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 +269 -0
- package/ccx/claude/host/session.py +5244 -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 +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -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/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -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 +1558 -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 +2424 -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/history_store.py +131 -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 +1454 -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/models.py +225 -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 +296 -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 +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -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 +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -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 +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -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/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,2332 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import stat
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import ExitStack, contextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, Iterator, Mapping
|
|
12
|
+
from uuid import uuid4, uuid5, NAMESPACE_URL
|
|
13
|
+
from ccx.store.liveness import recorded_host_loaded
|
|
14
|
+
from ccx.store.worker_state import normalize_native_state
|
|
15
|
+
from ccx.store.identity import (
|
|
16
|
+
ENGINES, EngineMismatch, InvalidTarget, OwnerConflict, UnknownWorker,
|
|
17
|
+
WorkerOwner, canonical_worker_id, parse_managed_target,
|
|
18
|
+
)
|
|
19
|
+
from ccx.store.root import (
|
|
20
|
+
CreationConflict, CreationIndeterminate, GenerationError, GenerationPointer, GenerationUnavailable, NativeActionDenied, NativeEffectIndeterminate, RecordConflict, StaleGeneration, StorageIntegrityError, _CLAIM_SCHEMA, _FAMILIES, _FILE_FLAGS, _HASH, _NATIVE_WRITES, _OWNER_SCHEMA, _RECEIPT_SCHEMA, _flock_at, _open_child_dir, _open_dir, _read_json, _write_json, inspect_generation
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CreationReservation:
|
|
26
|
+
worker_id: str
|
|
27
|
+
engine: str
|
|
28
|
+
create_sha256: str
|
|
29
|
+
reconciled: bool
|
|
30
|
+
record_committed: bool
|
|
31
|
+
# Original native activation fate, never inferred from reconciled/record_committed.
|
|
32
|
+
activation_state: str = "unproven"
|
|
33
|
+
submission_id: str | None = None
|
|
34
|
+
# Immutable managed caller from the first owner intent, not the retry env.
|
|
35
|
+
spawned_by: dict | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class NativeActivation:
|
|
40
|
+
"""Exact original activation intent (not permission to replay a native call)."""
|
|
41
|
+
worker_id: str
|
|
42
|
+
engine: str
|
|
43
|
+
source: str
|
|
44
|
+
submission_id: str
|
|
45
|
+
state: str
|
|
46
|
+
generation: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class OperationIntent:
|
|
51
|
+
worker_id: str
|
|
52
|
+
engine: str
|
|
53
|
+
family: str
|
|
54
|
+
operation_id: str
|
|
55
|
+
intent_sha256: str
|
|
56
|
+
state: str
|
|
57
|
+
generation: str
|
|
58
|
+
dispatch_token: str | None = None
|
|
59
|
+
replayed: bool = False
|
|
60
|
+
native_evidence_sha256: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _digest(obj: object) -> str:
|
|
64
|
+
try:
|
|
65
|
+
raw = json.dumps(obj, sort_keys=True, separators=(",", ":"),
|
|
66
|
+
ensure_ascii=False, allow_nan=False).encode("utf-8")
|
|
67
|
+
except (ValueError, TypeError) as exc:
|
|
68
|
+
raise StorageIntegrityError("noncanonical immutable input") from exc
|
|
69
|
+
return hashlib.sha256(raw).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def converted_binding_digest(worker_id: str, engine: str, native_binding: Mapping,
|
|
73
|
+
converted_from: Mapping) -> str:
|
|
74
|
+
"""One domain-separated offline-conversion digest, NOT a native create hash."""
|
|
75
|
+
return _digest({
|
|
76
|
+
"kind": "ccx-convert", "workerId": canonical_worker_id(worker_id),
|
|
77
|
+
"engine": engine, "nativeBinding": dict(native_binding),
|
|
78
|
+
"convertedFrom": dict(converted_from),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _converted_binding(owner: Mapping, claim: Mapping, record: Mapping) -> str:
|
|
83
|
+
"""Pure proof of one current-format converted native identity; never an ACK."""
|
|
84
|
+
intent = owner.get("createIntent")
|
|
85
|
+
cf = intent.get("convertedFrom") if isinstance(intent, dict) else None
|
|
86
|
+
native = record.get("native")
|
|
87
|
+
activation = claim.get("nativeActivation")
|
|
88
|
+
wid, engine = owner.get("workerId"), owner.get("engine")
|
|
89
|
+
if (owner.get("schemaVersion") != _OWNER_SCHEMA
|
|
90
|
+
or owner.get("status") != "worker" or owner.get("provenance") != "ccx-convert"
|
|
91
|
+
or not isinstance(intent, dict) or not isinstance(cf, dict)
|
|
92
|
+
or not isinstance(native, dict) or not isinstance(activation, dict)
|
|
93
|
+
or engine not in ENGINES or record.get("workerId") != wid
|
|
94
|
+
or record.get("engine") != engine
|
|
95
|
+
or claim.get("workerId") != wid or claim.get("engine") != engine
|
|
96
|
+
or owner.get("generation") != claim.get("generation")
|
|
97
|
+
or claim.get("schemaVersion") != _CLAIM_SCHEMA
|
|
98
|
+
or record.get("schemaVersion") != 2
|
|
99
|
+
or any(doc.get("fingerprintVersion") != 2
|
|
100
|
+
for doc in (intent, claim, record))
|
|
101
|
+
or intent.get("forkAnchor") is not None or claim.get("forkAnchor") is not None
|
|
102
|
+
or record.get("forkAnchor") is not None
|
|
103
|
+
or intent.get("activationStateAtReservation") != "converted"
|
|
104
|
+
or activation.get("state") != "confirmed"
|
|
105
|
+
or activation.get("submissionId") is not None
|
|
106
|
+
or claim.get("activatedAtMs") is not None
|
|
107
|
+
or record.get("terminalOnly") is not False
|
|
108
|
+
or record.get("nativeCreate") is not None
|
|
109
|
+
or record.get("spawnedBy") != intent.get("spawnedBy")
|
|
110
|
+
or record.get("importedFrom") != cf.get("importedFrom")):
|
|
111
|
+
raise StorageIntegrityError("converted owner/claim/record provenance differs")
|
|
112
|
+
if (not isinstance(cf.get("legacyShort"), str)
|
|
113
|
+
or not re.fullmatch(r"[0-9a-f]{8}", cf["legacyShort"])
|
|
114
|
+
or not isinstance(cf.get("sourcePath"), str) or not cf["sourcePath"]
|
|
115
|
+
or "\x00" in cf["sourcePath"]
|
|
116
|
+
or any(not isinstance(cf.get(key), str)
|
|
117
|
+
or not _HASH.fullmatch(cf[key])
|
|
118
|
+
for key in ("sourceRecordSha256", "planSha256"))
|
|
119
|
+
or any(cf.get(key) is not None
|
|
120
|
+
and (not isinstance(cf[key], str) or not _HASH.fullmatch(cf[key]))
|
|
121
|
+
for key in ("originalClaimSha256", "originalCreateSha256"))
|
|
122
|
+
or (cf.get("importedFrom") is not None
|
|
123
|
+
and not isinstance(cf.get("importedFrom"), dict))):
|
|
124
|
+
raise StorageIntegrityError("converted frozen source/plan proof malformed")
|
|
125
|
+
if cf.get("importedFrom") is None:
|
|
126
|
+
if record.get("origin") == "ccx-import" or "importedFrom" in record:
|
|
127
|
+
raise StorageIntegrityError("converted import history has no original proof")
|
|
128
|
+
elif record.get("origin") != "ccx-import":
|
|
129
|
+
raise StorageIntegrityError("converted import history lost original origin")
|
|
130
|
+
nb = intent.get("nativeBinding")
|
|
131
|
+
if not isinstance(nb, dict) or nb != claim.get("nativeBinding"):
|
|
132
|
+
raise StorageIntegrityError("converted original native binding differs")
|
|
133
|
+
if engine == "claude":
|
|
134
|
+
sid = nb.get("sessionId")
|
|
135
|
+
if (set(nb) != {"sessionId"} or not isinstance(sid, str) or not sid
|
|
136
|
+
or cf.get("nativeProof") is not None
|
|
137
|
+
or (cf.get("importedFrom") is not None
|
|
138
|
+
and cf["importedFrom"].get("sessionId") != sid)
|
|
139
|
+
or native.get("sessionId") != sid
|
|
140
|
+
or native.get("nativeState") != "session-bound"):
|
|
141
|
+
raise StorageIntegrityError("converted Claude SID/proof differs")
|
|
142
|
+
source = f"claude-session:{sid}"
|
|
143
|
+
else:
|
|
144
|
+
tid = nb.get("threadId")
|
|
145
|
+
proof = cf.get("nativeProof")
|
|
146
|
+
if (set(nb) != {"threadId"} or not isinstance(tid, str) or not tid
|
|
147
|
+
or not isinstance(proof, dict)
|
|
148
|
+
or set(proof) != {"kind", "threadId", "threadSource",
|
|
149
|
+
"headerPath", "headerSha256"}
|
|
150
|
+
or proof.get("kind") != "codex-session-meta"
|
|
151
|
+
or proof.get("threadId") != tid
|
|
152
|
+
or not isinstance(proof.get("headerPath"), str)
|
|
153
|
+
or not proof["headerPath"] or "\x00" in proof["headerPath"]
|
|
154
|
+
or not isinstance(proof.get("headerSha256"), str)
|
|
155
|
+
or not _HASH.fullmatch(proof["headerSha256"])):
|
|
156
|
+
raise StorageIntegrityError("converted Codex session-meta proof malformed")
|
|
157
|
+
source = proof.get("threadSource")
|
|
158
|
+
original = source.removeprefix("ccx-worker:") if isinstance(source, str) else ""
|
|
159
|
+
if (not isinstance(source, str) or not source.startswith("ccx-worker:")
|
|
160
|
+
or original[:8] != cf["legacyShort"]
|
|
161
|
+
or (original != cf["legacyShort"]
|
|
162
|
+
and not _is_canonical_worker_uuid(original))
|
|
163
|
+
or native.get("threadId") != tid
|
|
164
|
+
or native.get("threadSource") != source
|
|
165
|
+
or native.get("nativeState") != "thread-bound"):
|
|
166
|
+
raise StorageIntegrityError("converted Codex original source tag differs")
|
|
167
|
+
digest = converted_binding_digest(wid, engine, nb, cf)
|
|
168
|
+
if (any(doc.get("createSha256") != digest
|
|
169
|
+
for doc in (intent, claim, record))
|
|
170
|
+
or activation.get("nativeEvidenceSha256") != digest
|
|
171
|
+
or activation.get("nativeBinding") != nb
|
|
172
|
+
or activation.get("source") != source):
|
|
173
|
+
raise StorageIntegrityError("converted native binding digest/source differs")
|
|
174
|
+
return source
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _is_canonical_worker_uuid(value: str) -> bool:
|
|
178
|
+
try:
|
|
179
|
+
canonical_worker_id(value)
|
|
180
|
+
return True
|
|
181
|
+
except InvalidTarget:
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def codex_fork_lineage(anchor: Mapping, *, child_id: str | None = None) -> dict:
|
|
186
|
+
"""Project a frozen Codex native fork; external source has no CCX owner."""
|
|
187
|
+
from ccx.store.identity import InvalidTarget
|
|
188
|
+
keys = {"engine", "sourceWorkerId", "sourceNativeThreadId", "lastTurnId",
|
|
189
|
+
"cwd", "createArgs", "seed", "overrides", "forkedAtMs"}
|
|
190
|
+
if not isinstance(anchor, Mapping) or anchor.get("engine") != "codex":
|
|
191
|
+
raise CreationIndeterminate("Codex fork source identity is invalid")
|
|
192
|
+
external = anchor.get("sourceKind") == "codex-thread"
|
|
193
|
+
if set(anchor) != (keys | {"sourceKind"} if external else keys):
|
|
194
|
+
raise CreationIndeterminate("Codex fork source identity is invalid")
|
|
195
|
+
if external:
|
|
196
|
+
if anchor.get("sourceWorkerId") is not None:
|
|
197
|
+
raise CreationIndeterminate("external Codex source has no CCX worker owner")
|
|
198
|
+
else:
|
|
199
|
+
try:
|
|
200
|
+
source = canonical_worker_id(anchor.get("sourceWorkerId"))
|
|
201
|
+
except (InvalidTarget, TypeError, ValueError) as exc:
|
|
202
|
+
raise CreationIndeterminate("native Codex fork source worker ID invalid") from exc
|
|
203
|
+
if source == child_id:
|
|
204
|
+
raise CreationIndeterminate("Codex child may not fork its own worker ID")
|
|
205
|
+
lineage = {key: anchor[key] for key in (
|
|
206
|
+
"engine", "sourceWorkerId", "sourceNativeThreadId",
|
|
207
|
+
"lastTurnId", "forkedAtMs",
|
|
208
|
+
)}
|
|
209
|
+
if external:
|
|
210
|
+
lineage["sourceKind"] = "codex-thread"
|
|
211
|
+
return lineage
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def fingerprint_v2(*, engine: str, cwd: str, create_args: Mapping,
|
|
215
|
+
fork_anchor: Mapping | None = None) -> str:
|
|
216
|
+
"""Immutable creation spec with a frozen fork anchor.
|
|
217
|
+
|
|
218
|
+
An exact fork anchor must already be frozen by the caller. No display name
|
|
219
|
+
or mutable lifecycle field enters the immutable preimage.
|
|
220
|
+
"""
|
|
221
|
+
if engine not in ENGINES or not isinstance(cwd, str) or not cwd.startswith("/"):
|
|
222
|
+
raise CreationConflict("create engine/cwd must be explicit and canonical")
|
|
223
|
+
if not isinstance(create_args, Mapping):
|
|
224
|
+
raise CreationConflict("createArgs must be a mapping")
|
|
225
|
+
if fork_anchor is not None and not isinstance(fork_anchor, Mapping):
|
|
226
|
+
raise CreationConflict("forkAnchor must be a frozen mapping")
|
|
227
|
+
return _digest({
|
|
228
|
+
"version": 2, "engine": engine, "cwd": cwd,
|
|
229
|
+
"createArgs": dict(create_args),
|
|
230
|
+
"forkAnchor": dict(fork_anchor) if fork_anchor is not None else None,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GenerationStore:
|
|
235
|
+
"""ACTIVE backend pinned to one sealed generation; never hot-switches."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, pointer: GenerationPointer):
|
|
238
|
+
if pointer.phase != "ACTIVE":
|
|
239
|
+
raise GenerationUnavailable("only ACTIVE generation is operational")
|
|
240
|
+
# Validate again: caller must not forge a dataclass to bypass loader.
|
|
241
|
+
current = inspect_generation(pointer.root)
|
|
242
|
+
if current != pointer:
|
|
243
|
+
raise StaleGeneration("stale or forged generation context")
|
|
244
|
+
# Direct GenerationStore(pointer) must be equally fail-closed and
|
|
245
|
+
# typed as require_ccx_active, not leak raw FileNotFoundError.
|
|
246
|
+
try:
|
|
247
|
+
with _open_dir(pointer.root, "identity") as identity:
|
|
248
|
+
try:
|
|
249
|
+
lock_fd = os.open("activation.lock", _FILE_FLAGS, dir_fd=identity)
|
|
250
|
+
except OSError as exc:
|
|
251
|
+
raise GenerationUnavailable("activation lock missing/unsafe; offline stage required") from exc
|
|
252
|
+
try:
|
|
253
|
+
st = os.fstat(lock_fd)
|
|
254
|
+
named = os.stat("activation.lock", dir_fd=identity,
|
|
255
|
+
follow_symlinks=False)
|
|
256
|
+
if ((st.st_dev, st.st_ino) != (named.st_dev, named.st_ino)
|
|
257
|
+
or not stat.S_ISREG(st.st_mode)
|
|
258
|
+
or st.st_uid != os.getuid() or st.st_mode & 0o077):
|
|
259
|
+
raise GenerationUnavailable("activation lock inode is not safe/owner-private")
|
|
260
|
+
finally:
|
|
261
|
+
os.close(lock_fd)
|
|
262
|
+
except FileNotFoundError as exc:
|
|
263
|
+
raise GenerationUnavailable("activation lock not installed by offline stage") from exc
|
|
264
|
+
self.pointer = pointer
|
|
265
|
+
self.root = pointer.root
|
|
266
|
+
self.generation = pointer.generation
|
|
267
|
+
self._gen_parts = ("identity", "generations", self.generation)
|
|
268
|
+
|
|
269
|
+
def revalidate(self) -> None:
|
|
270
|
+
current = inspect_generation(self.root)
|
|
271
|
+
if current != self.pointer:
|
|
272
|
+
raise StaleGeneration("generation changed; refuse and restart on a new context")
|
|
273
|
+
|
|
274
|
+
@contextmanager
|
|
275
|
+
def lease(self, *, timeout: float = .5) -> Iterator[None]:
|
|
276
|
+
"""Stable shared activation lock for bounded local intent/dispatch only.
|
|
277
|
+
|
|
278
|
+
DO NOT hold across native provider turn waits, --wait, streaming or
|
|
279
|
+
arbitrary user callbacks. Every public store mutation uses this lease;
|
|
280
|
+
a later cutover runner needs LOCK_EX on the same never-replaced inode.
|
|
281
|
+
"""
|
|
282
|
+
self.revalidate()
|
|
283
|
+
with _open_dir(self.root, "identity") as identity:
|
|
284
|
+
with _flock_at(identity, "activation.lock", exclusive=False, timeout=timeout):
|
|
285
|
+
self.revalidate()
|
|
286
|
+
yield
|
|
287
|
+
self.revalidate()
|
|
288
|
+
|
|
289
|
+
@contextmanager
|
|
290
|
+
def _gen(self, *parts: str, create: bool = False) -> Iterator[int]:
|
|
291
|
+
with _open_dir(self.root, *self._gen_parts, *parts, create=create) as fd:
|
|
292
|
+
yield fd
|
|
293
|
+
|
|
294
|
+
@contextmanager
|
|
295
|
+
def _claim_lock(self, worker_id: str) -> Iterator[None]:
|
|
296
|
+
with self._gen("locks", "creation", create=True) as fd:
|
|
297
|
+
with _flock_at(fd, worker_id + ".lock", exclusive=True, create=True):
|
|
298
|
+
yield
|
|
299
|
+
|
|
300
|
+
def _owner(self, worker_id: str) -> dict | None:
|
|
301
|
+
try:
|
|
302
|
+
with self._gen("owners") as fd:
|
|
303
|
+
return _read_json(fd, worker_id + ".json")
|
|
304
|
+
except FileNotFoundError:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def _claim(self, worker_id: str) -> dict | None:
|
|
308
|
+
try:
|
|
309
|
+
with self._gen("claims") as fd:
|
|
310
|
+
return _read_json(fd, worker_id + ".json")
|
|
311
|
+
except FileNotFoundError:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
def _record(self, engine: str, worker_id: str) -> dict | None:
|
|
315
|
+
try:
|
|
316
|
+
with self._gen("workers", engine, worker_id) as fd:
|
|
317
|
+
return _read_json(fd, "record.json")
|
|
318
|
+
except FileNotFoundError:
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
def _write(self, parts: tuple[str, ...], basename: str, data: Mapping) -> None:
|
|
322
|
+
with self._gen(*parts, create=True) as fd:
|
|
323
|
+
_write_json(fd, basename, data)
|
|
324
|
+
|
|
325
|
+
def _owned(self, target: str, *, require_record: bool = True) -> tuple[WorkerOwner, dict | None]:
|
|
326
|
+
parsed = parse_managed_target(target)
|
|
327
|
+
record = self._owner(parsed.worker_id)
|
|
328
|
+
if record is None:
|
|
329
|
+
raise UnknownWorker("full UUID is not present in global owner index")
|
|
330
|
+
if (record.get("schemaVersion") != _OWNER_SCHEMA
|
|
331
|
+
or record.get("workerId") != parsed.worker_id
|
|
332
|
+
or record.get("generation") != self.generation
|
|
333
|
+
or record.get("engine") not in ENGINES):
|
|
334
|
+
raise StorageIntegrityError("global full-ID owner record disagrees")
|
|
335
|
+
if parsed.qualified_engine is not None and parsed.qualified_engine != record["engine"]:
|
|
336
|
+
raise EngineMismatch(
|
|
337
|
+
f"{parsed.worker_id} is a {record['engine'].capitalize()} worker; use "
|
|
338
|
+
f"{record['engine']}:{parsed.worker_id} or the bare UUID")
|
|
339
|
+
if record.get("status") != "worker":
|
|
340
|
+
raise UnknownWorker("full UUID is reserved/quarantined, not an active worker")
|
|
341
|
+
source = self._record(record["engine"], parsed.worker_id)
|
|
342
|
+
if require_record and source is None:
|
|
343
|
+
raise StorageIntegrityError("active owner has no matching full-ID worker record")
|
|
344
|
+
if source is not None and (source.get("workerId") != parsed.worker_id
|
|
345
|
+
or source.get("engine") != record["engine"]):
|
|
346
|
+
raise StorageIntegrityError("record and unique global worker owner disagree")
|
|
347
|
+
return WorkerOwner(parsed.worker_id, record["engine"]), source
|
|
348
|
+
|
|
349
|
+
def resolve(self, target: str) -> WorkerOwner:
|
|
350
|
+
with self.lease():
|
|
351
|
+
owner, _rec = self._owned(target)
|
|
352
|
+
return owner
|
|
353
|
+
|
|
354
|
+
def list_workers(self, *, engine: str | None = None, cwd: str | None = None,
|
|
355
|
+
state: str | None = None, parent: str | None = None,
|
|
356
|
+
limit: int | None = None,
|
|
357
|
+
max_owners: int = 60000,
|
|
358
|
+
records: dict | None = None) -> list[dict]:
|
|
359
|
+
"""Pure durable managed-worker inventory, no native discovery/spawn.
|
|
360
|
+
|
|
361
|
+
Every row is selected by its one global full-UUID owner file and a
|
|
362
|
+
separate exact engine/full-ID record. No short, display name or native
|
|
363
|
+
session/thread ID ever authorizes a row. Status is RECORDED, not a
|
|
364
|
+
guess of host liveness; callers inspect live host status separately.
|
|
365
|
+
"""
|
|
366
|
+
if engine is not None and engine not in ENGINES:
|
|
367
|
+
raise StorageIntegrityError("unsupported inventory engine")
|
|
368
|
+
if cwd is not None and not isinstance(cwd, str):
|
|
369
|
+
raise StorageIntegrityError("invalid explicit cwd filter")
|
|
370
|
+
if state is not None and (not isinstance(state, str) or not state):
|
|
371
|
+
raise StorageIntegrityError("invalid lifecycle-state filter")
|
|
372
|
+
if limit is not None and (type(limit) is not int or limit < 0):
|
|
373
|
+
raise StorageIntegrityError("worker list limit must be nonnegative")
|
|
374
|
+
if type(max_owners) is not int or not (0 < max_owners <= 1000000):
|
|
375
|
+
raise StorageIntegrityError("invalid bounded owner scan limit")
|
|
376
|
+
# Exact global owner, not a name, short, native ID or fork source.
|
|
377
|
+
if parent is not None:
|
|
378
|
+
with self.lease():
|
|
379
|
+
parent_owner, _ = self._owned(parent)
|
|
380
|
+
parent = parent_owner.worker
|
|
381
|
+
rows: list[dict] = []
|
|
382
|
+
# ONE shared lease and ONE pinned fd per directory for the whole bounded
|
|
383
|
+
# scan. Per-row leases and root-anchored path walks cost ~30 open()
|
|
384
|
+
# calls and three generation inspections per owner (seconds per list
|
|
385
|
+
# on a 2.8k-owner fleet that the dashboard polls). Pure local reads.
|
|
386
|
+
with self.lease(), ExitStack() as dirs:
|
|
387
|
+
try:
|
|
388
|
+
owners_fd = dirs.enter_context(self._gen("owners"))
|
|
389
|
+
except FileNotFoundError:
|
|
390
|
+
return [] # new empty generation has no owners directory yet
|
|
391
|
+
with os.scandir(owners_fd) as iterator:
|
|
392
|
+
names = sorted(entry.name for entry in iterator
|
|
393
|
+
if entry.name.endswith(".json"))
|
|
394
|
+
if len(names) > max_owners:
|
|
395
|
+
raise StorageIntegrityError("owner inventory exceeds explicit scan budget")
|
|
396
|
+
engine_fds: dict[str, int] = {}
|
|
397
|
+
for name in names:
|
|
398
|
+
worker_id = canonical_worker_id(name[:-5])
|
|
399
|
+
owner = _read_json(owners_fd, name)
|
|
400
|
+
if (owner is None or owner.get("schemaVersion") != _OWNER_SCHEMA
|
|
401
|
+
or owner.get("workerId") != worker_id
|
|
402
|
+
or owner.get("generation") != self.generation
|
|
403
|
+
or owner.get("engine") not in ENGINES
|
|
404
|
+
or owner.get("status") not in ("worker", "indeterminate-reserved",
|
|
405
|
+
"quarantined")):
|
|
406
|
+
raise StorageIntegrityError("malformed global owner inventory entry")
|
|
407
|
+
if owner["status"] != "worker":
|
|
408
|
+
continue # known reservation, not an addressable worker
|
|
409
|
+
if engine is not None and owner["engine"] != engine:
|
|
410
|
+
continue
|
|
411
|
+
try:
|
|
412
|
+
if owner["engine"] not in engine_fds:
|
|
413
|
+
engine_fds[owner["engine"]] = dirs.enter_context(
|
|
414
|
+
self._gen("workers", owner["engine"]))
|
|
415
|
+
with _open_child_dir(engine_fds[owner["engine"]], worker_id) as wfd:
|
|
416
|
+
rec = _read_json(wfd, "record.json")
|
|
417
|
+
except FileNotFoundError:
|
|
418
|
+
rec = None
|
|
419
|
+
# Same owner/record agreement `_owned` enforces, on the fds above.
|
|
420
|
+
if rec is None:
|
|
421
|
+
raise StorageIntegrityError("active owner has no matching full-ID worker record")
|
|
422
|
+
if rec.get("workerId") != worker_id or rec.get("engine") != owner["engine"]:
|
|
423
|
+
raise StorageIntegrityError("record and unique global worker owner disagree")
|
|
424
|
+
proven_owner = WorkerOwner(worker_id, owner["engine"])
|
|
425
|
+
spawned = rec.get("spawnedBy")
|
|
426
|
+
intent = owner.get("createIntent")
|
|
427
|
+
if spawned != (intent.get("spawnedBy") if isinstance(intent, dict) else None):
|
|
428
|
+
raise StorageIntegrityError("unproven managed parent edge in worker record")
|
|
429
|
+
if spawned is not None:
|
|
430
|
+
if (not isinstance(spawned, dict)
|
|
431
|
+
or spawned.get("kind") != "ccx-worker"
|
|
432
|
+
or spawned.get("engine") not in ENGINES):
|
|
433
|
+
raise StorageIntegrityError("unproven managed parent edge in worker record")
|
|
434
|
+
try:
|
|
435
|
+
canonical_worker_id(spawned.get("workerId"))
|
|
436
|
+
except InvalidTarget as exc:
|
|
437
|
+
raise StorageIntegrityError("invalid owned parent full UUID") from exc
|
|
438
|
+
if parent is not None and (
|
|
439
|
+
spawned is None or f"{spawned['engine']}:{spawned['workerId']}" != parent
|
|
440
|
+
):
|
|
441
|
+
continue
|
|
442
|
+
if cwd is not None and rec.get("cwd") != cwd:
|
|
443
|
+
continue
|
|
444
|
+
lifecycle = rec.get("lifecycle")
|
|
445
|
+
lc = lifecycle if isinstance(lifecycle, dict) else {}
|
|
446
|
+
durable_state = lc.get("state")
|
|
447
|
+
normalized_state = normalize_native_state(durable_state)
|
|
448
|
+
if owner["engine"] == "claude" and normalized_state == "idle":
|
|
449
|
+
from ccx.store.worker_state import claude_recorded_delivery_busy
|
|
450
|
+
# Stream owner-matched receipts and stop at the first
|
|
451
|
+
# actual pending new turn. File age is not a safe proxy
|
|
452
|
+
# for receipt timestamps (wall clock can move backwards).
|
|
453
|
+
def eligible_receipts():
|
|
454
|
+
try:
|
|
455
|
+
with self._gen("semantic-deliveries", "claude", worker_id) as rfd:
|
|
456
|
+
with os.scandir(rfd) as entries:
|
|
457
|
+
for entry in entries:
|
|
458
|
+
if not entry.name.endswith(".json"):
|
|
459
|
+
continue
|
|
460
|
+
try:
|
|
461
|
+
receipt = _read_json(rfd, entry.name)
|
|
462
|
+
except StorageIntegrityError:
|
|
463
|
+
continue
|
|
464
|
+
if (isinstance(receipt, dict)
|
|
465
|
+
and receipt.get("workerId") == worker_id
|
|
466
|
+
and receipt.get("engine") == "claude"
|
|
467
|
+
and receipt.get("generation") == self.generation):
|
|
468
|
+
yield receipt
|
|
469
|
+
except FileNotFoundError:
|
|
470
|
+
return
|
|
471
|
+
if claude_recorded_delivery_busy(rec, eligible_receipts()):
|
|
472
|
+
normalized_state = "busy"
|
|
473
|
+
if state is not None and state != normalized_state:
|
|
474
|
+
continue
|
|
475
|
+
born = rec.get("createdAt")
|
|
476
|
+
last = rec.get("lastTurnAt")
|
|
477
|
+
bad_stamps = []
|
|
478
|
+
if born is not None and (type(born) is not int or born < 0):
|
|
479
|
+
born = None
|
|
480
|
+
bad_stamps.append("createdAt")
|
|
481
|
+
if last is not None and (type(last) is not int or last < 0):
|
|
482
|
+
last = None
|
|
483
|
+
bad_stamps.append("lastTurnAt")
|
|
484
|
+
stats = rec.get("stats")
|
|
485
|
+
stats = stats if isinstance(stats, dict) else {}
|
|
486
|
+
args = rec.get("createArgs")
|
|
487
|
+
args = args if isinstance(args, dict) else {}
|
|
488
|
+
row = {
|
|
489
|
+
"worker": proven_owner.worker,
|
|
490
|
+
"worker_id": proven_owner.worker_id,
|
|
491
|
+
"engine": proven_owner.engine,
|
|
492
|
+
"short": proven_owner.short,
|
|
493
|
+
"name": rec.get("name"),
|
|
494
|
+
"cwd": rec.get("cwd"),
|
|
495
|
+
"spawned_by": spawned,
|
|
496
|
+
"state": normalized_state,
|
|
497
|
+
"state_source": "recorded", # no live/native guesses
|
|
498
|
+
"native_state": durable_state,
|
|
499
|
+
"terminal_only": bool(rec.get("terminalOnly")),
|
|
500
|
+
"created_at": born,
|
|
501
|
+
"last_turn_at": last,
|
|
502
|
+
"turns": stats.get("turns"),
|
|
503
|
+
**({"input_tokens": stats.get("inputTokens"),
|
|
504
|
+
"output_tokens": stats.get("outputTokens")}
|
|
505
|
+
if proven_owner.engine == "codex" else {}),
|
|
506
|
+
"requested_model": args.get("model"),
|
|
507
|
+
"requested_effort": args.get("effort"),
|
|
508
|
+
"loaded": recorded_host_loaded(proven_owner.engine, lc),
|
|
509
|
+
}
|
|
510
|
+
if bad_stamps:
|
|
511
|
+
row["warning"] = ("invalid optional timestamp: "
|
|
512
|
+
+ ", ".join(bad_stamps) + "; treated as unknown")
|
|
513
|
+
rows.append(row)
|
|
514
|
+
if records is not None:
|
|
515
|
+
records[proven_owner.worker] = (proven_owner, rec)
|
|
516
|
+
# Presentation order never grants authority: two same-prefix/full
|
|
517
|
+
# distinct IDs remain separate independent rows, even if names match.
|
|
518
|
+
rows.sort(key=lambda r: (
|
|
519
|
+
-max(r.get("last_turn_at") or 0, r.get("created_at") or 0),
|
|
520
|
+
r["engine"], r["worker_id"],
|
|
521
|
+
))
|
|
522
|
+
prefixes: dict[str, int] = {}
|
|
523
|
+
for row in rows:
|
|
524
|
+
prefixes[row["short"]] = prefixes.get(row["short"], 0) + 1
|
|
525
|
+
for row in rows:
|
|
526
|
+
row["ambiguous_short"] = prefixes[row["short"]] > 1
|
|
527
|
+
return rows[:limit] if limit is not None else rows
|
|
528
|
+
|
|
529
|
+
def owner_ids(self, *, prefix: str = "") -> list[str]:
|
|
530
|
+
"""Sorted full UUIDs whose owner file name starts with ``prefix``.
|
|
531
|
+
|
|
532
|
+
Names only: callers must still ``resolve`` each candidate, which
|
|
533
|
+
validates the owner, its status and the worker record.
|
|
534
|
+
"""
|
|
535
|
+
with self.lease():
|
|
536
|
+
try:
|
|
537
|
+
with self._gen("owners") as fd, os.scandir(fd) as iterator:
|
|
538
|
+
return sorted(entry.name[:-5] for entry in iterator
|
|
539
|
+
if entry.name.endswith(".json")
|
|
540
|
+
and entry.name.startswith(prefix))
|
|
541
|
+
except FileNotFoundError:
|
|
542
|
+
return []
|
|
543
|
+
|
|
544
|
+
def record(self, target: str) -> dict:
|
|
545
|
+
with self.lease():
|
|
546
|
+
_owner, record = self._owned(target)
|
|
547
|
+
assert record is not None
|
|
548
|
+
return record
|
|
549
|
+
|
|
550
|
+
def variables_info(self, target: str, name: str | None = None) -> dict:
|
|
551
|
+
"""Read full-ID durable variable bindings; no runtime Python/live-owner revival.
|
|
552
|
+
|
|
553
|
+
Variable *stateId* is an independent original identity and remains
|
|
554
|
+
unchanged when two managed full-UUID owners intentionally share state.
|
|
555
|
+
Reads are fd-relative bounded snapshots of atomic state replacements.
|
|
556
|
+
Writers serialize separately; readers never wait on their locks.
|
|
557
|
+
"""
|
|
558
|
+
import keyword
|
|
559
|
+
|
|
560
|
+
if name is not None and (
|
|
561
|
+
not isinstance(name, str) or not name.isidentifier()
|
|
562
|
+
or keyword.iskeyword(name)
|
|
563
|
+
):
|
|
564
|
+
raise StorageIntegrityError("variable name is not a Python identifier")
|
|
565
|
+
with self.lease():
|
|
566
|
+
owner, _source = self._owned(target)
|
|
567
|
+
root_result = {
|
|
568
|
+
"worker": owner.worker, "worker_id": owner.worker_id,
|
|
569
|
+
"engine": owner.engine, "short": owner.short,
|
|
570
|
+
"mode": None, "state_id": None, "names": [],
|
|
571
|
+
"available": False,
|
|
572
|
+
}
|
|
573
|
+
try:
|
|
574
|
+
with self._gen("variables", owner.engine) as fd:
|
|
575
|
+
sidecar = _read_json(fd, owner.worker_id + ".json")
|
|
576
|
+
except FileNotFoundError:
|
|
577
|
+
sidecar = None
|
|
578
|
+
if sidecar is None:
|
|
579
|
+
if name is not None:
|
|
580
|
+
raise UnknownWorker(
|
|
581
|
+
f"worker {owner.worker} has no variable {name!r} (has: none)"
|
|
582
|
+
)
|
|
583
|
+
return root_result
|
|
584
|
+
if (sidecar.get("schemaVersion") != 2
|
|
585
|
+
or sidecar.get("engine") != owner.engine
|
|
586
|
+
or sidecar.get("workerId") != owner.worker_id):
|
|
587
|
+
raise StorageIntegrityError("variable sidecar lacks full-ID owner provenance")
|
|
588
|
+
mode = sidecar.get("mode")
|
|
589
|
+
if mode not in ("durable", "live"):
|
|
590
|
+
raise StorageIntegrityError("unknown worker variable binding mode")
|
|
591
|
+
if mode == "live":
|
|
592
|
+
names = sidecar.get("names")
|
|
593
|
+
if not isinstance(names, list) or any(
|
|
594
|
+
not isinstance(item, str) or not item.isidentifier()
|
|
595
|
+
or keyword.iskeyword(item) for item in names
|
|
596
|
+
):
|
|
597
|
+
raise StorageIntegrityError("malformed live variable names")
|
|
598
|
+
result = {**root_result, "mode": "live", "names": sorted(names)}
|
|
599
|
+
if name is not None:
|
|
600
|
+
if name not in names:
|
|
601
|
+
raise UnknownWorker(
|
|
602
|
+
f"worker {owner.worker} has no variable {name!r} "
|
|
603
|
+
f"(has: {', '.join(sorted(names)) or 'none'})"
|
|
604
|
+
)
|
|
605
|
+
raise NativeActionDenied(
|
|
606
|
+
"live Python variable cannot be materialized; original "
|
|
607
|
+
"in-memory controller owner may be unavailable"
|
|
608
|
+
)
|
|
609
|
+
return result
|
|
610
|
+
state_id = sidecar.get("stateId")
|
|
611
|
+
if (not isinstance(state_id, str) or len(state_id) != 32
|
|
612
|
+
or any(c not in "0123456789abcdef" for c in state_id)):
|
|
613
|
+
raise StorageIntegrityError("durable variable stateId is invalid")
|
|
614
|
+
try:
|
|
615
|
+
with self._gen("variables", "states") as fd:
|
|
616
|
+
state = _read_json(fd, state_id + ".json")
|
|
617
|
+
except FileNotFoundError as exc:
|
|
618
|
+
raise StorageIntegrityError(
|
|
619
|
+
"full UUID durable variable binding references missing state"
|
|
620
|
+
) from exc
|
|
621
|
+
if (state is None or state.get("schemaVersion") != 1
|
|
622
|
+
or state.get("stateId") != state_id
|
|
623
|
+
or not isinstance(state.get("names"), list)
|
|
624
|
+
or not isinstance(state.get("variables"), dict)):
|
|
625
|
+
raise StorageIntegrityError("durable variable state not sealed/owned")
|
|
626
|
+
variables = state["variables"]
|
|
627
|
+
names = state["names"]
|
|
628
|
+
if (len(set(names)) != len(names)
|
|
629
|
+
or not all(isinstance(item, str) and item.isidentifier()
|
|
630
|
+
and not keyword.iskeyword(item) for item in names)
|
|
631
|
+
or set(names) != set(variables)):
|
|
632
|
+
raise StorageIntegrityError(
|
|
633
|
+
"durable variable names differ from original state entries"
|
|
634
|
+
)
|
|
635
|
+
# The worker sidecar is immutable during a concurrent state
|
|
636
|
+
# access by design. If it did change across this read, refuse
|
|
637
|
+
# rather than returning data belonging to an old/other binding.
|
|
638
|
+
try:
|
|
639
|
+
with self._gen("variables", owner.engine) as fd:
|
|
640
|
+
after = _read_json(fd, owner.worker_id + ".json")
|
|
641
|
+
except FileNotFoundError:
|
|
642
|
+
after = None
|
|
643
|
+
if after != sidecar:
|
|
644
|
+
raise StorageIntegrityError("worker variable binding changed while read")
|
|
645
|
+
result = {
|
|
646
|
+
**root_result,
|
|
647
|
+
"mode": "durable", "state_id": state_id,
|
|
648
|
+
"names": sorted(names), "available": True,
|
|
649
|
+
}
|
|
650
|
+
if name is not None:
|
|
651
|
+
if name not in variables:
|
|
652
|
+
raise UnknownWorker(
|
|
653
|
+
f"worker {owner.worker} has no variable {name!r} "
|
|
654
|
+
f"(has: {', '.join(sorted(names)) or 'none'})"
|
|
655
|
+
)
|
|
656
|
+
result["name"] = name
|
|
657
|
+
result["value"] = variables[name]
|
|
658
|
+
return result
|
|
659
|
+
|
|
660
|
+
def read_worker_sidecar(self, target: str, filename: str) -> dict | None:
|
|
661
|
+
"""Descriptor-relative JSON sidecar read, never a returned pathname open.
|
|
662
|
+
|
|
663
|
+
Host/client readers use this rather than opening an unpinned
|
|
664
|
+
pathname returned by worker_path().
|
|
665
|
+
"""
|
|
666
|
+
if filename not in ("record.json", "settings.json",
|
|
667
|
+
"statusline.json", "compact.json",
|
|
668
|
+
"context-observation.json"):
|
|
669
|
+
raise StorageIntegrityError("unsupported CCX-owned JSON worker sidecar")
|
|
670
|
+
with self.lease():
|
|
671
|
+
owner, _record = self._owned(target)
|
|
672
|
+
with self._gen("workers", owner.engine, owner.worker_id) as dir_fd:
|
|
673
|
+
return _read_json(dir_fd, filename)
|
|
674
|
+
|
|
675
|
+
def worker_path(self, target: str, filename: str = "record.json") -> Path:
|
|
676
|
+
if filename not in ("record.json", "settings.json",
|
|
677
|
+
"statusline.json", "events.ndjson",
|
|
678
|
+
"evidence.jsonl", "compact.json", "context-observation.json"):
|
|
679
|
+
raise StorageIntegrityError("unknown worker sidecar family")
|
|
680
|
+
with self.lease():
|
|
681
|
+
owner, _ = self._owned(target)
|
|
682
|
+
return self.pointer.generation_root / "workers" / owner.engine / owner.worker_id / filename
|
|
683
|
+
|
|
684
|
+
def receipt_path(self, target: str, family: str, token: str) -> Path:
|
|
685
|
+
if family not in _FAMILIES or not isinstance(token, str) or not token or len(token) > 512 or "\x00" in token:
|
|
686
|
+
raise StorageIntegrityError("invalid semantic operation family/token")
|
|
687
|
+
with self.lease():
|
|
688
|
+
owner, _rec = self._owned(target)
|
|
689
|
+
return (self.pointer.generation_root / "receipts" / family / owner.engine
|
|
690
|
+
/ owner.worker_id / (hashlib.sha256(token.encode("utf-8")).hexdigest() + ".json"))
|
|
691
|
+
|
|
692
|
+
@staticmethod
|
|
693
|
+
def _new_claim(worker_id: str, engine: str, generation: str, fingerprint: str,
|
|
694
|
+
native: dict, anchor: dict | None) -> dict:
|
|
695
|
+
return {
|
|
696
|
+
"schemaVersion": _CLAIM_SCHEMA, "generation": generation,
|
|
697
|
+
"workerId": worker_id, "engine": engine,
|
|
698
|
+
"createSha256": fingerprint, "fingerprintVersion": 2,
|
|
699
|
+
"forkAnchor": anchor, "nativeBinding": native,
|
|
700
|
+
"activatedAtMs": None,
|
|
701
|
+
"nativeActivation": {
|
|
702
|
+
"state": "unprepared", "source": None,
|
|
703
|
+
"submissionId": None, "nativeEvidenceSha256": None,
|
|
704
|
+
},
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
def _check_original_create_intent(self, old: dict, *, engine: str,
|
|
708
|
+
worker_id: str, fingerprint: str,
|
|
709
|
+
native: dict, anchor: dict | None,
|
|
710
|
+
provenance: str | None = None,
|
|
711
|
+
imported_from: Mapping | None = None) -> dict:
|
|
712
|
+
if old.get("engine") != engine:
|
|
713
|
+
raise OwnerConflict("full UUID globally belongs to a different engine")
|
|
714
|
+
if (old.get("workerId") != worker_id or old.get("generation") != self.generation
|
|
715
|
+
or old.get("schemaVersion") != _OWNER_SCHEMA):
|
|
716
|
+
raise StorageIntegrityError("existing global owner identity/generation mismatch")
|
|
717
|
+
intent = old.get("createIntent")
|
|
718
|
+
if not isinstance(intent, dict):
|
|
719
|
+
raise CreationIndeterminate("owner predates durable create intent; never infer native absence")
|
|
720
|
+
origin = old.get("provenance")
|
|
721
|
+
source = intent.get("importedFrom")
|
|
722
|
+
if origin == "ccx-convert":
|
|
723
|
+
if provenance is not None and provenance != origin:
|
|
724
|
+
raise CreationConflict("converted UUID is not a fresh creation/import")
|
|
725
|
+
if (intent.get("createSha256") != fingerprint
|
|
726
|
+
or intent.get("nativeBinding") != native
|
|
727
|
+
or intent.get("forkAnchor") != anchor):
|
|
728
|
+
raise CreationConflict("converted immutable owner intent changed")
|
|
729
|
+
claim = self._claim(worker_id)
|
|
730
|
+
record = self._record(engine, worker_id)
|
|
731
|
+
if not isinstance(claim, dict) or not isinstance(record, dict):
|
|
732
|
+
raise StorageIntegrityError("converted worker has no attested claim/record")
|
|
733
|
+
_converted_binding(old, claim, record)
|
|
734
|
+
return intent
|
|
735
|
+
if (origin not in {"ccx-new", "ccx-import"}
|
|
736
|
+
or (provenance is not None and provenance != origin)
|
|
737
|
+
or (origin == "ccx-import" and
|
|
738
|
+
(engine != "claude" or not isinstance(source, dict)
|
|
739
|
+
or source.get("sessionId") != native.get("sessionId")))
|
|
740
|
+
or (origin == "ccx-new" and source is not None)
|
|
741
|
+
or (provenance is not None and source !=
|
|
742
|
+
(dict(imported_from) if imported_from is not None else None))
|
|
743
|
+
or intent.get("fingerprintVersion") != 2
|
|
744
|
+
or intent.get("createSha256") != fingerprint
|
|
745
|
+
or intent.get("nativeBinding") != native
|
|
746
|
+
or intent.get("forkAnchor") != anchor):
|
|
747
|
+
raise CreationConflict("same UUID has different original owner create specification")
|
|
748
|
+
if intent.get("activationStateAtReservation") != "unprepared":
|
|
749
|
+
raise CreationIndeterminate("owner does not prove native activation was unprepared")
|
|
750
|
+
return intent
|
|
751
|
+
|
|
752
|
+
def creation_binding(self, target: str, *, engine: str,
|
|
753
|
+
fingerprint: str, provenance: str | None = None,
|
|
754
|
+
imported_from: Mapping | None = None) -> dict | None:
|
|
755
|
+
"""Read immutable new-v2 owner intent, including an indeterminate owner.
|
|
756
|
+
|
|
757
|
+
Omitted provenance observes the original owner; creation passes its explicit origin.
|
|
758
|
+
A generated full ID must NOT remint its original Claude native session
|
|
759
|
+
ID after a crash between owner intent and claim publication. Checking
|
|
760
|
+
this owner intent is read-only and never authorizes a native restart.
|
|
761
|
+
"""
|
|
762
|
+
from ccx.store.identity import parse_managed_target
|
|
763
|
+
parsed = parse_managed_target(target)
|
|
764
|
+
parsed.qualified(engine)
|
|
765
|
+
with self.lease():
|
|
766
|
+
old = self._owner(parsed.worker_id)
|
|
767
|
+
if old is None:
|
|
768
|
+
return None
|
|
769
|
+
intent = old.get("createIntent")
|
|
770
|
+
if not isinstance(intent, dict):
|
|
771
|
+
raise CreationIndeterminate(
|
|
772
|
+
"existing worker identity lacks original v2 native binding proof"
|
|
773
|
+
)
|
|
774
|
+
native = intent.get("nativeBinding")
|
|
775
|
+
anchor = intent.get("forkAnchor")
|
|
776
|
+
if not isinstance(native, dict):
|
|
777
|
+
raise CreationIndeterminate("owned create intent has no native binding")
|
|
778
|
+
self._check_original_create_intent(
|
|
779
|
+
old, engine=engine, worker_id=parsed.worker_id,
|
|
780
|
+
fingerprint=fingerprint, native=native, anchor=anchor,
|
|
781
|
+
provenance=provenance, imported_from=imported_from,
|
|
782
|
+
)
|
|
783
|
+
return dict(native)
|
|
784
|
+
|
|
785
|
+
@staticmethod
|
|
786
|
+
def _check_record_origin(owner: dict, record: Mapping) -> None:
|
|
787
|
+
"""First claimed source and recovered record must describe one origin."""
|
|
788
|
+
intent = owner.get("createIntent") or {}
|
|
789
|
+
if record.get("spawnedBy") != intent.get("spawnedBy"):
|
|
790
|
+
raise CreationConflict("first record changed original managed caller")
|
|
791
|
+
if owner.get("provenance") == "ccx-convert":
|
|
792
|
+
cf = intent.get("convertedFrom") or {}
|
|
793
|
+
if record.get("importedFrom") != cf.get("importedFrom"):
|
|
794
|
+
raise CreationConflict("converted import history differs")
|
|
795
|
+
if cf.get("importedFrom") is not None and record.get("origin") != "ccx-import":
|
|
796
|
+
raise CreationConflict("converted import origin differs")
|
|
797
|
+
return
|
|
798
|
+
if owner.get("provenance") == "ccx-import":
|
|
799
|
+
if (record.get("origin") != "ccx-import"
|
|
800
|
+
or record.get("importedFrom") != intent.get("importedFrom")
|
|
801
|
+
or (record.get("native") or {}).get("sessionId") !=
|
|
802
|
+
(intent.get("nativeBinding") or {}).get("sessionId")):
|
|
803
|
+
raise CreationConflict("original imported source/record differs")
|
|
804
|
+
elif record.get("origin") == "ccx-import" or "importedFrom" in record:
|
|
805
|
+
raise CreationConflict("ordinary create contradicts import provenance")
|
|
806
|
+
|
|
807
|
+
def recover_first_record(self, reservation: CreationReservation) -> bool:
|
|
808
|
+
"""Promote an exact previously fsynced first record after crash.
|
|
809
|
+
|
|
810
|
+
A new record with changing timestamps cannot be reconstructed from
|
|
811
|
+
mutable fields. Instead verify its immutable v2 spec/fork/native source
|
|
812
|
+
against original claim+owner and promote the *existing* bytes only
|
|
813
|
+
while nativeActivation is durably UNPREPARED.
|
|
814
|
+
"""
|
|
815
|
+
wid = canonical_worker_id(reservation.worker_id)
|
|
816
|
+
with self.lease():
|
|
817
|
+
with self._claim_lock(wid):
|
|
818
|
+
self.revalidate()
|
|
819
|
+
old, claim = self._owner(wid), self._claim(wid)
|
|
820
|
+
if not old or not claim:
|
|
821
|
+
raise CreationIndeterminate("original owner/claim unavailable")
|
|
822
|
+
immutable = self._check_original_create_intent(
|
|
823
|
+
old, engine=reservation.engine, worker_id=wid,
|
|
824
|
+
fingerprint=reservation.create_sha256,
|
|
825
|
+
native=claim.get("nativeBinding"),
|
|
826
|
+
anchor=claim.get("forkAnchor"),
|
|
827
|
+
)
|
|
828
|
+
if claim.get("createSha256") != reservation.create_sha256:
|
|
829
|
+
raise CreationConflict("original immutable claim changed")
|
|
830
|
+
act = claim.get("nativeActivation")
|
|
831
|
+
if not isinstance(act, dict) or act.get("state") != "unprepared":
|
|
832
|
+
raise CreationIndeterminate(
|
|
833
|
+
"first record replay not safe after native activation intent"
|
|
834
|
+
)
|
|
835
|
+
original = self._record(reservation.engine, wid)
|
|
836
|
+
if original is None:
|
|
837
|
+
return False
|
|
838
|
+
self._check_record_origin(old, original)
|
|
839
|
+
if (original.get("workerId") != wid
|
|
840
|
+
or original.get("engine") != reservation.engine
|
|
841
|
+
or original.get("createSha256") != reservation.create_sha256
|
|
842
|
+
or original.get("fingerprintVersion") != 2
|
|
843
|
+
or not isinstance(original.get("createArgs"), dict)
|
|
844
|
+
or fingerprint_v2(
|
|
845
|
+
engine=reservation.engine, cwd=original.get("cwd"),
|
|
846
|
+
create_args=original["createArgs"],
|
|
847
|
+
fork_anchor=claim.get("forkAnchor"),
|
|
848
|
+
) != reservation.create_sha256
|
|
849
|
+
or original.get("forkAnchor") != claim.get("forkAnchor")
|
|
850
|
+
or not isinstance(original.get("native"), dict)
|
|
851
|
+
or any(original["native"].get(k) != v for k, v
|
|
852
|
+
in claim.get("nativeBinding", {}).items())):
|
|
853
|
+
raise CreationIndeterminate(
|
|
854
|
+
"original first-record immutable full-UUID proof differs"
|
|
855
|
+
)
|
|
856
|
+
if old.get("status") == "indeterminate-reserved":
|
|
857
|
+
updated = dict(old)
|
|
858
|
+
updated["status"] = "worker"
|
|
859
|
+
self._write(("owners",), wid + ".json", updated)
|
|
860
|
+
elif old.get("status") != "worker":
|
|
861
|
+
raise CreationIndeterminate("original UUID remains quarantined")
|
|
862
|
+
return True
|
|
863
|
+
|
|
864
|
+
def _native_sid_owner(self, session_id: str) -> dict | None:
|
|
865
|
+
"""Current owner files, including pending/converted records; no index."""
|
|
866
|
+
with self.lease():
|
|
867
|
+
try:
|
|
868
|
+
with self._gen("owners") as fd:
|
|
869
|
+
with os.scandir(fd) as entries:
|
|
870
|
+
names = sorted(e.name for e in entries if e.name.endswith(".json"))
|
|
871
|
+
except FileNotFoundError:
|
|
872
|
+
return None
|
|
873
|
+
if len(names) > 60000:
|
|
874
|
+
raise StorageIntegrityError("native owner census exceeds bounded scan")
|
|
875
|
+
found = None
|
|
876
|
+
for filename in names:
|
|
877
|
+
wid = canonical_worker_id(filename[:-5])
|
|
878
|
+
owner = self._owner(wid)
|
|
879
|
+
if (not isinstance(owner, dict) or owner.get("workerId") != wid
|
|
880
|
+
or owner.get("generation") != self.generation
|
|
881
|
+
or owner.get("schemaVersion") != _OWNER_SCHEMA
|
|
882
|
+
or owner.get("engine") not in ENGINES):
|
|
883
|
+
raise StorageIntegrityError("native owner census contains invalid owner")
|
|
884
|
+
if owner.get("status") not in {"worker", "indeterminate-reserved", "quarantined"}:
|
|
885
|
+
raise StorageIntegrityError("native owner has unsupported claim state")
|
|
886
|
+
if owner["engine"] != "claude":
|
|
887
|
+
continue
|
|
888
|
+
intent = owner.get("createIntent")
|
|
889
|
+
if intent is not None and not isinstance(intent, dict):
|
|
890
|
+
raise StorageIntegrityError("invalid original Claude owner intent")
|
|
891
|
+
binding = intent.get("nativeBinding") if isinstance(intent, dict) else None
|
|
892
|
+
if isinstance(intent, dict) and not isinstance(binding, dict):
|
|
893
|
+
raise StorageIntegrityError("unverifiable owned Claude native identity")
|
|
894
|
+
record = self._record("claude", wid)
|
|
895
|
+
if record is not None and not isinstance(record.get("native"), dict):
|
|
896
|
+
raise StorageIntegrityError("invalid owned native Claude session binding")
|
|
897
|
+
recorded = (record["native"].get("sessionId")
|
|
898
|
+
if isinstance(record, dict) else None)
|
|
899
|
+
bound = binding.get("sessionId") if binding is not None else None
|
|
900
|
+
if (record is not None and (record.get("workerId") != wid
|
|
901
|
+
or record.get("engine") != "claude")):
|
|
902
|
+
raise StorageIntegrityError("native owner record disagrees")
|
|
903
|
+
if bound is not None and recorded is not None and bound != recorded:
|
|
904
|
+
raise StorageIntegrityError("owner and worker bind different native sessions")
|
|
905
|
+
if bound is None and recorded is None:
|
|
906
|
+
# An unprovable Claude owner is not evidence that its native
|
|
907
|
+
# session differs. Never silently permit another owner.
|
|
908
|
+
raise CreationIndeterminate("unproven historical Claude native owner")
|
|
909
|
+
if (bound or recorded) != session_id:
|
|
910
|
+
continue
|
|
911
|
+
if found is not None:
|
|
912
|
+
raise StorageIntegrityError("multiple owners of one native Claude session")
|
|
913
|
+
claim = self._claim(wid)
|
|
914
|
+
activation = claim.get("nativeActivation") if isinstance(claim, dict) else None
|
|
915
|
+
proven = (owner.get("provenance") == "ccx-import"
|
|
916
|
+
and owner.get("status") == "worker"
|
|
917
|
+
and isinstance(intent, dict)
|
|
918
|
+
and isinstance(activation, dict)
|
|
919
|
+
and activation.get("state") == "confirmed"
|
|
920
|
+
and isinstance(record, dict)
|
|
921
|
+
and record.get("origin") == "ccx-import"
|
|
922
|
+
and (record.get("native") or {}).get("nativeState") == "session-bound"
|
|
923
|
+
and record.get("importedFrom") == intent.get("importedFrom"))
|
|
924
|
+
found = {"worker": f"claude:{wid}", "worker_id": wid,
|
|
925
|
+
"provenance": owner.get("provenance"),
|
|
926
|
+
"confirmed": proven,
|
|
927
|
+
"activation_state": (activation or {}).get("state") or "unknown"}
|
|
928
|
+
return found
|
|
929
|
+
|
|
930
|
+
@staticmethod
|
|
931
|
+
def _import_sid_lock_name(session_id: str) -> str:
|
|
932
|
+
return str(uuid5(NAMESPACE_URL, "ccx-import:" + session_id))
|
|
933
|
+
|
|
934
|
+
def import_native_owner(self, session_id: str) -> dict | None:
|
|
935
|
+
"""Read one original owner under the same admission lock as import."""
|
|
936
|
+
with self._claim_lock(self._import_sid_lock_name(session_id)):
|
|
937
|
+
return self._native_sid_owner(session_id)
|
|
938
|
+
|
|
939
|
+
def reserve_import_creation(self, target: str, *, session_id: str,
|
|
940
|
+
fingerprint: str, imported_from: Mapping,
|
|
941
|
+
native_binding: Mapping) -> CreationReservation:
|
|
942
|
+
"""One atomically admitted SID→independent worker owner reservation."""
|
|
943
|
+
with self._claim_lock(self._import_sid_lock_name(session_id)):
|
|
944
|
+
claimed = self._native_sid_owner(session_id)
|
|
945
|
+
wid = parse_managed_target(target).worker_id
|
|
946
|
+
if claimed is not None and claimed["worker_id"] != wid:
|
|
947
|
+
raise CreationConflict("native Claude session already owned by " + claimed["worker"])
|
|
948
|
+
return self.reserve_creation(
|
|
949
|
+
target, engine="claude", fingerprint=fingerprint,
|
|
950
|
+
native_binding=native_binding, provenance="ccx-import",
|
|
951
|
+
imported_from=imported_from)
|
|
952
|
+
|
|
953
|
+
def reserve_creation(self, target: str, *, engine: str, fingerprint: str,
|
|
954
|
+
native_binding: Mapping | None = None,
|
|
955
|
+
fork_anchor: Mapping | None = None,
|
|
956
|
+
provenance: str = "ccx-new",
|
|
957
|
+
imported_from: Mapping | None = None,
|
|
958
|
+
spawned_by: Mapping | None = None,
|
|
959
|
+
_failpoint: Callable[[str], None] | None = None) -> CreationReservation:
|
|
960
|
+
"""Globally exact-ID creation fence. never performs native activation.
|
|
961
|
+
|
|
962
|
+
An existing orphan, historical claimless worker or unproven/changed
|
|
963
|
+
create preimage never triggers a second native start. A crash after
|
|
964
|
+
owner publication but before claim requires explicit reconciliation.
|
|
965
|
+
"""
|
|
966
|
+
parsed = parse_managed_target(target)
|
|
967
|
+
if engine not in ENGINES:
|
|
968
|
+
raise CreationConflict("invalid engine")
|
|
969
|
+
parsed.qualified(engine)
|
|
970
|
+
if not isinstance(fingerprint, str) or not _HASH.fullmatch(fingerprint):
|
|
971
|
+
raise CreationConflict("invalid immutable create digest")
|
|
972
|
+
if native_binding is not None and not isinstance(native_binding, Mapping):
|
|
973
|
+
raise CreationConflict("native binding must be a mapping")
|
|
974
|
+
if fork_anchor is not None and not isinstance(fork_anchor, Mapping):
|
|
975
|
+
raise CreationConflict("fork anchor must be a mapping")
|
|
976
|
+
native = dict(native_binding or {})
|
|
977
|
+
anchor = dict(fork_anchor) if fork_anchor is not None else None
|
|
978
|
+
if provenance not in {"ccx-new", "ccx-import"}:
|
|
979
|
+
raise CreationConflict("unsupported CCX owner origin")
|
|
980
|
+
frozen_import = dict(imported_from) if imported_from is not None else None
|
|
981
|
+
if (provenance == "ccx-import" and
|
|
982
|
+
(engine != "claude" or not isinstance(frozen_import, dict)
|
|
983
|
+
or frozen_import.get("sessionId") != native.get("sessionId"))):
|
|
984
|
+
raise CreationConflict("import provenance/native session mismatch")
|
|
985
|
+
if provenance == "ccx-new" and frozen_import is not None:
|
|
986
|
+
raise CreationConflict("non-import claim cannot carry imported history")
|
|
987
|
+
with self.lease():
|
|
988
|
+
with self._claim_lock(parsed.worker_id):
|
|
989
|
+
self.revalidate()
|
|
990
|
+
old = self._owner(parsed.worker_id)
|
|
991
|
+
# The first owner intent wins; later caller environment must
|
|
992
|
+
# neither overwrite its edge nor add an authority requirement
|
|
993
|
+
# to a compatible retry.
|
|
994
|
+
if old is None and spawned_by is not None:
|
|
995
|
+
if (not isinstance(spawned_by, Mapping)
|
|
996
|
+
or set(spawned_by) != {"kind", "engine", "workerId"}
|
|
997
|
+
or spawned_by.get("kind") != "ccx-worker"
|
|
998
|
+
or spawned_by.get("engine") not in ENGINES):
|
|
999
|
+
raise CreationConflict("managed caller stamp is invalid")
|
|
1000
|
+
caller, _ = self._owned(
|
|
1001
|
+
f"{spawned_by['engine']}:{spawned_by['workerId']}"
|
|
1002
|
+
)
|
|
1003
|
+
spawned_by = {"kind": "ccx-worker", "engine": caller.engine,
|
|
1004
|
+
"workerId": caller.worker_id}
|
|
1005
|
+
claim = self._claim(parsed.worker_id)
|
|
1006
|
+
if old is not None:
|
|
1007
|
+
self._check_original_create_intent(
|
|
1008
|
+
old, engine=engine, worker_id=parsed.worker_id,
|
|
1009
|
+
fingerprint=fingerprint, native=native, anchor=anchor,
|
|
1010
|
+
provenance=provenance, imported_from=frozen_import,
|
|
1011
|
+
)
|
|
1012
|
+
if claim is None:
|
|
1013
|
+
# same immutable intent may finish only the missing
|
|
1014
|
+
# claim in preparation phase. No native dispatch can be
|
|
1015
|
+
# reached without the claim AND a later durable
|
|
1016
|
+
# prepared/submitted fence. A missing record for an
|
|
1017
|
+
# already-active owner is never recreated here.
|
|
1018
|
+
if old.get("status") != "indeterminate-reserved":
|
|
1019
|
+
raise CreationIndeterminate("missing claim under committed owner")
|
|
1020
|
+
if self._record(engine, parsed.worker_id) is not None:
|
|
1021
|
+
raise CreationIndeterminate("record exists without claim; native result unknown")
|
|
1022
|
+
claim = self._new_claim(
|
|
1023
|
+
parsed.worker_id, engine, self.generation, fingerprint,
|
|
1024
|
+
native, anchor,
|
|
1025
|
+
)
|
|
1026
|
+
self._write(("claims",), parsed.worker_id + ".json", claim)
|
|
1027
|
+
if _failpoint:
|
|
1028
|
+
_failpoint("after-claim")
|
|
1029
|
+
return CreationReservation(parsed.worker_id, engine, fingerprint,
|
|
1030
|
+
True, False, "unprepared",
|
|
1031
|
+
spawned_by=old["createIntent"].get("spawnedBy"))
|
|
1032
|
+
if (claim.get("engine") != engine
|
|
1033
|
+
or claim.get("workerId") != parsed.worker_id
|
|
1034
|
+
or claim.get("createSha256") != fingerprint
|
|
1035
|
+
or claim.get("fingerprintVersion") != 2
|
|
1036
|
+
or claim.get("nativeBinding") != native
|
|
1037
|
+
or claim.get("forkAnchor") != anchor):
|
|
1038
|
+
raise CreationConflict("same UUID has incompatible immutable create intent")
|
|
1039
|
+
if claim.get("generation") != self.generation:
|
|
1040
|
+
raise StorageIntegrityError("claim generation disagrees")
|
|
1041
|
+
activation = claim.get("nativeActivation")
|
|
1042
|
+
if not isinstance(activation, dict) or activation.get("state") not in (
|
|
1043
|
+
"unprepared", "prepared", "submitted-unknown", "confirmed", "rejected", "local-refused",
|
|
1044
|
+
):
|
|
1045
|
+
# A pre-fence record has NO evidence of never-started.
|
|
1046
|
+
raise CreationIndeterminate("original activation fate is not durably known")
|
|
1047
|
+
if claim.get("activatedAtMs") is not None and activation.get("state") != "confirmed":
|
|
1048
|
+
raise CreationIndeterminate("old activation timestamp without matching proof")
|
|
1049
|
+
existing_record = self._record(engine, parsed.worker_id)
|
|
1050
|
+
if old.get("status") == "worker" and existing_record is None:
|
|
1051
|
+
raise CreationIndeterminate("active claim record missing; never create another native worker")
|
|
1052
|
+
if old.get("status") not in ("worker", "indeterminate-reserved"):
|
|
1053
|
+
raise CreationIndeterminate("historical/quarantined reservation cannot recreate")
|
|
1054
|
+
return CreationReservation(
|
|
1055
|
+
parsed.worker_id, engine, fingerprint, True,
|
|
1056
|
+
existing_record is not None, activation["state"],
|
|
1057
|
+
activation.get("submissionId"),
|
|
1058
|
+
old["createIntent"].get("spawnedBy"),
|
|
1059
|
+
)
|
|
1060
|
+
if claim is not None:
|
|
1061
|
+
raise CreationIndeterminate("orphan claim cannot recreate missing global owner")
|
|
1062
|
+
# Owner creates an immutable original v2 intent before the
|
|
1063
|
+
# claim. If this write crashes, the caller-supplied AND the
|
|
1064
|
+
# freshly generated UUID remain owned; no reminting.
|
|
1065
|
+
self._write(("owners",), parsed.worker_id + ".json", {
|
|
1066
|
+
"schemaVersion": _OWNER_SCHEMA, "generation": self.generation,
|
|
1067
|
+
"workerId": parsed.worker_id, "engine": engine,
|
|
1068
|
+
"status": "indeterminate-reserved", "provenance": provenance,
|
|
1069
|
+
"createIntent": {
|
|
1070
|
+
"fingerprintVersion": 2, "createSha256": fingerprint,
|
|
1071
|
+
"nativeBinding": native, "forkAnchor": anchor,
|
|
1072
|
+
**({"spawnedBy": spawned_by} if spawned_by is not None else {}),
|
|
1073
|
+
"activationStateAtReservation": "unprepared",
|
|
1074
|
+
**({"importedFrom": frozen_import}
|
|
1075
|
+
if frozen_import is not None else {}),
|
|
1076
|
+
},
|
|
1077
|
+
})
|
|
1078
|
+
if _failpoint:
|
|
1079
|
+
_failpoint("after-owner")
|
|
1080
|
+
self._write(
|
|
1081
|
+
("claims",), parsed.worker_id + ".json",
|
|
1082
|
+
self._new_claim(parsed.worker_id, engine, self.generation,
|
|
1083
|
+
fingerprint, native, anchor),
|
|
1084
|
+
)
|
|
1085
|
+
if _failpoint:
|
|
1086
|
+
_failpoint("after-claim")
|
|
1087
|
+
return CreationReservation(parsed.worker_id, engine, fingerprint,
|
|
1088
|
+
False, False, "unprepared",
|
|
1089
|
+
spawned_by=dict(spawned_by) if spawned_by is not None else None)
|
|
1090
|
+
|
|
1091
|
+
def publish_first_record(self, reservation: CreationReservation,
|
|
1092
|
+
record: Mapping, *, _failpoint: Callable[[str], None] | None = None) -> None:
|
|
1093
|
+
"""Commit first record in full-ID space; no native session/thread start."""
|
|
1094
|
+
canonical_worker_id(reservation.worker_id)
|
|
1095
|
+
if record.get("workerId") != reservation.worker_id or record.get("engine") != reservation.engine:
|
|
1096
|
+
raise StorageIntegrityError("initial record owner/native binding mismatch")
|
|
1097
|
+
if not isinstance(record.get("native"), dict):
|
|
1098
|
+
raise StorageIntegrityError("first record needs separate native binding")
|
|
1099
|
+
with self.lease():
|
|
1100
|
+
with self._claim_lock(reservation.worker_id):
|
|
1101
|
+
self.revalidate()
|
|
1102
|
+
old, claim = self._owner(reservation.worker_id), self._claim(reservation.worker_id)
|
|
1103
|
+
if not old or not claim or (old.get("engine"), old.get("workerId"), old.get("generation")) != (
|
|
1104
|
+
reservation.engine, reservation.worker_id, self.generation
|
|
1105
|
+
):
|
|
1106
|
+
raise CreationIndeterminate("missing original owner/create claim")
|
|
1107
|
+
if (claim.get("createSha256") != reservation.create_sha256
|
|
1108
|
+
or claim.get("fingerprintVersion") != 2
|
|
1109
|
+
or claim.get("workerId") != reservation.worker_id):
|
|
1110
|
+
raise CreationConflict("initial record differs from original immutable claim")
|
|
1111
|
+
if claim.get("activatedAtMs") is not None:
|
|
1112
|
+
raise CreationIndeterminate("original native activation may have happened")
|
|
1113
|
+
if not isinstance(record.get("native"), dict):
|
|
1114
|
+
raise StorageIntegrityError("native binding requires separate typed identity")
|
|
1115
|
+
expected_binding = claim.get("nativeBinding")
|
|
1116
|
+
if not isinstance(expected_binding, dict) or any(
|
|
1117
|
+
record["native"].get(k) != v for k, v in expected_binding.items()
|
|
1118
|
+
):
|
|
1119
|
+
raise CreationConflict("initial record contradicts immutable native binding intent")
|
|
1120
|
+
self._check_record_origin(old, record)
|
|
1121
|
+
original = self._record(reservation.engine, reservation.worker_id)
|
|
1122
|
+
if original is not None:
|
|
1123
|
+
self._check_record_origin(old, original)
|
|
1124
|
+
if original is None:
|
|
1125
|
+
self._write(("workers", reservation.engine, reservation.worker_id),
|
|
1126
|
+
"record.json", dict(record))
|
|
1127
|
+
elif _digest(original) != _digest(dict(record)):
|
|
1128
|
+
# Two processes can each inspect a missing record and
|
|
1129
|
+
# construct different *mutable* name/createdAt fields
|
|
1130
|
+
# before the first one atomically publishes. Different
|
|
1131
|
+
# prepared documents must not overwrite one another,
|
|
1132
|
+
# but a verified identical original immutable v2 spec,
|
|
1133
|
+
# native source and fork-anchor can safely adopt the
|
|
1134
|
+
# first published bytes under this UUID claim lock.
|
|
1135
|
+
if (claim.get("nativeActivation", {}).get("state") != "unprepared"
|
|
1136
|
+
or original.get("workerId") != reservation.worker_id
|
|
1137
|
+
or original.get("engine") != reservation.engine
|
|
1138
|
+
or original.get("fingerprintVersion") != 2
|
|
1139
|
+
or original.get("createSha256") != reservation.create_sha256
|
|
1140
|
+
or not isinstance(original.get("createArgs"), dict)
|
|
1141
|
+
or fingerprint_v2(
|
|
1142
|
+
engine=reservation.engine,
|
|
1143
|
+
cwd=original.get("cwd"),
|
|
1144
|
+
create_args=original["createArgs"],
|
|
1145
|
+
fork_anchor=claim.get("forkAnchor"),
|
|
1146
|
+
) != reservation.create_sha256
|
|
1147
|
+
or original.get("forkAnchor") != claim.get("forkAnchor")
|
|
1148
|
+
or not isinstance(original.get("native"), dict)
|
|
1149
|
+
or any(original["native"].get(k) != v for k, v
|
|
1150
|
+
in expected_binding.items())):
|
|
1151
|
+
raise CreationConflict(
|
|
1152
|
+
"existing first record lacks original immutable spec; no overwrite"
|
|
1153
|
+
)
|
|
1154
|
+
# Preserve the first writer's name, timestamp, current
|
|
1155
|
+
# revision and any subsequent separate metadata patches.
|
|
1156
|
+
if _failpoint:
|
|
1157
|
+
_failpoint("after-record")
|
|
1158
|
+
if old.get("status") == "worker":
|
|
1159
|
+
return # idempotent record-only retry
|
|
1160
|
+
if old.get("status") != "indeterminate-reserved":
|
|
1161
|
+
raise CreationIndeterminate("non-recreatable historical owner state")
|
|
1162
|
+
promoted = dict(old)
|
|
1163
|
+
promoted["status"] = "worker"
|
|
1164
|
+
self._write(("owners",), reservation.worker_id + ".json", promoted)
|
|
1165
|
+
|
|
1166
|
+
def native_activation(self, target: str) -> dict:
|
|
1167
|
+
"""Read-only original claim activation fate; no synthetic native proof."""
|
|
1168
|
+
with self.lease():
|
|
1169
|
+
owner, _ = self._owned(target)
|
|
1170
|
+
claim = self._claim(owner.worker_id)
|
|
1171
|
+
if claim is None:
|
|
1172
|
+
raise CreationIndeterminate("historical claimless worker has no activation proof")
|
|
1173
|
+
activation = claim.get("nativeActivation")
|
|
1174
|
+
if not isinstance(activation, dict) or activation.get("state") not in (
|
|
1175
|
+
"unprepared", "prepared", "submitted-unknown", "confirmed", "rejected", "local-refused",
|
|
1176
|
+
):
|
|
1177
|
+
raise CreationIndeterminate("activation state missing or unsupported")
|
|
1178
|
+
return dict(activation)
|
|
1179
|
+
|
|
1180
|
+
def prepare_native_activation(self, reservation: CreationReservation, *,
|
|
1181
|
+
native_source: str) -> NativeActivation:
|
|
1182
|
+
"""Durably bind one original native activation source, no provider call.
|
|
1183
|
+
|
|
1184
|
+
Only a fully committed *first* record with positive
|
|
1185
|
+
pre-native provenance may prepare. A no-thread migrated Codex worker,
|
|
1186
|
+
historical claimless owner, or an already submitted activation cannot.
|
|
1187
|
+
"""
|
|
1188
|
+
if (not isinstance(native_source, str) or not native_source
|
|
1189
|
+
or len(native_source) > 512 or "\x00" in native_source):
|
|
1190
|
+
raise CreationConflict("missing/invalid original native activation source")
|
|
1191
|
+
wid = canonical_worker_id(reservation.worker_id)
|
|
1192
|
+
with self.lease():
|
|
1193
|
+
with self._claim_lock(wid):
|
|
1194
|
+
old, claim = self._owner(wid), self._claim(wid)
|
|
1195
|
+
if not old or not claim or old.get("status") != "worker":
|
|
1196
|
+
raise CreationIndeterminate("native prepare requires original committed first record")
|
|
1197
|
+
if old.get("provenance") not in {"ccx-new", "ccx-import"}:
|
|
1198
|
+
raise CreationIndeterminate("conversion is not an original native start")
|
|
1199
|
+
self._check_original_create_intent(
|
|
1200
|
+
old, engine=reservation.engine, worker_id=wid,
|
|
1201
|
+
fingerprint=reservation.create_sha256,
|
|
1202
|
+
native=claim.get("nativeBinding"), anchor=claim.get("forkAnchor"),
|
|
1203
|
+
)
|
|
1204
|
+
if claim.get("createSha256") != reservation.create_sha256:
|
|
1205
|
+
raise CreationConflict("original create claim differs")
|
|
1206
|
+
record = self._record(reservation.engine, wid)
|
|
1207
|
+
if not isinstance(record, dict):
|
|
1208
|
+
raise CreationIndeterminate("missing exact first worker record")
|
|
1209
|
+
self._check_record_origin(old, record)
|
|
1210
|
+
if record.get("terminalOnly") or record.get("nativeCreate"):
|
|
1211
|
+
raise CreationIndeterminate("worker has unknown/previous native-start evidence")
|
|
1212
|
+
native = record.get("native")
|
|
1213
|
+
if not isinstance(native, dict) or native.get("nativeState") in (
|
|
1214
|
+
"unbound-unknown", "submitted-or-unknown",
|
|
1215
|
+
):
|
|
1216
|
+
raise CreationIndeterminate("no positive first-create native binding")
|
|
1217
|
+
if reservation.engine == "codex":
|
|
1218
|
+
if native.get("threadId") is not None:
|
|
1219
|
+
raise CreationIndeterminate("Codex record has native thread before first activation")
|
|
1220
|
+
if native_source != f"ccx-worker:{wid}":
|
|
1221
|
+
raise CreationConflict("native Codex source must name full CCX UUID")
|
|
1222
|
+
else:
|
|
1223
|
+
sid = native.get("sessionId")
|
|
1224
|
+
if not isinstance(sid, str) or native_source != f"claude-session:{sid}":
|
|
1225
|
+
raise CreationConflict("native Claude source must retain exact original sessionId")
|
|
1226
|
+
activation = claim.get("nativeActivation")
|
|
1227
|
+
if not isinstance(activation, dict):
|
|
1228
|
+
raise CreationIndeterminate("original activation fence unavailable")
|
|
1229
|
+
state = activation.get("state")
|
|
1230
|
+
if state == "prepared" and activation.get("source") == native_source:
|
|
1231
|
+
return NativeActivation(wid, reservation.engine, native_source,
|
|
1232
|
+
activation["submissionId"], state, self.generation)
|
|
1233
|
+
if state != "unprepared":
|
|
1234
|
+
raise CreationIndeterminate("native activation already submitted/settled/ambiguous")
|
|
1235
|
+
token = str(uuid4())
|
|
1236
|
+
updated = dict(claim)
|
|
1237
|
+
updated["nativeActivation"] = {
|
|
1238
|
+
"state": "prepared", "source": native_source,
|
|
1239
|
+
"submissionId": token, "nativeEvidenceSha256": None,
|
|
1240
|
+
}
|
|
1241
|
+
self._write(("claims",), wid + ".json", updated)
|
|
1242
|
+
return NativeActivation(wid, reservation.engine, native_source, token,
|
|
1243
|
+
"prepared", self.generation)
|
|
1244
|
+
|
|
1245
|
+
def native_activation_dispatch(self, prepared: NativeActivation, *,
|
|
1246
|
+
bounded_submit: Callable[[float], Mapping | None],
|
|
1247
|
+
dispatch_budget_s: float = 1.0,
|
|
1248
|
+
_failpoint: Callable[[str], None] | None = None) -> dict:
|
|
1249
|
+
"""Exactly one original native submission under a *bounded* activation lease.
|
|
1250
|
+
|
|
1251
|
+
The provided transport function MUST send one bounded native request
|
|
1252
|
+
and honor the supplied deadline; it must never wait for a model turn.
|
|
1253
|
+
This atomic lock+state+transport admission is mandatory: no separate
|
|
1254
|
+
unlocked submitted→provider gap where a cutover could flip generation.
|
|
1255
|
+
The submitted-unknown fence is fsynced before calling the transport.
|
|
1256
|
+
No retry of a lost reply. A completed positive response is reconciled
|
|
1257
|
+
with confirm_native_activation, not guessed from idle.
|
|
1258
|
+
"""
|
|
1259
|
+
if not 0 < dispatch_budget_s <= 2.0:
|
|
1260
|
+
raise GenerationError("native dispatch must have a bounded deadline <= 2 seconds")
|
|
1261
|
+
if prepared.generation != self.generation:
|
|
1262
|
+
raise StaleGeneration("native activation prepared for a different generation")
|
|
1263
|
+
if prepared.state != "prepared":
|
|
1264
|
+
raise CreationIndeterminate("only the original prepared submission can dispatch")
|
|
1265
|
+
wid = canonical_worker_id(prepared.worker_id)
|
|
1266
|
+
# One absolute admission budget includes the generation lease, claim
|
|
1267
|
+
# fsync, IPC admission and the daemon's eventual app-server pipe write.
|
|
1268
|
+
deadline = time.monotonic() + dispatch_budget_s
|
|
1269
|
+
with self.lease(timeout=min(dispatch_budget_s, 2.0)):
|
|
1270
|
+
with self._claim_lock(wid):
|
|
1271
|
+
self.revalidate()
|
|
1272
|
+
owner, record = self._owned(f"{prepared.engine}:{wid}")
|
|
1273
|
+
claim = self._claim(wid)
|
|
1274
|
+
if claim is None:
|
|
1275
|
+
raise CreationIndeterminate("original native activation claim missing")
|
|
1276
|
+
act = claim.get("nativeActivation")
|
|
1277
|
+
if (not isinstance(act, dict) or act.get("state") != "prepared"
|
|
1278
|
+
or act.get("source") != prepared.source
|
|
1279
|
+
or act.get("submissionId") != prepared.submission_id):
|
|
1280
|
+
raise CreationIndeterminate("native activation already submitted or claimed by another caller")
|
|
1281
|
+
if _failpoint:
|
|
1282
|
+
_failpoint("before-submitted")
|
|
1283
|
+
updated = dict(claim)
|
|
1284
|
+
updated["nativeActivation"] = {**act, "state": "submitted-unknown"}
|
|
1285
|
+
self._write(("claims",), wid + ".json", updated)
|
|
1286
|
+
if _failpoint:
|
|
1287
|
+
_failpoint("after-submitted")
|
|
1288
|
+
self.revalidate() # still inside activation lease and owner lock
|
|
1289
|
+
# The transport's timeout applies only to a SINGLE native IPC
|
|
1290
|
+
# dispatch. A late/lost ack cannot un-fence this submission.
|
|
1291
|
+
try:
|
|
1292
|
+
remaining = deadline - time.monotonic()
|
|
1293
|
+
if remaining <= 0:
|
|
1294
|
+
raise NativeEffectIndeterminate(
|
|
1295
|
+
"original native admission expired after durable submission fence"
|
|
1296
|
+
)
|
|
1297
|
+
evidence = bounded_submit(remaining)
|
|
1298
|
+
except Exception as exc:
|
|
1299
|
+
raise NativeEffectIndeterminate(
|
|
1300
|
+
"native transport result unknown; reconcile original submission"
|
|
1301
|
+
) from exc
|
|
1302
|
+
if evidence is None or not isinstance(evidence, Mapping):
|
|
1303
|
+
raise NativeEffectIndeterminate(
|
|
1304
|
+
"no conclusive native acceptance evidence; original submission fenced"
|
|
1305
|
+
)
|
|
1306
|
+
# No assertion of success until explicit positive original
|
|
1307
|
+
# native evidence + matching ID/source is checked.
|
|
1308
|
+
return dict(evidence)
|
|
1309
|
+
|
|
1310
|
+
def confirm_native_activation(self, prepared: NativeActivation, *,
|
|
1311
|
+
native_evidence_sha256: str,
|
|
1312
|
+
native_binding: Mapping) -> NativeActivation:
|
|
1313
|
+
"""Positive original native evidence only, never a retry of thread/start."""
|
|
1314
|
+
if (not isinstance(native_evidence_sha256, str)
|
|
1315
|
+
or not _HASH.fullmatch(native_evidence_sha256)
|
|
1316
|
+
or not isinstance(native_binding, Mapping)):
|
|
1317
|
+
raise CreationIndeterminate("native activation needs exact positive proof")
|
|
1318
|
+
if prepared.generation != self.generation:
|
|
1319
|
+
raise StaleGeneration("activation submitted under different generation")
|
|
1320
|
+
wid = canonical_worker_id(prepared.worker_id)
|
|
1321
|
+
with self.lease():
|
|
1322
|
+
with self._claim_lock(wid):
|
|
1323
|
+
owner, record = self._owned(f"{prepared.engine}:{wid}")
|
|
1324
|
+
claim = self._claim(wid)
|
|
1325
|
+
if not claim:
|
|
1326
|
+
raise CreationIndeterminate("missing original native activation claim")
|
|
1327
|
+
act = claim.get("nativeActivation")
|
|
1328
|
+
if (not isinstance(act, dict)
|
|
1329
|
+
or act.get("source") != prepared.source
|
|
1330
|
+
or act.get("submissionId") != prepared.submission_id):
|
|
1331
|
+
raise CreationIndeterminate("native submission identity mismatch")
|
|
1332
|
+
if act.get("state") == "confirmed":
|
|
1333
|
+
if (act.get("nativeEvidenceSha256") == native_evidence_sha256
|
|
1334
|
+
and act.get("nativeBinding") == dict(native_binding)):
|
|
1335
|
+
return NativeActivation(wid, prepared.engine, prepared.source,
|
|
1336
|
+
prepared.submission_id, "confirmed", self.generation)
|
|
1337
|
+
raise CreationConflict("native activation confirmation cannot change")
|
|
1338
|
+
if act.get("state") == "rejected":
|
|
1339
|
+
raise CreationConflict("native activation is terminal rejected; cannot confirm")
|
|
1340
|
+
if act.get("state") == "local-refused":
|
|
1341
|
+
raise CreationConflict("native activation is terminal local-refused; cannot confirm")
|
|
1342
|
+
if act.get("state") != "submitted-unknown":
|
|
1343
|
+
raise CreationIndeterminate("native activation never submitted")
|
|
1344
|
+
if prepared.engine == "claude":
|
|
1345
|
+
if native_binding.get("sessionId") != record["native"].get("sessionId"):
|
|
1346
|
+
raise CreationConflict("Claude native session does not match original CCX source")
|
|
1347
|
+
elif not isinstance(native_binding.get("threadId"), str) or not native_binding.get("threadId"):
|
|
1348
|
+
raise CreationConflict("Codex native confirmation requires exact threadId")
|
|
1349
|
+
result = dict(claim)
|
|
1350
|
+
result["nativeActivation"] = {
|
|
1351
|
+
**act, "state": "confirmed",
|
|
1352
|
+
"nativeEvidenceSha256": native_evidence_sha256,
|
|
1353
|
+
"nativeBinding": dict(native_binding),
|
|
1354
|
+
}
|
|
1355
|
+
# Claim confirmation is immutable and monotonically records
|
|
1356
|
+
# first native activation; follow-up normal operations may
|
|
1357
|
+
# only proceed after a separate record binding by CAS.
|
|
1358
|
+
result["activatedAtMs"] = int(time.time() * 1000)
|
|
1359
|
+
self._write(("claims",), wid + ".json", result)
|
|
1360
|
+
return NativeActivation(wid, prepared.engine, prepared.source,
|
|
1361
|
+
prepared.submission_id, "confirmed", self.generation)
|
|
1362
|
+
|
|
1363
|
+
def reconcile_codex_native_rejection(self, target: str) -> dict | None:
|
|
1364
|
+
"""Seal/replay a definite original Codex provider rejection, offline.
|
|
1365
|
+
|
|
1366
|
+
The existing generation-scoped one-shot daemon effect marker must have
|
|
1367
|
+
fsynced the *matched coded JSON-RPC error response* before a claim can
|
|
1368
|
+
become terminal `rejected`. This method never contacts the app-server,
|
|
1369
|
+
never unlocks a native create for the same UUID and never infers a
|
|
1370
|
+
provider rejection from missing/timeout/transport evidence. It is the
|
|
1371
|
+
original claim's only submitted-unknown -> rejected transition, not a
|
|
1372
|
+
second creation receipt/state machine.
|
|
1373
|
+
"""
|
|
1374
|
+
from ccx.store.identity import EngineMismatch
|
|
1375
|
+
wid = canonical_worker_id(parse_managed_target(target).worker_id)
|
|
1376
|
+
with self.lease():
|
|
1377
|
+
with self._claim_lock(wid):
|
|
1378
|
+
owner, record = self._owned(target)
|
|
1379
|
+
if owner.engine != "codex":
|
|
1380
|
+
raise EngineMismatch("native thread/start rejection requires Codex owner")
|
|
1381
|
+
claim = self._claim(wid)
|
|
1382
|
+
if not isinstance(claim, dict):
|
|
1383
|
+
raise CreationIndeterminate("missing original Codex claim")
|
|
1384
|
+
anchor = record.get("forkAnchor")
|
|
1385
|
+
if anchor is not None:
|
|
1386
|
+
lineage = record.get("forkedFrom")
|
|
1387
|
+
if (not isinstance(anchor, dict) or anchor.get("engine") != "codex"
|
|
1388
|
+
or anchor.get("cwd") != record.get("cwd")
|
|
1389
|
+
or not isinstance(anchor.get("createArgs"), dict)
|
|
1390
|
+
or fingerprint_v2(
|
|
1391
|
+
engine="codex", cwd=anchor["cwd"],
|
|
1392
|
+
create_args=anchor["createArgs"], fork_anchor=anchor,
|
|
1393
|
+
) != record.get("createSha256")
|
|
1394
|
+
or not isinstance(lineage, dict)
|
|
1395
|
+
or lineage != codex_fork_lineage(anchor, child_id=wid)):
|
|
1396
|
+
raise CreationIndeterminate("Codex fork terminal original anchor mismatch")
|
|
1397
|
+
self._check_original_create_intent(
|
|
1398
|
+
self._owner(wid), engine="codex", worker_id=wid,
|
|
1399
|
+
fingerprint=record.get("createSha256"), native={}, anchor=anchor,
|
|
1400
|
+
)
|
|
1401
|
+
activation = claim.get("nativeActivation")
|
|
1402
|
+
source = f"ccx-worker:{wid}"
|
|
1403
|
+
# The first create is not yet natively submitted. There can
|
|
1404
|
+
# be no original provider rejection to recover here; leave
|
|
1405
|
+
# the existing one-shot prepare/admission path untouched.
|
|
1406
|
+
if (isinstance(activation, dict)
|
|
1407
|
+
and activation.get("state") in (
|
|
1408
|
+
"unprepared", "prepared", "local-refused",
|
|
1409
|
+
)):
|
|
1410
|
+
# Terminal local-refused is validated against its OWN
|
|
1411
|
+
# durable marker by the native-start reconciler, never
|
|
1412
|
+
# treated as a provider-side coded error.
|
|
1413
|
+
return None
|
|
1414
|
+
if (not isinstance(activation, dict)
|
|
1415
|
+
or activation.get("state") not in {
|
|
1416
|
+
"submitted-unknown", "confirmed", "rejected",
|
|
1417
|
+
}
|
|
1418
|
+
or activation.get("source") != source
|
|
1419
|
+
or not isinstance(activation.get("submissionId"), str)
|
|
1420
|
+
or not activation["submissionId"]
|
|
1421
|
+
or claim.get("workerId") != wid
|
|
1422
|
+
or claim.get("engine") != "codex"
|
|
1423
|
+
or claim.get("generation") != self.generation
|
|
1424
|
+
or claim.get("fingerprintVersion") != 2
|
|
1425
|
+
or claim.get("createSha256") != record.get("createSha256")
|
|
1426
|
+
or claim.get("nativeBinding") != {}
|
|
1427
|
+
or claim.get("forkAnchor") != anchor
|
|
1428
|
+
or record.get("fingerprintVersion") != 2
|
|
1429
|
+
or record.get("terminalOnly") or record.get("nativeCreate")
|
|
1430
|
+
or not isinstance(record.get("native"), dict)
|
|
1431
|
+
or (activation.get("state") != "confirmed" and (
|
|
1432
|
+
record["native"].get("threadId") is not None
|
|
1433
|
+
or record["native"].get("nativeState") != "not-started-proven"
|
|
1434
|
+
))):
|
|
1435
|
+
raise CreationIndeterminate("original Codex submitted claim is not eligible")
|
|
1436
|
+
token = activation["submissionId"]
|
|
1437
|
+
key = hashlib.sha256(("activate\x00" + token).encode()).hexdigest()
|
|
1438
|
+
try:
|
|
1439
|
+
with self._gen("runtime-metadata", "codex-native-effects", wid) as fd:
|
|
1440
|
+
with _flock_at(fd, key + ".lock", exclusive=False,
|
|
1441
|
+
create=False, timeout=.5):
|
|
1442
|
+
effect = _read_json(fd, key + ".json")
|
|
1443
|
+
except (FileNotFoundError, GenerationUnavailable):
|
|
1444
|
+
if activation["state"] == "rejected":
|
|
1445
|
+
raise StorageIntegrityError(
|
|
1446
|
+
"sealed Codex rejection lacks its original one-shot effect"
|
|
1447
|
+
)
|
|
1448
|
+
return None
|
|
1449
|
+
if effect is None:
|
|
1450
|
+
if activation["state"] == "rejected":
|
|
1451
|
+
raise StorageIntegrityError("sealed Codex rejection marker missing")
|
|
1452
|
+
return None
|
|
1453
|
+
expected = {
|
|
1454
|
+
"workerId": wid, "engine": "codex",
|
|
1455
|
+
"generation": self.generation, "kind": "activate",
|
|
1456
|
+
"token": token, "state": "submitted-unknown",
|
|
1457
|
+
"source": source, "createSha256": record["createSha256"],
|
|
1458
|
+
}
|
|
1459
|
+
if (not isinstance(effect, dict)
|
|
1460
|
+
or any(effect.get(k) != v for k, v in expected.items())
|
|
1461
|
+
or (
|
|
1462
|
+
(effect.get("nativeMethod") != "thread/fork"
|
|
1463
|
+
or effect.get("forkAnchor") != anchor)
|
|
1464
|
+
if anchor is not None else
|
|
1465
|
+
("nativeMethod" in effect or "forkAnchor" in effect)
|
|
1466
|
+
)
|
|
1467
|
+
or not isinstance(effect.get("daemonInstanceId"), str)
|
|
1468
|
+
or not effect["daemonInstanceId"]
|
|
1469
|
+
or type(effect.get("daemonPid")) is not int
|
|
1470
|
+
or effect["daemonPid"] <= 0
|
|
1471
|
+
or type(effect.get("appServerPid")) is not int
|
|
1472
|
+
or effect["appServerPid"] <= 0):
|
|
1473
|
+
raise StorageIntegrityError(
|
|
1474
|
+
"original Codex rejection effect identity/source/generation changed"
|
|
1475
|
+
)
|
|
1476
|
+
rejection = effect.get("nativeRejected")
|
|
1477
|
+
if rejection is None:
|
|
1478
|
+
if activation["state"] == "rejected":
|
|
1479
|
+
raise StorageIntegrityError("sealed rejection missing provider error proof")
|
|
1480
|
+
return None
|
|
1481
|
+
if (not isinstance(rejection, dict)
|
|
1482
|
+
or effect.get("nativeCandidate") is not None
|
|
1483
|
+
or effect.get("nativeNotSubmitted") is not None
|
|
1484
|
+
or type(rejection.get("code")) is not int
|
|
1485
|
+
or not isinstance(rejection.get("message"), str)
|
|
1486
|
+
or len(rejection["message"].encode("utf-8")) > 8192
|
|
1487
|
+
or type(rejection.get("requestId")) is not int
|
|
1488
|
+
or rejection["requestId"] <= 0
|
|
1489
|
+
or rejection.get("appServerPid") != effect["appServerPid"]
|
|
1490
|
+
or rejection.get("nativeErrorSha256") != _digest({
|
|
1491
|
+
"code": rejection["code"],
|
|
1492
|
+
"message": rejection["message"],
|
|
1493
|
+
"requestId": rejection["requestId"],
|
|
1494
|
+
"appServerPid": rejection["appServerPid"],
|
|
1495
|
+
})
|
|
1496
|
+
or not isinstance(rejection.get("messageSha256"), str)
|
|
1497
|
+
or rejection["messageSha256"] != hashlib.sha256(
|
|
1498
|
+
rejection["message"].encode("utf-8")
|
|
1499
|
+
).hexdigest()
|
|
1500
|
+
or not isinstance(rejection.get("nativeReplySha256"), str)
|
|
1501
|
+
or not _HASH.fullmatch(rejection["nativeReplySha256"])):
|
|
1502
|
+
raise StorageIntegrityError(
|
|
1503
|
+
"Codex provider rejection marker malformed or contradictory"
|
|
1504
|
+
)
|
|
1505
|
+
evidence = _digest({
|
|
1506
|
+
"workerId": wid, "generation": self.generation,
|
|
1507
|
+
"createSha256": record["createSha256"],
|
|
1508
|
+
"source": source, "submissionId": token,
|
|
1509
|
+
"nativeRejected": rejection,
|
|
1510
|
+
})
|
|
1511
|
+
if activation["state"] == "confirmed":
|
|
1512
|
+
raise CreationConflict("confirmed native activation contradicts provider rejection")
|
|
1513
|
+
if activation["state"] == "rejected":
|
|
1514
|
+
if (activation.get("nativeRejection") != rejection
|
|
1515
|
+
or activation.get("nativeEvidenceSha256") != evidence):
|
|
1516
|
+
raise CreationConflict("terminal native rejection proof cannot change")
|
|
1517
|
+
else:
|
|
1518
|
+
newer = dict(claim)
|
|
1519
|
+
newer["nativeActivation"] = {
|
|
1520
|
+
**activation, "state": "rejected",
|
|
1521
|
+
"nativeEvidenceSha256": evidence,
|
|
1522
|
+
"nativeRejection": dict(rejection),
|
|
1523
|
+
}
|
|
1524
|
+
self._write(("claims",), wid + ".json", newer)
|
|
1525
|
+
return {
|
|
1526
|
+
"rejected": True, "workerId": wid, "engine": "codex",
|
|
1527
|
+
"source": source, "submissionId": token,
|
|
1528
|
+
"code": rejection["code"], "message": rejection["message"],
|
|
1529
|
+
"messageSha256": rejection["messageSha256"],
|
|
1530
|
+
"nativeEvidenceSha256": evidence,
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
def reconcile_codex_local_refusal(self, target: str) -> dict | None:
|
|
1534
|
+
"""Seal/replay only a durably proven local, pre-native Codex refusal.
|
|
1535
|
+
|
|
1536
|
+
The original one-shot daemon effect marker must prove that this exact
|
|
1537
|
+
attempt never entered a potentially writing `thread/start`. This is
|
|
1538
|
+
LOCAL attribution, not a cryptographic signature or a claim that an
|
|
1539
|
+
absent marker proves non-submission. One terminal claim, no new start.
|
|
1540
|
+
"""
|
|
1541
|
+
from ccx.store.identity import EngineMismatch
|
|
1542
|
+
wid = canonical_worker_id(parse_managed_target(target).worker_id)
|
|
1543
|
+
with self.lease():
|
|
1544
|
+
with self._claim_lock(wid):
|
|
1545
|
+
owner, record = self._owned(target)
|
|
1546
|
+
if owner.engine != "codex":
|
|
1547
|
+
raise EngineMismatch("Codex local refusal requires original Codex owner")
|
|
1548
|
+
claim = self._claim(wid)
|
|
1549
|
+
if not isinstance(claim, dict):
|
|
1550
|
+
raise CreationIndeterminate("original local refusal claim absent")
|
|
1551
|
+
anchor = record.get("forkAnchor")
|
|
1552
|
+
if anchor is not None:
|
|
1553
|
+
lineage = record.get("forkedFrom")
|
|
1554
|
+
if (not isinstance(anchor, dict) or anchor.get("engine") != "codex"
|
|
1555
|
+
or anchor.get("cwd") != record.get("cwd")
|
|
1556
|
+
or not isinstance(anchor.get("createArgs"), dict)
|
|
1557
|
+
or fingerprint_v2(
|
|
1558
|
+
engine="codex", cwd=anchor["cwd"],
|
|
1559
|
+
create_args=anchor["createArgs"], fork_anchor=anchor,
|
|
1560
|
+
) != record.get("createSha256")
|
|
1561
|
+
or not isinstance(lineage, dict)
|
|
1562
|
+
or lineage != codex_fork_lineage(anchor, child_id=wid)):
|
|
1563
|
+
raise CreationIndeterminate("Codex fork terminal original anchor mismatch")
|
|
1564
|
+
self._check_original_create_intent(
|
|
1565
|
+
self._owner(wid), engine="codex", worker_id=wid,
|
|
1566
|
+
fingerprint=record.get("createSha256"), native={}, anchor=anchor,
|
|
1567
|
+
)
|
|
1568
|
+
activation = claim.get("nativeActivation")
|
|
1569
|
+
if isinstance(activation, dict) and activation.get("state") in (
|
|
1570
|
+
"unprepared", "prepared",
|
|
1571
|
+
):
|
|
1572
|
+
return None
|
|
1573
|
+
source = f"ccx-worker:{wid}"
|
|
1574
|
+
if (not isinstance(activation, dict)
|
|
1575
|
+
or activation.get("state") not in {
|
|
1576
|
+
"submitted-unknown", "confirmed", "rejected", "local-refused",
|
|
1577
|
+
}
|
|
1578
|
+
or activation.get("source") != source
|
|
1579
|
+
or not isinstance(activation.get("submissionId"), str)
|
|
1580
|
+
or not activation["submissionId"]
|
|
1581
|
+
or claim.get("workerId") != wid
|
|
1582
|
+
or claim.get("engine") != "codex"
|
|
1583
|
+
or claim.get("generation") != self.generation
|
|
1584
|
+
or claim.get("fingerprintVersion") != 2
|
|
1585
|
+
or claim.get("createSha256") != record.get("createSha256")
|
|
1586
|
+
or claim.get("nativeBinding") != {}
|
|
1587
|
+
or claim.get("forkAnchor") != anchor
|
|
1588
|
+
or record.get("fingerprintVersion") != 2
|
|
1589
|
+
or record.get("terminalOnly") or record.get("nativeCreate")
|
|
1590
|
+
or not isinstance(record.get("native"), dict)
|
|
1591
|
+
or (activation.get("state") not in {"confirmed", "rejected"}
|
|
1592
|
+
and (record["native"].get("threadId") is not None
|
|
1593
|
+
or record["native"].get("nativeState") != "not-started-proven"))):
|
|
1594
|
+
raise CreationIndeterminate("original local refusal claim/record ineligible")
|
|
1595
|
+
token = activation["submissionId"]
|
|
1596
|
+
key = hashlib.sha256(("activate\x00" + token).encode()).hexdigest()
|
|
1597
|
+
try:
|
|
1598
|
+
with self._gen("runtime-metadata", "codex-native-effects", wid) as fd:
|
|
1599
|
+
with _flock_at(fd, key + ".lock", exclusive=False,
|
|
1600
|
+
create=False, timeout=.5):
|
|
1601
|
+
effect = _read_json(fd, key + ".json")
|
|
1602
|
+
except FileNotFoundError:
|
|
1603
|
+
effect = None
|
|
1604
|
+
if effect is None:
|
|
1605
|
+
if activation["state"] == "local-refused":
|
|
1606
|
+
raise StorageIntegrityError("terminal local refusal lacks original effect marker")
|
|
1607
|
+
return None # absent evidence never establishes not-submitted
|
|
1608
|
+
expected = {
|
|
1609
|
+
"workerId": wid, "engine": "codex",
|
|
1610
|
+
"generation": self.generation, "kind": "activate",
|
|
1611
|
+
"token": token, "state": "submitted-unknown",
|
|
1612
|
+
"source": source, "createSha256": record["createSha256"],
|
|
1613
|
+
}
|
|
1614
|
+
if (not isinstance(effect, dict)
|
|
1615
|
+
or any(effect.get(k) != v for k, v in expected.items())
|
|
1616
|
+
or (
|
|
1617
|
+
(effect.get("nativeMethod") != "thread/fork"
|
|
1618
|
+
or effect.get("forkAnchor") != anchor)
|
|
1619
|
+
if anchor is not None else
|
|
1620
|
+
("nativeMethod" in effect or "forkAnchor" in effect)
|
|
1621
|
+
)
|
|
1622
|
+
or not isinstance(effect.get("daemonInstanceId"), str)
|
|
1623
|
+
or not effect["daemonInstanceId"]
|
|
1624
|
+
or type(effect.get("daemonPid")) is not int
|
|
1625
|
+
or effect["daemonPid"] <= 0
|
|
1626
|
+
or (effect.get("appServerPid") is not None
|
|
1627
|
+
and (type(effect["appServerPid"]) is not int
|
|
1628
|
+
or effect["appServerPid"] <= 0))):
|
|
1629
|
+
raise StorageIntegrityError("original local refusal marker identity changed")
|
|
1630
|
+
proof = effect.get("nativeNotSubmitted")
|
|
1631
|
+
if proof is None:
|
|
1632
|
+
if activation["state"] == "local-refused":
|
|
1633
|
+
raise StorageIntegrityError("terminal local refusal has no original proof")
|
|
1634
|
+
return None
|
|
1635
|
+
if (not isinstance(proof, dict)
|
|
1636
|
+
or effect.get("nativeCandidate") is not None
|
|
1637
|
+
or effect.get("nativeRejected") is not None
|
|
1638
|
+
or proof.get("stage") not in {
|
|
1639
|
+
"capacity", "not-ready", "daemon-stopping",
|
|
1640
|
+
"transport-zero-bytes", "shell-env",
|
|
1641
|
+
}
|
|
1642
|
+
or proof.get("nativeRequestWritten") is not False
|
|
1643
|
+
or not isinstance(proof.get("reason"), str)
|
|
1644
|
+
or not proof["reason"]
|
|
1645
|
+
or len(proof["reason"].encode("utf-8")) > 1024
|
|
1646
|
+
or proof.get("reasonSha256") != hashlib.sha256(
|
|
1647
|
+
proof["reason"].encode("utf-8")
|
|
1648
|
+
).hexdigest()
|
|
1649
|
+
or proof.get("appServerPid") != effect.get("appServerPid")
|
|
1650
|
+
or proof.get("localEvidenceSha256") != _digest({
|
|
1651
|
+
"workerId": wid, "generation": self.generation,
|
|
1652
|
+
"createSha256": record["createSha256"],
|
|
1653
|
+
"source": source, "submissionId": token,
|
|
1654
|
+
"daemonInstanceId": effect["daemonInstanceId"],
|
|
1655
|
+
"daemonPid": effect["daemonPid"],
|
|
1656
|
+
"nativeNotSubmitted": {
|
|
1657
|
+
"stage": proof["stage"], "reason": proof["reason"],
|
|
1658
|
+
"reasonSha256": proof["reasonSha256"],
|
|
1659
|
+
"nativeRequestWritten": False,
|
|
1660
|
+
"appServerPid": proof["appServerPid"],
|
|
1661
|
+
},
|
|
1662
|
+
})):
|
|
1663
|
+
raise StorageIntegrityError("local refusal proof malformed/contradictory")
|
|
1664
|
+
evidence = _digest({
|
|
1665
|
+
"workerId": wid, "generation": self.generation,
|
|
1666
|
+
"createSha256": record["createSha256"],
|
|
1667
|
+
"source": source, "submissionId": token,
|
|
1668
|
+
"nativeNotSubmitted": proof,
|
|
1669
|
+
})
|
|
1670
|
+
if activation["state"] in ("confirmed", "rejected"):
|
|
1671
|
+
raise CreationConflict("provider outcome contradicts local non-submission")
|
|
1672
|
+
if activation["state"] == "local-refused":
|
|
1673
|
+
if (activation.get("nativeNotSubmitted") != proof
|
|
1674
|
+
or activation.get("nativeEvidenceSha256") != evidence):
|
|
1675
|
+
raise CreationConflict("terminal local refusal proof cannot change")
|
|
1676
|
+
else:
|
|
1677
|
+
update = dict(claim)
|
|
1678
|
+
update["nativeActivation"] = {
|
|
1679
|
+
**activation, "state": "local-refused",
|
|
1680
|
+
"nativeEvidenceSha256": evidence,
|
|
1681
|
+
"nativeNotSubmitted": dict(proof),
|
|
1682
|
+
}
|
|
1683
|
+
self._write(("claims",), wid + ".json", update)
|
|
1684
|
+
return {
|
|
1685
|
+
"localRefused": True, "workerId": wid, "engine": "codex",
|
|
1686
|
+
"source": source, "submissionId": token,
|
|
1687
|
+
"reason": proof["reason"], "stage": proof["stage"],
|
|
1688
|
+
"nativeEvidenceSha256": evidence,
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
def bind_confirmed_native(self, target: str) -> dict:
|
|
1692
|
+
"""Publish exact positive native owner binding without stale record write.
|
|
1693
|
+
|
|
1694
|
+
Native creation claim confirmation may have been fsynced just before a
|
|
1695
|
+
process crash. This separate under-lock reconciliation is idempotent:
|
|
1696
|
+
only the original confirmed session/thread/source may be installed in
|
|
1697
|
+
the immutable native record. Never invent a thread from missing IDs,
|
|
1698
|
+
and never bind an unconfirmed/unknown submission.
|
|
1699
|
+
"""
|
|
1700
|
+
parsed = parse_managed_target(target)
|
|
1701
|
+
with self.lease():
|
|
1702
|
+
with self._claim_lock(parsed.worker_id):
|
|
1703
|
+
with self._record_lock(parsed.worker_id):
|
|
1704
|
+
owner, record = self._owned(target)
|
|
1705
|
+
claim = self._claim(parsed.worker_id)
|
|
1706
|
+
activation = (claim.get("nativeActivation")
|
|
1707
|
+
if isinstance(claim, dict) else None)
|
|
1708
|
+
if (not isinstance(activation, dict)
|
|
1709
|
+
or activation.get("state") != "confirmed"
|
|
1710
|
+
or not isinstance(activation.get("nativeEvidenceSha256"), str)
|
|
1711
|
+
or not _HASH.fullmatch(activation["nativeEvidenceSha256"])):
|
|
1712
|
+
raise CreationIndeterminate(
|
|
1713
|
+
"positive immutable original native activation not confirmed"
|
|
1714
|
+
)
|
|
1715
|
+
expected = activation.get("nativeBinding")
|
|
1716
|
+
if not isinstance(expected, dict):
|
|
1717
|
+
raise CreationIndeterminate(
|
|
1718
|
+
"confirmed native binding lacks original source proof"
|
|
1719
|
+
)
|
|
1720
|
+
original = record.get("native")
|
|
1721
|
+
if not isinstance(original, dict):
|
|
1722
|
+
raise StorageIntegrityError("worker has no separate native binding")
|
|
1723
|
+
if owner.engine == "claude":
|
|
1724
|
+
session_id = expected.get("sessionId")
|
|
1725
|
+
if (not isinstance(session_id, str)
|
|
1726
|
+
or original.get("sessionId") != session_id
|
|
1727
|
+
or activation.get("source") != f"claude-session:{session_id}"):
|
|
1728
|
+
raise CreationConflict("confirmed native Claude session differs")
|
|
1729
|
+
name = "session-bound"
|
|
1730
|
+
binding = {"sessionId": session_id}
|
|
1731
|
+
else:
|
|
1732
|
+
thread_id = expected.get("threadId")
|
|
1733
|
+
if (not isinstance(thread_id, str) or not thread_id
|
|
1734
|
+
or original.get("threadId") not in (None, thread_id)
|
|
1735
|
+
or activation.get("source")
|
|
1736
|
+
!= f"ccx-worker:{owner.worker_id}"):
|
|
1737
|
+
raise CreationConflict("confirmed native Codex source/thread differs")
|
|
1738
|
+
name = "thread-bound"
|
|
1739
|
+
binding = {"threadId": thread_id,
|
|
1740
|
+
"threadSource": activation["source"]}
|
|
1741
|
+
# A formerly terminal-only/migrated worker never reaches
|
|
1742
|
+
# this path: it has no nativeActivation originally created
|
|
1743
|
+
# by the new-v2 owner and matching claim.
|
|
1744
|
+
if record.get("terminalOnly"):
|
|
1745
|
+
raise NativeActionDenied(
|
|
1746
|
+
"terminal-only worker may not be rebound through creation"
|
|
1747
|
+
)
|
|
1748
|
+
newer = dict(record)
|
|
1749
|
+
newer_native = dict(original)
|
|
1750
|
+
newer_native.update(binding)
|
|
1751
|
+
newer_native["nativeState"] = name
|
|
1752
|
+
if newer_native == original:
|
|
1753
|
+
return record
|
|
1754
|
+
newer["native"] = newer_native
|
|
1755
|
+
revision = newer.get("recordRevision", 0)
|
|
1756
|
+
if type(revision) is not int or revision < 0:
|
|
1757
|
+
raise StorageIntegrityError("bad record revision during native binding")
|
|
1758
|
+
newer["recordRevision"] = revision + 1
|
|
1759
|
+
self._write(
|
|
1760
|
+
("workers", owner.engine, owner.worker_id),
|
|
1761
|
+
"record.json", newer,
|
|
1762
|
+
)
|
|
1763
|
+
return newer
|
|
1764
|
+
|
|
1765
|
+
def authorize_native_action(self, target: str, action: str) -> WorkerOwner:
|
|
1766
|
+
"""Read-only capability gate before any provider/client/daemon call.
|
|
1767
|
+
|
|
1768
|
+
Direct worker methods and durable-intent paths must pass this gate;
|
|
1769
|
+
it does not start a provider.
|
|
1770
|
+
"""
|
|
1771
|
+
with self.lease():
|
|
1772
|
+
owner, source = self._owned(target)
|
|
1773
|
+
assert source is not None
|
|
1774
|
+
self._native_guard(source, action)
|
|
1775
|
+
return owner
|
|
1776
|
+
|
|
1777
|
+
@contextmanager
|
|
1778
|
+
def _record_lock(self, worker_id: str) -> Iterator[None]:
|
|
1779
|
+
"""Per-full-UUID stable lock; only synchronous local RMW, no native waits."""
|
|
1780
|
+
with self._gen("locks", "records", create=True) as fd:
|
|
1781
|
+
with _flock_at(fd, worker_id + ".lock", exclusive=True, create=True):
|
|
1782
|
+
yield
|
|
1783
|
+
|
|
1784
|
+
@staticmethod
|
|
1785
|
+
def _record_field_path(path: str) -> tuple[str, ...]:
|
|
1786
|
+
"""Restrict semantic mutation to mutable CCX-owned fields.
|
|
1787
|
+
|
|
1788
|
+
Never allow a generic stale record writer to modify immutable
|
|
1789
|
+
workerId/engine, native identity, original create/fork binding,
|
|
1790
|
+
receipt identity or recordRevision.
|
|
1791
|
+
"""
|
|
1792
|
+
if not isinstance(path, str):
|
|
1793
|
+
raise StorageIntegrityError("record patch key must be a string")
|
|
1794
|
+
parts = tuple(path.split("."))
|
|
1795
|
+
if (len(parts) == 1 and parts[0] in (
|
|
1796
|
+
"name", "waitingFor", "activeCap", "lastTurnAt",
|
|
1797
|
+
"lastSettledAt", "lastSettledTurn",
|
|
1798
|
+
"backgroundTaskSummary",
|
|
1799
|
+
"status", "lastTurn", "lastReport", "activeTurnError",
|
|
1800
|
+
"pendingApprovals", "context", "activeCompact", "lastCompact",
|
|
1801
|
+
"compactCount", "evidence", "tokenUsage", "historyKnownEmpty",
|
|
1802
|
+
"nativeArchive", "resolvedModel", "parentNotifyFences",
|
|
1803
|
+
"lastTurnError", "pendingSend", "lastConversation", "nativeTurnStart",
|
|
1804
|
+
"quotaStop",
|
|
1805
|
+
)):
|
|
1806
|
+
return parts
|
|
1807
|
+
if (len(parts) == 2 and parts[0] in (
|
|
1808
|
+
"lifecycle", "createArgs", "stats"
|
|
1809
|
+
) and parts[1].isidentifier() and not parts[1].startswith("_")):
|
|
1810
|
+
return parts
|
|
1811
|
+
raise StorageIntegrityError("immutable or unsupported worker record patch field")
|
|
1812
|
+
|
|
1813
|
+
@staticmethod
|
|
1814
|
+
def _at_field(doc: Mapping, path: tuple[str, ...]) -> object:
|
|
1815
|
+
value: object = doc
|
|
1816
|
+
for part in path:
|
|
1817
|
+
if not isinstance(value, Mapping):
|
|
1818
|
+
return None
|
|
1819
|
+
value = value.get(part)
|
|
1820
|
+
return value
|
|
1821
|
+
|
|
1822
|
+
@staticmethod
|
|
1823
|
+
def _set_field(doc: dict, path: tuple[str, ...], value: object) -> None:
|
|
1824
|
+
if len(path) == 1:
|
|
1825
|
+
doc[path[0]] = value
|
|
1826
|
+
return
|
|
1827
|
+
branch = doc.get(path[0])
|
|
1828
|
+
if branch is None:
|
|
1829
|
+
branch = {}
|
|
1830
|
+
doc[path[0]] = branch
|
|
1831
|
+
if not isinstance(branch, dict):
|
|
1832
|
+
raise StorageIntegrityError("record field parent is not an owned JSON object")
|
|
1833
|
+
branch[path[1]] = value
|
|
1834
|
+
|
|
1835
|
+
_SNAPSHOT_MUTABLE_FIELDS = frozenset({
|
|
1836
|
+
# All are CCX-owned mutable projection fields. Immutable worker
|
|
1837
|
+
# identity/native binding/fork/creation evidence and provider-owned
|
|
1838
|
+
# transcript fields are excluded intentionally.
|
|
1839
|
+
"name", "status", "waitingFor", "updatedAt",
|
|
1840
|
+
"lastTurnAt", "lastSettledAt", "lastSettledTurn",
|
|
1841
|
+
"backgroundTaskSummary",
|
|
1842
|
+
"lastTurn", "lastReport", "activeTurnError",
|
|
1843
|
+
"lastTurnError", "pendingApprovals", "context",
|
|
1844
|
+
"activeCompact", "lastCompact", "compactCount",
|
|
1845
|
+
"lifecycle", "createArgs", "stats",
|
|
1846
|
+
"nativeArchive", "lastConversation", "pendingSend",
|
|
1847
|
+
})
|
|
1848
|
+
|
|
1849
|
+
def replace_record_if_revision(
|
|
1850
|
+
self, target: str, record: Mapping, *,
|
|
1851
|
+
expected_revision: int,
|
|
1852
|
+
) -> dict:
|
|
1853
|
+
"""Exact full-ID fresh on-disk CAS for old host/codexd snapshot writers.
|
|
1854
|
+
|
|
1855
|
+
Unlike locking an arbitrarily stale whole-record write, this refuses
|
|
1856
|
+
a snapshot whose explicit monotonic revision is no longer current.
|
|
1857
|
+
It never changes immutable provider/native/owner/fork identity, never
|
|
1858
|
+
deletes unseen fields, and preserves independent modifications by
|
|
1859
|
+
rejecting stale contenders instead of losing data. Prefer the
|
|
1860
|
+
semantic field-patch API for liveness/config; this CAS is
|
|
1861
|
+
for audited operations that truly update several related fields.
|
|
1862
|
+
"""
|
|
1863
|
+
if (not isinstance(record, Mapping) or type(expected_revision) is not int
|
|
1864
|
+
or expected_revision < 0
|
|
1865
|
+
or type(record.get("recordRevision")) is not int
|
|
1866
|
+
or record["recordRevision"] != expected_revision):
|
|
1867
|
+
raise RecordConflict(
|
|
1868
|
+
"explicit current whole-record revision proof is required"
|
|
1869
|
+
)
|
|
1870
|
+
parsed = parse_managed_target(target)
|
|
1871
|
+
snapshot = json.loads(json.dumps(dict(record), allow_nan=False))
|
|
1872
|
+
with self.lease():
|
|
1873
|
+
# Invalid, foreign and quarantined IDs must not leave even an
|
|
1874
|
+
# otherwise harmless full-UUID lock file behind.
|
|
1875
|
+
self._owned(target)
|
|
1876
|
+
with self._record_lock(parsed.worker_id):
|
|
1877
|
+
owner, latest = self._owned(target)
|
|
1878
|
+
assert latest is not None
|
|
1879
|
+
if (snapshot.get("workerId") != owner.worker_id
|
|
1880
|
+
or snapshot.get("engine") != owner.engine):
|
|
1881
|
+
raise StorageIntegrityError("CAS snapshot belongs to foreign full worker")
|
|
1882
|
+
actual_revision = latest.get("recordRevision", 0)
|
|
1883
|
+
if type(actual_revision) is not int or actual_revision < 0:
|
|
1884
|
+
raise StorageIntegrityError("durable worker revision malformed")
|
|
1885
|
+
if actual_revision != expected_revision:
|
|
1886
|
+
raise RecordConflict(
|
|
1887
|
+
"stale whole-record writer would overwrite another process"
|
|
1888
|
+
)
|
|
1889
|
+
if not set(latest).issubset(snapshot):
|
|
1890
|
+
raise StorageIntegrityError(
|
|
1891
|
+
"CAS snapshot cannot silently remove existing record fields"
|
|
1892
|
+
)
|
|
1893
|
+
modified = {
|
|
1894
|
+
key for key in set(snapshot) | set(latest)
|
|
1895
|
+
if snapshot.get(key) != latest.get(key)
|
|
1896
|
+
}
|
|
1897
|
+
if modified - self._SNAPSHOT_MUTABLE_FIELDS:
|
|
1898
|
+
raise StorageIntegrityError(
|
|
1899
|
+
"CAS snapshot cannot change native, owner, original "
|
|
1900
|
+
"creation/fork binding or unknown record fields"
|
|
1901
|
+
)
|
|
1902
|
+
for parent in ("lifecycle", "createArgs", "stats",
|
|
1903
|
+
"nativeArchive"):
|
|
1904
|
+
previous = latest.get(parent)
|
|
1905
|
+
now = snapshot.get(parent)
|
|
1906
|
+
if previous is not None:
|
|
1907
|
+
if not isinstance(previous, dict) or not isinstance(now, dict):
|
|
1908
|
+
raise StorageIntegrityError(
|
|
1909
|
+
"CAS cannot replace a structured worker record field"
|
|
1910
|
+
)
|
|
1911
|
+
if not set(previous).issubset(now):
|
|
1912
|
+
raise StorageIntegrityError(
|
|
1913
|
+
"CAS cannot drop existing nested record fields"
|
|
1914
|
+
)
|
|
1915
|
+
if not modified:
|
|
1916
|
+
return latest
|
|
1917
|
+
snapshot["recordRevision"] = expected_revision + 1
|
|
1918
|
+
snapshot["updatedAt"] = int(time.time() * 1000)
|
|
1919
|
+
self._write(("workers", owner.engine, owner.worker_id),
|
|
1920
|
+
"record.json", snapshot)
|
|
1921
|
+
return snapshot
|
|
1922
|
+
|
|
1923
|
+
def patch_lifecycle(self, target: str, *, changes: Mapping) -> dict:
|
|
1924
|
+
"""Fresh per-worker atomic lifecycle field patch (not stale snapshot)."""
|
|
1925
|
+
if not isinstance(changes, Mapping) or not changes:
|
|
1926
|
+
raise StorageIntegrityError("nonempty lifecycle changes required")
|
|
1927
|
+
if any(not isinstance(k, str) or not k.isidentifier()
|
|
1928
|
+
or k.startswith("_") for k in changes):
|
|
1929
|
+
raise StorageIntegrityError("invalid lifecycle patch key")
|
|
1930
|
+
patches = {f"lifecycle.{k}": value for k, value in changes.items()}
|
|
1931
|
+
if "state" in changes:
|
|
1932
|
+
patches["lifecycle.since"] = int(time.time() * 1000)
|
|
1933
|
+
return self.mutate_record(target, patches=patches)
|
|
1934
|
+
|
|
1935
|
+
def rename_worker(self, target: str, *, engine: str, name: str | None) -> dict:
|
|
1936
|
+
"""One canonical full-ID semantic rename for CLI and Python callers.
|
|
1937
|
+
|
|
1938
|
+
Never accept a native/session display alias; no host restart and no
|
|
1939
|
+
existing stale full-record snapshot write. Only the name field is
|
|
1940
|
+
patched under the exact worker's per-UUID fresh-disk CAS lock.
|
|
1941
|
+
"""
|
|
1942
|
+
from ccx.store.records import sanitize_name
|
|
1943
|
+
if engine not in ENGINES:
|
|
1944
|
+
raise StorageIntegrityError("unsupported engine for managed rename")
|
|
1945
|
+
owner = self.resolve(target)
|
|
1946
|
+
if owner.engine != engine:
|
|
1947
|
+
raise EngineMismatch("target worker belongs to a different engine")
|
|
1948
|
+
new_name = sanitize_name(name) or (
|
|
1949
|
+
("ccx-" if engine == "claude" else "codex-") + owner.short
|
|
1950
|
+
)
|
|
1951
|
+
return self.mutate_record(owner.worker, patches={"name": new_name})
|
|
1952
|
+
|
|
1953
|
+
def mutate_record(
|
|
1954
|
+
self, target: str, *, patches: Mapping[str, object] | None = None,
|
|
1955
|
+
increments: Mapping[str, int] | None = None,
|
|
1956
|
+
expect: Mapping[str, object] | None = None,
|
|
1957
|
+
expected_revision: int | None = None,
|
|
1958
|
+
removals: tuple[str, ...] = (),
|
|
1959
|
+
) -> dict:
|
|
1960
|
+
"""Fresh on-disk READ→semantic PATCH/CAS→fsync WRITE under owner lock.
|
|
1961
|
+
|
|
1962
|
+
Never read an old in-memory whole record and then overwrite another
|
|
1963
|
+
process's independent changes. A field-specific expectation gives CAS
|
|
1964
|
+
without globally serializing unrelated config/lifecycle.
|
|
1965
|
+
Monotonic integer increments cannot lose updates. This method must be
|
|
1966
|
+
called by host, daemon and CLI writers to preserve field-level CAS.
|
|
1967
|
+
"""
|
|
1968
|
+
changes = dict(patches or {})
|
|
1969
|
+
additions = dict(increments or {})
|
|
1970
|
+
comparisons = dict(expect or {})
|
|
1971
|
+
# Only the two Claude host-owned transient fields may be REMOVED. This
|
|
1972
|
+
# is not a general deletion mechanism for owner/claim/native evidence.
|
|
1973
|
+
if (not isinstance(removals, tuple)
|
|
1974
|
+
or any(type(field) is not str for field in removals)
|
|
1975
|
+
or len(removals) != len(set(removals))
|
|
1976
|
+
or set(removals) - {"activeCap", "waitingFor"}):
|
|
1977
|
+
raise StorageIntegrityError("only Claude host transient fields may be removed")
|
|
1978
|
+
if not changes and not additions and not removals:
|
|
1979
|
+
raise StorageIntegrityError("record semantic mutation requires field changes")
|
|
1980
|
+
if (set(changes) & (set(additions) | set(removals))
|
|
1981
|
+
or set(additions) & set(removals)):
|
|
1982
|
+
raise StorageIntegrityError("record semantic field mutated more than once")
|
|
1983
|
+
if len(changes) + len(additions) > 64:
|
|
1984
|
+
raise StorageIntegrityError("record patch exceeds bounded field count")
|
|
1985
|
+
for path in (*changes, *additions, *comparisons):
|
|
1986
|
+
self._record_field_path(path)
|
|
1987
|
+
if expected_revision is not None and (
|
|
1988
|
+
type(expected_revision) is not int or expected_revision < 0):
|
|
1989
|
+
raise StorageIntegrityError("invalid expected record revision")
|
|
1990
|
+
for path, amount in additions.items():
|
|
1991
|
+
if (not path.startswith("stats.")
|
|
1992
|
+
or type(amount) is not int or amount < 0):
|
|
1993
|
+
raise StorageIntegrityError("record increments are nonnegative stats only")
|
|
1994
|
+
parsed = parse_managed_target(target)
|
|
1995
|
+
with self.lease():
|
|
1996
|
+
# Ownership validation precedes any incidental lock-file mkdir.
|
|
1997
|
+
# We repeat it after acquiring the stable per-full-UUID lock.
|
|
1998
|
+
self._owned(target)
|
|
1999
|
+
with self._record_lock(parsed.worker_id):
|
|
2000
|
+
# THIS fresh read under both locks, never a stale cached
|
|
2001
|
+
# record from before another CLI/host write.
|
|
2002
|
+
owner, current = self._owned(target)
|
|
2003
|
+
assert current is not None
|
|
2004
|
+
revision = current.get("recordRevision", 0)
|
|
2005
|
+
if type(revision) is not int or revision < 0:
|
|
2006
|
+
raise StorageIntegrityError("bad worker recordRevision")
|
|
2007
|
+
if expected_revision is not None and revision != expected_revision:
|
|
2008
|
+
raise RecordConflict("stale expected whole-record revision")
|
|
2009
|
+
for field, expected in comparisons.items():
|
|
2010
|
+
if self._at_field(current, self._record_field_path(field)) != expected:
|
|
2011
|
+
raise RecordConflict("record semantic field changed before CAS")
|
|
2012
|
+
# JSON roundtrip makes a detached copy and rejects objects not
|
|
2013
|
+
# serializable in the durable record (no arbitrary Python).
|
|
2014
|
+
candidate = json.loads(json.dumps(current, allow_nan=False))
|
|
2015
|
+
for field, value in changes.items():
|
|
2016
|
+
self._set_field(candidate, self._record_field_path(field), value)
|
|
2017
|
+
for field, delta in additions.items():
|
|
2018
|
+
path = self._record_field_path(field)
|
|
2019
|
+
previous = self._at_field(candidate, path)
|
|
2020
|
+
if type(previous) is not int:
|
|
2021
|
+
raise RecordConflict("stat increment requires existing integer counter")
|
|
2022
|
+
self._set_field(candidate, path, previous + delta)
|
|
2023
|
+
if removals:
|
|
2024
|
+
if owner.engine != "claude":
|
|
2025
|
+
raise StorageIntegrityError("host transient removal requires Claude owner")
|
|
2026
|
+
for field in removals:
|
|
2027
|
+
candidate.pop(field, None)
|
|
2028
|
+
if candidate == current and removals and not changes and not additions:
|
|
2029
|
+
# Deleting an already-absent host transient is idempotent.
|
|
2030
|
+
# Keep previous revision semantics for ordinary equal-value
|
|
2031
|
+
# patch/increment callers outside this small host slice.
|
|
2032
|
+
return current
|
|
2033
|
+
candidate["recordRevision"] = revision + 1
|
|
2034
|
+
# immutable worker identity/native fields always survive.
|
|
2035
|
+
for frozen in ("workerId", "engine", "native",
|
|
2036
|
+
"nativeCreate", "terminalOnly", "forkedFrom", "spawnedBy"):
|
|
2037
|
+
if candidate.get(frozen) != current.get(frozen):
|
|
2038
|
+
raise StorageIntegrityError("immutable native/owner field changed")
|
|
2039
|
+
self._write(("workers", owner.engine, owner.worker_id), "record.json", candidate)
|
|
2040
|
+
return candidate
|
|
2041
|
+
|
|
2042
|
+
def _native_guard(self, source: Mapping, action: str) -> None:
|
|
2043
|
+
if action not in _NATIVE_WRITES:
|
|
2044
|
+
raise NativeActionDenied("unknown native operation action")
|
|
2045
|
+
if action == "start":
|
|
2046
|
+
raise NativeActionDenied(
|
|
2047
|
+
"generic native thread/session start is forbidden;"
|
|
2048
|
+
" original create/activation proof must be reconciled separately"
|
|
2049
|
+
)
|
|
2050
|
+
native = source.get("native")
|
|
2051
|
+
if not isinstance(native, dict):
|
|
2052
|
+
raise NativeActionDenied("native binding not present; forensic read only")
|
|
2053
|
+
# New v2 workers may have a first record and *even a Claude native
|
|
2054
|
+
# sessionId* before a single native start was submitted. Neither
|
|
2055
|
+
# record existence nor a native UUID proves provider activation.
|
|
2056
|
+
# All native mutators must consult the durable original activation
|
|
2057
|
+
# receipt. Unknown/missing/temporary states never authorize send.
|
|
2058
|
+
if source.get("fingerprintVersion") != 2:
|
|
2059
|
+
raise NativeActionDenied("current full-UUID native binding requires v2")
|
|
2060
|
+
wid = canonical_worker_id(source.get("workerId"))
|
|
2061
|
+
owner, claim = self._owner(wid), self._claim(wid)
|
|
2062
|
+
activation = claim.get("nativeActivation") if isinstance(claim, dict) else None
|
|
2063
|
+
if (not isinstance(owner, dict) or not isinstance(claim, dict)
|
|
2064
|
+
or owner.get("status") != "worker"
|
|
2065
|
+
or owner.get("workerId") != wid
|
|
2066
|
+
or owner.get("engine") != source.get("engine")
|
|
2067
|
+
or owner.get("generation") != self.generation
|
|
2068
|
+
or claim.get("workerId") != wid
|
|
2069
|
+
or claim.get("engine") != source.get("engine")
|
|
2070
|
+
or claim.get("generation") != self.generation
|
|
2071
|
+
or claim.get("fingerprintVersion") != 2
|
|
2072
|
+
or not isinstance(activation, dict)
|
|
2073
|
+
or activation.get("state") != "confirmed"
|
|
2074
|
+
or not isinstance(activation.get("nativeEvidenceSha256"), str)
|
|
2075
|
+
or not _HASH.fullmatch(activation["nativeEvidenceSha256"])):
|
|
2076
|
+
raise NativeActionDenied("full-UUID native activation unconfirmed or unowned")
|
|
2077
|
+
if owner.get("provenance") == "ccx-convert":
|
|
2078
|
+
_converted_binding(owner, claim, source)
|
|
2079
|
+
elif owner.get("provenance") not in {"ccx-new", "ccx-import"}:
|
|
2080
|
+
raise NativeActionDenied("unsupported original native owner provenance")
|
|
2081
|
+
original_binding = activation.get("nativeBinding")
|
|
2082
|
+
if not isinstance(original_binding, dict):
|
|
2083
|
+
raise NativeActionDenied("confirmed claim has no original provider binding")
|
|
2084
|
+
if source["engine"] == "claude":
|
|
2085
|
+
if (native.get("sessionId") != original_binding.get("sessionId")
|
|
2086
|
+
or native.get("nativeState") != "session-bound"
|
|
2087
|
+
or activation.get("source")
|
|
2088
|
+
!= f"claude-session:{native.get('sessionId')}"):
|
|
2089
|
+
raise NativeActionDenied("Claude provider identity is not bound to original receipt")
|
|
2090
|
+
else:
|
|
2091
|
+
if (native.get("threadId") != original_binding.get("threadId")
|
|
2092
|
+
or native.get("nativeState") != "thread-bound"
|
|
2093
|
+
or native.get("threadSource") != activation.get("source")
|
|
2094
|
+
or (owner.get("provenance") != "ccx-convert"
|
|
2095
|
+
and activation.get("source") != f"ccx-worker:{wid}")):
|
|
2096
|
+
raise NativeActionDenied("Codex provider thread is not bound to original receipt")
|
|
2097
|
+
if source.get("terminalOnly") or native.get("nativeState") in (
|
|
2098
|
+
"unbound-unknown", "submitted-or-unknown",
|
|
2099
|
+
):
|
|
2100
|
+
raise NativeActionDenied("terminal-only or pending native binding: never start/resume/send")
|
|
2101
|
+
if source.get("engine") == "codex" and not native.get("threadId"):
|
|
2102
|
+
raise NativeActionDenied("Codex has no proven native thread binding")
|
|
2103
|
+
if source.get("engine") == "claude" and not native.get("sessionId"):
|
|
2104
|
+
raise NativeActionDenied("Claude has no proven native session binding")
|
|
2105
|
+
if (isinstance(source.get("nativeCreate"), dict)
|
|
2106
|
+
and source["nativeCreate"].get("state") in ("submitted", "unknown")):
|
|
2107
|
+
raise NativeActionDenied("native creation pending original receipt/source reconciliation")
|
|
2108
|
+
|
|
2109
|
+
def codex_source(self, target: str) -> str:
|
|
2110
|
+
"""Privately prove original bound Codex source, never infer it from UUID."""
|
|
2111
|
+
with self.lease():
|
|
2112
|
+
owner, record = self._owned(target)
|
|
2113
|
+
if owner.engine != "codex" or record is None:
|
|
2114
|
+
raise NativeActionDenied("Codex full-UUID owner required")
|
|
2115
|
+
self._native_guard(record, "send")
|
|
2116
|
+
return self._claim(owner.worker_id)["nativeActivation"]["source"]
|
|
2117
|
+
|
|
2118
|
+
def begin_native_intent(self, target: str, *, action: str, family: str,
|
|
2119
|
+
operation_id: str, intent_sha256: str) -> OperationIntent:
|
|
2120
|
+
"""Persist semantic operation *prepared* before any possible native call.
|
|
2121
|
+
|
|
2122
|
+
A sealed accepted/rejected receipt is replayed as a public terminal
|
|
2123
|
+
outcome, NOT dispatched again. An unknown submitted operation refuses.
|
|
2124
|
+
An unsubmitted prepared intent may be reclaimed by same spec: replacing
|
|
2125
|
+
its random permit invalidates the original caller under the receipt lock.
|
|
2126
|
+
Only dispatch_native_intent_once can reach the native transport.
|
|
2127
|
+
"""
|
|
2128
|
+
if (family not in _FAMILIES or not isinstance(operation_id, str)
|
|
2129
|
+
or not operation_id or len(operation_id) > 512 or "\\x00" in operation_id):
|
|
2130
|
+
raise StorageIntegrityError("invalid semantic operation identity")
|
|
2131
|
+
if not isinstance(intent_sha256, str) or not _HASH.fullmatch(intent_sha256):
|
|
2132
|
+
raise StorageIntegrityError("invalid operation intent digest")
|
|
2133
|
+
with self.lease():
|
|
2134
|
+
owner, source = self._owned(target)
|
|
2135
|
+
assert source is not None
|
|
2136
|
+
key = hashlib.sha256(operation_id.encode("utf-8")).hexdigest()
|
|
2137
|
+
parts = ("receipts", family, owner.engine, owner.worker_id)
|
|
2138
|
+
# Already terminal receipts are read-only replay evidence even
|
|
2139
|
+
# when the worker has since become terminalOnly/unbound. No native
|
|
2140
|
+
# capability is required to show an accepted/rejected original
|
|
2141
|
+
# outcome, and no receipt directory is created on that path.
|
|
2142
|
+
try:
|
|
2143
|
+
with self._gen(*parts) as read_fd:
|
|
2144
|
+
prior_terminal = _read_json(read_fd, key + ".json")
|
|
2145
|
+
except FileNotFoundError:
|
|
2146
|
+
prior_terminal = None
|
|
2147
|
+
if prior_terminal is not None and prior_terminal.get("state") in ("accepted", "rejected"):
|
|
2148
|
+
if (prior_terminal.get("workerId") != owner.worker_id
|
|
2149
|
+
or prior_terminal.get("engine") != owner.engine
|
|
2150
|
+
or prior_terminal.get("family") != family
|
|
2151
|
+
or prior_terminal.get("action") != action
|
|
2152
|
+
or prior_terminal.get("operationId") != operation_id
|
|
2153
|
+
or prior_terminal.get("intentSha256") != intent_sha256
|
|
2154
|
+
or prior_terminal.get("generation") != self.generation):
|
|
2155
|
+
raise CreationConflict("terminal semantic receipt identity mismatch")
|
|
2156
|
+
return OperationIntent(
|
|
2157
|
+
owner.worker_id, owner.engine, family, operation_id,
|
|
2158
|
+
intent_sha256, prior_terminal["state"], self.generation,
|
|
2159
|
+
replayed=True,
|
|
2160
|
+
native_evidence_sha256=prior_terminal.get("nativeEvidenceSha256"),
|
|
2161
|
+
)
|
|
2162
|
+
self._native_guard(source, action)
|
|
2163
|
+
with self._gen(*parts, create=True) as fd:
|
|
2164
|
+
with _flock_at(fd, key + ".lock", exclusive=True, create=True):
|
|
2165
|
+
self.revalidate()
|
|
2166
|
+
original = _read_json(fd, key + ".json")
|
|
2167
|
+
if original is not None:
|
|
2168
|
+
if (original.get("workerId") != owner.worker_id
|
|
2169
|
+
or original.get("engine") != owner.engine
|
|
2170
|
+
or original.get("family") != family
|
|
2171
|
+
or original.get("action") != action
|
|
2172
|
+
or original.get("operationId") != operation_id
|
|
2173
|
+
or original.get("intentSha256") != intent_sha256
|
|
2174
|
+
or original.get("generation") != self.generation):
|
|
2175
|
+
raise CreationConflict("operation token already bound to different owner/intent")
|
|
2176
|
+
state = original.get("state")
|
|
2177
|
+
if state in ("accepted", "rejected"):
|
|
2178
|
+
return OperationIntent(
|
|
2179
|
+
owner.worker_id, owner.engine, family, operation_id,
|
|
2180
|
+
intent_sha256, state, self.generation,
|
|
2181
|
+
replayed=True,
|
|
2182
|
+
native_evidence_sha256=original.get("nativeEvidenceSha256"),
|
|
2183
|
+
)
|
|
2184
|
+
if state != "prepared":
|
|
2185
|
+
raise NativeEffectIndeterminate(
|
|
2186
|
+
"submitted native operation has unknown result; no replay"
|
|
2187
|
+
)
|
|
2188
|
+
# Prepared means all new-code native dispatches are
|
|
2189
|
+
# impossible without first durably recording submitted.
|
|
2190
|
+
permit = uuid4().hex
|
|
2191
|
+
pending = {
|
|
2192
|
+
"schemaVersion": _RECEIPT_SCHEMA,
|
|
2193
|
+
"workerId": owner.worker_id, "engine": owner.engine,
|
|
2194
|
+
"generation": self.generation, "family": family,
|
|
2195
|
+
"action": action, "operationId": operation_id,
|
|
2196
|
+
"intentSha256": intent_sha256, "state": "prepared",
|
|
2197
|
+
"dispatchPermitSha256": hashlib.sha256(
|
|
2198
|
+
permit.encode("ascii")).hexdigest(),
|
|
2199
|
+
"nativeEvidenceSha256": None,
|
|
2200
|
+
}
|
|
2201
|
+
_write_json(fd, key + ".json", pending)
|
|
2202
|
+
return OperationIntent(
|
|
2203
|
+
owner.worker_id, owner.engine, family, operation_id,
|
|
2204
|
+
intent_sha256, "prepared", self.generation,
|
|
2205
|
+
dispatch_token=permit,
|
|
2206
|
+
)
|
|
2207
|
+
|
|
2208
|
+
def dispatch_native_intent_once(self, prepared: OperationIntent, *,
|
|
2209
|
+
bounded_submit: Callable[[float], Mapping | None],
|
|
2210
|
+
dispatch_budget_s: float = 1.0,
|
|
2211
|
+
_failpoint: Callable[[str], None] | None = None) -> dict:
|
|
2212
|
+
"""Same critical section rechecks ACTIVE + marks submitted before native IPC.
|
|
2213
|
+
|
|
2214
|
+
The provided transport MUST honor the supplied single-request timeout,
|
|
2215
|
+
cannot wait for a provider turn, and cannot run arbitrary user Python.
|
|
2216
|
+
No pointer can flip under a legitimate runner's exclusive lock during
|
|
2217
|
+
bounded send; a pointer changed outside that protocol still triggers
|
|
2218
|
+
pre-dispatch refusal. Unknown results leave a durable replay fence.
|
|
2219
|
+
"""
|
|
2220
|
+
if not 0 < dispatch_budget_s <= 2:
|
|
2221
|
+
raise GenerationError("native receipt dispatch must have deadline <= 2 seconds")
|
|
2222
|
+
if prepared.replayed or prepared.state != "prepared" or not prepared.dispatch_token:
|
|
2223
|
+
raise NativeEffectIndeterminate("terminal or unknown receipt cannot dispatch natively")
|
|
2224
|
+
if prepared.generation != self.generation:
|
|
2225
|
+
raise StaleGeneration("receipt prepared under different generation")
|
|
2226
|
+
with self.lease(timeout=min(dispatch_budget_s, 2)):
|
|
2227
|
+
owner, source = self._owned(f"{prepared.engine}:{prepared.worker_id}")
|
|
2228
|
+
assert source is not None
|
|
2229
|
+
with self._gen("receipts", prepared.family, owner.engine,
|
|
2230
|
+
owner.worker_id) as fd:
|
|
2231
|
+
key = hashlib.sha256(prepared.operation_id.encode("utf-8")).hexdigest()
|
|
2232
|
+
with _flock_at(fd, key + ".lock", exclusive=True):
|
|
2233
|
+
self.revalidate()
|
|
2234
|
+
record = _read_json(fd, key + ".json")
|
|
2235
|
+
if record is None:
|
|
2236
|
+
raise StorageIntegrityError("missing original prepared receipt")
|
|
2237
|
+
self._native_guard(source, record.get("action", ""))
|
|
2238
|
+
if (record.get("workerId") != owner.worker_id
|
|
2239
|
+
or record.get("engine") != prepared.engine
|
|
2240
|
+
or record.get("family") != prepared.family
|
|
2241
|
+
or record.get("operationId") != prepared.operation_id
|
|
2242
|
+
or record.get("intentSha256") != prepared.intent_sha256
|
|
2243
|
+
or record.get("generation") != self.generation):
|
|
2244
|
+
raise StorageIntegrityError("native dispatch claim owner/intent mismatch")
|
|
2245
|
+
if record.get("state") != "prepared":
|
|
2246
|
+
raise NativeEffectIndeterminate("native dispatch already submitted or terminal")
|
|
2247
|
+
expected = hashlib.sha256(
|
|
2248
|
+
prepared.dispatch_token.encode("ascii")).hexdigest()
|
|
2249
|
+
if record.get("dispatchPermitSha256") != expected:
|
|
2250
|
+
raise NativeEffectIndeterminate("superseded receipt dispatch permit")
|
|
2251
|
+
if _failpoint:
|
|
2252
|
+
_failpoint("before-submitted")
|
|
2253
|
+
self.revalidate()
|
|
2254
|
+
unknown = dict(record)
|
|
2255
|
+
unknown["state"] = "submitted-unknown"
|
|
2256
|
+
unknown["dispatchPermitSha256"] = None
|
|
2257
|
+
_write_json(fd, key + ".json", unknown)
|
|
2258
|
+
if _failpoint:
|
|
2259
|
+
_failpoint("after-submitted")
|
|
2260
|
+
self.revalidate()
|
|
2261
|
+
try:
|
|
2262
|
+
answer = bounded_submit(dispatch_budget_s)
|
|
2263
|
+
except Exception as exc:
|
|
2264
|
+
raise NativeEffectIndeterminate(
|
|
2265
|
+
"native receipt response unknown; do not replay"
|
|
2266
|
+
) from exc
|
|
2267
|
+
if not isinstance(answer, Mapping):
|
|
2268
|
+
raise NativeEffectIndeterminate(
|
|
2269
|
+
"native receipt missing exact acceptance evidence"
|
|
2270
|
+
)
|
|
2271
|
+
return dict(answer)
|
|
2272
|
+
|
|
2273
|
+
def settle_native_intent(self, attempt: OperationIntent, *, state: str,
|
|
2274
|
+
native_evidence_sha256: str | None) -> dict:
|
|
2275
|
+
"""Only terminal with durable positive evidence; changed pointer refuses.
|
|
2276
|
+
|
|
2277
|
+
If the native acknowledgment was lost, leave submitted-unknown fenced
|
|
2278
|
+
and use a separately reviewed exact-native-history reconciliation.
|
|
2279
|
+
"""
|
|
2280
|
+
if state not in ("accepted", "rejected", "unknown"):
|
|
2281
|
+
raise StorageIntegrityError("invalid native settlement")
|
|
2282
|
+
if state != "unknown" and (not isinstance(native_evidence_sha256, str)
|
|
2283
|
+
or not _HASH.fullmatch(native_evidence_sha256)):
|
|
2284
|
+
raise NativeEffectIndeterminate("cannot assert native terminal without exact evidence")
|
|
2285
|
+
if attempt.generation != self.generation:
|
|
2286
|
+
raise StaleGeneration("attempt from a different generation")
|
|
2287
|
+
with self.lease():
|
|
2288
|
+
owner, _source = self._owned(f"{attempt.engine}:{attempt.worker_id}")
|
|
2289
|
+
if owner.worker_id != attempt.worker_id:
|
|
2290
|
+
raise StorageIntegrityError("receipt owner changed")
|
|
2291
|
+
key = hashlib.sha256(attempt.operation_id.encode("utf-8")).hexdigest()
|
|
2292
|
+
with self._gen("receipts", attempt.family, attempt.engine, attempt.worker_id) as fd:
|
|
2293
|
+
with _flock_at(fd, key + ".lock", exclusive=True):
|
|
2294
|
+
prior = _read_json(fd, key + ".json")
|
|
2295
|
+
if not prior or (prior.get("operationId"), prior.get("intentSha256"),
|
|
2296
|
+
prior.get("workerId"), prior.get("generation")) != (
|
|
2297
|
+
attempt.operation_id, attempt.intent_sha256,
|
|
2298
|
+
attempt.worker_id, self.generation,
|
|
2299
|
+
):
|
|
2300
|
+
raise StorageIntegrityError("missing/mismatched original receipt fence")
|
|
2301
|
+
if prior.get("state") in ("accepted", "rejected"):
|
|
2302
|
+
if (prior["state"], prior.get("nativeEvidenceSha256")) != (state, native_evidence_sha256):
|
|
2303
|
+
raise CreationConflict("terminal native proof is immutable")
|
|
2304
|
+
return prior
|
|
2305
|
+
if state == "unknown":
|
|
2306
|
+
return prior
|
|
2307
|
+
if prior.get("state") != "submitted-unknown":
|
|
2308
|
+
raise NativeEffectIndeterminate(
|
|
2309
|
+
"cannot assert accepted/rejected before durable native dispatch"
|
|
2310
|
+
)
|
|
2311
|
+
result = dict(prior)
|
|
2312
|
+
result.update(state=state, nativeEvidenceSha256=native_evidence_sha256)
|
|
2313
|
+
_write_json(fd, key + ".json", result)
|
|
2314
|
+
return result
|
|
2315
|
+
|
|
2316
|
+
def read_receipt(self, target: str, family: str, operation_id: str) -> dict | None:
|
|
2317
|
+
path = self.receipt_path(target, family, operation_id)
|
|
2318
|
+
with self.lease():
|
|
2319
|
+
owner, _ = self._owned(target)
|
|
2320
|
+
key = path.name
|
|
2321
|
+
try:
|
|
2322
|
+
with self._gen("receipts", family, owner.engine, owner.worker_id) as fd:
|
|
2323
|
+
value = _read_json(fd, key)
|
|
2324
|
+
except FileNotFoundError:
|
|
2325
|
+
return None
|
|
2326
|
+
if value is not None and (
|
|
2327
|
+
value.get("workerId") != owner.worker_id or value.get("engine") != owner.engine
|
|
2328
|
+
or value.get("operationId") != operation_id
|
|
2329
|
+
or value.get("generation") != self.generation
|
|
2330
|
+
):
|
|
2331
|
+
raise StorageIntegrityError("semantic receipt identity mismatch")
|
|
2332
|
+
return value
|