ccx-native 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +163 -0
  5. package/ccx/__main__.py +28 -0
  6. package/ccx/claude/__init__.py +0 -0
  7. package/ccx/claude/analytics.py +466 -0
  8. package/ccx/claude/attach.py +762 -0
  9. package/ccx/claude/context.py +465 -0
  10. package/ccx/claude/delivery.py +806 -0
  11. package/ccx/claude/external.py +253 -0
  12. package/ccx/claude/host/__init__.py +0 -0
  13. package/ccx/claude/host/events.py +297 -0
  14. package/ccx/claude/host/hooks.py +438 -0
  15. package/ccx/claude/host/pty.py +250 -0
  16. package/ccx/claude/host/session.py +5184 -0
  17. package/ccx/claude/host/subagents.py +169 -0
  18. package/ccx/claude/host/successor.py +267 -0
  19. package/ccx/claude/managed_turns.py +240 -0
  20. package/ccx/claude/oauth.py +176 -0
  21. package/ccx/claude/settings.py +339 -0
  22. package/ccx/claude/spawn.py +557 -0
  23. package/ccx/claude/tasks.py +72 -0
  24. package/ccx/claude/telemetry.py +282 -0
  25. package/ccx/claude/transcript.py +2281 -0
  26. package/ccx/claude/usage.py +837 -0
  27. package/ccx/claude/usage_control.py +152 -0
  28. package/ccx/cli/__init__.py +0 -0
  29. package/ccx/cli/codex.py +1009 -0
  30. package/ccx/cli/commands/__init__.py +0 -0
  31. package/ccx/cli/commands/_common.py +407 -0
  32. package/ccx/cli/commands/control.py +161 -0
  33. package/ccx/cli/commands/create.py +669 -0
  34. package/ccx/cli/commands/read.py +1020 -0
  35. package/ccx/cli/commands/turns.py +1081 -0
  36. package/ccx/cli/controller.py +757 -0
  37. package/ccx/cli/main.py +301 -0
  38. package/ccx/cli/parser.py +1324 -0
  39. package/ccx/cli/skills.py +215 -0
  40. package/ccx/cli/statusline.py +160 -0
  41. package/ccx/client.py +1900 -0
  42. package/ccx/codex/__init__.py +0 -0
  43. package/ccx/codex/attach_tty.py +267 -0
  44. package/ccx/codex/client.py +732 -0
  45. package/ccx/codex/completion.py +121 -0
  46. package/ccx/codex/control.py +288 -0
  47. package/ccx/codex/daemon/__init__.py +0 -0
  48. package/ccx/codex/daemon/__main__.py +4 -0
  49. package/ccx/codex/daemon/daemon.py +1556 -0
  50. package/ccx/codex/daemon/models.py +140 -0
  51. package/ccx/codex/daemon/native.py +1590 -0
  52. package/ccx/codex/daemon/release.py +367 -0
  53. package/ccx/codex/daemon/worker.py +2396 -0
  54. package/ccx/codex/delivery.py +758 -0
  55. package/ccx/codex/delivery_recovery.py +232 -0
  56. package/ccx/codex/evidence.py +827 -0
  57. package/ccx/codex/history.py +158 -0
  58. package/ccx/codex/notify_history.py +140 -0
  59. package/ccx/codex/ops.py +666 -0
  60. package/ccx/codex/paths.py +70 -0
  61. package/ccx/codex/protocol.py +805 -0
  62. package/ccx/codex/provenance.py +105 -0
  63. package/ccx/codex/read.py +29 -0
  64. package/ccx/codex/records.py +51 -0
  65. package/ccx/codex/reports.py +177 -0
  66. package/ccx/codex/runtime.py +1412 -0
  67. package/ccx/codex/settle.py +363 -0
  68. package/ccx/codex/telemetry.py +792 -0
  69. package/ccx/codex/types.py +659 -0
  70. package/ccx/codex/worker_environment.py +25 -0
  71. package/ccx/constants.py +288 -0
  72. package/ccx/controllers/__init__.py +0 -0
  73. package/ccx/controllers/controller.py +1449 -0
  74. package/ccx/controllers/live_vars.py +1706 -0
  75. package/ccx/controllers/live_vars_runner.py +216 -0
  76. package/ccx/controllers/notify.py +213 -0
  77. package/ccx/controllers/orchestrator.py +193 -0
  78. package/ccx/controllers/policy.py +57 -0
  79. package/ccx/controllers/store.py +681 -0
  80. package/ccx/errors.py +634 -0
  81. package/ccx/notify/__init__.py +0 -0
  82. package/ccx/notify/__main__.py +5 -0
  83. package/ccx/notify/client.py +116 -0
  84. package/ccx/notify/daemon.py +480 -0
  85. package/ccx/notify/deliver.py +304 -0
  86. package/ccx/notify/paths.py +51 -0
  87. package/ccx/notify/registry.py +649 -0
  88. package/ccx/notify/select.py +750 -0
  89. package/ccx/notify/settle.py +684 -0
  90. package/ccx/ops/__init__.py +0 -0
  91. package/ccx/ops/background.py +115 -0
  92. package/ccx/ops/cancel.py +289 -0
  93. package/ccx/ops/codex_import.py +96 -0
  94. package/ccx/ops/common.py +397 -0
  95. package/ccx/ops/compact.py +337 -0
  96. package/ccx/ops/compact_ops.py +550 -0
  97. package/ccx/ops/context_events.py +48 -0
  98. package/ccx/ops/conversation.py +1540 -0
  99. package/ccx/ops/conversation_paging.py +1338 -0
  100. package/ccx/ops/create.py +56 -0
  101. package/ccx/ops/creation.py +952 -0
  102. package/ccx/ops/creation_steps.py +579 -0
  103. package/ccx/ops/event_feed.py +710 -0
  104. package/ccx/ops/forking.py +711 -0
  105. package/ccx/ops/input.py +556 -0
  106. package/ccx/ops/maintenance.py +295 -0
  107. package/ccx/ops/report.py +770 -0
  108. package/ccx/ops/semantics.py +490 -0
  109. package/ccx/ops/send.py +961 -0
  110. package/ccx/ops/steer_ops.py +867 -0
  111. package/ccx/ops/terminate.py +383 -0
  112. package/ccx/ops/turn_reports.py +306 -0
  113. package/ccx/ops/wait.py +762 -0
  114. package/ccx/ops/worker_events.py +636 -0
  115. package/ccx/rpc/__init__.py +0 -0
  116. package/ccx/rpc/local_rpc.py +1602 -0
  117. package/ccx/rpc/rpc.py +559 -0
  118. package/ccx/rpc/serve.py +374 -0
  119. package/ccx/rpc/transport.py +409 -0
  120. package/ccx/rpc/view.py +125 -0
  121. package/ccx/store/__init__.py +0 -0
  122. package/ccx/store/discovery.py +170 -0
  123. package/ccx/store/identity.py +105 -0
  124. package/ccx/store/journal_io.py +79 -0
  125. package/ccx/store/liveness.py +194 -0
  126. package/ccx/store/paths.py +254 -0
  127. package/ccx/store/record_cache.py +40 -0
  128. package/ccx/store/records.py +309 -0
  129. package/ccx/store/root.py +804 -0
  130. package/ccx/store/runtime_presence.py +137 -0
  131. package/ccx/store/store.py +2332 -0
  132. package/ccx/store/worker_state.py +86 -0
  133. package/ccx/system/__init__.py +0 -0
  134. package/ccx/system/bounded.py +296 -0
  135. package/ccx/system/cwd_paths.py +25 -0
  136. package/ccx/system/engine_binary.py +59 -0
  137. package/ccx/system/executable.py +50 -0
  138. package/ccx/system/oslayer.py +667 -0
  139. package/ccx/system/pipes.py +46 -0
  140. package/ccx/system/sanitize.py +43 -0
  141. package/ccx/types.py +705 -0
  142. package/ccx/version.py +29 -0
  143. package/ccx/workers.py +2081 -0
  144. package/ccx.py +9 -0
  145. package/package.json +39 -7
  146. package/pyproject.toml +87 -0
  147. package/skills/ccx/SKILL.md +155 -0
  148. package/skills/ccx/references/controllers.md +244 -0
  149. package/skills/ccx/references/troubleshooting.md +94 -0
  150. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,1706 @@
1
+ """Shared/durable Python variables for CCX-managed workers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from collections.abc import Mapping, MutableMapping
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ import importlib
10
+ import io
11
+ import json
12
+ import keyword
13
+ import os
14
+ from pathlib import Path
15
+ import queue
16
+ import shlex
17
+ import shutil
18
+ import socket
19
+ import stat
20
+ import subprocess
21
+ import sys
22
+ import sysconfig
23
+ import threading
24
+ import time
25
+ import uuid
26
+ from typing import Any
27
+
28
+ from ccx import constants
29
+ from ccx.system import oslayer
30
+ from ccx.errors import CapabilityUnsupported, InvalidRequest, SessionUnavailable
31
+ from ccx.system.executable import current_executable_path
32
+ from ccx.controllers.live_vars_runner import execute_target, strict_json, STATE_FILE_LIMIT
33
+ from ccx.store.paths import accept_same_user, bind_private_unix_socket, _require_unix_socket_budget
34
+
35
+
36
+ VARIABLE_SCHEMA_VERSION = 2
37
+ VARIABLE_STATE_SCHEMA_VERSION = 1
38
+ _WIRE_DUMPS = json.dumps
39
+ _WIRE_LOADS = json.loads
40
+ _NATIVE_FALLTHROUGH_MODULES = frozenset({"pip", "venv", "ensurepip"})
41
+
42
+
43
+ def _root() -> Path:
44
+ from ccx.store.root import require_ccx_active
45
+ return require_ccx_active(constants.CCX_ROOT).pointer.generation_root / "variables"
46
+
47
+
48
+ def sidecar_path(engine: str, short: str) -> Path:
49
+ store, owner = _env_worker(engine, short)
50
+ return store.pointer.generation_root / "variables" / engine / f"{owner.worker_id}.json"
51
+
52
+
53
+ def sidecar_lock_path(engine: str, short: str) -> Path:
54
+ store, owner = _env_worker(engine, short)
55
+ return store.pointer.generation_root / "variables" / engine / f"{owner.worker_id}.lock"
56
+
57
+
58
+ def states_dir() -> Path:
59
+ return _root() / "states"
60
+
61
+
62
+ def state_path(state_id: str) -> Path:
63
+ return states_dir() / f"{_valid_state_id(state_id)}.json"
64
+
65
+
66
+ def state_lock_path(state_id: str) -> Path:
67
+ return states_dir() / f"{_valid_state_id(state_id)}.lock"
68
+
69
+
70
+ def _env_worker(engine: str, worker_id: str):
71
+ from ccx.store.root import require_ccx_active
72
+ from ccx.store.identity import EngineMismatch
73
+ store = require_ccx_active(constants.CCX_ROOT)
74
+ owner = store.resolve(worker_id)
75
+ if owner.engine != engine:
76
+ raise EngineMismatch("Worker uses a different engine; check the handle")
77
+ return store, owner
78
+
79
+
80
+ def shell_env_path(engine: str, short: str) -> Path:
81
+ store, owner = _env_worker(engine, short)
82
+ return (store.pointer.generation_root / "variables" / "env" /
83
+ engine / f"{owner.worker_id}.sh")
84
+
85
+
86
+ def claude_bridge_path(short: str) -> Path:
87
+ store, owner = _env_worker("claude", short)
88
+ return (store.pointer.generation_root / "variables" / "env" /
89
+ "claude" / f"{owner.worker_id}.bridge.json")
90
+
91
+
92
+ def _owner_runtime(
93
+ basename: str | None = None,
94
+ ) -> tuple[Path, oslayer.RuntimeChoice]:
95
+ """Plan the full live-owner socket suffix before any filesystem writes."""
96
+ dirname = f"ccx-live-vars-{os.getuid()}"
97
+ suffix = (max(
98
+ len(os.fsencode(f"/{dirname}/{basename}")) + 1,
99
+ len(os.fsencode(f"/{dirname}/.XXXXXXXX/s")) + 1,
100
+ ) if basename else 0)
101
+ choice = oslayer.runtime_base(purpose="live_vars", suffix_bytes=suffix)
102
+ if choice is None:
103
+ raise PermissionError("no private owner-socket runtime base available")
104
+ return choice.path / dirname, choice
105
+
106
+
107
+ def _owners_path() -> Path:
108
+ return _owner_runtime()[0]
109
+
110
+
111
+ def owners_dir(
112
+ path: Path | None = None, *, runtime: oslayer.RuntimeChoice | None = None,
113
+ ) -> Path:
114
+ if path is None:
115
+ path, runtime = _owner_runtime()
116
+ if runtime is None:
117
+ runtime = oslayer.RuntimeChoice(path.parent)
118
+ from ccx.rpc.transport import _ensure_private_runtime_directory
119
+ _ensure_private_runtime_directory(path, runtime)
120
+ st = os.lstat(path)
121
+ if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid():
122
+ raise PermissionError(
123
+ f"live-vars owner directory is not private and user-owned: {path}"
124
+ )
125
+ os.chmod(path, 0o700)
126
+ return path
127
+
128
+
129
+ def _atomic_json(path: Path, value: dict[str, Any]) -> None:
130
+ """Main's atomic state replacement inside the validated private generation."""
131
+ text = strict_json(value)
132
+ if len(text.encode("utf-8")) > STATE_FILE_LIMIT:
133
+ raise InvalidRequest("worker variables exceed the 4 MiB state limit")
134
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
135
+ tmp.write_text(text, encoding="utf-8")
136
+ try:
137
+ os.chmod(tmp, 0o600)
138
+ os.replace(tmp, path)
139
+ finally:
140
+ tmp.unlink(missing_ok=True)
141
+
142
+
143
+ @contextmanager
144
+ def _worker_lock(engine: str, short: str):
145
+ from ccx.store.root import _flock_at
146
+ store, owner = _env_worker(engine, short)
147
+ with store.lease():
148
+ with store._gen("variables", engine, create=True) as directory:
149
+ with _flock_at(directory, owner.worker_id + ".lock",
150
+ exclusive=True, create=True):
151
+ yield
152
+
153
+
154
+ @contextmanager
155
+ def _state_lock(state_id: str, *, create: bool = False):
156
+ from ccx.store.root import _flock_at, require_ccx_active
157
+ store = require_ccx_active(constants.CCX_ROOT)
158
+ with store.lease():
159
+ with store._gen("variables", "states", create=create) as directory:
160
+ with _flock_at(directory, _valid_state_id(state_id) + ".lock",
161
+ exclusive=True, create=create):
162
+ yield
163
+
164
+
165
+ def _valid_state_id(state_id: Any) -> str:
166
+ value = str(state_id or "")
167
+ if len(value) != 32 or any(ch not in "0123456789abcdef" for ch in value):
168
+ raise SessionUnavailable(f"invalid durable variable state id: {value!r}")
169
+ return value
170
+
171
+
172
+ def _new_state_id() -> str:
173
+ return uuid.uuid4().hex
174
+
175
+
176
+ def read_sidecar(engine: str, short: str) -> dict[str, Any] | None:
177
+ """Read a binding only after resolving its exact global full-UUID owner."""
178
+ from ccx.store.root import _read_json, StorageIntegrityError
179
+ store, owner = _env_worker(engine, short)
180
+ with store.lease():
181
+ try:
182
+ with store._gen("variables", engine) as directory:
183
+ value = _read_json(directory, owner.worker_id + ".json")
184
+ except FileNotFoundError:
185
+ return None
186
+ if value is not None and (
187
+ value.get("schemaVersion") != VARIABLE_SCHEMA_VERSION
188
+ or value.get("engine") != owner.engine
189
+ or value.get("workerId") != owner.worker_id
190
+ or value.get("mode") not in {"live", "durable"}
191
+ ):
192
+ raise StorageIntegrityError("Worker variable reference is incomplete; use the full handle")
193
+ return value
194
+
195
+
196
+ def read_state(state_id: str) -> dict[str, Any]:
197
+ from ccx.store.root import _read_json, require_ccx_active, StorageIntegrityError
198
+ store = require_ccx_active(constants.CCX_ROOT)
199
+ with store.lease():
200
+ try:
201
+ with store._gen("variables", "states") as directory:
202
+ value = _read_json(directory, _valid_state_id(state_id) + ".json")
203
+ except FileNotFoundError:
204
+ value = None
205
+ if (value is None or value.get("schemaVersion") != VARIABLE_STATE_SCHEMA_VERSION
206
+ or value.get("stateId") != state_id
207
+ or not isinstance(value.get("names"), list)
208
+ or not isinstance(value.get("variables"), dict)):
209
+ raise StorageIntegrityError("Worker variables are unavailable; inspect the worker")
210
+ names, variables = value["names"], value["variables"]
211
+ if (any(not isinstance(name, str) or not name.isidentifier()
212
+ or keyword.iskeyword(name) for name in names)
213
+ or len(set(names)) != len(names)
214
+ or set(names) != set(variables)):
215
+ raise StorageIntegrityError("Worker variable names do not match saved values; inspect them")
216
+ return value
217
+
218
+
219
+ def _new_state_document(
220
+ state_id: str,
221
+ *,
222
+ names: list[str],
223
+ variables: dict[str, Any],
224
+ ) -> dict[str, Any]:
225
+ return {
226
+ "schemaVersion": VARIABLE_STATE_SCHEMA_VERSION,
227
+ "stateId": _valid_state_id(state_id),
228
+ "names": sorted(names),
229
+ "variables": _json_clone(variables, "durable variables"),
230
+ }
231
+
232
+
233
+ def durable_state_id(
234
+ engine: str,
235
+ short: str,
236
+ value: dict[str, Any] | None = None,
237
+ ) -> str:
238
+ from ccx.store.root import StorageIntegrityError
239
+ sidecar = value if value is not None else read_sidecar(engine, short)
240
+ if not sidecar or sidecar.get("mode") != "durable":
241
+ raise SessionUnavailable(f"{engine}:{short} has no shared variables; attach variables when creating it")
242
+ if not sidecar.get("stateId"):
243
+ raise StorageIntegrityError("Shared variable state is missing; inspect the worker")
244
+ return _valid_state_id(sidecar["stateId"])
245
+
246
+
247
+ def _valid_name(name: Any) -> str:
248
+ if (not isinstance(name, str) or not name.isidentifier()
249
+ or keyword.iskeyword(name) or name.startswith("__")):
250
+ raise InvalidRequest(
251
+ f"variable names must be valid non-keyword Python identifiers, got {name!r}"
252
+ )
253
+ return name
254
+
255
+
256
+ def _names(values: Mapping[str, Any]) -> list[str]:
257
+ return sorted(_valid_name(name) for name in values.keys())
258
+
259
+
260
+ def _json_clone(value: Any, label: str) -> Any:
261
+ try:
262
+ return json.loads(strict_json(value))
263
+ except (TypeError, ValueError) as error:
264
+ raise InvalidRequest(f"{label} must be JSON-compatible: {error}") from error
265
+
266
+
267
+ def parse_cli_vars(items: list[str] | None) -> dict[str, Any]:
268
+ result: dict[str, Any] = {}
269
+ for raw in items or []:
270
+ if "=" not in raw:
271
+ raise InvalidRequest(
272
+ f"--var expects NAME=JSON, got {raw!r}"
273
+ )
274
+ name, encoded = raw.split("=", 1)
275
+ name = _valid_name(name)
276
+ if name in result:
277
+ raise InvalidRequest(f"duplicate --var binding {name!r}")
278
+ try:
279
+ value = json.loads(encoded)
280
+ except ValueError as error:
281
+ raise InvalidRequest(
282
+ f"--var {name}=... must contain valid JSON: {error}"
283
+ ) from error
284
+ result[name] = _json_clone(value, f"variable {name!r}")
285
+ return result
286
+
287
+
288
+ def _active_controller_runtime():
289
+ try:
290
+ module = importlib.import_module('ccx.controllers.controller')
291
+ return module._active_runtime.get()
292
+ except Exception:
293
+ return None
294
+
295
+
296
+ @dataclass
297
+ class PreparedAttachment:
298
+ mode: str
299
+ names: list[str]
300
+ mapping: MutableMapping[str, Any] | None = None
301
+ values: dict[str, Any] | None = None
302
+ state_id: str | None = None
303
+ owner: "LiveOwner | None" = None
304
+
305
+
306
+ def _check_initial_durable_size(names: list[str], values: dict[str, Any]) -> None:
307
+ # A fresh state has a fixed-length state ID; reject before worker creation.
308
+ document = {"schemaVersion": VARIABLE_STATE_SCHEMA_VERSION,
309
+ "stateId": "0" * 32, "names": names, "variables": values}
310
+ if len(strict_json(document).encode("utf-8")) > STATE_FILE_LIMIT:
311
+ raise InvalidRequest("worker variables exceed the 4 MiB state limit")
312
+
313
+
314
+ def prepare_attachment(values: Mapping[str, Any] | None) -> PreparedAttachment | None:
315
+ if values is None:
316
+ return None
317
+ if not isinstance(values, Mapping):
318
+ raise InvalidRequest("variables must be a mapping of Python names to values")
319
+ names = _names(values)
320
+ runtime = _active_controller_runtime()
321
+ if runtime is not None:
322
+ if not isinstance(values, MutableMapping):
323
+ raise InvalidRequest(
324
+ "controller-backed variables require a mutable mapping so worker rebinding can write back"
325
+ )
326
+ owner = runtime.live_vars_owner()
327
+ return PreparedAttachment(
328
+ mode="live",
329
+ names=names,
330
+ mapping=values,
331
+ owner=owner,
332
+ )
333
+ durable = _json_clone(dict(values), "variables")
334
+ _check_initial_durable_size(names, durable)
335
+ return PreparedAttachment(mode="durable", names=names, values=durable)
336
+
337
+
338
+ def prepare_durable_attachment(values: Mapping[str, Any] | None) -> PreparedAttachment | None:
339
+ if values is None:
340
+ return None
341
+ if not isinstance(values, Mapping):
342
+ raise InvalidRequest("variables must be a mapping of Python names to values")
343
+ names = _names(values)
344
+ durable = _json_clone(dict(values), "variables")
345
+ _check_initial_durable_size(names, durable)
346
+ return PreparedAttachment(mode="durable", names=names, values=durable)
347
+
348
+
349
+ def prepare_shared_durable_attachment(
350
+ engine: str,
351
+ short: str,
352
+ ) -> PreparedAttachment:
353
+ store, owner = _env_worker(engine, short)
354
+ info = store.variables_info(owner.worker)
355
+ if info["mode"] != "durable":
356
+ raise InvalidRequest(f"{owner.worker} has no durable variables to share")
357
+ if not info["available"]:
358
+ raise SessionUnavailable(f"{owner.worker} durable variables are unavailable")
359
+ return PreparedAttachment(
360
+ mode="durable",
361
+ names=info["names"],
362
+ state_id=info["state_id"],
363
+ )
364
+
365
+
366
+ def _owner_gone(owner: Mapping[str, Any]) -> bool:
367
+ socket_path = owner.get("socket")
368
+ if not isinstance(socket_path, str) or not socket_path:
369
+ return False
370
+ try:
371
+ os.lstat(socket_path)
372
+ except FileNotFoundError:
373
+ return True
374
+ except OSError:
375
+ return False
376
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
377
+ try:
378
+ client.settimeout(0.25)
379
+ client.connect(socket_path)
380
+ return False
381
+ except (FileNotFoundError, ConnectionRefusedError):
382
+ return True
383
+ except OSError:
384
+ return False
385
+ finally:
386
+ client.close()
387
+
388
+
389
+ def _unlink_sidecar(store, engine: str, worker_id: str) -> None:
390
+ with store.lease():
391
+ with store._gen("variables", engine) as directory:
392
+ os.unlink(worker_id + ".json", dir_fd=directory)
393
+ os.fsync(directory)
394
+
395
+
396
+ def _clear_dead_binding(engine: str, worker_id: str, token: str) -> None:
397
+ store, owner = _env_worker(engine, worker_id)
398
+ with _worker_lock(engine, owner.worker_id):
399
+ value = read_sidecar(engine, owner.worker_id)
400
+ if (value and value.get("mode") == "live"
401
+ and (value.get("owner") or {}).get("token") == token):
402
+ # Recheck after acquiring the writer lock.
403
+ if _owner_gone(value["owner"]):
404
+ _unlink_sidecar(store, engine, owner.worker_id)
405
+
406
+
407
+
408
+ def publish_attachment(
409
+ attachment: PreparedAttachment | None,
410
+ engine: str,
411
+ short: str,
412
+ ) -> None:
413
+ if attachment is None:
414
+ return
415
+ if not attachment.names and attachment.state_id is None:
416
+ return
417
+
418
+ store, owner = _env_worker(engine, short)
419
+ with _worker_lock(engine, owner.worker_id):
420
+ existing = read_sidecar(engine, owner.worker_id)
421
+ if attachment.mode == "live":
422
+ assert attachment.owner is not None
423
+ assert attachment.mapping is not None
424
+ if existing and existing["mode"] == "durable":
425
+ raise InvalidRequest(
426
+ f"{owner.worker} already has shared variables; choose one variable mode"
427
+ )
428
+ if existing:
429
+ old_owner = existing.get("owner")
430
+ if isinstance(old_owner, dict):
431
+ if old_owner.get("token") != attachment.owner.token:
432
+ if _owner_gone(old_owner):
433
+ _unlink_sidecar(store, engine, owner.worker_id)
434
+ existing = None
435
+ else:
436
+ raise InvalidRequest(
437
+ f"{owner.worker} already has a reachable live variable owner"
438
+ )
439
+ attachment.owner.register_worker(
440
+ engine, owner.worker_id, attachment.mapping, attachment.names
441
+ )
442
+ _atomic_json(sidecar_path(engine, owner.worker_id), {
443
+ "schemaVersion": VARIABLE_SCHEMA_VERSION,
444
+ "engine": engine,
445
+ "workerId": owner.worker_id,
446
+ "mode": "live",
447
+ "names": attachment.owner.worker_names(engine, owner.worker_id),
448
+ "owner": attachment.owner.public_identity(),
449
+ })
450
+ return
451
+
452
+ if attachment.mode != "durable":
453
+ raise InvalidRequest(f"unknown variable mode {attachment.mode!r}")
454
+ if existing and existing["mode"] == "live":
455
+ old_owner = existing.get("owner")
456
+ if isinstance(old_owner, dict) and _owner_gone(old_owner):
457
+ _unlink_sidecar(store, engine, owner.worker_id)
458
+ existing = None
459
+ else:
460
+ raise InvalidRequest(
461
+ f"{owner.worker} already owns live variables; cannot mix modes"
462
+ )
463
+ if attachment.state_id is not None:
464
+ state_id = _valid_state_id(attachment.state_id)
465
+ with _state_lock(state_id):
466
+ read_state(state_id)
467
+ _atomic_json(sidecar_path(engine, owner.worker_id), {
468
+ "schemaVersion": VARIABLE_SCHEMA_VERSION,
469
+ "engine": engine,
470
+ "workerId": owner.worker_id,
471
+ "mode": "durable",
472
+ "stateId": state_id,
473
+ })
474
+ return
475
+
476
+ assert attachment.values is not None
477
+ if existing:
478
+ state_id = durable_state_id(engine, owner.worker_id, existing)
479
+ else:
480
+ state_id = _new_state_id()
481
+ with _state_lock(state_id, create=existing is None):
482
+ if existing:
483
+ document = read_state(state_id)
484
+ values = dict(document["variables"])
485
+ names = set(document["names"])
486
+ else:
487
+ values, names = {}, set()
488
+ values.update(attachment.values)
489
+ names.update(attachment.names)
490
+ _atomic_json(state_path(state_id), _new_state_document(
491
+ state_id, names=sorted(names), variables=values,
492
+ ))
493
+ _atomic_json(sidecar_path(engine, owner.worker_id), {
494
+ "schemaVersion": VARIABLE_SCHEMA_VERSION,
495
+ "engine": engine,
496
+ "workerId": owner.worker_id,
497
+ "mode": "durable",
498
+ "stateId": state_id,
499
+ })
500
+
501
+
502
+ def variables_info(engine: str, short: str, name: str | None = None) -> dict[str, Any]:
503
+ """Use the already-accepted single full-UUID variable read authority."""
504
+ store, owner = _env_worker(engine, short)
505
+ return store.variables_info(owner.worker, name=name)
506
+
507
+
508
+ def _runtime_state(owner) -> str | None:
509
+ try:
510
+ if owner.engine == "claude":
511
+ from ccx.rpc.rpc import _rpc
512
+ reply = _rpc(owner.worker, {"op": "status"}, timeout=0.5)
513
+ return reply.get("state") if reply.get("ok") is True else None
514
+ from ccx.codex.runtime import observe_worker
515
+ observation = observe_worker(owner.worker_id, timeout=0.5)
516
+ if not observation["daemon_served"]:
517
+ return None
518
+ return observation["reply"].get("state")
519
+ except Exception: # an unreadable runtime never refuses; the send decides
520
+ return None
521
+
522
+
523
+ def should_publish_send_attachment(store, owner, delivery_id: str | None,
524
+ *, caps: dict | None = None) -> bool:
525
+ """Preflight likely native refusal before touching attached variables.
526
+
527
+ Native admission may race after this check. A previously admitted delivery
528
+ token never reapplies its original variable values.
529
+ """
530
+ if delivery_id:
531
+ if owner.engine == "claude":
532
+ from ccx.claude.delivery import read_receipt
533
+ else:
534
+ from ccx.codex.delivery import read_receipt
535
+ prior = read_receipt(owner.worker, delivery_id)
536
+ if prior and prior.get("state") in (
537
+ "owned", "submitted", "accepted", "rejected", "delivered", "abandoned",
538
+ ):
539
+ return False
540
+
541
+ from ccx.errors import BudgetStopped, DeliveryRejected
542
+ record = store.record(owner.worker)
543
+ state = (record.get("lifecycle") or {}).get("state")
544
+ # A host or daemon that died mid-turn leaves a stale busy record that the
545
+ # send itself repairs, so only a live runtime's own state refuses here.
546
+ if state in ("busy", "waiting"):
547
+ state = _runtime_state(owner)
548
+ if state in ("busy", "waiting"):
549
+ raise DeliveryRejected(
550
+ f"worker is {state}; wait until idle before sending variables",
551
+ short=owner.short, state=state,
552
+ refusal="busy" if state == "busy" else "state_unavailable",
553
+ delivery_status="rejected",
554
+ )
555
+ if caps:
556
+ if owner.engine == "claude":
557
+ from ccx.claude.usage import cap_tripped, read_usage_cached
558
+ from ccx.constants import USAGE_STALE_LIMIT_S
559
+ data = read_usage_cached(stale_limit=USAGE_STALE_LIMIT_S)
560
+ trip = cap_tripped(data, caps)
561
+ else:
562
+ from ccx.codex.types import codex_cap_tripped
563
+ # A cap preflight may observe the existing daemon, never launch
564
+ # a second provider solely for a speculative send.
565
+ from ccx.codex.runtime import observation_daemon, daemon_call
566
+ deadline = time.monotonic() + 0.5
567
+ try:
568
+ data = (daemon_call({"op": "account-read", "tokens": False}, timeout=0.5)
569
+ if observation_daemon(deadline) else None)
570
+ if isinstance(data, dict) and "data" in data:
571
+ data = data["data"]
572
+ except (OSError, ValueError, SessionUnavailable):
573
+ data = None
574
+ trip = codex_cap_tripped(data, caps)
575
+ if trip:
576
+ key, used, limit = trip
577
+ raise BudgetStopped(
578
+ f"budget stop: usage cap {key} already exceeded ({used:g}% >= {limit:g}%)",
579
+ short=owner.short, reason=f"usage cap {key} exceeded", phase="preflight",
580
+ )
581
+ return True
582
+
583
+
584
+ def with_variable_hint(text: str, names: list[str]) -> str:
585
+ if not names:
586
+ return text
587
+ joined = ", ".join(names)
588
+ return (
589
+ text.rstrip()
590
+ + "\n\n[CCX variables available in direct python/python3: "
591
+ + joined
592
+ + ". Rebinding or deleting these names writes back to shared state. "
593
+ + "command/env/explicit Python paths are ordinary native Python.]"
594
+ )
595
+
596
+
597
+ def _managed_shell_env_file(value: str | None) -> bool:
598
+ if not value:
599
+ return False
600
+ candidate = Path(value).expanduser()
601
+ # Identify a CCX bridge from this, a retired, or a foreign generation.
602
+ # No ACTIVE-pointer lookup or foreign shell-file read is needed.
603
+ if _generated_shell_file_shape(candidate):
604
+ return True
605
+ try:
606
+ candidate_resolved = candidate.resolve()
607
+ if _generated_shell_file_shape(candidate_resolved):
608
+ return True
609
+ from ccx.store.root import require_ccx_active
610
+ managed_root = (
611
+ require_ccx_active(constants.CCX_ROOT).pointer.generation_root
612
+ / "variables" / "env"
613
+ ).resolve()
614
+ return candidate_resolved.is_relative_to(managed_root)
615
+ except (OSError, RuntimeError):
616
+ return False
617
+
618
+
619
+ def _generated_shell_file_shape(path: Path) -> bool:
620
+ if (path.suffix != ".sh" or path.parent.name not in {"claude", "codex"}
621
+ or path.parent.parent.name != "env"
622
+ or path.parent.parent.parent.name != "variables"):
623
+ return False
624
+ from ccx.store.identity import canonical_worker_id, IdentityError
625
+ try:
626
+ canonical_worker_id(path.stem)
627
+ except IdentityError:
628
+ return False
629
+ return True
630
+
631
+
632
+ def _managed_zdotdir(value: str | None) -> bool:
633
+ """Recognize this or a foreign generation's Codex zsh trampoline directory.
634
+
635
+ A user directory with the same shape is deliberately treated as managed:
636
+ never read or source a foreign worker's .zshenv.
637
+ """
638
+ if not value:
639
+ return False
640
+
641
+ def matches(path: Path) -> bool:
642
+ return path.parts[-5:-1] == ("variables", "env", "codex", "zsh")
643
+
644
+ candidate = Path(value).expanduser()
645
+ if matches(candidate):
646
+ return True
647
+ try:
648
+ return matches(candidate.resolve())
649
+ except (OSError, RuntimeError):
650
+ return False
651
+
652
+
653
+ _BRIDGE_ORIGINALS = {
654
+ "BASH_ENV": "CCX_BRIDGE_ORIGINAL_BASH_ENV",
655
+ "ZDOTDIR": "CCX_BRIDGE_ORIGINAL_ZDOTDIR",
656
+ "CLAUDE_ENV_FILE": "CCX_BRIDGE_ORIGINAL_CLAUDE_ENV_FILE",
657
+ }
658
+
659
+
660
+ def sanitize_inherited_shell_bridge_env(
661
+ env: dict[str, str] | None = None,
662
+ ) -> dict[str, str]:
663
+ """Restore native user startup paths, never a parent's CCX Python bridge.
664
+
665
+ Each original worker-owned shell shim exports one-use snapshots of the
666
+ user's startup paths. A nested worker consumes those snapshots only if
667
+ the inherited value is itself a managed CCX path. Explicit user overrides
668
+ and unset-versus-empty original values are preserved.
669
+ """
670
+ target = os.environ if env is None else env
671
+ for key, marker in _BRIDGE_ORIGINALS.items():
672
+ snapshot_text = target.pop(marker, None)
673
+ value = target.get(key)
674
+ managed = (_managed_zdotdir(value) if key == "ZDOTDIR"
675
+ else _managed_shell_env_file(value))
676
+ if not managed:
677
+ continue
678
+ target.pop(key, None)
679
+ try:
680
+ original = json.loads(snapshot_text) if isinstance(snapshot_text, str) else None
681
+ except ValueError:
682
+ original = None
683
+ if (isinstance(original, dict) and original.get("set") is True
684
+ and isinstance(original.get("value"), str)):
685
+ restored = original["value"]
686
+ # Refuse a pointer to any other worker, even when forged as a
687
+ # user-original path. Do not source foreign CCX-owned scripts.
688
+ if not (_managed_zdotdir(restored) if key == "ZDOTDIR"
689
+ else _managed_shell_env_file(restored)):
690
+ target[key] = restored
691
+ return target
692
+
693
+
694
+ def _shell_original_exports(env: Mapping[str, str]) -> list[str]:
695
+ """Emit exact user startup-path snapshots for a managed tool shell."""
696
+ return [
697
+ f"export {marker}={shlex.quote(json.dumps({'set': key in env, 'value': env.get(key)}))}"
698
+ for key, marker in _BRIDGE_ORIGINALS.items()
699
+ ]
700
+
701
+
702
+ def _external_shell_env_file(value: str | None) -> str | None:
703
+ if not value:
704
+ return None
705
+ if _managed_shell_env_file(value):
706
+ return None
707
+ # A bridge from a different CCX_ROOT/generation must not override this
708
+ # worker's python/python3 functions. Do not source or read foreign scripts.
709
+ try:
710
+ path = Path(value).expanduser().resolve()
711
+ if _generated_shell_file_shape(path):
712
+ return None
713
+ except (OSError, RuntimeError):
714
+ return None
715
+ return str(Path(value).expanduser())
716
+
717
+
718
+ def _python_bridge_functions(engine: str, short: str, *, root: Path, zsh: bool) -> list[str]:
719
+ """Render the same Python bridge command for both shell startup files."""
720
+ python = shlex.quote(sys.executable)
721
+ executable = shlex.quote(current_executable_path())
722
+ engine_q = shlex.quote(engine)
723
+ short_q = shlex.quote(short)
724
+ root_q = shlex.quote(str(root))
725
+ lines: list[str] = []
726
+ for name in ("python3", "python"):
727
+ name_q = shlex.quote(name)
728
+ command = (
729
+ f"{python} {executable} _live-vars-exec "
730
+ f"--root {root_q} --engine {engine_q} --short {short_q} "
731
+ f"--invoked-as {name_q} -- \"$@\""
732
+ )
733
+ # macOS may supply python3 without a native ``python`` executable.
734
+ # Do not publish a python function that can only fail with rc 127;
735
+ # do not silently remap python to python3, either.
736
+ native_python = (("whence -p python" if zsh else "type -P python")
737
+ + " >/dev/null 2>&1 && ") if name == "python" else ""
738
+ lines.extend([
739
+ f"if {native_python}! typeset -f {name} >/dev/null 2>&1 && ! alias {name} >/dev/null 2>&1; then",
740
+ # Quoted zsh function names cannot expand a user-defined alias
741
+ # while the startup script is parsed.
742
+ (f" function '{name}' {{ {command}; }}" if zsh
743
+ else f" {name}() {{ {command}; }}"),
744
+ "fi",
745
+ ])
746
+ return lines
747
+
748
+
749
+ def _publish_shell_script(
750
+ store, parts: tuple[str, ...], name: str, data: bytes, *,
751
+ exact_dir_mode: bool = False,
752
+ ) -> None:
753
+ """Reuse matching private script bytes; publish changed bytes atomically.
754
+
755
+ Every comparison occurs against a nofollow, user-owned regular file inside
756
+ the generation's pinned directory. A warm call needs no replace or fsync.
757
+ """
758
+ from ccx.store.root import StorageIntegrityError
759
+
760
+ with store.lease():
761
+ with store._gen(*parts, create=True) as directory:
762
+ if exact_dir_mode:
763
+ directory_stat = os.fstat(directory)
764
+ if (not stat.S_ISDIR(directory_stat.st_mode)
765
+ or directory_stat.st_uid != os.getuid()
766
+ or stat.S_IMODE(directory_stat.st_mode) != 0o700):
767
+ raise StorageIntegrityError(
768
+ "Codex zsh directory must be owner-owned mode 0700"
769
+ )
770
+ try:
771
+ old = os.stat(name, dir_fd=directory, follow_symlinks=False)
772
+ except FileNotFoundError:
773
+ pass
774
+ else:
775
+ if (not stat.S_ISREG(old.st_mode) or old.st_uid != os.getuid()
776
+ or stat.S_IMODE(old.st_mode) != 0o600 or old.st_nlink != 1):
777
+ raise StorageIntegrityError(
778
+ "managed shell script must be a private regular file"
779
+ )
780
+ source_fd = os.open(
781
+ name, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC |
782
+ os.O_NONBLOCK, dir_fd=directory,
783
+ )
784
+ with os.fdopen(source_fd, "rb") as source:
785
+ opened = os.fstat(source.fileno())
786
+ if (not stat.S_ISREG(opened.st_mode)
787
+ or opened.st_uid != os.getuid()
788
+ or stat.S_IMODE(opened.st_mode) != 0o600
789
+ or opened.st_nlink != 1):
790
+ raise StorageIntegrityError(
791
+ "managed shell script changed ownership or mode"
792
+ )
793
+ existing = source.read(len(data) + 1)
794
+ finished = os.fstat(source.fileno())
795
+ now = os.stat(name, dir_fd=directory, follow_symlinks=False)
796
+ if (old.st_dev == opened.st_dev == finished.st_dev == now.st_dev
797
+ and old.st_ino == opened.st_ino == finished.st_ino == now.st_ino
798
+ and old.st_mtime_ns == finished.st_mtime_ns == now.st_mtime_ns
799
+ and old.st_size == finished.st_size == now.st_size
800
+ and existing == data):
801
+ return
802
+
803
+ temp_name = "." + name + "." + uuid.uuid4().hex + ".tmp"
804
+ script_fd = os.open(
805
+ temp_name, os.O_WRONLY | os.O_CREAT | os.O_EXCL |
806
+ os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=directory,
807
+ )
808
+ try:
809
+ with os.fdopen(script_fd, "wb") as sink:
810
+ sink.write(data)
811
+ sink.flush()
812
+ os.fsync(sink.fileno())
813
+ os.replace(
814
+ temp_name, name, src_dir_fd=directory, dst_dir_fd=directory,
815
+ )
816
+ os.fsync(directory)
817
+ except BaseException:
818
+ try:
819
+ os.unlink(temp_name, dir_fd=directory)
820
+ except FileNotFoundError:
821
+ pass
822
+ raise
823
+
824
+
825
+ def ensure_shell_env(
826
+ engine: str,
827
+ short: str,
828
+ *,
829
+ inherited_env_file: str | None = None,
830
+ ) -> Path:
831
+ store, owner = _env_worker(engine, short)
832
+ return _ensure_shell_env_owned(
833
+ store, engine, owner.worker_id, inherited_env_file=inherited_env_file,
834
+ )
835
+
836
+
837
+ def _ensure_shell_env_owned(
838
+ store, engine: str, worker_id: str, *,
839
+ inherited_env_file: str | None,
840
+ native_env: Mapping[str, str] | None = None,
841
+ ) -> Path:
842
+ """Use an already-attested full UUID; the generation lease revalidates it."""
843
+ path = (store.pointer.generation_root / "variables" / "env" /
844
+ engine / f"{worker_id}.sh")
845
+ inherited_env_file = _external_shell_env_file(inherited_env_file)
846
+
847
+ lines = ["# generated by ccx; sourced for managed worker shell commands"]
848
+ if inherited_env_file:
849
+ inherited = Path(inherited_env_file).expanduser()
850
+ try:
851
+ same = inherited.resolve() == path.resolve()
852
+ except OSError:
853
+ same = False
854
+ if not same:
855
+ lines.extend(
856
+ [
857
+ f"if [ -f {shlex.quote(str(inherited))} ]; then",
858
+ f" . {shlex.quote(str(inherited))}",
859
+ "fi",
860
+ ]
861
+ )
862
+ lines.extend(_shell_original_exports(os.environ if native_env is None else native_env))
863
+ lines.extend(_python_bridge_functions(engine, worker_id, root=store.root, zsh=False))
864
+ text = "\n".join(lines) + "\n"
865
+ # Only the sealed ACTIVE generation's uid-private owner directory may
866
+ # receive this shell script. Never resurrect CCX_ROOT/variables/<short>.
867
+ from ccx.store.root import _FILE_LIMIT
868
+ data = text.encode("utf-8")
869
+ if len(data) > _FILE_LIMIT:
870
+ raise SessionUnavailable("managed full-UUID shell environment script too large")
871
+ _publish_shell_script(store, ("variables", "env", engine), path.name, data)
872
+ return path
873
+
874
+
875
+ def codex_zdotdir_path(worker_id: str) -> Path:
876
+ store, owner = _env_worker("codex", worker_id)
877
+ return (store.pointer.generation_root / "variables" / "env" / "codex" /
878
+ "zsh" / owner.worker_id)
879
+
880
+
881
+ def ensure_codex_zshenv(worker_id: str, *, original_zdotdir: str | None) -> Path:
882
+ """Install the restore-first .zshenv in the sealed, full-UUID generation."""
883
+ store, owner = _env_worker("codex", worker_id)
884
+ return _ensure_codex_zshenv_owned(
885
+ store, owner.worker_id, original_zdotdir=original_zdotdir,
886
+ )
887
+
888
+
889
+ def _zsh_user_startup_lines(stage: str, managed: Path, *,
890
+ original_zdotdir: str | None = None) -> list[str]:
891
+ """Replay native user startup stages while keeping CCX ZDOTDIR effective.
892
+
893
+ A nested zsh is a new shell of the *same worker*, so its own .zshenv must
894
+ see the managed directory. The original user's directory is remembered
895
+ only for startup-file sourcing; the previous user's .zshenv (or any later
896
+ startup stage) may update its subsequent native lookup directory.
897
+ """
898
+ lines = [f"# generated by ccx; native user {stage} once, CCX bridge retained"]
899
+ if stage == ".zshenv":
900
+ lines.extend([
901
+ f"typeset -g __ccx_managed_zdotdir={shlex.quote(str(managed))}",
902
+ f"typeset -g __ccx_user_zdotdir_set={'0' if original_zdotdir is None else '1'}",
903
+ f"typeset -g __ccx_user_zdotdir_value={shlex.quote(original_zdotdir or '')}",
904
+ ])
905
+ lines.extend([
906
+ 'if [[ $__ccx_user_zdotdir_set == 0 ]]; then',
907
+ ' unset ZDOTDIR',
908
+ f' __ccx_user_startup="$HOME/{stage}"',
909
+ 'elif [[ -z $__ccx_user_zdotdir_value ]]; then',
910
+ " export ZDOTDIR=''",
911
+ ' __ccx_user_startup=""',
912
+ 'else',
913
+ ' export ZDOTDIR="$__ccx_user_zdotdir_value"',
914
+ f' __ccx_user_startup="${{__ccx_user_zdotdir_value}}/{stage}"',
915
+ 'fi',
916
+ 'if [[ -n $__ccx_user_startup && -f $__ccx_user_startup ]]; then',
917
+ ' case "${__ccx_user_startup:A}" in',
918
+ # A retired/foreign CCX-generated shim is not a user startup file.
919
+ f' {shlex.quote(str(managed))}/.*|*/variables/env/codex/zsh/*/.*) ;;',
920
+ ' *) builtin source "$__ccx_user_startup" ;;',
921
+ ' esac',
922
+ 'fi',
923
+ 'if [[ -z ${ZDOTDIR+x} ]]; then',
924
+ ' __ccx_user_zdotdir_set=0',
925
+ ' __ccx_user_zdotdir_value=""',
926
+ 'else',
927
+ ' __ccx_user_zdotdir_set=1',
928
+ ' __ccx_user_zdotdir_value="$ZDOTDIR"',
929
+ 'fi',
930
+ 'export ZDOTDIR="$__ccx_managed_zdotdir"',
931
+ 'unset __ccx_user_startup',
932
+ ])
933
+ return lines
934
+
935
+
936
+ def _ensure_codex_zshenv_owned(
937
+ store, worker_id: str, *, original_zdotdir: str | None,
938
+ native_env: Mapping[str, str] | None = None,
939
+ ) -> Path:
940
+ from ccx.store.root import _FILE_LIMIT
941
+
942
+ directory = (store.pointer.generation_root / "variables" / "env" /
943
+ "codex" / "zsh" / worker_id)
944
+ if _managed_zdotdir(original_zdotdir):
945
+ original_zdotdir = None
946
+ for stage in (".zshenv", ".zprofile", ".zshrc", ".zlogin", ".zlogout"):
947
+ lines = _zsh_user_startup_lines(
948
+ stage, directory, original_zdotdir=original_zdotdir,
949
+ )
950
+ if stage == ".zshenv":
951
+ lines.extend(_shell_original_exports(
952
+ os.environ if native_env is None else native_env,
953
+ ))
954
+ lines.extend(_python_bridge_functions("codex", worker_id, root=store.root, zsh=True))
955
+ data = ("\n".join(lines) + "\n").encode("utf-8")
956
+ if len(data) > _FILE_LIMIT:
957
+ raise SessionUnavailable("managed full-UUID zsh startup script too large")
958
+ _publish_shell_script(
959
+ store, ("variables", "env", "codex", "zsh", worker_id),
960
+ stage, data, exact_dir_mode=True,
961
+ )
962
+ return directory
963
+
964
+
965
+ def configure_claude_worker_env(
966
+ env: dict[str, str], short: str, *, host_pid: int,
967
+ ) -> dict[str, str]:
968
+ sanitize_inherited_shell_bridge_env(env)
969
+ inherited = env.get("CLAUDE_ENV_FILE")
970
+ store, owner = _env_worker("claude", short)
971
+ path = _ensure_shell_env_owned(
972
+ store, "claude", owner.worker_id, inherited_env_file=inherited,
973
+ native_env=env,
974
+ )
975
+ env["CLAUDE_ENV_FILE"] = str(path)
976
+ mark_claude_bridge(short, host_pid)
977
+ return env
978
+
979
+
980
+ def codex_thread_config(worker_id: str) -> dict[str, Any]:
981
+ """Attested full-UUID Bash/zsh bridges for one original native Codex thread.
982
+
983
+ The daemon first validates the native claim/source/dispatch; this function
984
+ only prepares the shell config and cannot start/adopt a native thread.
985
+ Preserve Codex's user/default inheritance policy.
986
+ """
987
+ store, owner = _env_worker("codex", worker_id)
988
+ # The per-thread policy may be constructed by a nested CCX caller as
989
+ # well as by the shared (already scrubbed) codexd. Never capture that
990
+ # caller's bridge as this worker's genuine user-shell baseline.
991
+ native_env = sanitize_inherited_shell_bridge_env(dict(os.environ))
992
+ bash_env = _ensure_shell_env_owned(
993
+ store, "codex", owner.worker_id,
994
+ inherited_env_file=native_env.get("BASH_ENV"), native_env=native_env,
995
+ )
996
+ zdotdir = _ensure_codex_zshenv_owned(
997
+ store, owner.worker_id, original_zdotdir=native_env.get("ZDOTDIR"),
998
+ native_env=native_env,
999
+ )
1000
+ return {"shell_environment_policy": {"set": {
1001
+ "CCX_SELF_WORKER_ID": owner.worker_id,
1002
+ "CCX_SELF_ENGINE": "codex",
1003
+ "CCX_ROOT": str(constants.CCX_ROOT),
1004
+ "BASH_ENV": str(bash_env),
1005
+ "ZDOTDIR": str(zdotdir),
1006
+ }}}
1007
+
1008
+
1009
+ def mark_claude_bridge(short: str, host_pid: int) -> None:
1010
+ """Store exact full owner + current host pid as an atomic owned sidecar."""
1011
+ from ccx.store.root import _write_json
1012
+ store, owner = _env_worker("claude", short)
1013
+ with store.lease():
1014
+ with store._gen("variables", "env", "claude", create=True) as fd:
1015
+ _write_json(fd, owner.worker_id + ".bridge.json", {
1016
+ "workerId": owner.worker_id,
1017
+ "engine": "claude",
1018
+ "generation": store.generation,
1019
+ "hostPid": int(host_pid),
1020
+ })
1021
+
1022
+
1023
+ def claude_bridge_ready(short: str, host_pid: int | None) -> bool:
1024
+ if not host_pid:
1025
+ return False
1026
+ from ccx.store.root import _read_json
1027
+ store, owner = _env_worker("claude", short)
1028
+ with store.lease():
1029
+ try:
1030
+ with store._gen("variables", "env", "claude") as fd:
1031
+ value = _read_json(fd, owner.worker_id + ".bridge.json")
1032
+ except FileNotFoundError:
1033
+ return False
1034
+ return (
1035
+ isinstance(value, dict)
1036
+ and value.get("workerId") == owner.worker_id
1037
+ and value.get("engine") == "claude"
1038
+ and value.get("generation") == store.generation
1039
+ and value.get("hostPid") == int(host_pid)
1040
+ )
1041
+
1042
+
1043
+ def require_send_variable_permission(owner, record: dict) -> None:
1044
+ """Refuse a later-SEND variable attachment the original worker cannot take."""
1045
+ create_args = record.get("createArgs") or {}
1046
+ if owner.engine == "claude":
1047
+ if not bool(create_args.get("skip_permissions", True)):
1048
+ raise CapabilityUnsupported(
1049
+ 'Shared variables require full permissions (permissions="full")',
1050
+ short=owner.short, engine=owner.engine,
1051
+ capability="shared_python_variables",
1052
+ )
1053
+ lifecycle = record.get("lifecycle") or {}
1054
+ state = lifecycle.get("state")
1055
+ host_pid = lifecycle.get("hostPid")
1056
+ if (state not in {"dead", "crashed", "idle-killed", "budget-stopped"}
1057
+ and host_pid
1058
+ and not claude_bridge_ready(owner.worker_id, host_pid)):
1059
+ raise SessionUnavailable(
1060
+ "this live Claude host predates the CCX shared-Python bridge; "
1061
+ "kill/revive it or create a new worker before attaching variables",
1062
+ short=owner.short,
1063
+ )
1064
+ elif not (create_args.get("sandbox") == "danger-full-access"
1065
+ and create_args.get("approvalPolicy") == "never"):
1066
+ raise CapabilityUnsupported(
1067
+ 'Shared variables require full permissions (permissions="full")',
1068
+ short=owner.short, engine=owner.engine,
1069
+ capability="shared_python_variables",
1070
+ )
1071
+
1072
+
1073
+ class _ThreadStreamProxy:
1074
+ def __init__(self, original):
1075
+ self.original = original
1076
+ self._routes: dict[int, Any] = {}
1077
+ self._lock = threading.Lock()
1078
+
1079
+ def _target(self):
1080
+ with self._lock:
1081
+ return self._routes.get(threading.get_ident(), self.original)
1082
+
1083
+ @contextmanager
1084
+ def route(self, target):
1085
+ ident = threading.get_ident()
1086
+ with self._lock:
1087
+ self._routes[ident] = target
1088
+ try:
1089
+ yield
1090
+ finally:
1091
+ with self._lock:
1092
+ self._routes.pop(ident, None)
1093
+
1094
+ def write(self, value):
1095
+ return self._target().write(value)
1096
+
1097
+ def read(self, *args):
1098
+ return self._target().read(*args)
1099
+
1100
+ def readline(self, *args):
1101
+ return self._target().readline(*args)
1102
+
1103
+ def flush(self):
1104
+ return self._target().flush()
1105
+
1106
+ def isatty(self):
1107
+ return self._target().isatty()
1108
+
1109
+ def fileno(self):
1110
+ return self._target().fileno()
1111
+
1112
+ @property
1113
+ def buffer(self):
1114
+ return self._target().buffer
1115
+
1116
+ @property
1117
+ def encoding(self):
1118
+ return getattr(self._target(), "encoding", None)
1119
+
1120
+ @property
1121
+ def errors(self):
1122
+ return getattr(self._target(), "errors", None)
1123
+
1124
+ def __iter__(self):
1125
+ return iter(self._target())
1126
+
1127
+ def __getattr__(self, name):
1128
+ return getattr(self._target(), name)
1129
+
1130
+
1131
+ @dataclass
1132
+ class _Binding:
1133
+ mapping: MutableMapping[str, Any]
1134
+ key: str
1135
+
1136
+
1137
+ class LiveOwner:
1138
+ def __init__(self):
1139
+ self.pid = os.getpid()
1140
+ self.token = uuid.uuid4().hex
1141
+ basename = f"{self.pid}-{self.token}.sock"
1142
+ owner_dir, choice = _owner_runtime(basename)
1143
+ candidate = owner_dir / basename
1144
+ _require_unix_socket_budget(candidate) # before any parent writes
1145
+ self.socket_path = owners_dir(owner_dir, runtime=choice) / basename
1146
+ self._registry_lock = threading.RLock()
1147
+ self._exec_lock = threading.Lock()
1148
+ self._workers: dict[tuple[str, str], dict[str, _Binding]] = {}
1149
+ self._queue: queue.Queue[Any] = queue.Queue()
1150
+ self._closed = threading.Event()
1151
+ self._current: dict[str, Any] | None = None
1152
+
1153
+ self._old_stdin = sys.stdin
1154
+ self._old_stdout = sys.stdout
1155
+ self._old_stderr = sys.stderr
1156
+ self._stdin_proxy = _ThreadStreamProxy(sys.stdin)
1157
+ self._stdout_proxy = _ThreadStreamProxy(sys.stdout)
1158
+ self._stderr_proxy = _ThreadStreamProxy(sys.stderr)
1159
+ sys.stdin = self._stdin_proxy
1160
+ sys.stdout = self._stdout_proxy
1161
+ sys.stderr = self._stderr_proxy
1162
+
1163
+ self._server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1164
+ try:
1165
+ try:
1166
+ self.socket_path.unlink()
1167
+ except FileNotFoundError:
1168
+ pass
1169
+ bind_private_unix_socket(self._server, self.socket_path)
1170
+ self._server.listen()
1171
+ # Closing a listening fd from another thread does not reliably
1172
+ # interrupt a blocked accept() on Linux. Poll for shutdown.
1173
+ self._server.settimeout(0.1)
1174
+ except BaseException:
1175
+ try:
1176
+ self._server.close()
1177
+ except OSError:
1178
+ pass
1179
+ if sys.stdin is self._stdin_proxy:
1180
+ sys.stdin = self._old_stdin
1181
+ if sys.stdout is self._stdout_proxy:
1182
+ sys.stdout = self._old_stdout
1183
+ if sys.stderr is self._stderr_proxy:
1184
+ sys.stderr = self._old_stderr
1185
+ raise
1186
+
1187
+ self._executor_thread = threading.Thread(
1188
+ target=self._executor_loop,
1189
+ name=f"ccx-live-vars-{self.pid}",
1190
+ daemon=True,
1191
+ )
1192
+ self._accept_thread = threading.Thread(
1193
+ target=self._accept_loop,
1194
+ name=f"ccx-live-vars-accept-{self.pid}",
1195
+ daemon=True,
1196
+ )
1197
+ self._executor_thread.start()
1198
+ self._accept_thread.start()
1199
+
1200
+ def public_identity(self) -> dict[str, Any]:
1201
+ return {
1202
+ "pid": self.pid,
1203
+ "token": self.token,
1204
+ "socket": str(self.socket_path),
1205
+ "python": {
1206
+ "major": sys.version_info.major,
1207
+ "minor": sys.version_info.minor,
1208
+ "cacheTag": sys.implementation.cache_tag,
1209
+ "platform": sysconfig.get_platform(),
1210
+ },
1211
+ }
1212
+
1213
+ def register_worker(
1214
+ self,
1215
+ engine: str,
1216
+ short: str,
1217
+ mapping: MutableMapping[str, Any],
1218
+ names: list[str],
1219
+ ) -> None:
1220
+ self._exec_lock.acquire()
1221
+ try:
1222
+ with self._registry_lock:
1223
+ bindings = self._workers.setdefault((engine, short), {})
1224
+ for name in names:
1225
+ bindings[name] = _Binding(mapping=mapping, key=name)
1226
+ finally:
1227
+ self._exec_lock.release()
1228
+
1229
+ def worker_names(self, engine: str, short: str) -> list[str]:
1230
+ with self._registry_lock:
1231
+ return sorted(self._workers.get((engine, short), {}))
1232
+
1233
+ def _accept_loop(self) -> None:
1234
+ while not self._closed.is_set():
1235
+ try:
1236
+ conn = accept_same_user(self._server)
1237
+ except socket.timeout:
1238
+ continue
1239
+ except OSError:
1240
+ return
1241
+ if conn is None:
1242
+ continue
1243
+ threading.Thread(
1244
+ target=self._handle_connection,
1245
+ args=(conn,),
1246
+ daemon=True,
1247
+ name="ccx-live-vars-client",
1248
+ ).start()
1249
+
1250
+ @staticmethod
1251
+ def _read_line(conn: socket.socket) -> bytes:
1252
+ data = bytearray()
1253
+ while b"\n" not in data:
1254
+ chunk = conn.recv(65536)
1255
+ if not chunk:
1256
+ break
1257
+ data.extend(chunk)
1258
+ return bytes(data).split(b"\n", 1)[0]
1259
+
1260
+ @staticmethod
1261
+ def _send(conn: socket.socket, value: dict[str, Any]) -> None:
1262
+ conn.sendall((_WIRE_DUMPS(value, separators=(",", ":")) + "\n").encode("utf-8"))
1263
+
1264
+ def _handle_connection(self, conn: socket.socket) -> None:
1265
+ try:
1266
+ if self._closed.is_set():
1267
+ self._send(conn, {"ok": False, "errorKind": "state_unavailable"})
1268
+ conn.close()
1269
+ return
1270
+ raw = self._read_line(conn)
1271
+ request = _WIRE_LOADS(raw) if raw else {}
1272
+ if request.get("token") != self.token:
1273
+ self._send(conn, {"ok": False, "errorKind": "state_unavailable"})
1274
+ conn.close()
1275
+ return
1276
+ if request.get("op") == "ping":
1277
+ self._send(conn, {"ok": True})
1278
+ conn.close()
1279
+ return
1280
+ if request.get("op") != "exec":
1281
+ self._send(conn, {"ok": False, "errorKind": "invalid_request"})
1282
+ conn.close()
1283
+ return
1284
+ self._exec_lock.acquire()
1285
+ if self._closed.is_set():
1286
+ self._exec_lock.release()
1287
+ self._send(conn, {"ok": False, "errorKind": "state_unavailable"})
1288
+ conn.close()
1289
+ return
1290
+ self._current = {
1291
+ "worker": f"{request.get('engine')}:{request.get('short')}",
1292
+ "startedAt": time.time(),
1293
+ }
1294
+ self._queue.put((conn, request))
1295
+ except Exception as error:
1296
+ try:
1297
+ self._send(
1298
+ conn,
1299
+ {
1300
+ "ok": False,
1301
+ "errorKind": "protocol_error",
1302
+ "message": f"{type(error).__name__}: {error}",
1303
+ },
1304
+ )
1305
+ except OSError:
1306
+ pass
1307
+ conn.close()
1308
+
1309
+ def _restore_streams(self) -> None:
1310
+ if sys.stdin is self._stdin_proxy:
1311
+ sys.stdin = self._old_stdin
1312
+ if sys.stdout is self._stdout_proxy:
1313
+ sys.stdout = self._old_stdout
1314
+ if sys.stderr is self._stderr_proxy:
1315
+ sys.stderr = self._old_stderr
1316
+
1317
+ def _executor_loop(self) -> None:
1318
+ while True:
1319
+ item = self._queue.get()
1320
+ if item is None:
1321
+ self._restore_streams()
1322
+ return
1323
+ conn, request = item
1324
+ try:
1325
+ response = self._execute(request)
1326
+ self._send(conn, response)
1327
+ except BaseException as error:
1328
+ try:
1329
+ self._send(
1330
+ conn,
1331
+ {
1332
+ "ok": False,
1333
+ "errorKind": "owner_error",
1334
+ "message": f"{type(error).__name__}: {error}",
1335
+ },
1336
+ )
1337
+ except BaseException:
1338
+ pass
1339
+ finally:
1340
+ try:
1341
+ conn.close()
1342
+ except OSError:
1343
+ pass
1344
+ self._current = None
1345
+ self._exec_lock.release()
1346
+
1347
+ def _execute(self, request: dict[str, Any]) -> dict[str, Any]:
1348
+ engine = str(request.get("engine") or "")
1349
+ short = str(request.get("short") or "")
1350
+ with self._registry_lock:
1351
+ bindings = dict(self._workers.get((engine, short), {}))
1352
+ if not bindings:
1353
+ return {"ok": False, "errorKind": "state_unavailable"}
1354
+
1355
+ seed: dict[str, Any] = {}
1356
+ for name, binding in bindings.items():
1357
+ if name in binding.mapping:
1358
+ seed[name] = binding.mapping[name]
1359
+
1360
+ native = request.get("nativePython")
1361
+ if not isinstance(native, dict):
1362
+ native = {}
1363
+ stdin_bytes = base64.b64decode(request.get("stdinBase64") or "")
1364
+ stdin_buffer = io.BytesIO(stdin_bytes)
1365
+ stdout_buffer = io.BytesIO()
1366
+ stderr_buffer = io.BytesIO()
1367
+ stdin_text = io.TextIOWrapper(
1368
+ stdin_buffer,
1369
+ encoding=getattr(self._old_stdin, "encoding", None) or "utf-8",
1370
+ errors=getattr(self._old_stdin, "errors", None) or "strict",
1371
+ )
1372
+ stdout_text = io.TextIOWrapper(
1373
+ stdout_buffer,
1374
+ encoding=getattr(self._old_stdout, "encoding", None) or "utf-8",
1375
+ errors=getattr(self._old_stdout, "errors", None) or "replace",
1376
+ write_through=True,
1377
+ )
1378
+ stderr_text = io.TextIOWrapper(
1379
+ stderr_buffer,
1380
+ encoding=getattr(self._old_stderr, "encoding", None) or "utf-8",
1381
+ errors=getattr(self._old_stderr, "errors", None) or "replace",
1382
+ write_through=True,
1383
+ )
1384
+
1385
+ old_cwd = os.getcwd()
1386
+ old_argv = sys.argv[:]
1387
+ old_path = sys.path[:]
1388
+ try:
1389
+ cwd = str(request.get("cwd") or old_cwd)
1390
+ os.chdir(cwd)
1391
+ native_path = native.get("sysPath")
1392
+ if isinstance(native_path, list) and all(
1393
+ isinstance(value, str) for value in native_path
1394
+ ):
1395
+ sys.path[:] = native_path
1396
+ args = request.get("args")
1397
+ if not isinstance(args, list) or not all(isinstance(v, str) for v in args):
1398
+ return {"ok": False, "errorKind": "invalid_request"}
1399
+ with (
1400
+ self._stdin_proxy.route(stdin_text),
1401
+ self._stdout_proxy.route(stdout_text),
1402
+ self._stderr_proxy.route(stderr_text),
1403
+ ):
1404
+ exit_code, final_ns = execute_target(args, seed)
1405
+ stdout_text.flush()
1406
+ stderr_text.flush()
1407
+ finally:
1408
+ os.chdir(old_cwd)
1409
+ sys.argv = old_argv
1410
+ sys.path[:] = old_path
1411
+
1412
+ deleted: list[str] = []
1413
+ with self._registry_lock:
1414
+ current = self._workers.get((engine, short), {})
1415
+ for name, binding in bindings.items():
1416
+ if name in final_ns:
1417
+ binding.mapping[name] = final_ns[name]
1418
+ else:
1419
+ binding.mapping.pop(name, None)
1420
+ if current.get(name) is binding:
1421
+ current.pop(name, None)
1422
+ deleted.append(name)
1423
+ names = sorted(current)
1424
+
1425
+ self._publish_live_names(engine, short, names)
1426
+ return {
1427
+ "ok": True,
1428
+ "exitCode": int(exit_code),
1429
+ "stdoutBase64": base64.b64encode(stdout_buffer.getvalue()).decode("ascii"),
1430
+ "stderrBase64": base64.b64encode(stderr_buffer.getvalue()).decode("ascii"),
1431
+ "deleted": deleted,
1432
+ }
1433
+
1434
+ def _publish_live_names(self, engine: str, short: str, names: list[str]) -> None:
1435
+ with _worker_lock(engine, short):
1436
+ value = read_sidecar(engine, short)
1437
+ if (
1438
+ not value
1439
+ or value.get("mode") != "live"
1440
+ or (value.get("owner") or {}).get("token") != self.token
1441
+ ):
1442
+ return
1443
+ value["names"] = names
1444
+ _atomic_json(sidecar_path(engine, short), value)
1445
+
1446
+ def close(self) -> None:
1447
+ if self._closed.is_set():
1448
+ return
1449
+ self._closed.set()
1450
+ try:
1451
+ self._server.close()
1452
+ except OSError:
1453
+ pass
1454
+ try:
1455
+ self.socket_path.unlink()
1456
+ except FileNotFoundError:
1457
+ pass
1458
+ self._queue.put(None)
1459
+ # Wait briefly for both daemon loops to finish. In particular, the
1460
+ # executor restores sys.stdin/stdout/stderr; without this join a new
1461
+ # LiveOwner can wrap a proxy from a closed owner and strand it forever.
1462
+ for worker in (self._accept_thread, self._executor_thread):
1463
+ if threading.current_thread() is not worker:
1464
+ worker.join(timeout=1.0)
1465
+ # Never remove a sidecar that a newer controller or durable send owns.
1466
+ for engine, worker_id in list(self._workers):
1467
+ try:
1468
+ store, owned = _env_worker(engine, worker_id)
1469
+ with _worker_lock(engine, owned.worker_id):
1470
+ value = read_sidecar(engine, owned.worker_id)
1471
+ if (value and value.get("mode") == "live"
1472
+ and (value.get("owner") or {}).get("token") == self.token):
1473
+ _unlink_sidecar(store, engine, owned.worker_id)
1474
+ except (OSError, SessionUnavailable):
1475
+ pass
1476
+
1477
+
1478
+ def _native_python_metadata(executable: str) -> dict[str, Any] | None:
1479
+ code = (
1480
+ "import json,sys,sysconfig;"
1481
+ "print(json.dumps({'version':list(sys.version_info[:3]),"
1482
+ "'cacheTag':sys.implementation.cache_tag,"
1483
+ "'platform':sysconfig.get_platform(),"
1484
+ "'executable':sys.executable,'prefix':sys.prefix,"
1485
+ "'execPrefix':sys.exec_prefix,'sysPath':sys.path}))"
1486
+ )
1487
+ try:
1488
+ output = subprocess.check_output(
1489
+ [executable, "-c", code],
1490
+ text=True,
1491
+ stderr=subprocess.DEVNULL,
1492
+ timeout=5,
1493
+ )
1494
+ value = json.loads(output)
1495
+ except (OSError, subprocess.SubprocessError, ValueError):
1496
+ return None
1497
+ return value if isinstance(value, dict) else None
1498
+
1499
+
1500
+ def _compatible(owner: Mapping[str, Any], native: Mapping[str, Any]) -> bool:
1501
+ version = native.get("version")
1502
+ return bool(
1503
+ isinstance(version, list)
1504
+ and len(version) >= 2
1505
+ and version[0] == owner.get("major")
1506
+ and version[1] == owner.get("minor")
1507
+ and native.get("platform") == owner.get("platform")
1508
+ and native.get("cacheTag") == owner.get("cacheTag")
1509
+ )
1510
+
1511
+
1512
+ def _durable_runner_argv(
1513
+ executable: str,
1514
+ *,
1515
+ state: Path,
1516
+ lock: Path,
1517
+ cwd: str,
1518
+ args: list[str],
1519
+ ) -> list[str]:
1520
+ runner = Path(__file__).with_name("live_vars_runner.py")
1521
+ bootstrap = (
1522
+ "import runpy; "
1523
+ f"runpy.run_path({str(runner)!r}, run_name='__main__')"
1524
+ )
1525
+ return [
1526
+ executable,
1527
+ "-c",
1528
+ bootstrap,
1529
+ "--state",
1530
+ str(state),
1531
+ "--lock",
1532
+ str(lock),
1533
+ "--cwd",
1534
+ cwd,
1535
+ "--real-python",
1536
+ executable,
1537
+ "--",
1538
+ *args,
1539
+ ]
1540
+
1541
+
1542
+ def _exec_native(executable: str, args: list[str], *, env: dict[str, str] | None = None) -> int:
1543
+ os.execvpe(executable, [executable, *args], os.environ.copy() if env is None else env)
1544
+ return 127
1545
+
1546
+
1547
+ def worker_exec(engine: str, short: str, invoked_as: str, args: list[str], *,
1548
+ native_env: dict[str, str] | None = None) -> int:
1549
+ from ccx.store.root import GenerationError
1550
+ from ccx.store.identity import IdentityError
1551
+
1552
+ real_python = shutil.which(invoked_as)
1553
+ if not real_python:
1554
+ print(f"ccx: cannot resolve native {invoked_as}", file=sys.stderr)
1555
+ return 127
1556
+
1557
+ # Stage0's exact ACTIVE owner/read gate stays before any native execution.
1558
+ try:
1559
+ store, owned = _env_worker(engine, short)
1560
+ binding = read_sidecar(engine, owned.worker_id)
1561
+ except (IdentityError, GenerationError, OSError) as error:
1562
+ print(f"CcxStateUnavailable: managed worker variables unavailable: {error}",
1563
+ file=sys.stderr)
1564
+ return 69
1565
+ if binding is None:
1566
+ return _exec_native(real_python, args, env=native_env)
1567
+
1568
+ if len(args) >= 2 and args[0] == "-m" and args[1] in _NATIVE_FALLTHROUGH_MODULES:
1569
+ return _exec_native(real_python, args, env=native_env)
1570
+ # Shared variables cover `-c`, `-m`, stdin and scripts; any other leading
1571
+ # interpreter option (--version, -V, -u, -I, ...) runs native Python.
1572
+ if args and args[0].startswith("-") and args[0] not in ("-", "-c", "-m"):
1573
+ return _exec_native(real_python, args, env=native_env)
1574
+
1575
+ short = owned.worker_id
1576
+ if binding["mode"] == "durable":
1577
+ try:
1578
+ state_id = _valid_state_id(binding.get("stateId"))
1579
+ with store.lease():
1580
+ state = state_path(state_id)
1581
+ lock = state_lock_path(state_id)
1582
+ except (GenerationError, SessionUnavailable, OSError) as error:
1583
+ print(f"CcxStateUnavailable: durable variable state unavailable: {error}",
1584
+ file=sys.stderr)
1585
+ return 69
1586
+ os.execvpe(
1587
+ real_python,
1588
+ _durable_runner_argv(
1589
+ real_python, state=state, lock=lock, cwd=os.getcwd(), args=args,
1590
+ ),
1591
+ os.environ.copy() if native_env is None else native_env,
1592
+ )
1593
+ return 127
1594
+
1595
+ if binding["mode"] != "live":
1596
+ print("CcxStateUnavailable: invalid worker variable mode", file=sys.stderr)
1597
+ return 69
1598
+ try:
1599
+ value = read_sidecar(engine, short)
1600
+ except (IdentityError, GenerationError, OSError) as error:
1601
+ print(f"CcxStateUnavailable: live variable binding unavailable: {error}",
1602
+ file=sys.stderr)
1603
+ return 69
1604
+ if not value or value.get("mode") != "live":
1605
+ print("CcxStateUnavailable: live variable binding changed", file=sys.stderr)
1606
+ return 69
1607
+
1608
+ owner = value.get("owner")
1609
+ if not isinstance(owner, dict):
1610
+ print("CcxStateUnavailable: live variable owner metadata is missing", file=sys.stderr)
1611
+ return 69
1612
+ if _owner_gone(owner):
1613
+ _clear_dead_binding(engine, short, str(owner.get("token") or ""))
1614
+ return _exec_native(real_python, args, env=native_env)
1615
+ owner_python = owner.get("python")
1616
+ native = _native_python_metadata(real_python)
1617
+ if not isinstance(owner_python, dict) or native is None:
1618
+ print(
1619
+ "ccx: cannot verify Python compatibility for shared variables; running native Python",
1620
+ file=sys.stderr,
1621
+ )
1622
+ return _exec_native(real_python, args, env=native_env)
1623
+ if not _compatible(owner_python, native):
1624
+ print(
1625
+ "ccx: selected Python is incompatible with the live variable owner; running native Python",
1626
+ file=sys.stderr,
1627
+ )
1628
+ return _exec_native(real_python, args, env=native_env)
1629
+
1630
+ request = {
1631
+ "schemaVersion": VARIABLE_SCHEMA_VERSION,
1632
+ "op": "exec",
1633
+ "token": owner.get("token"),
1634
+ "engine": engine,
1635
+ "short": short,
1636
+ "args": args,
1637
+ "cwd": os.getcwd(),
1638
+ "stdinBase64": base64.b64encode(
1639
+ sys.stdin.buffer.read() if not args or args[0] == "-" else b""
1640
+ ).decode("ascii"),
1641
+ "nativePython": native,
1642
+ }
1643
+ sock = owner.get("socket")
1644
+ if not isinstance(sock, str):
1645
+ print("CcxStateUnavailable: live variable owner socket is missing", file=sys.stderr)
1646
+ return 69
1647
+
1648
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1649
+ try:
1650
+ client.connect(sock)
1651
+ client.sendall((json.dumps(request, separators=(",", ":")) + "\n").encode("utf-8"))
1652
+ raw = LiveOwner._read_line(client)
1653
+ except OSError as error:
1654
+ print(
1655
+ f"CcxStateUnavailable: shared variable owner is unavailable: {error}",
1656
+ file=sys.stderr,
1657
+ )
1658
+ return 69
1659
+ finally:
1660
+ client.close()
1661
+
1662
+ try:
1663
+ response = json.loads(raw)
1664
+ except ValueError:
1665
+ print("CcxStateUnavailable: invalid response from shared variable owner", file=sys.stderr)
1666
+ return 69
1667
+
1668
+ if not response.get("ok"):
1669
+ kind = response.get("errorKind")
1670
+ if kind == "owner_busy":
1671
+ print(
1672
+ f"CcxStateBusy: shared Python owner is busy: {response.get('current')}",
1673
+ file=sys.stderr,
1674
+ )
1675
+ return 75
1676
+ print(
1677
+ "CcxStateUnavailable: shared variable execution is unavailable"
1678
+ + (f": {response.get('message')}" if response.get("message") else ""),
1679
+ file=sys.stderr,
1680
+ )
1681
+ return 69
1682
+
1683
+ stdout = base64.b64decode(response.get("stdoutBase64") or "")
1684
+ stderr = base64.b64decode(response.get("stderrBase64") or "")
1685
+ if stdout:
1686
+ sys.stdout.buffer.write(stdout)
1687
+ sys.stdout.buffer.flush()
1688
+ if stderr:
1689
+ sys.stderr.buffer.write(stderr)
1690
+ sys.stderr.buffer.flush()
1691
+ return int(response.get("exitCode") or 0)
1692
+
1693
+
1694
+ def cmd_worker_exec(args) -> None:
1695
+ target_args = list(getattr(args, "target_args", []) or [])
1696
+ if target_args and target_args[0] == "--":
1697
+ target_args = target_args[1:]
1698
+ raise SystemExit(
1699
+ worker_exec(
1700
+ str(args.engine),
1701
+ str(args.short),
1702
+ str(args.invoked_as),
1703
+ target_args,
1704
+ native_env=getattr(args, "_bridge_native_env", None),
1705
+ )
1706
+ )