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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- 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,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.
|