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,177 @@
1
+ """Install a verified capability token or installation entitlement through Key Vault.
2
+
3
+ The managed host reads the token from standard input, verifies it against the packaged
4
+ upstream key and the exact deployment bindings, writes it to the fixed Key Vault
5
+ secret, and verifies the stored content again before any runtime consumes it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ from collections.abc import Callable
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from fdai_deployment_cli import standalone_planned_outputs
20
+ from fdai_deployment_cli.license import (
21
+ INSTALLATION_ENTITLEMENT_SCHEMA,
22
+ inspect_installation_entitlement,
23
+ inspect_license,
24
+ signed_token_schema,
25
+ )
26
+ from fdai_deployment_cli.standalone_host_state import private_json, replace_private_json
27
+ from fdai_deployment_cli.standalone_host_values import vault_name
28
+ from fdai_deployment_cli.trust_roots import license_public_key_pem
29
+
30
+ LICENSE_SECRET_NAME = "fdai-capability-license"
31
+
32
+
33
+ def install_license(
34
+ args: argparse.Namespace,
35
+ work_dir: Path,
36
+ *,
37
+ login: Callable[[dict[str, object], Path], None],
38
+ ) -> dict[str, object]:
39
+ """Store one verified token and record the Container Apps license variables."""
40
+
41
+ context = private_json(work_dir / "context.json", "standalone host context")
42
+ login(context, work_dir)
43
+ token = sys.stdin.read(8193)
44
+ if len(token) > 8192 or token != token.strip():
45
+ raise ValueError("license token stdin is invalid")
46
+ _inspect(token, args)
47
+ token_digest = hashlib.sha256(token.encode("ascii")).hexdigest()
48
+ infra = Path(str(context["infra"]))
49
+ key_vault = vault_name(_output(infra, "key_vault_uri"))
50
+ result = subprocess.run(
51
+ (
52
+ "az",
53
+ "keyvault",
54
+ "secret",
55
+ "set",
56
+ "--vault-name",
57
+ key_vault,
58
+ "--name",
59
+ LICENSE_SECRET_NAME,
60
+ "--file",
61
+ "/dev/stdin",
62
+ "--encoding",
63
+ "utf-8",
64
+ "--query",
65
+ "id",
66
+ "--output",
67
+ "tsv",
68
+ "--only-show-errors",
69
+ ),
70
+ input=token,
71
+ check=False,
72
+ capture_output=True,
73
+ text=True,
74
+ timeout=120,
75
+ )
76
+ token = ""
77
+ if result.returncode != 0:
78
+ raise ValueError("license token Key Vault installation failed")
79
+ versioned_secret_id = result.stdout.strip()
80
+ match = re.fullmatch(
81
+ rf"https://{re.escape(key_vault)}[.]vault[.]azure[.]net/secrets/"
82
+ rf"{LICENSE_SECRET_NAME}/([0-9a-f]{{32}})",
83
+ versioned_secret_id,
84
+ )
85
+ if match is None:
86
+ raise ValueError("license token Key Vault readback is invalid")
87
+ readback = subprocess.run(
88
+ (
89
+ "az",
90
+ "keyvault",
91
+ "secret",
92
+ "show",
93
+ "--id",
94
+ versioned_secret_id,
95
+ "--query",
96
+ "value",
97
+ "--output",
98
+ "tsv",
99
+ "--only-show-errors",
100
+ ),
101
+ cwd=work_dir,
102
+ check=False,
103
+ capture_output=True,
104
+ text=True,
105
+ timeout=120,
106
+ )
107
+ if readback.returncode != 0:
108
+ raise ValueError("license token Key Vault content readback failed")
109
+ stored = readback.stdout.strip()
110
+ _inspect(stored, args)
111
+ if hashlib.sha256(stored.encode("ascii")).hexdigest() != token_digest:
112
+ raise ValueError("license token Key Vault content differs")
113
+ values = private_json(work_dir / "application.auto.tfvars.json", "application variables")
114
+ values["license"] = {
115
+ "token_secret_id": versioned_secret_id.rsplit("/", 1)[0],
116
+ "image_digest": args.image_digest,
117
+ "deployment_digest": args.deployment_binding,
118
+ "token_revision": hashlib.sha256(
119
+ f"{args.image_digest}:{args.deployment_binding}".encode()
120
+ ).hexdigest(),
121
+ }
122
+ replace_private_json(work_dir / "application.auto.tfvars.json", values)
123
+ return {
124
+ "schema_version": "fdai.standalone-license-installation.v1",
125
+ "state": "installed",
126
+ "secret_metadata_verified": True,
127
+ "secret_content_verified": True,
128
+ "mutation_performed": True,
129
+ "subscription_ready": False,
130
+ }
131
+
132
+
133
+ def aks_license_environment(
134
+ application_values: dict[str, Any],
135
+ ) -> tuple[dict[str, str], dict[str, str]]:
136
+ """Return Core's plain and Key Vault-backed license environment once a token is installed."""
137
+
138
+ license_values = application_values.get("license")
139
+ if not isinstance(license_values, dict) or not license_values.get("token_secret_id"):
140
+ return {}, {}
141
+ return (
142
+ {
143
+ "FDAI_LICENSE_IMAGE_DIGEST": str(license_values["image_digest"]),
144
+ "FDAI_LICENSE_TOKEN_REVISION": str(license_values["token_revision"]),
145
+ },
146
+ {"FDAI_LICENSE_TOKEN": LICENSE_SECRET_NAME},
147
+ )
148
+
149
+
150
+ def _inspect(token: str, args: argparse.Namespace) -> None:
151
+ if signed_token_schema(token) == INSTALLATION_ENTITLEMENT_SCHEMA:
152
+ installation_binding = getattr(args, "installation_binding", None)
153
+ if not installation_binding:
154
+ raise ValueError("an installation entitlement requires its installation binding")
155
+ inspect_installation_entitlement(
156
+ token,
157
+ public_key_pem=license_public_key_pem(),
158
+ expected_installation_binding=installation_binding,
159
+ expected_deployment_binding=args.deployment_binding,
160
+ )
161
+ return
162
+ inspect_license(
163
+ token,
164
+ public_key_pem=license_public_key_pem(),
165
+ expected_image_digest=args.image_digest,
166
+ expected_tenant_binding=args.deployment_binding,
167
+ )
168
+
169
+
170
+ def _output(infra: Path, name: str) -> str:
171
+ value = standalone_planned_outputs.read_output(
172
+ infra, name, raw=True, reason="Terraform output readback failed"
173
+ )
174
+ return str(value)
175
+
176
+
177
+ __all__ = ["LICENSE_SECRET_NAME", "aks_license_environment", "install_license"]
@@ -0,0 +1,83 @@
1
+ """Derive the AKS API server authorized range from the managed host's egress.
2
+
3
+ The AKS baseline keeps a public API server with authorized IP ranges. Only the managed deployment
4
+ host runs Terraform, ``kubectl``, and readback against it, and that host leaves the operations
5
+ network through its NAT gateway. The range is therefore the NAT gateway's single public address,
6
+ read back from Azure Resource Manager rather than from any external address-echo service.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ipaddress
12
+ import json
13
+ import subprocess
14
+ from collections.abc import Mapping
15
+ from pathlib import Path
16
+
17
+
18
+ def management_egress_cidrs(context: Mapping[str, object], *, cwd: Path) -> list[str]:
19
+ """Return the one ``/32`` range of the operations NAT gateway, or fail closed."""
20
+
21
+ ops_resource_group = str(context["state_resource_group"])
22
+ subscription_id = str(context["subscription_id"])
23
+ listed = _az(
24
+ (
25
+ "network",
26
+ "nat",
27
+ "gateway",
28
+ "list",
29
+ "--resource-group",
30
+ ops_resource_group,
31
+ "--subscription",
32
+ subscription_id,
33
+ "--query",
34
+ "[].publicIpAddresses[].id",
35
+ "--output",
36
+ "json",
37
+ ),
38
+ cwd,
39
+ )
40
+ try:
41
+ addresses = json.loads(listed)
42
+ except json.JSONDecodeError as exc:
43
+ raise ValueError("management NAT gateway readback is invalid") from exc
44
+ if not isinstance(addresses, list) or len(addresses) != 1 or not isinstance(addresses[0], str):
45
+ raise ValueError("management egress requires exactly one NAT gateway public IP")
46
+ raw = _az(
47
+ (
48
+ "network",
49
+ "public-ip",
50
+ "show",
51
+ "--ids",
52
+ addresses[0],
53
+ "--query",
54
+ "ipAddress",
55
+ "--output",
56
+ "tsv",
57
+ ),
58
+ cwd,
59
+ ).strip()
60
+ try:
61
+ address = ipaddress.IPv4Address(raw)
62
+ except ValueError as exc:
63
+ raise ValueError("management NAT gateway public IP is invalid") from exc
64
+ if not address.is_global:
65
+ raise ValueError("management NAT gateway public IP is not globally routable")
66
+ return [f"{address}/32"]
67
+
68
+
69
+ def _az(arguments: tuple[str, ...], cwd: Path) -> str:
70
+ result = subprocess.run(
71
+ ("az", *arguments, "--only-show-errors"),
72
+ cwd=cwd,
73
+ check=False,
74
+ capture_output=True,
75
+ text=True,
76
+ timeout=60,
77
+ )
78
+ if result.returncode != 0:
79
+ raise ValueError("management egress readback failed")
80
+ return result.stdout
81
+
82
+
83
+ __all__ = ["management_egress_cidrs"]
@@ -0,0 +1,38 @@
1
+ """Check the adoption evidence one service migration bootstrap may write."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from fdai_deployment_cli.standalone_host_state import private_json
8
+
9
+
10
+ def verify_service_evidence(evidence: Path, service: str) -> bool:
11
+ """Verify fresh adoption evidence; return False when the lineage was already adopted.
12
+
13
+ `bootstrap` writes adoption evidence only when it adopts a legacy baseline. When an earlier
14
+ run already adopted the service lineage, bootstrap only advances it to head and writes
15
+ nothing, so absent evidence is a verified resume rather than a failure. A partial or
16
+ inconsistent pair still fails closed.
17
+ """
18
+
19
+ migration_path = evidence / f"{service}.json"
20
+ schema_path = evidence / f"{service}-schema.json"
21
+ if not migration_path.exists() and not schema_path.exists():
22
+ return False
23
+ for path in (migration_path, schema_path):
24
+ path.chmod(0o600)
25
+ migration = private_json(migration_path, f"{service} migration evidence")
26
+ schema = private_json(schema_path, f"{service} migration schema")
27
+ if (
28
+ migration.get("service_id") != service
29
+ or not isinstance(migration.get("observed_schema_fingerprint"), str)
30
+ or schema.get("schema_version") != 1
31
+ or schema.get("service_id") != service
32
+ or schema.get("observed_schema_fingerprint") != migration.get("observed_schema_fingerprint")
33
+ ):
34
+ raise ValueError("service migration evidence is incomplete")
35
+ return True
36
+
37
+
38
+ __all__ = ["verify_service_evidence"]
@@ -0,0 +1,356 @@
1
+ """AKS workload rendering for the independent operational evidence verifier."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from collections.abc import Mapping
8
+ from typing import Any
9
+
10
+ _GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
11
+ _OPERATIONAL_EVIDENCE_PORT = 8791
12
+ _OPERATIONAL_EVIDENCE_READINESS_PATH = "/v1/operational-evidence/readiness"
13
+ _SERVICE_DATABASE_ROLES = {
14
+ "core": "fdai_core",
15
+ "core-control-plane": "fdai_core",
16
+ "operator": "fdai_operator",
17
+ "operator-service": "fdai_operator",
18
+ "executor": "fdai_executor",
19
+ "isolated-executor": "fdai_executor",
20
+ "ingestion": "fdai_ingestion_api",
21
+ "document-ingestion-api": "fdai_ingestion_api",
22
+ "worker": "fdai_ingestion_worker",
23
+ "document-processing-worker": "fdai_ingestion_worker",
24
+ }
25
+
26
+
27
+ def bind_service_database_role(service: str, environment: Mapping[str, object]) -> dict[str, str]:
28
+ """Return one workload environment with its service-owned PostgreSQL role."""
29
+
30
+ runtime_environment = {name: str(value) for name, value in environment.items()}
31
+ database_role = _SERVICE_DATABASE_ROLES.get(service)
32
+ if database_role is not None:
33
+ runtime_environment["FDAI_DATABASE_ROLE"] = database_role
34
+ runtime_environment["PGOPTIONS"] = f"-c role={database_role}"
35
+ return runtime_environment
36
+
37
+
38
+ def aks_workload(
39
+ component: str,
40
+ refs: dict[str, Any],
41
+ identity: dict[str, Any],
42
+ environment: Mapping[str, object],
43
+ secret_environment: Mapping[str, str],
44
+ readiness_path: str,
45
+ liveness_path: str,
46
+ *,
47
+ external: bool = False,
48
+ service_port: int | None = None,
49
+ fs_group: int | None = None,
50
+ additional_identities: dict[str, dict[str, Any]] | None = None,
51
+ sidecars: dict[str, dict[str, object]] | None = None,
52
+ ) -> dict[str, object]:
53
+ """Build one runtime-neutral AKS long-running workload specification."""
54
+
55
+ image_names = {
56
+ "core": "core-control-plane",
57
+ "operator": "operator-service",
58
+ "executor": "isolated-executor",
59
+ "ingestion": "document-ingestion-api",
60
+ "worker": "document-processing-worker",
61
+ }
62
+ image_name = image_names.get(component)
63
+ if image_name is None or not isinstance(refs.get(image_name), str):
64
+ raise ValueError(f"AKS {component} workload image is unavailable")
65
+ cpu, memory = {
66
+ "core": ("1000m", "2Gi"),
67
+ "operator": ("500m", "1Gi"),
68
+ "executor": ("500m", "1Gi"),
69
+ "ingestion": ("500m", "1Gi"),
70
+ "worker": ("500m", "1Gi"),
71
+ }[component]
72
+ runtime_environment = bind_service_database_role(image_name, environment)
73
+ runtime_environment["FDAI_EXECUTION_VENUE"] = "deployed"
74
+ return {
75
+ "component": component,
76
+ "image": refs[image_name],
77
+ "identity_resource_id": identity["resource_id"],
78
+ "identity_client_id": identity["client_id"],
79
+ "additional_identities": additional_identities or {},
80
+ "command": [],
81
+ "args": [],
82
+ "replicas": 2,
83
+ "max_replicas": 4,
84
+ "cpu": cpu,
85
+ "memory": memory,
86
+ "port": 8000,
87
+ "service_port": service_port,
88
+ "external": external,
89
+ "readiness_path": readiness_path,
90
+ "liveness_path": liveness_path,
91
+ "fs_group": fs_group,
92
+ "environment": runtime_environment,
93
+ "secret_environment": secret_environment,
94
+ "sidecars": sidecars or {},
95
+ }
96
+
97
+
98
+ def aks_operational_evidence_verifier_workload(
99
+ *,
100
+ refs: dict[str, Any],
101
+ verifier_identity: object,
102
+ core_identity: dict[str, Any],
103
+ executor_identity: object,
104
+ deploy_runner_principal: str,
105
+ application_values: dict[str, Any],
106
+ postgres_fqdn: str,
107
+ postgres_database: str,
108
+ ) -> dict[str, object] | None:
109
+ """Render the optional non-agent verifier workload from deployment-owned bindings."""
110
+
111
+ binding = application_values.get("operational_evidence_verifier")
112
+ if binding is None:
113
+ return None
114
+ if not isinstance(binding, dict) or binding.get("enabled") is not True:
115
+ raise ValueError("operational evidence verifier binding is invalid")
116
+ if not isinstance(verifier_identity, dict):
117
+ raise TypeError("operational evidence verifier identity is unavailable")
118
+
119
+ verifier = _identity_binding(verifier_identity, "operational evidence verifier identity")
120
+ core = _identity_binding(core_identity, "Core runtime identity")
121
+ if isinstance(executor_identity, dict):
122
+ executor_principal = _identity_binding(executor_identity, "isolated executor identity")[
123
+ "principal_id"
124
+ ]
125
+ else:
126
+ executor_principal = _anchor_principal(
127
+ binding.get("anchors_json"), "anchor:isolated-executor"
128
+ )
129
+ if verifier["principal_id"] in {
130
+ core["principal_id"],
131
+ executor_principal,
132
+ deploy_runner_principal,
133
+ }:
134
+ raise ValueError("operational evidence verifier identity overlaps an independent anchor")
135
+
136
+ json_fields = {
137
+ "anchors_json": "FDAI_OPERATIONAL_EVIDENCE_ANCHORS_JSON",
138
+ "caller_token_jwks_json": "FDAI_OPERATIONAL_EVIDENCE_CALLER_TOKEN_JWKS_JSON",
139
+ "role_readback_scopes_json": "FDAI_OPERATIONAL_EVIDENCE_ROLE_READBACK_SCOPES_JSON",
140
+ "allowed_role_scopes_json": "FDAI_OPERATIONAL_EVIDENCE_ALLOWED_ROLE_SCOPES_JSON",
141
+ "vertical_executor_principals_json": (
142
+ "FDAI_OPERATIONAL_EVIDENCE_VERTICAL_EXECUTOR_PRINCIPALS_JSON"
143
+ ),
144
+ "writer_members_json": "FDAI_OPERATIONAL_EVIDENCE_WRITER_MEMBERS_JSON",
145
+ }
146
+ optional_fields = {
147
+ "grant_registry_json": "FDAI_OPERATIONAL_EVIDENCE_GRANT_REGISTRY_JSON",
148
+ "observation_metric_workspace_id": (
149
+ "FDAI_OPERATIONAL_EVIDENCE_OBSERVATION_METRIC_WORKSPACE_ID"
150
+ ),
151
+ "observation_metric_queries_json": (
152
+ "FDAI_OPERATIONAL_EVIDENCE_OBSERVATION_METRIC_QUERIES_JSON"
153
+ ),
154
+ "observation_scope_rows_json": "FDAI_OPERATIONAL_EVIDENCE_OBSERVATION_SCOPE_ROWS_JSON",
155
+ "forecast_history_sources_json": "FDAI_FORECAST_HISTORY_SOURCES_JSON",
156
+ }
157
+ environment: dict[str, object] = {
158
+ "AZURE_CLIENT_ID": verifier["client_id"],
159
+ "FDAI_MI_CLIENT_ID": verifier["client_id"],
160
+ "POSTGRES_HOST": postgres_fqdn,
161
+ "POSTGRES_DATABASE": postgres_database,
162
+ "FDAI_DATABASE_ROLE": "fdai_operational_evidence_verifier",
163
+ "PGOPTIONS": "-c role=fdai_operational_evidence_verifier",
164
+ "RUNTIME_ENV": application_values["env"],
165
+ "FDAI_OPERATIONAL_EVIDENCE_ENABLED": "1",
166
+ "FDAI_OPERATIONAL_EVIDENCE_TRUST_REGISTRY_PATH": _optional_text(
167
+ binding,
168
+ "trust_registry_path",
169
+ default="config/operational-evidence-trust-registry.json",
170
+ ),
171
+ "FDAI_OPERATIONAL_EVIDENCE_TRUST_REGISTRY_PIN": _required_text(
172
+ binding, "trust_registry_pin"
173
+ ),
174
+ "FDAI_OPERATIONAL_EVIDENCE_GRANT_REGISTRY_PATH": _required_text(
175
+ binding, "grant_registry_path"
176
+ ),
177
+ "FDAI_OPERATIONAL_EVIDENCE_GRANT_REGISTRY_PIN": _required_text(
178
+ binding, "grant_registry_pin"
179
+ ),
180
+ "FDAI_OPERATIONAL_EVIDENCE_VERIFIER_URL": (
181
+ f"http://127.0.0.1:{_OPERATIONAL_EVIDENCE_PORT}"
182
+ ),
183
+ "FDAI_OPERATIONAL_EVIDENCE_READER_ROLE": "fdai_core",
184
+ "FDAI_OPERATIONAL_EVIDENCE_CORE_EXECUTOR_PRINCIPAL_ID": core["principal_id"],
185
+ "FDAI_OPERATIONAL_EVIDENCE_ISOLATED_EXECUTOR_PRINCIPAL_ID": executor_principal,
186
+ "FDAI_OPERATIONAL_EVIDENCE_DEV_GATEWAY_EXECUTOR_PRINCIPAL_ID": _required_text(
187
+ binding, "dev_gateway_executor_principal_id"
188
+ ),
189
+ "FDAI_OPERATIONAL_EVIDENCE_DEPLOY_RUNNER_PRINCIPAL_ID": deploy_runner_principal,
190
+ "FDAI_OPERATIONAL_EVIDENCE_CALLER_TOKEN_ISSUER": _required_text(
191
+ binding, "caller_token_issuer"
192
+ ),
193
+ "FDAI_OPERATIONAL_EVIDENCE_CALLER_TOKEN_AUDIENCE": _required_text(
194
+ binding, "caller_token_audience"
195
+ ),
196
+ }
197
+ for key, env_name in json_fields.items():
198
+ environment[env_name] = _required_json_text(binding, key)
199
+ for key, env_name in optional_fields.items():
200
+ if key in binding:
201
+ environment[env_name] = (
202
+ _required_text(binding, key)
203
+ if key == "observation_metric_workspace_id"
204
+ else _required_json_text(binding, key)
205
+ )
206
+ vertical_principals = json.loads(
207
+ str(environment["FDAI_OPERATIONAL_EVIDENCE_VERTICAL_EXECUTOR_PRINCIPALS_JSON"])
208
+ )
209
+ if not isinstance(vertical_principals, list) or not all(
210
+ isinstance(principal, str) and _GUID.fullmatch(principal) is not None
211
+ for principal in vertical_principals
212
+ ):
213
+ raise ValueError("vertical_executor_principals_json must be a JSON list of GUIDs")
214
+ environment["FDAI_OPERATIONAL_EVIDENCE_EXECUTOR_PRINCIPALS_JSON"] = json.dumps(
215
+ sorted(
216
+ {
217
+ str(environment["FDAI_OPERATIONAL_EVIDENCE_CORE_EXECUTOR_PRINCIPAL_ID"]),
218
+ str(environment["FDAI_OPERATIONAL_EVIDENCE_ISOLATED_EXECUTOR_PRINCIPAL_ID"]),
219
+ str(environment["FDAI_OPERATIONAL_EVIDENCE_DEV_GATEWAY_EXECUTOR_PRINCIPAL_ID"]),
220
+ deploy_runner_principal,
221
+ *vertical_principals,
222
+ }
223
+ ),
224
+ separators=(",", ":"),
225
+ )
226
+ image = refs.get("core-control-plane")
227
+ if not isinstance(image, str):
228
+ raise TypeError("AKS operational evidence verifier image is unavailable")
229
+ return {
230
+ "component": "operational-evidence-verifier",
231
+ "image": image,
232
+ "identity_resource_id": verifier["resource_id"],
233
+ "identity_client_id": verifier["client_id"],
234
+ "additional_identities": {},
235
+ "command": ["python", "-m", "fdai.delivery.operational_evidence_server"],
236
+ "args": ["--host", "0.0.0.0", "--port", str(_OPERATIONAL_EVIDENCE_PORT)],
237
+ "replicas": 1,
238
+ "max_replicas": 1,
239
+ "cpu": "500m",
240
+ "memory": "1Gi",
241
+ "port": _OPERATIONAL_EVIDENCE_PORT,
242
+ "service_port": None,
243
+ "external": False,
244
+ "readiness_path": _OPERATIONAL_EVIDENCE_READINESS_PATH,
245
+ "liveness_path": _OPERATIONAL_EVIDENCE_READINESS_PATH,
246
+ "fs_group": None,
247
+ "environment": {
248
+ name: str(value)
249
+ for name, value in {**environment, "FDAI_EXECUTION_VENUE": "deployed"}.items()
250
+ },
251
+ "secret_environment": {"FDAI_OPERATIONAL_EVIDENCE_VERIFIER_DSN": "fdai-state-store-dsn"},
252
+ "sidecars": {},
253
+ }
254
+
255
+
256
+ def add_aks_operational_evidence_verifier_workload(
257
+ workloads: dict[str, dict[str, object]],
258
+ *,
259
+ refs: dict[str, Any],
260
+ identities: dict[str, Any],
261
+ substrate_outputs: Mapping[str, object],
262
+ context: Mapping[str, object],
263
+ application_values: dict[str, Any],
264
+ ) -> None:
265
+ """Add the optional verifier workload to ``workloads`` when its binding is present."""
266
+
267
+ if application_values.get("operational_evidence_verifier") is None:
268
+ return
269
+ verifier_workload = aks_operational_evidence_verifier_workload(
270
+ refs=refs,
271
+ verifier_identity=substrate_outputs["operational_evidence_verifier_identity"],
272
+ core_identity=_mapping(identities.get("core"), "Core runtime identity"),
273
+ executor_identity=identities.get("executor"),
274
+ deploy_runner_principal=_required_text(context, "principal_id"),
275
+ application_values=application_values,
276
+ postgres_fqdn=_required_text(substrate_outputs, "postgres_fqdn"),
277
+ postgres_database=_required_text(substrate_outputs, "postgres_database"),
278
+ )
279
+ if verifier_workload is not None:
280
+ workloads["operational-evidence-verifier"] = verifier_workload
281
+
282
+
283
+ def runtime_principal_ids_with_operational_evidence_verifier(
284
+ identities: Mapping[str, Any], names: tuple[str, ...]
285
+ ) -> set[str]:
286
+ """Return runtime principal ids, including the optional verifier identity."""
287
+
288
+ principals = {
289
+ str(_mapping(identities.get(name), f"{name} runtime identity")["principal_id"])
290
+ for name in names
291
+ }
292
+ verifier_identity = identities.get("operational_evidence_verifier")
293
+ if verifier_identity is not None:
294
+ principals.add(
295
+ str(
296
+ _mapping(verifier_identity, "operational evidence verifier runtime identity")[
297
+ "principal_id"
298
+ ]
299
+ )
300
+ )
301
+ return principals
302
+
303
+
304
+ def _mapping(value: object, label: str) -> dict[str, Any]:
305
+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
306
+ raise ValueError(f"{label} is invalid")
307
+ return {str(key): item for key, item in value.items()}
308
+
309
+
310
+ def _identity_binding(value: dict[str, Any], label: str) -> dict[str, str]:
311
+ result = {
312
+ "resource_id": _required_text(value, "resource_id"),
313
+ "client_id": _required_text(value, "client_id"),
314
+ "principal_id": _required_text(value, "principal_id"),
315
+ }
316
+ if any(_GUID.fullmatch(result[key]) is None for key in ("client_id", "principal_id")):
317
+ raise ValueError(f"{label} is invalid")
318
+ return result
319
+
320
+
321
+ def _required_text(value: Mapping[str, Any], key: str) -> str:
322
+ item = value.get(key)
323
+ if not isinstance(item, str) or not item.strip():
324
+ raise ValueError(f"{key} is required")
325
+ return item.strip()
326
+
327
+
328
+ def _optional_text(value: Mapping[str, Any], key: str, *, default: str) -> str:
329
+ item = value.get(key, default)
330
+ if not isinstance(item, str) or not item.strip():
331
+ raise ValueError(f"{key} is required")
332
+ return item.strip()
333
+
334
+
335
+ def _required_json_text(value: Mapping[str, Any], key: str) -> str:
336
+ item = _required_text(value, key)
337
+ try:
338
+ json.loads(item)
339
+ except json.JSONDecodeError as exc:
340
+ raise ValueError(f"{key} must be valid JSON") from exc
341
+ return item
342
+
343
+
344
+ def _anchor_principal(anchors_json: object, anchor_id: str) -> str:
345
+ if isinstance(anchors_json, str) and anchors_json.strip():
346
+ try:
347
+ parsed = json.loads(anchors_json)
348
+ except json.JSONDecodeError:
349
+ parsed = None
350
+ if isinstance(parsed, dict) and isinstance(parsed.get("anchors"), list):
351
+ for entry in parsed["anchors"]:
352
+ if isinstance(entry, dict) and entry.get("anchor_id") == anchor_id:
353
+ principal = entry.get("principal_id")
354
+ if isinstance(principal, str) and _GUID.fullmatch(principal) is not None:
355
+ return principal
356
+ raise TypeError("operational evidence verifier requires the isolated executor anchor")