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,227 @@
1
+ """Sanitized readiness contract for the standalone Entra identity operation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from fdai_deployment_cli.contracts import canonical_digest
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class IdentityProfileEvidence:
12
+ """Value-free readback facts; ``None`` means the provider read was unavailable."""
13
+
14
+ premium_license_eligible: bool | None
15
+ role_groups_present: bool | None
16
+ role_group_profile_match: bool | None
17
+ conditional_access_policy_present: bool | None
18
+ access_review_present: bool | None
19
+ authentication_method_policy_present: bool | None
20
+ phishing_resistant_method_enabled: bool | None
21
+ azure_policy_assignment_present: bool | None
22
+ human_identity_present: bool | None
23
+ human_approver_authorized: bool | None
24
+ executor_identity_present: bool | None
25
+ executor_is_managed_identity: bool | None
26
+ executor_profile_match: bool | None
27
+ human_executor_separated: bool | None
28
+
29
+ def __post_init__(self) -> None:
30
+ if any(
31
+ value is not None and type(value) is not bool
32
+ for value in (
33
+ self.premium_license_eligible,
34
+ self.role_groups_present,
35
+ self.role_group_profile_match,
36
+ self.conditional_access_policy_present,
37
+ self.access_review_present,
38
+ self.authentication_method_policy_present,
39
+ self.phishing_resistant_method_enabled,
40
+ self.azure_policy_assignment_present,
41
+ self.human_identity_present,
42
+ self.human_approver_authorized,
43
+ self.executor_identity_present,
44
+ self.executor_is_managed_identity,
45
+ self.executor_profile_match,
46
+ self.human_executor_separated,
47
+ )
48
+ ):
49
+ raise ValueError("identity profile evidence values MUST be booleans or unknown")
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class IdentityProfileObservation:
54
+ """Sanitized read-only identity posture with stable fail-closed blockers."""
55
+
56
+ evidence: IdentityProfileEvidence
57
+ blockers: tuple[str, ...]
58
+
59
+ @classmethod
60
+ def from_evidence(cls, evidence: IdentityProfileEvidence) -> IdentityProfileObservation:
61
+ """Classify every required readback without exposing provider values."""
62
+
63
+ blockers: list[str] = []
64
+ _classify(
65
+ blockers,
66
+ evidence.premium_license_eligible,
67
+ unavailable="premium_license_readback_unavailable",
68
+ absent="premium_license_ineligible",
69
+ )
70
+ _classify(
71
+ blockers,
72
+ evidence.role_groups_present,
73
+ unavailable="role_group_readback_unavailable",
74
+ absent="role_group_missing",
75
+ )
76
+ if evidence.role_groups_present is True:
77
+ _classify(
78
+ blockers,
79
+ evidence.role_group_profile_match,
80
+ unavailable="role_group_readback_unavailable",
81
+ absent="role_group_profile_mismatch",
82
+ )
83
+ _classify(
84
+ blockers,
85
+ evidence.conditional_access_policy_present,
86
+ unavailable="conditional_access_readback_unavailable",
87
+ absent="conditional_access_policy_missing",
88
+ )
89
+ _classify(
90
+ blockers,
91
+ evidence.access_review_present,
92
+ unavailable="access_review_readback_unavailable",
93
+ absent="access_review_missing",
94
+ )
95
+ _classify(
96
+ blockers,
97
+ evidence.authentication_method_policy_present,
98
+ unavailable="authentication_method_policy_readback_unavailable",
99
+ absent="authentication_method_policy_missing",
100
+ )
101
+ if evidence.authentication_method_policy_present is True:
102
+ _classify(
103
+ blockers,
104
+ evidence.phishing_resistant_method_enabled,
105
+ unavailable="authentication_method_policy_readback_unavailable",
106
+ absent="phishing_resistant_method_missing",
107
+ )
108
+ _classify(
109
+ blockers,
110
+ evidence.azure_policy_assignment_present,
111
+ unavailable="azure_policy_assignment_readback_unavailable",
112
+ absent="azure_policy_assignment_missing",
113
+ )
114
+ _classify(
115
+ blockers,
116
+ evidence.human_identity_present,
117
+ unavailable="human_identity_readback_unavailable",
118
+ absent="human_identity_missing",
119
+ )
120
+ if evidence.human_identity_present is True:
121
+ _classify(
122
+ blockers,
123
+ evidence.human_approver_authorized,
124
+ unavailable="human_approver_membership_readback_unavailable",
125
+ absent="human_approver_membership_missing",
126
+ )
127
+ _classify(
128
+ blockers,
129
+ evidence.executor_identity_present,
130
+ unavailable="executor_identity_readback_unavailable",
131
+ absent="executor_identity_missing",
132
+ )
133
+ if evidence.executor_identity_present is True:
134
+ _classify(
135
+ blockers,
136
+ evidence.executor_is_managed_identity,
137
+ unavailable="executor_identity_readback_unavailable",
138
+ absent="executor_is_not_managed_identity",
139
+ )
140
+ if evidence.executor_is_managed_identity is True:
141
+ _classify(
142
+ blockers,
143
+ evidence.executor_profile_match,
144
+ unavailable="executor_identity_readback_unavailable",
145
+ absent="executor_profile_mismatch",
146
+ )
147
+ if evidence.human_identity_present is True and evidence.executor_identity_present is True:
148
+ _classify(
149
+ blockers,
150
+ evidence.human_executor_separated,
151
+ unavailable="human_executor_separation_unavailable",
152
+ absent="human_executor_not_separated",
153
+ )
154
+ return cls(evidence=evidence, blockers=tuple(blockers))
155
+
156
+ @property
157
+ def ready(self) -> bool:
158
+ """Return whether every required profile and identity readback passed."""
159
+
160
+ return not self.blockers
161
+
162
+ @property
163
+ def digest(self) -> str:
164
+ """Bind the exact sanitized observation used by the mutation preflight."""
165
+
166
+ return canonical_digest(self._projection())
167
+
168
+ def to_mapping(self) -> dict[str, object]:
169
+ """Return only generic booleans, stable blockers, and an opaque digest."""
170
+
171
+ projection = self._projection()
172
+ projection["profile_digest"] = canonical_digest(projection)
173
+ return projection
174
+
175
+ def _projection(self) -> dict[str, object]:
176
+ evidence = self.evidence
177
+ return {
178
+ "schema_version": "fdai.identity-profile-observation.v1",
179
+ "premium_license": _fact(evidence.premium_license_eligible, "eligible"),
180
+ "role_groups": {
181
+ "readback_available": evidence.role_groups_present is not None,
182
+ "all_present": evidence.role_groups_present is True,
183
+ "profile_match": evidence.role_group_profile_match is True,
184
+ },
185
+ "conditional_access": _fact(
186
+ evidence.conditional_access_policy_present, "policy_present"
187
+ ),
188
+ "access_review": _fact(evidence.access_review_present, "definition_present"),
189
+ "authentication_method_policy": {
190
+ **_fact(evidence.authentication_method_policy_present, "policy_present"),
191
+ "phishing_resistant_method_enabled": bool(
192
+ evidence.phishing_resistant_method_enabled
193
+ ),
194
+ },
195
+ "azure_policy": _fact(evidence.azure_policy_assignment_present, "assignment_present"),
196
+ "identity_separation": {
197
+ "human_readback_available": evidence.human_identity_present is not None,
198
+ "human_present": evidence.human_identity_present is True,
199
+ "human_approver_authorized": evidence.human_approver_authorized is True,
200
+ "executor_readback_available": evidence.executor_identity_present is not None,
201
+ "executor_present": evidence.executor_identity_present is True,
202
+ "executor_is_managed_identity": evidence.executor_is_managed_identity is True,
203
+ "executor_profile_match": evidence.executor_profile_match is True,
204
+ "human_executor_separated": evidence.human_executor_separated is True,
205
+ },
206
+ "blockers": list(self.blockers),
207
+ "ready": self.ready,
208
+ "mutation_performed": False,
209
+ }
210
+
211
+
212
+ def _classify(
213
+ blockers: list[str],
214
+ value: bool | None,
215
+ *,
216
+ unavailable: str,
217
+ absent: str,
218
+ ) -> None:
219
+ if value is None:
220
+ if unavailable not in blockers:
221
+ blockers.append(unavailable)
222
+ elif not value:
223
+ blockers.append(absent)
224
+
225
+
226
+ def _fact(value: bool | None, field: str) -> dict[str, bool]:
227
+ return {"readback_available": value is not None, field: value is True}
@@ -0,0 +1,197 @@
1
+ """Resolve changed repository paths to independently deployable service images."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Iterable
7
+ from dataclasses import asdict, dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ImageTarget:
12
+ target: str
13
+ service: str
14
+ dockerfile: str
15
+ image: str
16
+
17
+
18
+ IMAGE_TARGETS = (
19
+ ImageTarget(
20
+ target="core-control-plane",
21
+ service="core-control-plane",
22
+ dockerfile="services/core-control-plane/docker/Dockerfile",
23
+ image="fdai-core-control-plane",
24
+ ),
25
+ ImageTarget(
26
+ target="cost-governance",
27
+ service="core-control-plane",
28
+ dockerfile="extensions/cost-governance/docker/Dockerfile",
29
+ image="fdai-cost-governance",
30
+ ),
31
+ ImageTarget(
32
+ target="operator-service",
33
+ service="operator-service",
34
+ dockerfile="services/operator-service/docker/Dockerfile",
35
+ image="fdai-operator-service",
36
+ ),
37
+ ImageTarget(
38
+ target="document-ingestion-api",
39
+ service="document-ingestion-api",
40
+ dockerfile="services/document-ingestion-api/docker/Dockerfile",
41
+ image="fdai-document-ingestion-api",
42
+ ),
43
+ ImageTarget(
44
+ target="document-processing-worker",
45
+ service="document-processing-worker",
46
+ dockerfile="services/document-processing-worker/docker/Dockerfile",
47
+ image="fdai-document-processing-worker",
48
+ ),
49
+ ImageTarget(
50
+ target="isolated-executor",
51
+ service="isolated-executor",
52
+ dockerfile="services/isolated-executor/docker/Dockerfile",
53
+ image="fdai-isolated-executor",
54
+ ),
55
+ ImageTarget(
56
+ target="system-knowledge-service",
57
+ service="system-knowledge-service",
58
+ dockerfile="services/system-knowledge-service/docker/Dockerfile",
59
+ image="fdai-system-knowledge-service",
60
+ ),
61
+ )
62
+
63
+ _ALL_TARGET_EXACT_PATHS = {
64
+ ".dockerignore",
65
+ ".github/workflows/container-supply-chain.yml",
66
+ ".trivyignore.yaml",
67
+ "config/service-image-build-override.json",
68
+ "LICENSE",
69
+ "README.md",
70
+ "pyproject.toml",
71
+ "scripts/deployment/service/apply_image_build_override.py",
72
+ "scripts/deployment/service/select_changed_images.py",
73
+ "uv.lock",
74
+ }
75
+ _ALL_TARGET_PREFIXES = (
76
+ "evaluation-sdk/",
77
+ "packages/service-contracts/",
78
+ "service-contracts/",
79
+ "src/",
80
+ )
81
+ _ALL_TARGET_METADATA_PATHS = {
82
+ "benchmarks/cybergym/pyproject.toml",
83
+ "benchmarks/sregym/pyproject.toml",
84
+ "extensions/code-assurance/pyproject.toml",
85
+ "extensions/cost-governance/pyproject.toml",
86
+ "packages/github-app-auth/pyproject.toml",
87
+ *(f"services/{target.service}/pyproject.toml" for target in IMAGE_TARGETS),
88
+ }
89
+ _CORE_EXACT_PATHS = {
90
+ "alembic.ini",
91
+ "docs/internals/sregym-absorption-ledger.json",
92
+ "scripts/deployment/local/materialize-authoritative-catalogs.py",
93
+ }
94
+ _CORE_PREFIXES = (
95
+ "alembic/",
96
+ "config/",
97
+ "policies/",
98
+ "provider-schema-catalog/",
99
+ "rule-catalog/",
100
+ "services/assets/",
101
+ "tests/scenarios/",
102
+ )
103
+ _COST_GOVERNANCE_PREFIX = "extensions/cost-governance/"
104
+ _GITHUB_APP_AUTH_PREFIX = "packages/github-app-auth/"
105
+ _RUNTIME_DIAGNOSTICS_PREFIX = "packages/runtime-diagnostics/"
106
+ _SOURCE_PACKAGE_PREFIXES = (
107
+ *(f"services/{target.service}/" for target in IMAGE_TARGETS),
108
+ _COST_GOVERNANCE_PREFIX,
109
+ _GITHUB_APP_AUTH_PREFIX,
110
+ _RUNTIME_DIAGNOSTICS_PREFIX,
111
+ "packages/service-contracts/",
112
+ "evaluation-sdk/",
113
+ )
114
+
115
+
116
+ def select_requested_images(selection: str) -> tuple[ImageTarget, ...]:
117
+ """Resolve explicit target names without expanding optional image profiles."""
118
+ names = [name.strip() for name in selection.split(",")]
119
+ if names == ["all"]:
120
+ return IMAGE_TARGETS
121
+ by_name = {name: target for target in IMAGE_TARGETS for name in (target.target, target.image)}
122
+ selected: set[str] = set()
123
+ for name in names:
124
+ if name not in by_name:
125
+ raise ValueError("images must name known targets or image names, or only 'all'")
126
+ target = by_name[name]
127
+ if target.target in selected:
128
+ raise ValueError("images must not repeat a target or its image alias")
129
+ selected.add(target.target)
130
+ return tuple(target for target in IMAGE_TARGETS if target.target in selected)
131
+
132
+
133
+ def _is_packaging_input(path: str) -> bool:
134
+ if path == "README.md":
135
+ return False
136
+ for prefix in _SOURCE_PACKAGE_PREFIXES:
137
+ if not path.startswith(prefix):
138
+ continue
139
+ relative = path.removeprefix(prefix)
140
+ if relative.startswith("tests/") and not (
141
+ prefix == "services/core-control-plane/" and relative.startswith("tests/scenarios/")
142
+ ):
143
+ return False
144
+ if relative.startswith("docs/"):
145
+ return False
146
+ return not (relative.startswith("src/") and relative.endswith((".py", ".pyi")))
147
+ return not (path.startswith("src/") and path.endswith((".py", ".pyi")))
148
+
149
+
150
+ def select_image_targets(
151
+ changed_paths: Iterable[str], *, packaging_only: bool = False
152
+ ) -> tuple[ImageTarget, ...]:
153
+ """Select every image whose declared source or packaged asset changed."""
154
+ paths = {path.removeprefix("./") for path in changed_paths if path}
155
+ if packaging_only:
156
+ paths = {path for path in paths if _is_packaging_input(path)}
157
+ if any(
158
+ path in _ALL_TARGET_EXACT_PATHS
159
+ or path in _ALL_TARGET_METADATA_PATHS
160
+ or path.startswith(_ALL_TARGET_PREFIXES)
161
+ for path in paths
162
+ ):
163
+ return IMAGE_TARGETS
164
+
165
+ selected: set[str] = set()
166
+ for path in paths:
167
+ if path.startswith(_RUNTIME_DIAGNOSTICS_PREFIX):
168
+ selected.update({"core-control-plane", "cost-governance", "operator-service"})
169
+ continue
170
+ if path.startswith(_GITHUB_APP_AUTH_PREFIX):
171
+ selected.update({"core-control-plane", "cost-governance", "document-ingestion-api"})
172
+ continue
173
+ if path.startswith(_COST_GOVERNANCE_PREFIX):
174
+ selected.add("cost-governance")
175
+ continue
176
+ if path in _CORE_EXACT_PATHS or path.startswith(_CORE_PREFIXES):
177
+ selected.update({"core-control-plane", "cost-governance"})
178
+ if path == "config/agent-stewardship.yaml":
179
+ selected.add("document-ingestion-api")
180
+ if not path.startswith("services/") or path.startswith("services/assets/"):
181
+ continue
182
+ matching = {
183
+ target.target
184
+ for target in IMAGE_TARGETS
185
+ if path.startswith(f"services/{target.service}/")
186
+ }
187
+ if not matching:
188
+ return IMAGE_TARGETS
189
+ selected.update(matching)
190
+ return tuple(target for target in IMAGE_TARGETS if target.target in selected)
191
+
192
+
193
+ def matrix_json(targets: Iterable[ImageTarget]) -> str:
194
+ return json.dumps(
195
+ {"include": [asdict(target) for target in targets]},
196
+ separators=(",", ":"),
197
+ )