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,48 @@
1
+ """Bounded private artifact and GitHub JSON decoding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import stat
8
+ from collections.abc import Mapping
9
+ from pathlib import Path
10
+
11
+
12
+ def _private_artifact_json(path: Path, label: str) -> dict[str, object]:
13
+ return dict(_json_object(_private_artifact_bytes(path, label).decode("utf-8"), label))
14
+
15
+
16
+ def _private_artifact_bytes(path: Path, label: str) -> bytes:
17
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
18
+ with os.fdopen(descriptor, "rb") as stream:
19
+ details = os.fstat(stream.fileno())
20
+ if (
21
+ not stat.S_ISREG(details.st_mode)
22
+ or details.st_uid != os.geteuid()
23
+ or details.st_size > 262_144
24
+ ):
25
+ raise ValueError(f"github_{label.replace(' ', '_')}_file_invalid")
26
+ return stream.read(262_145)
27
+
28
+
29
+ def _json_array(raw: str, label: str) -> list[Mapping[str, object]]:
30
+ try:
31
+ payload = json.loads(raw)
32
+ except json.JSONDecodeError as exc:
33
+ raise ValueError(f"{label} response is invalid") from exc
34
+ if not isinstance(payload, list) or not all(isinstance(item, dict) for item in payload):
35
+ raise ValueError(f"{label} response MUST be an array of objects")
36
+ if len(payload) > 50:
37
+ raise ValueError(f"{label} response exceeds the requested bound")
38
+ return payload
39
+
40
+
41
+ def _json_object(raw: str, label: str) -> Mapping[str, object]:
42
+ try:
43
+ payload = json.loads(raw)
44
+ except json.JSONDecodeError as exc:
45
+ raise ValueError(f"{label} response is invalid") from exc
46
+ if not isinstance(payload, dict):
47
+ raise ValueError(f"{label} response MUST be an object") # noqa: TRY004
48
+ return payload
@@ -0,0 +1,248 @@
1
+ """Protected GitHub workflow plan and apply dispatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fdai_deployment_cli.github_transport import run_github_cli
6
+ from fdai_deployment_cli.github_workflow_values import (
7
+ _COMMIT,
8
+ _DIGEST,
9
+ _ENVIRONMENTS,
10
+ _PLAN_ID,
11
+ _REQUEST_ID,
12
+ CommandRunner,
13
+ DeploymentSelection,
14
+ WorkflowDispatch,
15
+ _validate_repository,
16
+ deployment_context_digest,
17
+ enforce_plan_not_expired,
18
+ request_id,
19
+ )
20
+
21
+
22
+ def dispatch_plan(
23
+ *,
24
+ repository: str,
25
+ environment: str,
26
+ commit_sha: str,
27
+ target_binding: str,
28
+ region: str,
29
+ run_id: str,
30
+ selection: DeploymentSelection,
31
+ attempt: int = 1,
32
+ run: CommandRunner | None = None,
33
+ ) -> WorkflowDispatch:
34
+ """Dispatch one protected plan-only run."""
35
+
36
+ if environment == "prod":
37
+ raise ValueError("fdaictl production deployment inputs are not implemented")
38
+ context_digest = deployment_context_digest(
39
+ environment=environment,
40
+ commit_sha=commit_sha,
41
+ selection=selection,
42
+ )
43
+ bounded_request_id = request_id(
44
+ "plan",
45
+ run_id=run_id,
46
+ context_digest=context_digest,
47
+ target_binding=target_binding,
48
+ region=region,
49
+ attempt=attempt,
50
+ )
51
+ if selection.deploy_rca_reader_identity:
52
+ bounded_request_id = bounded_request_id.replace("plan-", "plan-rca-", 1)
53
+ elif selection.deploy_provider_schema:
54
+ request_prefix = (
55
+ "plan-provider-cost-"
56
+ if selection.runtime_image_profile == "cost-governance"
57
+ else "plan-provider-"
58
+ )
59
+ bounded_request_id = bounded_request_id.replace("plan-", request_prefix, 1)
60
+ elif selection.deploy_identity_migration:
61
+ bounded_request_id = bounded_request_id.replace("plan-", "plan-identity-", 1)
62
+ elif selection.deploy_operational_history:
63
+ bounded_request_id = bounded_request_id.replace("plan-", "plan-history-", 1)
64
+ elif selection.runtime_image_profile == "cost-governance":
65
+ bounded_request_id = bounded_request_id.replace("plan-", "plan-cost-", 1)
66
+ _dispatch(
67
+ repository=repository,
68
+ environment=environment,
69
+ commit_sha=commit_sha,
70
+ context_digest=context_digest,
71
+ request_id_value=bounded_request_id,
72
+ apply=False,
73
+ plan_id=None,
74
+ plan_digest=None,
75
+ resume_verification=False,
76
+ selection=selection,
77
+ run=run,
78
+ )
79
+ return WorkflowDispatch(
80
+ request_id=bounded_request_id,
81
+ run_name=f"deploy-{bounded_request_id}",
82
+ context_digest=context_digest,
83
+ mode="plan",
84
+ )
85
+
86
+
87
+ def dispatch_apply(
88
+ *,
89
+ repository: str,
90
+ environment: str,
91
+ commit_sha: str,
92
+ target_binding: str,
93
+ region: str,
94
+ approval_quorum: int,
95
+ run_id: str,
96
+ plan_id: str,
97
+ plan_digest: str,
98
+ plan_expires_at: str,
99
+ resume_verification: bool,
100
+ selection: DeploymentSelection,
101
+ attempt: int = 1,
102
+ run: CommandRunner | None = None,
103
+ ) -> WorkflowDispatch:
104
+ """Dispatch exact-plan apply or verification-only resume."""
105
+
106
+ if environment == "prod":
107
+ raise ValueError("fdaictl production deployment inputs are not implemented")
108
+ if _PLAN_ID.fullmatch(plan_id) is None:
109
+ raise ValueError("plan_id is invalid")
110
+ if _DIGEST.fullmatch(plan_digest) is None:
111
+ raise ValueError("plan_digest MUST be a lowercase SHA-256")
112
+ if not resume_verification:
113
+ enforce_plan_not_expired(plan_expires_at)
114
+ context_digest = deployment_context_digest(
115
+ environment=environment,
116
+ commit_sha=commit_sha,
117
+ selection=selection,
118
+ )
119
+ if _DIGEST.fullmatch(target_binding) is None:
120
+ raise ValueError("target_binding MUST be a lowercase SHA-256")
121
+ if approval_quorum < 1:
122
+ raise ValueError("approval_quorum MUST be positive")
123
+ runner = run or run_github_cli
124
+ bounded_request_id = request_id(
125
+ "resume" if resume_verification else "apply",
126
+ run_id=run_id,
127
+ context_digest=context_digest,
128
+ target_binding=target_binding,
129
+ region=region,
130
+ attempt=attempt,
131
+ )
132
+ if selection.deploy_rca_reader_identity:
133
+ bounded_request_id = bounded_request_id.replace("apply-", "apply-rca-", 1)
134
+ elif selection.deploy_provider_schema:
135
+ request_prefix = (
136
+ "apply-provider-cost-"
137
+ if selection.runtime_image_profile == "cost-governance"
138
+ else "apply-provider-"
139
+ )
140
+ bounded_request_id = bounded_request_id.replace("apply-", request_prefix, 1)
141
+ elif selection.deploy_identity_migration:
142
+ bounded_request_id = bounded_request_id.replace("apply-", "apply-identity-", 1)
143
+ elif selection.deploy_operational_history:
144
+ bounded_request_id = bounded_request_id.replace("apply-", "apply-history-", 1)
145
+ elif selection.runtime_image_profile == "cost-governance":
146
+ bounded_request_id = bounded_request_id.replace("apply-", "apply-cost-", 1)
147
+ _dispatch(
148
+ repository=repository,
149
+ environment=environment,
150
+ commit_sha=commit_sha,
151
+ context_digest=context_digest,
152
+ request_id_value=bounded_request_id,
153
+ apply=True,
154
+ plan_id=plan_id,
155
+ plan_digest=plan_digest,
156
+ resume_verification=resume_verification,
157
+ selection=selection,
158
+ run=runner,
159
+ )
160
+ return WorkflowDispatch(
161
+ request_id=bounded_request_id,
162
+ run_name=f"deploy-{bounded_request_id}",
163
+ context_digest=context_digest,
164
+ mode="resume-verification" if resume_verification else "apply",
165
+ )
166
+
167
+
168
+ def _dispatch(
169
+ *,
170
+ repository: str,
171
+ environment: str,
172
+ commit_sha: str,
173
+ context_digest: str,
174
+ request_id_value: str,
175
+ apply: bool,
176
+ plan_id: str | None,
177
+ plan_digest: str | None,
178
+ resume_verification: bool,
179
+ selection: DeploymentSelection,
180
+ run: CommandRunner | None,
181
+ ) -> None:
182
+ _validate_repository(repository)
183
+ if environment not in _ENVIRONMENTS:
184
+ raise ValueError("environment is unsupported")
185
+ if _COMMIT.fullmatch(commit_sha) is None:
186
+ raise ValueError("commit_sha MUST be a lowercase git SHA")
187
+ if _REQUEST_ID.fullmatch(request_id_value) is None:
188
+ raise ValueError("request_id is invalid")
189
+ fields: dict[str, str] = {
190
+ "environment": environment,
191
+ "apply": str(apply).lower(),
192
+ "promote_runtime_image": str(bool(selection.runtime_image_revision) and not apply).lower(),
193
+ "resume_verification": str(resume_verification).lower(),
194
+ "request_id": request_id_value,
195
+ "context_digest": context_digest,
196
+ "commit_sha": commit_sha,
197
+ **{
198
+ key: str(value).lower()
199
+ for key, value in selection.to_mapping().items()
200
+ if key
201
+ not in {
202
+ "deploy_identity_migration",
203
+ "deploy_operational_history",
204
+ "deploy_provider_schema",
205
+ "deploy_rca_reader_identity",
206
+ "runtime_call_evidence_transition",
207
+ "runtime_image_profile",
208
+ }
209
+ },
210
+ }
211
+ if apply:
212
+ if plan_id is None or plan_digest is None:
213
+ raise ValueError("exact apply requires plan id and digest")
214
+ fields["plan_id"] = plan_id
215
+ fields["plan_digest"] = plan_digest
216
+ workflow = "deploy-dev.yml"
217
+ if apply and (selection.deploy_operational_history or selection.deploy_rca_reader_identity):
218
+ workflow = "request-protected-operation.yml"
219
+ fields = {
220
+ "operation": (
221
+ "operational-history-apply"
222
+ if selection.deploy_operational_history
223
+ else "rca-reader-apply"
224
+ ),
225
+ "environment": environment,
226
+ "commit_sha": commit_sha,
227
+ "request_id": request_id_value,
228
+ "context_digest": context_digest,
229
+ "plan_id": fields["plan_id"],
230
+ "plan_digest": fields["plan_digest"],
231
+ "resume_verification": str(resume_verification).lower(),
232
+ }
233
+ if selection.deploy_operational_history:
234
+ fields["runtime_image_revision"] = selection.runtime_image_revision
235
+ arguments = [
236
+ "workflow",
237
+ "run",
238
+ workflow,
239
+ "--repo",
240
+ repository,
241
+ "--ref",
242
+ "main",
243
+ ]
244
+ for key, value in fields.items():
245
+ arguments.extend(("--field", f"{key}={value}"))
246
+ result = (run or run_github_cli)(tuple(arguments))
247
+ if result.returncode != 0:
248
+ raise ValueError("github_workflow_dispatch_failed")
@@ -0,0 +1,181 @@
1
+ """Protected plan artifact download and validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from fdai_deployment_cli.contracts import canonical_digest
9
+ from fdai_deployment_cli.github_artifact_io import _private_artifact_json
10
+ from fdai_deployment_cli.github_workflow_values import (
11
+ _COMMIT,
12
+ _DIGEST,
13
+ _OCI_DIGEST,
14
+ _PLAN_ID,
15
+ CommandRunner,
16
+ )
17
+
18
+ _CORE_POST_APPLY_OBSERVATIONS = [
19
+ "database-migrations",
20
+ "runtime-health",
21
+ "initial-inventory-execution",
22
+ "canary-publisher",
23
+ "terraform-zero-change",
24
+ ]
25
+
26
+ _COST_GOVERNANCE_POST_APPLY_OBSERVATIONS = [
27
+ "terraform-zero-change",
28
+ "cost-governance-job-image-readback",
29
+ ]
30
+
31
+ _PROVIDER_SCHEMA_POST_APPLY_OBSERVATIONS = [
32
+ "terraform-zero-change",
33
+ "provider-schema-core-baseline",
34
+ "provider-schema-job-execution",
35
+ "provider-schema-durable-generation",
36
+ "provider-schema-agent-review",
37
+ ]
38
+
39
+
40
+ def _download_plan_metadata(
41
+ *,
42
+ repository: str,
43
+ workflow_run_id: int,
44
+ request_id_value: str,
45
+ expected_commit: str,
46
+ expected_context_digest: str,
47
+ run: CommandRunner,
48
+ ) -> dict[str, object]:
49
+ with tempfile.TemporaryDirectory(prefix="fdai-plan-status-") as raw_directory:
50
+ directory = Path(raw_directory)
51
+ result = run(
52
+ (
53
+ "run",
54
+ "download",
55
+ str(workflow_run_id),
56
+ "--repo",
57
+ repository,
58
+ "--name",
59
+ f"deployment-plan-metadata-{request_id_value}",
60
+ "--dir",
61
+ str(directory),
62
+ )
63
+ )
64
+ if result.returncode != 0:
65
+ raise ValueError("github_plan_metadata_unavailable")
66
+ payload = _private_artifact_json(directory / "plan-metadata.json", "plan metadata")
67
+ required = {
68
+ "schema_version": "fdai.deployment-plan.v1",
69
+ "request_id": request_id_value,
70
+ "commit_sha": expected_commit,
71
+ "context_digest": expected_context_digest,
72
+ "status": "ready",
73
+ }
74
+ if any(payload.get(key) != value for key, value in required.items()):
75
+ raise ValueError("github_plan_metadata_context_mismatch")
76
+ plan_id = payload.get("plan_id")
77
+ plan_digest = payload.get("plan_digest")
78
+ context_digest = payload.get("context_digest")
79
+ expires_at = payload.get("expires_at")
80
+ if (
81
+ not isinstance(plan_id, str)
82
+ or _PLAN_ID.fullmatch(plan_id) is None
83
+ or not isinstance(plan_digest, str)
84
+ or _DIGEST.fullmatch(plan_digest) is None
85
+ or not isinstance(context_digest, str)
86
+ or _DIGEST.fullmatch(context_digest) is None
87
+ or not isinstance(expires_at, str)
88
+ ):
89
+ raise ValueError("github_plan_metadata_invalid")
90
+ summary = _plan_summary(payload.get("plan_summary"))
91
+ observations = payload.get("post_apply_observations")
92
+ provider_schema = request_id_value.startswith("plan-provider-")
93
+ cost_governance = request_id_value.startswith("plan-cost-")
94
+ expected_observations = (
95
+ _PROVIDER_SCHEMA_POST_APPLY_OBSERVATIONS
96
+ if provider_schema
97
+ else (
98
+ _COST_GOVERNANCE_POST_APPLY_OBSERVATIONS
99
+ if cost_governance
100
+ else _CORE_POST_APPLY_OBSERVATIONS
101
+ )
102
+ )
103
+ if observations != expected_observations:
104
+ raise ValueError("github_plan_metadata_observations_invalid")
105
+ runtime_image = payload.get("runtime_image")
106
+ expected_profile = (
107
+ "cost-governance"
108
+ if cost_governance or request_id_value.startswith("plan-provider-cost-")
109
+ else "core-control-plane"
110
+ )
111
+ if (cost_governance or provider_schema) and (
112
+ not isinstance(runtime_image, dict)
113
+ or set(runtime_image) != {"source_revision", "digest", "profile"}
114
+ or not isinstance(runtime_image.get("source_revision"), str)
115
+ or _COMMIT.fullmatch(runtime_image["source_revision"]) is None
116
+ or not isinstance(runtime_image.get("digest"), str)
117
+ or _OCI_DIGEST.fullmatch(runtime_image["digest"]) is None
118
+ or runtime_image.get("profile") != expected_profile
119
+ ):
120
+ raise ValueError("github_plan_metadata_runtime_image_invalid")
121
+ projected: dict[str, object] = {
122
+ "plan_id": plan_id,
123
+ "plan_digest": plan_digest,
124
+ "context_digest": context_digest,
125
+ "expires_at": expires_at,
126
+ "status": "ready",
127
+ "plan_summary": summary,
128
+ "post_apply_observations": observations,
129
+ }
130
+ if provider_schema and isinstance(runtime_image, dict):
131
+ projected["runtime_image"] = dict(runtime_image)
132
+ return projected
133
+
134
+
135
+ def _plan_summary(value: object) -> dict[str, object]:
136
+ """Validate one address-free protected-plan action summary."""
137
+
138
+ if not isinstance(value, dict) or set(value) != {
139
+ "schema_version",
140
+ "action_counts",
141
+ "resource_type_counts",
142
+ "managed_resources",
143
+ "destructive",
144
+ "summary_digest",
145
+ }:
146
+ raise ValueError("github_plan_summary_invalid")
147
+ digest = value.get("summary_digest")
148
+ body = {key: item for key, item in value.items() if key != "summary_digest"}
149
+ if (
150
+ value.get("schema_version") != "fdai.deployment-plan-summary.v1"
151
+ or not isinstance(digest, str)
152
+ or _DIGEST.fullmatch(digest) is None
153
+ or canonical_digest(body) != digest
154
+ or type(value.get("managed_resources")) is not int
155
+ or int(value["managed_resources"]) < 0
156
+ or type(value.get("destructive")) is not bool
157
+ ):
158
+ raise ValueError("github_plan_summary_invalid")
159
+ actions = value.get("action_counts")
160
+ expected_actions = {"create", "update", "delete", "replace", "no_op", "read"}
161
+ if (
162
+ not isinstance(actions, dict)
163
+ or set(actions) != expected_actions
164
+ or any(type(count) is not int or count < 0 for count in actions.values())
165
+ or sum(actions.values()) != value["managed_resources"]
166
+ ):
167
+ raise ValueError("github_plan_summary_invalid")
168
+ resource_types = value.get("resource_type_counts")
169
+ if not isinstance(resource_types, dict) or len(resource_types) > 256:
170
+ raise ValueError("github_plan_summary_invalid")
171
+ for resource_type, counts in resource_types.items():
172
+ if (
173
+ not isinstance(resource_type, str)
174
+ or not resource_type
175
+ or len(resource_type) > 128
176
+ or not isinstance(counts, dict)
177
+ or not set(counts).issubset(expected_actions)
178
+ or any(type(count) is not int or count <= 0 for count in counts.values())
179
+ ):
180
+ raise ValueError("github_plan_summary_invalid")
181
+ return {str(key): item for key, item in value.items()}
@@ -0,0 +1,170 @@
1
+ """Provider-schema deployment evidence validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ from fdai_deployment_cli.github_workflow_values import (
8
+ _DIGEST,
9
+ _OCI_DIGEST,
10
+ _PROVIDER_SOURCE_REVISION,
11
+ )
12
+
13
+
14
+ def _validate_provider_schema_core_baseline(
15
+ value: Mapping[str, object],
16
+ *,
17
+ expected_source_revision: str,
18
+ expected_image_digest: str,
19
+ ) -> None:
20
+ """Reject a provider baseline that does not prove the exact healthy Core image."""
21
+
22
+ max_inactive_revisions = value.get("max_inactive_revisions")
23
+ if (
24
+ set(value)
25
+ != {
26
+ "schema_version",
27
+ "source_revision",
28
+ "image_digest",
29
+ "core_app_ref_digest",
30
+ "active_revision_ref_digest",
31
+ "max_inactive_revisions",
32
+ "health_state",
33
+ "provisioning_state",
34
+ "observed_at",
35
+ "grants_authority",
36
+ }
37
+ or value.get("schema_version") != "fdai.provider-schema-core-baseline.v1"
38
+ or value.get("source_revision") != expected_source_revision
39
+ or value.get("image_digest") != expected_image_digest
40
+ or value.get("health_state") != "Healthy"
41
+ or value.get("provisioning_state") != "Provisioned"
42
+ or value.get("grants_authority") is not False
43
+ or not isinstance(max_inactive_revisions, int)
44
+ or isinstance(max_inactive_revisions, bool)
45
+ or max_inactive_revisions < 1
46
+ or not isinstance(value.get("observed_at"), str)
47
+ ):
48
+ raise ValueError("github_provider_schema_core_baseline_invalid")
49
+ for field in ("core_app_ref_digest", "active_revision_ref_digest"):
50
+ digest = value.get(field)
51
+ if not isinstance(digest, str) or _DIGEST.fullmatch(digest) is None:
52
+ raise ValueError("github_provider_schema_core_baseline_invalid")
53
+
54
+
55
+ def _validate_provider_schema_evidence(
56
+ value: Mapping[str, object],
57
+ *,
58
+ expected_application_source: str,
59
+ expected_runtime_revision: str,
60
+ expected_plan_id: str,
61
+ ) -> dict[str, object]:
62
+ """Validate durable provider generation and conditional agent-review evidence."""
63
+
64
+ expected_fields = {
65
+ "schema_version",
66
+ "application_source_commit",
67
+ "runtime_image_revision",
68
+ "plan_id",
69
+ "job_execution_ref_digest",
70
+ "job_execution_status",
71
+ "checked_at",
72
+ "provider_source_revision",
73
+ "baseline_digest",
74
+ "observed_digest",
75
+ "drift_digest",
76
+ "disposition",
77
+ "durable_generation_digest",
78
+ "durable_generation_revision",
79
+ "run_receipt_digest",
80
+ "review_package_digest",
81
+ "heimdall_review_dispatched",
82
+ "review_evidence_status",
83
+ "correlation_id",
84
+ "forseti_risk_verdict",
85
+ "forseti_reason",
86
+ "saga_audit_entry_hash",
87
+ "grants_authority",
88
+ }
89
+ generation_revision = value.get("durable_generation_revision")
90
+ if (
91
+ set(value) != expected_fields
92
+ or value.get("schema_version") != "fdai.provider-schema-deployment-evidence.v1"
93
+ or value.get("application_source_commit") != expected_application_source
94
+ or value.get("runtime_image_revision") != expected_runtime_revision
95
+ or value.get("plan_id") != expected_plan_id
96
+ or value.get("job_execution_status") != "Succeeded"
97
+ or value.get("grants_authority") is not False
98
+ or not isinstance(value.get("checked_at"), str)
99
+ or value.get("disposition") not in {"unchanged", "compatible", "breaking"}
100
+ or not isinstance(generation_revision, int)
101
+ or isinstance(generation_revision, bool)
102
+ or generation_revision < 1
103
+ ):
104
+ raise ValueError("github_provider_schema_evidence_invalid")
105
+ job_execution_digest = value.get("job_execution_ref_digest")
106
+ if not isinstance(job_execution_digest, str) or _DIGEST.fullmatch(job_execution_digest) is None:
107
+ raise ValueError("github_provider_schema_evidence_invalid")
108
+ drift_digest = value.get("drift_digest")
109
+ if drift_digest is not None and (
110
+ not isinstance(drift_digest, str) or _DIGEST.fullmatch(drift_digest) is None
111
+ ):
112
+ raise ValueError("github_provider_schema_evidence_invalid")
113
+ provider_revision = value.get("provider_source_revision")
114
+ if (
115
+ not isinstance(provider_revision, str)
116
+ or _PROVIDER_SOURCE_REVISION.fullmatch(provider_revision) is None
117
+ ):
118
+ raise ValueError("github_provider_schema_evidence_invalid")
119
+ for field in (
120
+ "baseline_digest",
121
+ "observed_digest",
122
+ "durable_generation_digest",
123
+ "run_receipt_digest",
124
+ ):
125
+ digest = value.get(field)
126
+ if not isinstance(digest, str) or _OCI_DIGEST.fullmatch(digest) is None:
127
+ raise ValueError("github_provider_schema_evidence_invalid")
128
+ review_status = value.get("review_evidence_status")
129
+ if review_status == "verified":
130
+ if (
131
+ value.get("disposition") != "breaking"
132
+ or value.get("heimdall_review_dispatched") is not True
133
+ or not isinstance(drift_digest, str)
134
+ or _DIGEST.fullmatch(drift_digest) is None
135
+ or value.get("correlation_id") != f"provider-schema:azure:{drift_digest}"
136
+ or value.get("forseti_risk_verdict") != "hil"
137
+ or value.get("forseti_reason") != "no_rule_match"
138
+ or not isinstance(value.get("review_package_digest"), str)
139
+ or _OCI_DIGEST.fullmatch(str(value["review_package_digest"])) is None
140
+ or not isinstance(value.get("saga_audit_entry_hash"), str)
141
+ ):
142
+ raise ValueError("github_provider_schema_evidence_invalid")
143
+ elif review_status == "not_applicable":
144
+ if (
145
+ value.get("disposition") == "breaking"
146
+ or value.get("heimdall_review_dispatched") is not False
147
+ or any(
148
+ value.get(field) is not None
149
+ for field in (
150
+ "review_package_digest",
151
+ "correlation_id",
152
+ "forseti_risk_verdict",
153
+ "forseti_reason",
154
+ "saga_audit_entry_hash",
155
+ )
156
+ )
157
+ ):
158
+ raise ValueError("github_provider_schema_evidence_invalid")
159
+ else:
160
+ raise ValueError("github_provider_schema_evidence_invalid")
161
+ return {
162
+ "provider_source_revision": provider_revision,
163
+ "durable_generation_digest": value["durable_generation_digest"],
164
+ "durable_generation_revision": value["durable_generation_revision"],
165
+ "disposition": value["disposition"],
166
+ "review_evidence_status": review_status,
167
+ "heimdall_review_dispatched": value["heimdall_review_dispatched"],
168
+ "forseti_risk_verdict": value["forseti_risk_verdict"],
169
+ "saga_audit_entry_hash": value["saga_audit_entry_hash"],
170
+ }
@@ -0,0 +1,46 @@
1
+ """Bounded GitHub CLI process transport."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+
8
+ from fdai_deployment_cli.github_workflow_values import CommandResult, CommandRunner
9
+
10
+ _ARTIFACT_DOWNLOAD_TIMEOUT = 90
11
+
12
+ _DEFAULT_GH_TIMEOUT = 30
13
+
14
+
15
+ def run_github_cli(
16
+ arguments: tuple[str, ...],
17
+ timeout: int = _DEFAULT_GH_TIMEOUT,
18
+ ) -> CommandResult:
19
+ """Execute one fixed GitHub CLI command with bounded output and duration."""
20
+
21
+ executable = shutil.which("gh")
22
+ if executable is None:
23
+ raise OSError("GitHub CLI is unavailable")
24
+ try:
25
+ completed = subprocess.run(
26
+ [executable, *arguments],
27
+ check=False,
28
+ capture_output=True,
29
+ text=True,
30
+ timeout=timeout,
31
+ )
32
+ except subprocess.TimeoutExpired:
33
+ return CommandResult(returncode=124, stdout="", stderr="")
34
+ return CommandResult(
35
+ returncode=completed.returncode,
36
+ stdout=completed.stdout[:65_536],
37
+ stderr=completed.stderr[:65_536],
38
+ )
39
+
40
+
41
+ def _artifact_runner(base: CommandRunner) -> CommandRunner:
42
+ """Wrap the real runner with a longer timeout for artifact downloads."""
43
+
44
+ if base is not run_github_cli:
45
+ return base # Test runners handle their own timing.
46
+ return lambda args: run_github_cli(args, timeout=_ARTIFACT_DOWNLOAD_TIMEOUT)