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,97 @@
1
+ """Sanitized provisioning status shared by CLI and operator projections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from fdai_deployment_cli.compiler import GENESIS_ENTRY_IDS_BY_VERSION
8
+ from fdai_deployment_cli.progress import ProgressSnapshot
9
+ from fdai_deployment_cli.state import ProvisionEvent, RunState
10
+
11
+ _TERMINAL_STAGE_STATES = frozenset({RunState.COMPLETED, RunState.READY})
12
+
13
+
14
+ def project_status(
15
+ events: Sequence[ProvisionEvent],
16
+ *,
17
+ progress: ProgressSnapshot | None = None,
18
+ ) -> dict[str, object]:
19
+ """Project one bounded run without exposing target or evidence identifiers."""
20
+
21
+ if not events:
22
+ raise ValueError("provision journal is empty")
23
+ latest = events[-1]
24
+ if any(event.run_id != latest.run_id for event in events):
25
+ raise ValueError("provision journal contains multiple run ids")
26
+ stage_ids = GENESIS_ENTRY_IDS_BY_VERSION[latest.manifest_version]
27
+ completed = {
28
+ event.stage
29
+ for event in events
30
+ if event.state in _TERMINAL_STAGE_STATES and event.stage in stage_ids
31
+ }
32
+ stages = [
33
+ {
34
+ "id": stage,
35
+ "status": _stage_status(stage, latest=latest, completed=completed),
36
+ }
37
+ for stage in stage_ids
38
+ ]
39
+ currently_completed = {str(stage["id"]) for stage in stages if stage["status"] == "completed"}
40
+ ready = latest.state is RunState.READY
41
+ result: dict[str, object] = {
42
+ "schema_version": "fdai.provision-status.v1",
43
+ "type": "provision.snapshot",
44
+ "run_id": latest.run_id,
45
+ "sequence": latest.sequence,
46
+ "attempt": latest.attempt,
47
+ "state": latest.state.value,
48
+ "current_stage": latest.stage,
49
+ "stages_completed": len(currently_completed),
50
+ "stages_total": len(stage_ids),
51
+ "last_progress_at": latest.occurred_at,
52
+ "reason_code": latest.reason_code,
53
+ "ready": ready,
54
+ "readiness": {
55
+ "database": "database" in currently_completed,
56
+ "semantic": "semantic-defaults" in currently_completed,
57
+ "models": "model-deployments" in currently_completed,
58
+ "runtime": "console" in currently_completed,
59
+ "inventory": "initial-inventory" in currently_completed,
60
+ "system": ready,
61
+ },
62
+ "stages": stages,
63
+ }
64
+ if progress is not None:
65
+ if progress.sequence > latest.sequence:
66
+ raise ValueError("progress sequence cannot exceed the journal sequence")
67
+ result["checkpoints_completed"] = progress.checkpoints_completed
68
+ result["checkpoints_total"] = progress.checkpoints_total
69
+ result["inventory"] = {
70
+ "resources_observed": progress.resources_observed,
71
+ "resources_expected": progress.resources_expected,
72
+ "pages_completed": progress.pages_completed,
73
+ "pages_expected": progress.pages_expected,
74
+ }
75
+ return result
76
+
77
+
78
+ def _stage_status(
79
+ stage: str,
80
+ *,
81
+ latest: ProvisionEvent,
82
+ completed: set[str],
83
+ ) -> str:
84
+ if stage == latest.stage:
85
+ if latest.state in _TERMINAL_STAGE_STATES:
86
+ return "completed"
87
+ if latest.state is RunState.WAITING:
88
+ return "waiting"
89
+ if latest.state in {RunState.BLOCKED, RunState.FAILED, RunState.INCOMPLETE}:
90
+ return latest.state.value
91
+ if latest.state is RunState.CANCELLED:
92
+ return "cancelled"
93
+ return "active"
94
+ return "completed" if stage in completed else "pending"
95
+
96
+
97
+ __all__ = ["project_status"]
@@ -0,0 +1,257 @@
1
+ """Install an isolated deployment-support interpreter from authenticated local wheels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from fdai_deployment_cli.contracts import load_json_object
16
+ from fdai_deployment_cli.offline_kit import (
17
+ _copy_verified_file,
18
+ _read_regular,
19
+ verify_offline_kit,
20
+ )
21
+ from fdai_deployment_cli.private_output import _open_private_parent, write_private_output
22
+
23
+ _PREFIX = "support/python/"
24
+ _REQUIREMENT = re.compile(r"[A-Za-z0-9_.-]+(?:\[[A-Za-z0-9_,.-]+\])?==[A-Za-z0-9_.!+-]+")
25
+ _HASH = re.compile(r"--hash=sha256:[0-9a-f]{64}")
26
+ _RUNTIME_PACKAGES = {
27
+ "fdai-service-contracts",
28
+ "fdai-core-control-plane",
29
+ "fdai-operator-service",
30
+ "fdai-document-ingestion-api",
31
+ "fdai-document-processing-worker",
32
+ "fdai-isolated-executor-service",
33
+ }
34
+ _SUPPORT_PACKAGES = {"fdai-github-app-auth", "fdai-runtime-diagnostics"}
35
+
36
+
37
+ class SupportInstallationError(ValueError):
38
+ """An authenticated support payload cannot be installed or verified."""
39
+
40
+
41
+ def _package_versions(
42
+ inventory: dict[str, object], requirements: bytes, digests: dict[str, str]
43
+ ) -> dict[str, str]:
44
+ runtime = inventory.get("packages")
45
+ support = inventory.get("support_packages")
46
+ if not isinstance(runtime, dict) or set(runtime) != _RUNTIME_PACKAGES:
47
+ raise ValueError("support inventory MUST include every runtime distribution")
48
+ if not isinstance(support, dict) or not set(support) <= _SUPPORT_PACKAGES:
49
+ raise ValueError("support inventory contains unsupported workspace dependencies")
50
+ lines = set(requirements.decode("utf-8").splitlines())
51
+ versions: dict[str, str] = {}
52
+ for name, record in {**runtime, **support}.items():
53
+ if not isinstance(record, dict):
54
+ raise SupportInstallationError("support package record is invalid")
55
+ version, wheel = record.get("version"), record.get("wheel")
56
+ if not isinstance(version, str) or not isinstance(wheel, str):
57
+ raise SupportInstallationError("support package version and wheel MUST be declared")
58
+ digest = digests.get(_PREFIX + wheel)
59
+ if digest is None or f"{name}=={version} --hash=sha256:{digest}" not in lines:
60
+ raise ValueError(
61
+ "support requirements do not pin every owned wheel to its signed digest"
62
+ )
63
+ versions[name] = version
64
+ return versions
65
+
66
+
67
+ def _verify_installed_packages(content: bytes, expected: dict[str, str]) -> None:
68
+ if len(content) > 1024 * 1024:
69
+ raise ValueError("installed package readback exceeds its size limit")
70
+ entries = json.loads(content)
71
+ if not isinstance(entries, list):
72
+ raise SupportInstallationError("installed package readback is invalid")
73
+ observed: dict[str, str] = {}
74
+ for entry in entries:
75
+ if not isinstance(entry, dict):
76
+ raise SupportInstallationError("installed package entry is invalid")
77
+ name, version = entry.get("name"), entry.get("version")
78
+ if not isinstance(name, str) or not isinstance(version, str):
79
+ raise SupportInstallationError("installed package identity is invalid")
80
+ normalized = re.sub(r"[-_.]+", "-", name.lower())
81
+ if normalized in observed:
82
+ raise ValueError("installed package readback contains duplicate distributions")
83
+ observed[normalized] = version
84
+ if any(observed.get(name) != version for name, version in expected.items()):
85
+ raise ValueError("installed runtime packages do not match the signed inventory")
86
+
87
+
88
+ def _validate_requirements(content: bytes) -> None:
89
+ """Refuse editable, source, index, local-path, and direct-URL requirements."""
90
+ for raw in content.decode("utf-8").splitlines():
91
+ line = raw.strip().rstrip("\\").strip()
92
+ if not line or line.startswith("#"):
93
+ continue
94
+ if _HASH.fullmatch(line):
95
+ continue
96
+ if (
97
+ _REQUIREMENT.match(line) is None
98
+ or "@" in line
99
+ or "://" in line
100
+ or re.search(r"--(?!hash=sha256:)", line)
101
+ ):
102
+ raise ValueError(
103
+ "support requirements MUST contain only pinned hashed wheel requirements"
104
+ )
105
+
106
+
107
+ def install_support(
108
+ kit: Path,
109
+ *,
110
+ work_dir: Path,
111
+ release_root_pem: bytes,
112
+ cli_version: str,
113
+ platform_tag: str,
114
+ ) -> dict[str, object]:
115
+ """Install support libraries, never runtime services, into a new private workdir.
116
+
117
+ Keys come from the caller's independently trusted verification boundary.
118
+ Installation disables index access, downloads, source builds and caches.
119
+ A failed installation retains its private workspace without a success receipt;
120
+ it is never resumed by reusing an existing environment or running Azure.
121
+ """
122
+ started = time.monotonic()
123
+ parent = _open_private_parent(work_dir / "support-installation.json")
124
+ os.close(parent)
125
+ if any(work_dir.iterdir()):
126
+ raise ValueError("support installation requires an empty private work directory")
127
+ verification = verify_offline_kit(
128
+ kit, release_root_pem=release_root_pem, cli_version=cli_version, platform_tag=platform_tag
129
+ )
130
+ digests = dict(verification.file_digests)
131
+ sizes = dict(verification.file_sizes)
132
+ inventory_path = _PREFIX + "inventory.json"
133
+ requirements_path = _PREFIX + "requirements/support.txt"
134
+ if inventory_path not in digests or requirements_path not in digests:
135
+ raise ValueError("offline kit lacks the complete runtime support wheelhouse")
136
+ wheels = work_dir / "wheelhouse"
137
+ wheels.mkdir(mode=0o700)
138
+ for relative in sorted(name for name in digests if name.startswith(_PREFIX)):
139
+ if time.monotonic() - started > 600:
140
+ raise ValueError("support installation preparation deadline exceeded")
141
+ target = wheels / relative.removeprefix(_PREFIX)
142
+ target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
143
+ _copy_verified_file(
144
+ kit / relative, target, expected_digest=digests[relative], expected_size=sizes[relative]
145
+ )
146
+ inventory_bytes = _read_regular(wheels / "inventory.json", 4 * 1024 * 1024)
147
+ inventory = load_json_object(
148
+ inventory_bytes, label="support inventory", max_bytes=4 * 1024 * 1024
149
+ )
150
+ if (
151
+ inventory.get("schema_version") != "fdai.runtime-wheelhouse.v1"
152
+ or inventory.get("artifact_kind") != "local-runtime-wheelhouse"
153
+ or inventory.get("status") != "complete"
154
+ or inventory.get("support_requirements") != "requirements/support.txt"
155
+ ):
156
+ raise ValueError("unsupported or incomplete runtime support inventory")
157
+ if inventory.get("files") != {
158
+ name.removeprefix(_PREFIX): digest
159
+ for name, digest in digests.items()
160
+ if name.startswith(_PREFIX) and name != inventory_path
161
+ }:
162
+ raise ValueError("support inventory file map does not match the signed kit")
163
+ requirements = wheels / "requirements/support.txt"
164
+ requirement_bytes = _read_regular(requirements, 4 * 1024 * 1024)
165
+ _validate_requirements(requirement_bytes)
166
+ versions = _package_versions(inventory, requirement_bytes, digests)
167
+ locations = sorted(
168
+ {
169
+ str((wheels / name.removeprefix(_PREFIX)).parent)
170
+ for name in digests
171
+ if name.startswith(_PREFIX) and name.endswith(".whl")
172
+ }
173
+ )
174
+ if not 1 <= len(locations) <= 64:
175
+ raise ValueError("runtime support inventory MUST contain 1 through 64 wheel directories")
176
+ executable = shutil.which("uv")
177
+ if executable is None:
178
+ raise ValueError("a trusted preinstalled uv executable is required")
179
+ environment = {
180
+ key: os.environ[key] for key in ("PATH", "HOME", "SYSTEMROOT") if key in os.environ
181
+ }
182
+ environment.update({"UV_OFFLINE": "1", "UV_NO_CACHE": "1", "UV_PYTHON_DOWNLOADS": "never"})
183
+ venv = work_dir / "support-env"
184
+
185
+ def run(arguments: list[str], stage: str, *, capture: bool = False) -> bytes:
186
+ remaining = 900 - (time.monotonic() - started)
187
+ if remaining <= 0:
188
+ raise ValueError("support installation deadline exceeded")
189
+ print(json.dumps({"stage": stage, "status": "running"}), file=sys.stderr, flush=True)
190
+ try:
191
+ completed = subprocess.run(
192
+ [executable, *arguments],
193
+ env=environment,
194
+ cwd=work_dir,
195
+ stdin=subprocess.DEVNULL,
196
+ stdout=subprocess.PIPE if capture else subprocess.DEVNULL,
197
+ stderr=subprocess.DEVNULL,
198
+ check=True,
199
+ timeout=min(300, remaining),
200
+ umask=0o077,
201
+ )
202
+ except subprocess.TimeoutExpired as exc:
203
+ raise ValueError(f"support installation {stage} timed out") from exc
204
+ except subprocess.CalledProcessError as exc:
205
+ raise ValueError(
206
+ f"support installation {stage} failed with exit code {exc.returncode}"
207
+ ) from exc
208
+ if capture and not isinstance(completed.stdout, bytes):
209
+ raise SupportInstallationError("support installation returned no package readback")
210
+ print(json.dumps({"stage": stage, "status": "completed"}), file=sys.stderr, flush=True)
211
+ return completed.stdout if capture else b""
212
+
213
+ run(["venv", "--no-config", "--offline", "--python", sys.executable, str(venv)], "venv")
214
+ python = str(venv / "bin/python")
215
+ links = [argument for location in locations for argument in ("--find-links", location)]
216
+ run(
217
+ [
218
+ "pip",
219
+ "install",
220
+ "--no-config",
221
+ "--offline",
222
+ "--no-cache",
223
+ "--no-index",
224
+ "--only-binary",
225
+ ":all:",
226
+ "--require-hashes",
227
+ "--python",
228
+ python,
229
+ *links,
230
+ "--requirements",
231
+ str(requirements),
232
+ ],
233
+ "install",
234
+ )
235
+ run(["pip", "check", "--no-config", "--offline", "--python", python], "dependency-check")
236
+ observed = run(
237
+ ["pip", "list", "--no-config", "--offline", "--python", python, "--format", "json"],
238
+ "package-readback",
239
+ capture=True,
240
+ )
241
+ _verify_installed_packages(observed, versions)
242
+ result = {
243
+ "schema_version": "fdai.support-installation.v1",
244
+ "offline_manifest_digest": verification.manifest_digest,
245
+ "inventory_digest": hashlib.sha256(inventory_bytes).hexdigest(),
246
+ "environment": "support-env",
247
+ "dependencies_verified": True,
248
+ "packages": versions,
249
+ "services_started": False,
250
+ "cloud_mutation_performed": False,
251
+ "subscription_ready": False,
252
+ }
253
+ write_private_output(
254
+ work_dir / "support-installation.json",
255
+ json.dumps(result, sort_keys=True, separators=(",", ":")) + "\n",
256
+ )
257
+ return result
@@ -0,0 +1,46 @@
1
+ """Canonical secret-free Azure target binding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+
9
+ _GUID = re.compile(
10
+ r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
11
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
12
+ )
13
+
14
+
15
+ def compute_target_binding(*, tenant_id: str, subscription_id: str) -> str:
16
+ """Bind one concrete tenant and subscription without retaining either value."""
17
+
18
+ if _GUID.fullmatch(tenant_id) is None or _GUID.fullmatch(subscription_id) is None:
19
+ raise ValueError("Azure tenant and subscription MUST be GUIDs")
20
+ material = f"{tenant_id.lower()}:{subscription_id.lower()}".encode()
21
+ return hashlib.sha256(material).hexdigest()
22
+
23
+
24
+ def azure_target_selection_arguments() -> tuple[str, ...]:
25
+ """Pin Azure CLI reads when the caller selected one explicit subscription."""
26
+
27
+ subscription_id = os.environ.get("AZURE_SUBSCRIPTION_ID", "").strip()
28
+ if not subscription_id:
29
+ return ()
30
+ if _GUID.fullmatch(subscription_id) is None:
31
+ raise ValueError("AZURE_SUBSCRIPTION_ID MUST be a GUID")
32
+ return ("--subscription", subscription_id)
33
+
34
+
35
+ def require_selected_azure_target(*, subscription_id: str, tenant_id: str) -> None:
36
+ """Reject an Azure CLI result that differs from explicit non-secret target inputs."""
37
+
38
+ selected_subscription = os.environ.get("AZURE_SUBSCRIPTION_ID", "").strip()
39
+ selected_tenant = os.environ.get("AZURE_TENANT_ID", "").strip()
40
+ if selected_subscription and selected_subscription.casefold() != subscription_id.casefold():
41
+ raise ValueError("active Azure subscription differs from AZURE_SUBSCRIPTION_ID")
42
+ if selected_tenant:
43
+ if _GUID.fullmatch(selected_tenant) is None:
44
+ raise ValueError("AZURE_TENANT_ID MUST be a GUID")
45
+ if selected_tenant.casefold() != tenant_id.casefold():
46
+ raise ValueError("active Azure tenant differs from AZURE_TENANT_ID")
@@ -0,0 +1,62 @@
1
+ [
2
+ "audit.append-only",
3
+ "automation.specialists",
4
+ "automation.workflow",
5
+ "chaos.run_experiment",
6
+ "communication.email",
7
+ "communication.teams",
8
+ "cost.metering",
9
+ "devops.azure-devops",
10
+ "devops.github",
11
+ "extensibility.http",
12
+ "extensibility.kusto",
13
+ "extensibility.link",
14
+ "extensibility.mcp",
15
+ "extensibility.python",
16
+ "extensibility.shell",
17
+ "incident.create",
18
+ "incident.platform-reception",
19
+ "incident.platform-writeback",
20
+ "incident.response-filter",
21
+ "incident.rich-card",
22
+ "incident.run-mode",
23
+ "incident.specialist-routing",
24
+ "investigation.background",
25
+ "investigation.cross-source",
26
+ "investigation.delegation",
27
+ "investigation.parallel",
28
+ "investigation.report",
29
+ "investigation.run",
30
+ "irp.author",
31
+ "irp.respond",
32
+ "knowledge.auto-learning",
33
+ "knowledge.document-search",
34
+ "knowledge.graph",
35
+ "knowledge.past-incidents",
36
+ "knowledge.register",
37
+ "knowledge.resource-priority",
38
+ "knowledge.tsg",
39
+ "knowledge.user-memory",
40
+ "measurement.dora",
41
+ "measurement.slo",
42
+ "observability.activity-log",
43
+ "observability.deep-diagnostics",
44
+ "observability.log-query",
45
+ "observability.metric-discovery",
46
+ "observability.metric-query",
47
+ "observability.resource-discovery",
48
+ "observability.topology",
49
+ "observability.trace-query",
50
+ "observability.trend-analysis",
51
+ "operations.azure-cli-read",
52
+ "operations.kubectl",
53
+ "operations.typed-mutation",
54
+ "paging.on-call",
55
+ "paging.pagerduty",
56
+ "reporting.pdf",
57
+ "reporting.visualization",
58
+ "safety.human-approval",
59
+ "scheduler.create_task",
60
+ "scheduling.history",
61
+ "scheduling.lifecycle"
62
+ ]
@@ -0,0 +1,3 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MCowBQYDK2VwAyEAgA/j+8XAU5LJHp1ZuPDvl/H0yY4F0z42i4wbvdv59yQ=
3
+ -----END PUBLIC KEY-----
@@ -0,0 +1,3 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MCowBQYDK2VwAyEAgA/j+8XAU5LJHp1ZuPDvl/H0yY4F0z42i4wbvdv59yQ=
3
+ -----END PUBLIC KEY-----
@@ -0,0 +1,3 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MCowBQYDK2VwAyEAE8+WXzm6Kf5L/O9MFat/HMmPLtvESGQAIjhZ4zsPsGE=
3
+ -----END PUBLIC KEY-----
@@ -0,0 +1,50 @@
1
+ """Package-pinned public roots for standalone deployment verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.resources import files
6
+
7
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
8
+ from cryptography.hazmat.primitives.serialization import load_pem_public_key
9
+
10
+ _TRUST_PACKAGE = "fdai_deployment_cli"
11
+ _TRUST_DIRECTORY = "trust"
12
+ _DEPLOYMENT_RELEASE_CHANNEL = "DEV"
13
+
14
+
15
+ def deployment_release_root_pem() -> bytes:
16
+ """Return the public key that verifies complete deployment kits."""
17
+
18
+ return _read("deployment-release-root.pub")
19
+
20
+
21
+ def deployment_bundle_root_pem() -> bytes:
22
+ """Return the public key that verifies the deployment bundle inside a kit."""
23
+
24
+ return _read("deployment-bundle-root.pub")
25
+
26
+
27
+ def deployment_release_channel() -> str:
28
+ """Return the only release channel trusted by this package version."""
29
+
30
+ return _DEPLOYMENT_RELEASE_CHANNEL
31
+
32
+
33
+ def license_public_key_pem() -> bytes:
34
+ """Return the upstream integrity public key, the only license verification key."""
35
+
36
+ return _read("upstream-signing-key.pub")
37
+
38
+
39
+ def _read(name: str) -> bytes:
40
+ resource = files(_TRUST_PACKAGE).joinpath(_TRUST_DIRECTORY, name)
41
+ value = resource.read_bytes()
42
+ if not value.startswith(b"-----BEGIN PUBLIC KEY-----\n") or len(value) > 65_536:
43
+ raise ValueError("packaged deployment trust root is invalid")
44
+ try:
45
+ key = load_pem_public_key(value)
46
+ except (TypeError, ValueError) as exc:
47
+ raise ValueError("packaged deployment trust root is invalid") from exc
48
+ if not isinstance(key, Ed25519PublicKey):
49
+ raise ValueError("packaged deployment trust root must be Ed25519")
50
+ return value
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: fdai-deployment-cli
3
+ Version: 0.1.2
4
+ Summary: Installable FDAI deployment and subscription genesis CLI
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: cryptography<51,>=45
7
+ Requires-Dist: fdai-service-contracts==0.1.0
8
+ Requires-Dist: rich<15,>=14
9
+ Description-Content-Type: text/markdown
10
+
11
+ # FDAI Deployment CLI
12
+
13
+ `fdai-deployment-cli` installs the `fdaictl` command. You can install it from a connected package
14
+ source or from a small signed offline wheelhouse. Package installation does not authenticate to
15
+ Azure or grant deployment authority.
16
+
17
+ ## Install from a signed offline package
18
+
19
+ The offline package contains the CLI wheel, its dependency wheels, one requirements file, one
20
+ checksum list, and one detached Ed25519 signature.
21
+
22
+ Verify and install it with standard tools:
23
+
24
+ ```bash
25
+ tar -xzf fdai-deployment-cli-0.1.1-offline.tar.gz
26
+ cd package
27
+ openssl pkeyutl -verify -pubin \
28
+ -inkey /private/trusted-package-signer.pub \
29
+ -rawin -in SHA256SUMS -sigfile SHA256SUMS.sig
30
+ sha256sum -c SHA256SUMS
31
+ python -m pip install --no-index --find-links wheels -r requirements.txt
32
+ fdaictl --version
33
+ ```
34
+
35
+ The trusted public key should come from the operator, not from the package being verified. Pip
36
+ handles interpreter, ABI, platform, dependency, and installation compatibility.
37
+
38
+ ## Build the offline package
39
+
40
+ Use one operator-held Ed25519 private key:
41
+
42
+ ```bash
43
+ scripts/deployment/release/build-signed-python-package.sh \
44
+ --out /private/fdai-python-package \
45
+ --signing-key /private/deployment-signing-key.pem
46
+ ```
47
+
48
+ The private key must be an owner-only mode-`0600` file. The builder produces:
49
+
50
+ ```text
51
+ fdai-python-package/
52
+ fdai-deployment-cli-0.1.1-offline.tar.gz
53
+ signer.pub
54
+ package/
55
+ INSTALL.txt
56
+ requirements.txt
57
+ SHA256SUMS
58
+ SHA256SUMS.sig
59
+ wheels/
60
+ ```
61
+
62
+ No TUF root, nested bundle signature, artifact profile, SBOM, provenance document, deployment
63
+ appliance, runtime image, or Azure receipt is required to complete the Python package.
64
+
65
+ ## Install from a clone
66
+
67
+ For connected development, use the locked repository environment:
68
+
69
+ ```bash
70
+ uv tool install --from ./packages/deployment-cli fdai-deployment-cli
71
+ fdaictl --version
72
+ ```
73
+
74
+ See the
75
+ [deployment quickstart](../../docs/user-guide/deploy-quickstart.md#install-the-command-once)
76
+ for PATH setup, updates, and editable development.
77
+
78
+ ## Use the command
79
+
80
+ Run `fdaictl --help` for the current command tree. Common entry points include:
81
+
82
+ | Command | Purpose |
83
+ |---------|---------|
84
+ | `fdaictl version` | Show the installed CLI version. |
85
+ | `fdaictl doctor` | Check local tools and Azure authentication. |
86
+ | `fdaictl provision inspect` | Inspect a target without changing it. |
87
+ | `fdaictl provision azure` | Start the guarded Azure deployment coordinator. |
88
+ | `fdaictl onboard guided --simulate` | Rehearse the finite stage graph without Azure changes. |
89
+
90
+ Package verification and Azure deployment are separate operations. Azure plans, approvals, Managed
91
+ Identity, recovery, migrations, service health, and cleanup remain deployment-owner controls after
92
+ the CLI is installed.
93
+
94
+ ## Progress output
95
+
96
+ Interactive text mode shows the current deployment phase and elapsed time. Use `--progress plain`
97
+ for line-oriented logs, `--progress off` to hide progress, or `--output json` for machine output.
98
+ Progress is not readiness evidence; the final deployment receipts remain authoritative.
99
+
100
+ ## Test
101
+
102
+ Run the package tests with:
103
+
104
+ ```bash
105
+ uv run --project packages/deployment-cli pytest -q --no-cov packages/deployment-cli/tests
106
+ uv run pytest -q --no-cov tests/integration/scripts/test_package_assurance.py \
107
+ tests/integration/scripts/test_signed_python_package.py
108
+ ```
109
+
110
+ See [Package Assurance](../../docs/roadmap/architecture/package-assurance.md) for the minimal signing
111
+ contract and [Installable Deployment CLI](../../docs/roadmap/deployment/installable-deployment-cli.md)
112
+ for Azure deployment behavior.