fdai-deployment-cli 0.1.2__py3-none-any.whl

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 (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,448 @@
1
+ """Attached customer executor: ``python -m fdai_deployment_cli.bootstrap_worker``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import fcntl
7
+ import json
8
+ import multiprocessing
9
+ import os
10
+ import signal
11
+ import stat
12
+ import subprocess
13
+ import threading
14
+ import time
15
+ from collections.abc import Callable
16
+ from datetime import UTC, datetime
17
+ from multiprocessing.process import BaseProcess
18
+ from pathlib import Path
19
+ from types import FrameType
20
+ from uuid import uuid4
21
+
22
+ from fdai_deployment_cli.bootstrap_channel import (
23
+ _read,
24
+ _write,
25
+ initial_status,
26
+ locked,
27
+ prepare_directory,
28
+ validate_request,
29
+ )
30
+ from fdai_deployment_cli.bootstrap_executor import BootstrapBlocked, BootstrapExecutor
31
+
32
+ TOTAL_SECONDS = 8 * 3600
33
+ OPERATION_SECONDS = {
34
+ "login": 660,
35
+ "accounts": 120,
36
+ "inspect": 120,
37
+ "install": 14400,
38
+ "resume": 14400,
39
+ }
40
+ NO_PROGRESS_SECONDS = 1800
41
+
42
+
43
+ def _update(root: Path, **fields: object) -> None:
44
+ with locked(root):
45
+ record = _read(root)
46
+ status = record.get("status", initial_status())
47
+ status.update(fields)
48
+ record.update(status=status, progress=time.time())
49
+ _write(root, record)
50
+
51
+
52
+ def channel_decision(root: Path, review: dict[str, object]) -> dict[str, object]:
53
+ """Expose one safe exact review and consume only its current one-shot decision."""
54
+ expires = datetime.fromisoformat(str(review["expires_at"]))
55
+ remaining = min(600, (expires - datetime.now(UTC)).total_seconds())
56
+ if remaining <= 0:
57
+ raise ValueError("Exact checkpoint expired before review")
58
+ current = {
59
+ "id": str(uuid4()),
60
+ "digest": review["digest"],
61
+ "title": review["title"],
62
+ "details": review["details"],
63
+ "confirmation_required": review["confirmation_required"],
64
+ "expires_at": review["expires_at"],
65
+ }
66
+ if len(json.dumps(current, allow_nan=False).encode()) > 16384:
67
+ raise BootstrapBlocked(
68
+ "Exact plan exceeds the bounded browser review. Inspect its retained plan "
69
+ "with the existing CLI; no browser approval was granted."
70
+ )
71
+ _update(
72
+ root,
73
+ phase="review",
74
+ review=current,
75
+ message="Review this exact checkpoint before continuing.",
76
+ )
77
+ deadline = time.monotonic() + remaining
78
+ while time.monotonic() < deadline:
79
+ with locked(root):
80
+ record = _read(root)
81
+ decision = record.get("decision")
82
+ if decision is not None:
83
+ status = record["status"]
84
+ if (
85
+ not isinstance(decision, dict)
86
+ or decision.get("request_id") != status["request_id"]
87
+ or decision.get("review_id") != current["id"]
88
+ or decision.get("digest") != current["digest"]
89
+ ):
90
+ raise ValueError("Current checkpoint decision binding changed")
91
+ record["decision"] = None
92
+ _write(root, record)
93
+ return decision
94
+ time.sleep(0.1)
95
+ raise TimeoutError("Exact checkpoint decision expired; no approval was granted")
96
+
97
+
98
+ def run_request(
99
+ root: Path,
100
+ request: dict[str, object],
101
+ *,
102
+ executor_factory: Callable[
103
+ [Path, Callable[[dict[str, object]], dict[str, object]]], BootstrapExecutor
104
+ ] = BootstrapExecutor,
105
+ ) -> None:
106
+ """Execute a claimed request; failures never expose raw exception or process output."""
107
+ action = str(request["action"])
108
+ try:
109
+ raw_payload = request["payload"]
110
+ if not isinstance(raw_payload, dict):
111
+ raise ValueError("Malformed executor request") # noqa: TRY004 - IPC rejection contract
112
+ payload = validate_request(action, raw_payload)
113
+ executor = executor_factory(
114
+ root / "executor", lambda review: channel_decision(root, review)
115
+ )
116
+ if action == "login":
117
+ _update(root, message="Complete Microsoft browser sign-in on this customer machine.")
118
+ result = executor.execute(action, payload)
119
+ ready = result.get("deployment_ready") is True
120
+ retained = bool(_read(root / "executor", "configuration.json"))
121
+ message = result.get("message", "Customer executor operation completed.")
122
+ if retained and action in {"login", "accounts", "inspect"}:
123
+ message = "Retained installation preserved. Resume to refresh its exact target, plans and health."
124
+ _update(
125
+ root,
126
+ phase="ready"
127
+ if ready
128
+ else "blocked"
129
+ if action in {"install", "resume"} or retained
130
+ else "idle",
131
+ request_id=request["id"] if retained or action in {"install", "resume"} else None,
132
+ review=None,
133
+ deployment_ready=ready,
134
+ console_url=result.get("console_url") if ready else None,
135
+ message=message,
136
+ **({"accounts": result["accounts"]} if "accounts" in result else {}),
137
+ )
138
+ except BootstrapBlocked as exc:
139
+ _update(
140
+ root,
141
+ phase="blocked",
142
+ review=None,
143
+ deployment_ready=False,
144
+ console_url=None,
145
+ error_code="prerequisite_unavailable",
146
+ message=str(exc),
147
+ )
148
+ except (TimeoutError, subprocess.TimeoutExpired):
149
+ _update(
150
+ root,
151
+ phase="interrupted",
152
+ review=None,
153
+ deployment_ready=False,
154
+ console_url=None,
155
+ error_code="operation_timeout",
156
+ message="Operation expired. Inspect retained state; do not repeat ambiguous effects.",
157
+ )
158
+ except Exception: # noqa: BLE001 - executor boundary must never publish raw provider errors
159
+ _update(
160
+ root,
161
+ phase="failed",
162
+ review=None,
163
+ deployment_ready=False,
164
+ console_url=None,
165
+ error_code="executor_failed",
166
+ message="Executor stopped. Inspect private traces and retained evidence before resuming.",
167
+ )
168
+
169
+
170
+ def _child(root: Path, request: dict[str, object], output: int, reader: int, parent: int) -> None:
171
+ os.setsid()
172
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
173
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
174
+ os.umask(0o077)
175
+ os.close(reader)
176
+ area = root / "executor"
177
+ prepare_directory(area)
178
+ os.dup2(output, 1)
179
+ os.dup2(output, 2)
180
+ os.close(output)
181
+ null = os.open(os.devnull, os.O_RDONLY)
182
+ os.dup2(null, 0)
183
+ os.close(null)
184
+
185
+ def parent_watchdog() -> None:
186
+ while os.getppid() == parent:
187
+ time.sleep(0.25)
188
+ # A forced worker kill must also stop the isolated operation's descendants.
189
+ os.killpg(os.getpid(), signal.SIGKILL)
190
+
191
+ threading.Thread(target=parent_watchdog, daemon=True).start()
192
+ run_request(root, request)
193
+
194
+
195
+ def _trace(root: Path) -> int:
196
+ area = root / "executor"
197
+ prepare_directory(area)
198
+ current, previous = area / "engine-trace.log", area / "engine-trace.previous.log"
199
+ if current.is_symlink() or previous.is_symlink():
200
+ raise ValueError("Private executor traces must not be symlinks")
201
+ if current.exists():
202
+ info = current.lstat()
203
+ if (
204
+ not stat.S_ISREG(info.st_mode)
205
+ or info.st_uid != os.getuid()
206
+ or stat.S_IMODE(info.st_mode) != 0o600
207
+ or info.st_nlink != 1
208
+ ):
209
+ raise ValueError("Private executor trace is unsafe")
210
+ os.replace(current, previous)
211
+ fd = os.open(current, os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_NOFOLLOW, 0o600)
212
+ info = os.fstat(fd)
213
+ if (
214
+ not stat.S_ISREG(info.st_mode)
215
+ or info.st_uid != os.getuid()
216
+ or stat.S_IMODE(info.st_mode) != 0o600
217
+ or info.st_nlink != 1
218
+ ):
219
+ os.close(fd)
220
+ raise ValueError("Private executor trace is unsafe")
221
+ return fd
222
+
223
+
224
+ def _drain(reader: int, trace: int) -> bool:
225
+ """Keep engine traces bounded without limiting Terraform state or artifact file sizes."""
226
+ for _ in range(64):
227
+ try:
228
+ data = os.read(reader, 4096)
229
+ except BlockingIOError:
230
+ break
231
+ if not data:
232
+ break
233
+ if os.fstat(trace).st_size + len(data) > 16 * 1024 * 1024:
234
+ return False
235
+ os.write(trace, data)
236
+ return True
237
+
238
+
239
+ def _stop(process: BaseProcess) -> None:
240
+ if process.is_alive() and process.pid is not None:
241
+ try:
242
+ os.killpg(process.pid, signal.SIGTERM)
243
+ except ProcessLookupError:
244
+ process.terminate()
245
+ process.join(5)
246
+ try:
247
+ os.killpg(process.pid, signal.SIGKILL)
248
+ except ProcessLookupError:
249
+ pass
250
+ process.join(5)
251
+ elif process.pid is not None:
252
+ # Also remove children left behind by a crashed operation coordinator.
253
+ try:
254
+ os.killpg(process.pid, signal.SIGTERM)
255
+ except ProcessLookupError:
256
+ pass
257
+ try:
258
+ os.killpg(process.pid, signal.SIGKILL)
259
+ except ProcessLookupError:
260
+ pass
261
+
262
+
263
+ def serve(root: Path, *, total_seconds: int = TOTAL_SECONDS) -> None:
264
+ """Supervise serial process groups with heartbeat, hard deadlines and no replay."""
265
+ prepare_directory(root)
266
+ os.umask(0o077)
267
+ lock = os.open(root / "worker.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
268
+ info = os.fstat(lock)
269
+ if (
270
+ not stat.S_ISREG(info.st_mode)
271
+ or stat.S_IMODE(info.st_mode) != 0o600
272
+ or info.st_uid != os.getuid()
273
+ or info.st_nlink != 1
274
+ ):
275
+ os.close(lock)
276
+ raise ValueError("Worker lock is unsafe")
277
+ try:
278
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
279
+ except BlockingIOError as exc:
280
+ os.close(lock)
281
+ raise ValueError("A customer executor already owns this directory") from exc
282
+ stopping = False
283
+
284
+ def stop(_signal: int, _frame: FrameType | None) -> None:
285
+ nonlocal stopping
286
+ stopping = True
287
+
288
+ old_handlers = {sig: signal.signal(sig, stop) for sig in (signal.SIGTERM, signal.SIGINT)}
289
+ process = None
290
+ reader = trace_fd = None
291
+ started = time.monotonic()
292
+ operation_started = started
293
+ progress_time = started
294
+ last_progress = None
295
+ action = ""
296
+ try:
297
+ with locked(root):
298
+ record = _read(root)
299
+ status = record.get("status", initial_status())
300
+ if (
301
+ status["phase"] in {"working", "review"}
302
+ or record.get("pending")
303
+ or record.get("active_request_id")
304
+ ):
305
+ status.update(
306
+ phase="interrupted",
307
+ review=None,
308
+ deployment_ready=False,
309
+ console_url=None,
310
+ error_code="worker_restarted",
311
+ message="Prior work was interrupted. Inspect retained state before explicit resume.",
312
+ )
313
+ record.update(
314
+ status=status,
315
+ pending=None,
316
+ active_request_id=None,
317
+ decision=None,
318
+ heartbeat=time.time(),
319
+ )
320
+ _write(root, record)
321
+ while not stopping and time.monotonic() - started < min(total_seconds, TOTAL_SECONDS):
322
+ with locked(root):
323
+ record = _read(root)
324
+ record["heartbeat"] = time.time()
325
+ _write(root, record)
326
+ _write(
327
+ root,
328
+ {
329
+ "pid": os.getpid(),
330
+ "operation_pid": process.pid if process is not None else None,
331
+ "updated_at": datetime.now(UTC).isoformat(),
332
+ "heartbeat": time.time(),
333
+ },
334
+ "heartbeat.json",
335
+ )
336
+ pending = record.get("pending") if process is None else None
337
+ if pending is not None:
338
+ record.update(
339
+ pending=None, active_request_id=pending["id"], progress=time.time()
340
+ )
341
+ _write(root, record)
342
+ if pending is not None:
343
+ action = str(pending["action"])
344
+ trace_fd = _trace(root)
345
+ reader, writer = os.pipe()
346
+ os.set_blocking(reader, False)
347
+ process = multiprocessing.get_context("fork").Process(
348
+ target=_child, args=(root, pending, writer, reader, os.getpid())
349
+ )
350
+ process.start()
351
+ os.close(writer)
352
+ with locked(root):
353
+ _write(
354
+ root,
355
+ {
356
+ "pid": os.getpid(),
357
+ "operation_pid": process.pid,
358
+ "updated_at": datetime.now(UTC).isoformat(),
359
+ "heartbeat": time.time(),
360
+ },
361
+ "heartbeat.json",
362
+ )
363
+ operation_started = progress_time = time.monotonic()
364
+ last_progress = None
365
+ if process is not None:
366
+ assert reader is not None and trace_fd is not None
367
+ trace_within_bound = _drain(reader, trace_fd)
368
+ trace = root / "executor/engine-trace.log"
369
+ trace_size = (
370
+ trace.stat().st_size if trace.is_file() and not trace.is_symlink() else 0
371
+ )
372
+ progress = (record.get("progress"), trace_size)
373
+ if progress != last_progress:
374
+ last_progress, progress_time = progress, time.monotonic()
375
+ timed_out = (
376
+ time.monotonic() - operation_started > OPERATION_SECONDS.get(action, 120)
377
+ or time.monotonic() - progress_time > NO_PROGRESS_SECONDS
378
+ or not trace_within_bound
379
+ )
380
+ if timed_out or not process.is_alive():
381
+ _stop(process)
382
+ with locked(root):
383
+ record = _read(root)
384
+ status = record["status"]
385
+ if timed_out or status["phase"] in {"working", "review"}:
386
+ status.update(
387
+ phase="interrupted",
388
+ review=None,
389
+ deployment_ready=False,
390
+ console_url=None,
391
+ error_code="operation_interrupted",
392
+ message="Executor stopped. Inspect retained evidence before explicit resume.",
393
+ )
394
+ record.update(status=status, active_request_id=None, progress=time.time())
395
+ _write(root, record)
396
+ process.close()
397
+ process = None
398
+ os.close(reader)
399
+ os.close(trace_fd)
400
+ reader = trace_fd = None
401
+ time.sleep(2)
402
+ finally:
403
+ if process is not None:
404
+ _stop(process)
405
+ process.close()
406
+ _update(
407
+ root,
408
+ phase="interrupted",
409
+ review=None,
410
+ deployment_ready=False,
411
+ console_url=None,
412
+ error_code="worker_stopped",
413
+ message="Worker stopped; inspect before explicit resume.",
414
+ )
415
+ if reader is not None:
416
+ os.close(reader)
417
+ if trace_fd is not None:
418
+ os.close(trace_fd)
419
+ for sig, previous in old_handlers.items():
420
+ signal.signal(sig, previous)
421
+ os.close(lock)
422
+
423
+
424
+ def main() -> None:
425
+ parser = argparse.ArgumentParser(description=__doc__)
426
+ parser.add_argument("--state-dir", type=Path, required=True)
427
+ args = parser.parse_args()
428
+ root = args.state_dir.absolute()
429
+ try:
430
+ serve(root)
431
+ except Exception: # noqa: BLE001 - startup errors must not leak paths or provider diagnostics
432
+ prepare_directory(root)
433
+ area = root / "executor"
434
+ prepare_directory(area)
435
+ _write(
436
+ area,
437
+ {
438
+ "error_code": "worker_startup_failed",
439
+ "message": "Worker startup failed. Check private directory permissions and worker ownership.",
440
+ "recorded_at": datetime.now(UTC).isoformat(),
441
+ },
442
+ "worker-diagnostics.json",
443
+ )
444
+ raise SystemExit(1) from None
445
+
446
+
447
+ if __name__ == "__main__":
448
+ main()