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,638 @@
1
+ """Supervise signed-kit Azure Foundation without GitHub services or a source checkout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import json
7
+ import os
8
+ import re
9
+ import stat
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ from collections.abc import Mapping
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from fdai_service_contracts.product_profile import ProductAddOn
19
+
20
+ from fdai_deployment_cli.aks_preflight import inspect_aks_target
21
+ from fdai_deployment_cli.application_state_adoption import (
22
+ ApplicationStateAdoption,
23
+ stage_application_state_adoption,
24
+ )
25
+ from fdai_deployment_cli.azure_naming import selected_azure_region_short_name
26
+ from fdai_deployment_cli.catalog_review_profile import CatalogReviewDeploymentProfile
27
+ from fdai_deployment_cli.control_package import verify_control_package
28
+ from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
29
+ from fdai_deployment_cli.deployment_kit import DeploymentKit, acquire_deployment_kit
30
+ from fdai_deployment_cli.deployment_progress import begin_stage, progress_detail, terminal_output
31
+ from fdai_deployment_cli.foundation_failure import foundation_failure_summary
32
+ from fdai_deployment_cli.foundation_output import foundation_output
33
+ from fdai_deployment_cli.foundation_process import run_foundation_process
34
+ from fdai_deployment_cli.operational_evidence_verifier_input import (
35
+ OperationalEvidenceVerifierDeploymentInput,
36
+ )
37
+ from fdai_deployment_cli.private_output import read_private_bytes
38
+ from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
39
+ from fdai_deployment_cli.standalone_application_completion import complete_application
40
+ from fdai_deployment_cli.standalone_foundation_adoption import (
41
+ deploy_with_adopted_foundation,
42
+ )
43
+ from fdai_deployment_cli.standalone_foundation_transition import (
44
+ default_transition_runner,
45
+ run_foundation_transition,
46
+ )
47
+ from fdai_deployment_cli.standalone_status import current_status, prior_attempt
48
+ from fdai_deployment_cli.target import (
49
+ azure_target_selection_arguments,
50
+ require_selected_azure_target,
51
+ )
52
+
53
+ _GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
54
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
55
+ _SAFE_ENVIRONMENT_KEYS = frozenset(
56
+ {
57
+ "AZURE_CONFIG_DIR",
58
+ "CURL_CA_BUNDLE",
59
+ "HOME",
60
+ "LANG",
61
+ "LC_ALL",
62
+ "LC_CTYPE",
63
+ "LOGNAME",
64
+ "PATH",
65
+ "REQUESTS_CA_BUNDLE",
66
+ "SSL_CERT_FILE",
67
+ "TERM",
68
+ "TMPDIR",
69
+ "TZ",
70
+ "USER",
71
+ }
72
+ )
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class ActiveAzureTarget:
77
+ """Authenticated Azure user target selected by the current CLI profile."""
78
+
79
+ subscription_id: str
80
+ tenant_id: str
81
+
82
+
83
+ def deploy_azure_foundation(
84
+ *,
85
+ work_dir: Path,
86
+ offline_kit: Path | None,
87
+ online: bool = False,
88
+ online_url: str | None = None,
89
+ region: str,
90
+ monthly_cost_ceiling: int,
91
+ timeout_seconds: int,
92
+ trial_token: Path | None,
93
+ runtime_profile: RuntimeDeploymentProfile | None = None,
94
+ adopt_runner_image_receipt: Path | None = None,
95
+ adopt_application_state: Path | None = None,
96
+ adopt_application_recovery: Path | None = None,
97
+ adopt_resolved_models: Path | None = None,
98
+ adopt_foundation_directory: Path | None = None,
99
+ adopt_foundation_recovery_directory: Path | None = None,
100
+ catalog_review_profile: CatalogReviewDeploymentProfile | None = None,
101
+ operational_evidence_verifier_input: OperationalEvidenceVerifierDeploymentInput | None = None,
102
+ control_package: Path | None = None,
103
+ ) -> dict[str, object]:
104
+ """Advance one standalone deployment through verified application convergence.
105
+
106
+ ``control_package`` optionally selects a signed deployment-control wheelhouse that replaces
107
+ only the managed-host CLI; the verified kit still supplies every runtime payload.
108
+ """
109
+
110
+ deadline = DeploymentDeadline(timeout_seconds, clock=time.monotonic)
111
+ selected_runtime = runtime_profile or RuntimeDeploymentProfile.create(
112
+ runtime_platform="container-apps",
113
+ database_placement="postgres-flex",
114
+ )
115
+ begin_stage("azure")
116
+ progress_detail("Checking the active Azure CLI human identity")
117
+ target = active_azure_target()
118
+ deadline.remaining()
119
+ _create_or_validate_private_directory(work_dir)
120
+ kit_work = work_dir / "kit-work"
121
+ _create_or_validate_private_directory(kit_work)
122
+ begin_stage("kit")
123
+ kit = acquire_deployment_kit(
124
+ work_dir=kit_work,
125
+ online=online,
126
+ offline_kit=offline_kit,
127
+ online_url=online_url,
128
+ )
129
+ deadline.remaining()
130
+ control = None
131
+ if control_package is not None:
132
+ if online:
133
+ raise ValueError("a control package requires a local signed offline kit")
134
+ progress_detail("Verifying the signed deployment-control package")
135
+ control = verify_control_package(control_package)
136
+ adoption_inputs = (
137
+ adopt_application_state,
138
+ adopt_application_recovery,
139
+ adopt_resolved_models,
140
+ )
141
+ if any(value is not None for value in adoption_inputs) and not all(
142
+ value is not None for value in adoption_inputs
143
+ ):
144
+ raise ValueError("recovered public deployment requires all three adoption inputs")
145
+ adoption: ApplicationStateAdoption | None = None
146
+ if all(value is not None for value in adoption_inputs):
147
+ assert adopt_application_state is not None
148
+ assert adopt_application_recovery is not None
149
+ assert adopt_resolved_models is not None
150
+ region_short = selected_azure_region_short_name(
151
+ region=region,
152
+ subscription_id=target.subscription_id,
153
+ environment="dev",
154
+ workload="fdai",
155
+ retained_variables=work_dir / "run" / "foundation-variables.json",
156
+ )
157
+ adoption = stage_application_state_adoption(
158
+ source_state=adopt_application_state,
159
+ recovery_receipt=adopt_application_recovery,
160
+ resolved_models=adopt_resolved_models,
161
+ output_directory=work_dir / "application-state-adoption",
162
+ subscription_id=target.subscription_id,
163
+ resource_group_name=f"rg-fdai-dev-{region_short}",
164
+ environment="dev",
165
+ region_short=region_short,
166
+ )
167
+ adopted = deploy_with_adopted_foundation(
168
+ foundation_directory=adopt_foundation_directory,
169
+ recovery_directory=adopt_foundation_recovery_directory,
170
+ adopt_runner_image_receipt=adopt_runner_image_receipt,
171
+ work_dir=work_dir,
172
+ kit=kit,
173
+ tenant_id=target.tenant_id,
174
+ subscription_id=target.subscription_id,
175
+ region=region,
176
+ monthly_cost_ceiling=monthly_cost_ceiling,
177
+ deadline=deadline,
178
+ selected_runtime=selected_runtime,
179
+ trial_token=trial_token,
180
+ application_state_adoption=adoption,
181
+ catalog_review_profile=catalog_review_profile,
182
+ operational_evidence_verifier_input=operational_evidence_verifier_input,
183
+ current_operator_object_id=_current_operator_object_id,
184
+ control_package=control,
185
+ )
186
+ if adopted is not None:
187
+ return adopted
188
+ if (
189
+ selected_runtime.runtime_platform.value == "aks"
190
+ and adoption is None
191
+ and not (work_dir / "run" / "status.json").exists()
192
+ ):
193
+ if _identity_add_on_selected(selected_runtime):
194
+ _require_unique_entra_display_names(
195
+ selected_runtime, kit.bundle_root / "scripts/deployment/azure"
196
+ )
197
+ _require_feasible_new_aks_target(selected_runtime, region=region, deadline=deadline)
198
+ begin_stage("discovery")
199
+ progress_detail("Discovering image, storage name, and non-overlapping networks")
200
+ scripts = kit.bundle_root / "scripts/deployment/azure"
201
+ if not scripts.is_dir() or scripts.is_symlink():
202
+ raise ValueError("verified deployment bundle is missing Azure orchestration")
203
+ create_runner_image = not online and adopt_runner_image_receipt is None
204
+ sys.path.insert(0, str(scripts))
205
+ try:
206
+ prepare = importlib.import_module("genesis_prepare")
207
+ prepared: Any = prepare.prepare_standalone_genesis(
208
+ deployment_kit=kit,
209
+ tenant_id=target.tenant_id,
210
+ subscription_id=target.subscription_id,
211
+ region=region,
212
+ monthly_cost_ceiling=monthly_cost_ceiling,
213
+ connectivity="online" if online else "offline",
214
+ root=work_dir / "run",
215
+ create_runner_image=create_runner_image,
216
+ region_short=selected_azure_region_short_name(
217
+ region=region,
218
+ subscription_id=target.subscription_id,
219
+ environment="dev",
220
+ workload="fdai",
221
+ retained_variables=work_dir / "run" / "foundation-variables.json",
222
+ ),
223
+ )
224
+ foundation_variables = prepared.variables
225
+ if adopt_runner_image_receipt is not None:
226
+ runner_image = importlib.import_module("genesis_runner_image_contract")
227
+ foundation_variables = prepared.root / "foundation-with-runner-image.json"
228
+ if foundation_variables.exists():
229
+ runner_image.verify_foundation_image_input(
230
+ source=prepared.variables,
231
+ image_receipt=adopt_runner_image_receipt,
232
+ profile_path=prepared.profile,
233
+ destination=foundation_variables,
234
+ )
235
+ else:
236
+ runner_image.materialize_foundation_image_input(
237
+ source=prepared.variables,
238
+ image_receipt=adopt_runner_image_receipt,
239
+ profile_path=prepared.profile,
240
+ destination=foundation_variables,
241
+ )
242
+ finally:
243
+ sys.path.remove(str(scripts))
244
+ # An offline kit upgrade continues the retained Foundation under the revision that created
245
+ # it; the newly verified kit remains the application source.
246
+ foundation_source = str(getattr(prepared, "foundation_source_commit", "") or kit.source_commit)
247
+ lineage_continuation = foundation_source != kit.source_commit
248
+ source_evidence = json.dumps(
249
+ {
250
+ "source_commit": kit.source_commit,
251
+ "foundation_source_commit": foundation_source,
252
+ "kit_manifest_digest": kit.verification.manifest_digest,
253
+ "bundle_manifest_digest": kit.bundle_manifest_digest,
254
+ "runtime_release_digest": kit.runtime.digest,
255
+ },
256
+ sort_keys=True,
257
+ separators=(",", ":"),
258
+ )
259
+ approval = prepared.root / "current-foundation-approval.json"
260
+ status_path = prepared.root / "status.json"
261
+ begin_stage("foundation")
262
+ while True:
263
+ remaining = deadline.remaining()
264
+ if remaining < 1830:
265
+ raise TimeoutError("standalone Foundation deadline has insufficient remaining budget")
266
+ previous_attempt = prior_attempt(status_path)
267
+ stage_timeout = min(14_400, remaining - 30)
268
+ command = (
269
+ sys.executable,
270
+ str(scripts / "genesis_orchestrator.py"),
271
+ "--environment",
272
+ "dev",
273
+ "--region",
274
+ region,
275
+ "--apply",
276
+ "--allow-probe-resources",
277
+ "--source-commit",
278
+ foundation_source,
279
+ "--work-dir",
280
+ str(prepared.root),
281
+ "--foundation-offline-kit",
282
+ str(prepared.offline_kit),
283
+ "--foundation-release-root",
284
+ str(prepared.release_root),
285
+ "--foundation-bundle-public-key",
286
+ str(prepared.bundle_public_key),
287
+ "--foundation-profile",
288
+ str(prepared.profile),
289
+ "--foundation-variables-file",
290
+ str(foundation_variables),
291
+ *(
292
+ ()
293
+ if not create_runner_image
294
+ else (
295
+ "--create-runner-image",
296
+ "--runner-image-terraform",
297
+ str(prepared.terraform),
298
+ )
299
+ ),
300
+ "--runner-ssh-private-key",
301
+ str(prepared.ssh_private_key),
302
+ *(("--approval-file", str(approval)) if approval.exists() else ()),
303
+ "--execution-timeout-seconds",
304
+ str(stage_timeout),
305
+ "--output",
306
+ "json",
307
+ )
308
+ try:
309
+ with foundation_output() as stderr:
310
+ foundation_exit = run_foundation_process(
311
+ command,
312
+ cwd=kit.bundle_root,
313
+ env=_standalone_subprocess_environment(
314
+ PYTHONPATH=os.pathsep.join(
315
+ (str(scripts), str(Path(__file__).parent.parent))
316
+ ),
317
+ AZURE_SUBSCRIPTION_ID=target.subscription_id,
318
+ AZURE_TENANT_ID=target.tenant_id,
319
+ FDAI_SIGNED_SOURCE_EVIDENCE=source_evidence,
320
+ ),
321
+ stdout=subprocess.DEVNULL,
322
+ stderr=stderr,
323
+ timeout=stage_timeout + 15,
324
+ )
325
+ if foundation_exit.returncode not in {0, 2}:
326
+ raise ValueError(
327
+ foundation_failure_summary(
328
+ status_path,
329
+ previous=previous_attempt,
330
+ source_commit=foundation_source,
331
+ run_binding=prepared.run_binding,
332
+ )
333
+ )
334
+ except subprocess.TimeoutExpired as exc:
335
+ raise TimeoutError(
336
+ "standalone Foundation orchestration timed out; inspect retained status before recovery"
337
+ ) from exc
338
+ status = current_status(
339
+ status_path,
340
+ previous=previous_attempt,
341
+ source_commit=foundation_source,
342
+ run_binding=prepared.run_binding,
343
+ )
344
+ completed_stages = status.get("completed_stages")
345
+ if (
346
+ status.get("current_stage") == "application-plan"
347
+ and status.get("route") == "private-runner"
348
+ and isinstance(completed_stages, list)
349
+ and "foundation-state" in completed_stages
350
+ ):
351
+ foundation = _foundation_result(kit, prepared, status)
352
+ lineage_receipt_digest = _bind_application_to_foundation_lineage(
353
+ kit=kit,
354
+ prepared=prepared,
355
+ status=status,
356
+ lineage_continuation=lineage_continuation,
357
+ tenant_id=target.tenant_id,
358
+ subscription_id=target.subscription_id,
359
+ region=region,
360
+ monthly_cost_ceiling=monthly_cost_ceiling,
361
+ )
362
+ deadline.remaining()
363
+ return complete_application(
364
+ kit=kit,
365
+ prepared=prepared,
366
+ status=status,
367
+ scripts=scripts,
368
+ deadline=deadline,
369
+ selected_runtime=selected_runtime,
370
+ trial_token=trial_token,
371
+ application_state_adoption=adoption,
372
+ foundation_state_receipt_digest=str(foundation["foundation_state_receipt_digest"]),
373
+ foundation_adoption_receipt_digest=lineage_receipt_digest,
374
+ catalog_review_profile=(
375
+ catalog_review_profile or CatalogReviewDeploymentProfile.unselected()
376
+ ),
377
+ operational_evidence_verifier_input=operational_evidence_verifier_input,
378
+ current_operator_object_id=_current_operator_object_id,
379
+ control_package=control,
380
+ )
381
+ if foundation_exit.returncode != 2:
382
+ raise ValueError("standalone Foundation orchestration failed")
383
+ if lineage_continuation:
384
+ # A new Foundation plan computed from the newer kit must not be approved under the
385
+ # retained revision's lineage; continuation only verifies completed checkpoints.
386
+ raise ValueError(
387
+ "offline kit upgrade requires a completed Foundation and cannot approve a new "
388
+ "Foundation plan under its retained source lineage; inspect retained status"
389
+ )
390
+ approval.unlink(missing_ok=True)
391
+ try:
392
+ with terminal_output("Review the exact Foundation plan", approval=True):
393
+ prompt = subprocess.run(
394
+ (
395
+ sys.executable,
396
+ str(scripts / "genesis_approval_prompt.py"),
397
+ "--status",
398
+ str(status_path),
399
+ "--output",
400
+ str(approval),
401
+ ),
402
+ cwd=kit.bundle_root,
403
+ env=_standalone_subprocess_environment(
404
+ PYTHONPATH=os.pathsep.join(
405
+ (str(scripts), str(Path(__file__).parent.parent))
406
+ ),
407
+ FDAI_SIGNED_SOURCE_EVIDENCE=source_evidence,
408
+ ),
409
+ stdout=sys.stderr,
410
+ check=False,
411
+ timeout=deadline.remaining(600),
412
+ )
413
+ except subprocess.TimeoutExpired as exc:
414
+ raise TimeoutError("standalone Foundation approval prompt timed out") from exc
415
+ if prompt.returncode != 0:
416
+ raise ValueError("standalone Foundation approval was not granted")
417
+
418
+
419
+ def _require_feasible_new_aks_target(
420
+ profile: RuntimeDeploymentProfile,
421
+ *,
422
+ region: str,
423
+ deadline: DeploymentDeadline,
424
+ ) -> None:
425
+ """Stop a new AKS installation before its first Azure change when the target is infeasible."""
426
+
427
+ progress_detail("Checking AKS capacity and database availability before the first Azure change")
428
+ preflight = inspect_aks_target(
429
+ profile=profile, region=region, timeout_seconds=deadline.remaining(180)
430
+ )
431
+ if preflight.get("state") != "feasible":
432
+ blockers = preflight.get("blockers")
433
+ names = (
434
+ ", ".join(str(item) for item in blockers)
435
+ if isinstance(blockers, list) and blockers
436
+ else "unknown"
437
+ )
438
+ raise ValueError(f"standalone AKS preflight blocked a new installation: {names}")
439
+
440
+
441
+ def _require_unique_entra_display_names(
442
+ profile: RuntimeDeploymentProfile,
443
+ scripts: Path,
444
+ ) -> None:
445
+ """Stop identity-enabled new installations before first Azure effect if names are ambiguous."""
446
+
447
+ if not _identity_add_on_selected(profile):
448
+ return
449
+ sys.path.insert(0, str(scripts))
450
+ try:
451
+ genesis_entra = importlib.import_module("genesis_entra")
452
+ check = getattr(genesis_entra, "check_unique_display_names", None)
453
+ if callable(check):
454
+ check()
455
+ return
456
+ plan_entra = getattr(genesis_entra, "plan_entra", None)
457
+ if callable(plan_entra):
458
+ plan_entra()
459
+ finally:
460
+ sys.path.remove(str(scripts))
461
+
462
+
463
+ def _identity_add_on_selected(profile: RuntimeDeploymentProfile) -> bool:
464
+ product = profile.product_profile
465
+ return product.selects(ProductAddOn.READ_ONLY_CONSOLE) or product.selects(
466
+ ProductAddOn.ENTERPRISE_IDENTITY_GOVERNANCE
467
+ )
468
+
469
+
470
+ def active_azure_target() -> ActiveAzureTarget:
471
+ """Read the active Azure CLI user target without changing account selection."""
472
+
473
+ try:
474
+ completed = subprocess.run(
475
+ (
476
+ "az",
477
+ "account",
478
+ "show",
479
+ *azure_target_selection_arguments(),
480
+ "--query",
481
+ "{subscription_id:id,tenant_id:tenantId,user_type:user.type}",
482
+ "--output",
483
+ "json",
484
+ "--only-show-errors",
485
+ ),
486
+ check=False,
487
+ capture_output=True,
488
+ text=True,
489
+ timeout=60,
490
+ )
491
+ except (OSError, subprocess.SubprocessError):
492
+ raise ValueError(
493
+ "Azure authentication read failed; inspect the local login context"
494
+ ) from None
495
+ if completed.returncode != 0:
496
+ raise ValueError("Azure authentication is unavailable; run az login first")
497
+ value = json.loads(completed.stdout)
498
+ subscription = value.get("subscription_id") if isinstance(value, dict) else None
499
+ tenant = value.get("tenant_id") if isinstance(value, dict) else None
500
+ if (
501
+ not isinstance(value, dict)
502
+ or value.get("user_type") != "user"
503
+ or not isinstance(subscription, str)
504
+ or _GUID.fullmatch(subscription) is None
505
+ or not isinstance(tenant, str)
506
+ or _GUID.fullmatch(tenant) is None
507
+ ):
508
+ raise ValueError("standalone deployment requires an authenticated Azure human")
509
+ require_selected_azure_target(subscription_id=subscription, tenant_id=tenant)
510
+ return ActiveAzureTarget(subscription_id=subscription, tenant_id=tenant)
511
+
512
+
513
+ def _standalone_subprocess_environment(
514
+ source: Mapping[str, str] | None = None,
515
+ **overrides: str,
516
+ ) -> dict[str, str]:
517
+ """Return the minimal non-secret environment used by standalone child processes."""
518
+
519
+ inherited = os.environ if source is None else source
520
+ result = {key: inherited[key] for key in _SAFE_ENVIRONMENT_KEYS if key in inherited}
521
+ result.update(overrides)
522
+ return result
523
+
524
+
525
+ def _current_operator_object_id() -> str:
526
+ try:
527
+ completed = subprocess.run(
528
+ (
529
+ "az",
530
+ "ad",
531
+ "signed-in-user",
532
+ "show",
533
+ "--query",
534
+ "id",
535
+ "--output",
536
+ "tsv",
537
+ "--only-show-errors",
538
+ ),
539
+ check=False,
540
+ capture_output=True,
541
+ text=True,
542
+ timeout=60,
543
+ )
544
+ except (OSError, subprocess.SubprocessError):
545
+ raise ValueError("authenticated Azure operator read failed; no retry performed") from None
546
+ value = completed.stdout.strip()
547
+ if completed.returncode != 0 or _GUID.fullmatch(value) is None:
548
+ raise ValueError("authenticated Azure operator object ID is unavailable")
549
+ return value
550
+
551
+
552
+ def _bind_application_to_foundation_lineage(
553
+ *,
554
+ kit: DeploymentKit,
555
+ prepared: Any,
556
+ status: dict[str, Any],
557
+ lineage_continuation: bool,
558
+ tenant_id: str,
559
+ subscription_id: str,
560
+ region: str,
561
+ monthly_cost_ceiling: int,
562
+ ) -> str | None:
563
+ """Give the managed host verified evidence that binds a retained Foundation to this kit."""
564
+
565
+ receipt_path = prepared.root / "foundation-adoption-receipt.json"
566
+ if not lineage_continuation:
567
+ if receipt_path.exists() or receipt_path.is_symlink():
568
+ retained = _private_json(receipt_path)
569
+ if retained.get("foundation_source_commit") == kit.source_commit:
570
+ # The kit is back at the Foundation's own revision, so no adoption applies.
571
+ review = prepared.root / "foundation-adoption-review"
572
+ _create_or_validate_private_directory(review)
573
+ receipt_path.rename(review / f"{str(retained.get('receipt_digest'))[:16]}.json")
574
+ return None
575
+ report = status.get("foundation_report")
576
+ plan = report.get("foundation_plan") if isinstance(report, dict) else None
577
+ plan_ref = plan.get("plan_ref") if isinstance(plan, dict) else None
578
+ if (
579
+ not isinstance(plan_ref, str)
580
+ or re.fullmatch(r"foundation-plan-attempt-[1-9][0-9]*", plan_ref) is None
581
+ ):
582
+ raise ValueError("retained Foundation plan reference is invalid")
583
+ receipt = run_foundation_transition(
584
+ kit=kit,
585
+ run_root=prepared.root,
586
+ retained_plan_ref=plan_ref,
587
+ transition_plan_ref=None,
588
+ application_source_commit=kit.source_commit,
589
+ kit_manifest_digest=kit.verification.manifest_digest,
590
+ runtime_release_digest=kit.runtime.digest,
591
+ tenant_id=tenant_id,
592
+ subscription_id=subscription_id,
593
+ region=region,
594
+ monthly_cost_ceiling=monthly_cost_ceiling,
595
+ transition_runner=default_transition_runner,
596
+ )
597
+ return str(receipt["receipt_digest"])
598
+
599
+
600
+ def _foundation_result(
601
+ kit: DeploymentKit, prepared: Any, status: dict[str, Any]
602
+ ) -> dict[str, object]:
603
+ report = status.get("foundation_report")
604
+ handoff = report.get("state_handoff") if isinstance(report, dict) else None
605
+ receipt = handoff.get("receipt_digest") if isinstance(handoff, dict) else None
606
+ if not isinstance(receipt, str) or _DIGEST.fullmatch(receipt) is None:
607
+ raise ValueError("Foundation state handoff receipt is unavailable")
608
+ return {
609
+ "schema_version": "fdai.standalone-azure-deployment.v1",
610
+ "state": "foundation-ready",
611
+ "source_commit": kit.source_commit,
612
+ "kit_manifest_digest": kit.verification.manifest_digest,
613
+ "runtime_release_digest": kit.runtime.digest,
614
+ "foundation_state_receipt_digest": receipt,
615
+ "work_dir": str(prepared.root),
616
+ "next_action": "plan-standalone-application",
617
+ "mutation_performed": True,
618
+ "subscription_ready": False,
619
+ }
620
+
621
+
622
+ def _private_json(path: Path) -> dict[str, Any]:
623
+ value = json.loads(read_private_bytes(path, max_bytes=1_048_576))
624
+ if not isinstance(value, dict):
625
+ raise ValueError("standalone deployment status is invalid") # noqa: TRY004 - JSON contract.
626
+ return {str(key): item for key, item in value.items()}
627
+
628
+
629
+ def _create_or_validate_private_directory(path: Path) -> None:
630
+ path.mkdir(parents=True, mode=0o700, exist_ok=True)
631
+ details = path.lstat()
632
+ if (
633
+ not path.is_absolute()
634
+ or not stat.S_ISDIR(details.st_mode)
635
+ or stat.S_IMODE(details.st_mode) != 0o700
636
+ or details.st_uid != os.geteuid()
637
+ ):
638
+ raise PermissionError("standalone deployment directory must be current-UID mode 0700")