engineering-platform 2.3.102__tar.gz → 2.3.103__tar.gz
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.
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/PKG-INFO +1 -1
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/pyproject.toml +1 -1
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +1 -1
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +4 -4
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_version.py +1 -1
- engineering_platform-2.3.103/src/engineering_platform/system_instance_lifecycle.py +572 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_instance_provisioner.py +104 -1
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/templates/workspace-config.json +1 -1
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/PKG-INFO +1 -1
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/SOURCES.txt +1 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/README.md +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/setup.cfg +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/__init__.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/__main__.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/agent_state.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/agent_trust.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard.css +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard.js +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard_locales.mjs +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard_status_store.mjs +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard_translation.mjs +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/icon-dark.png +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/icon-light.png +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/manifest.webmanifest +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assurance_scope.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/capability_preflight.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/capability_review.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_data_transfer.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_database.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_operational_reset.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_store_migration.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/codex_capacity.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/codex_chat.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/codex_observability.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/component_lock.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/component_logging.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/console_presentation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/console_route_ownership.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/__init__.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/ep_consumer.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/models.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/projection.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_browser_validation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_state.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_supervisor.swift +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_translation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dependabot_producer.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/development_profile.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/drift_diagnostics.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/emergency_recovery.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/engineering_memory.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/ep_consumer_credentials.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/evidence_projection.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_activity.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_context.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_errors.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_evidence.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_executor.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_finalization.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_host.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_host_evidence.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_lease.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_lifecycle.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_models.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_readiness.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_reporting.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_repository.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_timeout_policy.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_timing.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_transaction.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/external_producer_binding.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/file_inbox.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/forensic_attribution.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/forensic_attribution_v2.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/forensic_delta.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/golden_scenario.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/historical_dashboard_configuration.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/host_admin.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/host_preflight.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_relocation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_activation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_admission.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_backup.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_composition.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_executor.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_migration.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_operation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_plan.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_preparation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/investigation_ledger.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/legacy_inbox_migration.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/legacy_installation_adoption.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/lifecycle_worker.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/live_status.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/local_api.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/local_api_keychain.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/local_repository_binding.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_autonomy.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_codex_runtime.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_workspace_readiness.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_workspace_recovery.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/merge_delegation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/operational_installation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/operational_installation_lock.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/operational_installation_record.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/owner_credential_recovery.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/parity_context.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/parity_lifecycle_dispatcher.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_admin.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_api.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_bootstrap.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_components.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/pr_check_repair.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/pr_evidence_backfill.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/producer.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/product_installation_readback.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/project_agent.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/project_agent_service.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/project_topology.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/prompt_history.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_context.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_context_benchmark.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_context_scope.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_interruption.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_process_identity.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_readiness.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_recovery.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_usage.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/providers.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/qualification.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/qualification_runtime.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/recommendation_handoff.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/reconciliation_adoption.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/release_operation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/report_analysis.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/repository_attachment.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/repository_handoff.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/resources.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/reviewer_evidence.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/revision_binding.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/producer-readback-v1.2.schema.json +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/producer-readback-v1.3.schema.json +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/producer-readback-v1.schema.json +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/repository-attachment.schema.json +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server_console_services.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server_relay.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server_service.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/status_model.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/status_reconciliation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/storage.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/submission_cli.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/submission_intake.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/submission_service.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_installation_topology.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_provider_context.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_server_service.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry_contract.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry_export.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry_metrics.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/terminal_evidence_reconciliation.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/validation_identity.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/validation_profile.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/version_preparation_delivery.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/workspace_preflight.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/worktree_provenance.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/worktree_tooling.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/dependency_links.txt +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/entry_points.txt +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/top_level.txt +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/tests/test_adaptive_action_sizing_design.py +0 -0
- {engineering_platform-2.3.102 → engineering_platform-2.3.103}/tests/test_role_task_model_policy_design.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "engineering-platform"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.103"
|
|
8
8
|
description = "Local-first Engineering Platform execution operations runtime"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14,<3.15"
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bootstrap_contract": "2026.12",
|
|
3
3
|
"checkpoint_format": 1,
|
|
4
|
-
"dashboard_version": "2.3.
|
|
4
|
+
"dashboard_version": "2.3.103",
|
|
5
5
|
"handoff_protocol": 1,
|
|
6
6
|
"memory_format": 2,
|
|
7
7
|
"minimum_codex_cli": "0.146.0",
|
|
8
8
|
"inbox_protocol": 1,
|
|
9
|
-
"platform_version": "2.3.
|
|
9
|
+
"platform_version": "2.3.103",
|
|
10
10
|
"report_format": 2,
|
|
11
|
-
"runner_version": "2.3.
|
|
11
|
+
"runner_version": "2.3.103",
|
|
12
12
|
"status_model": 1,
|
|
13
13
|
"storage_schema": 45,
|
|
14
|
-
"watcher_version": "2.3.
|
|
14
|
+
"watcher_version": "2.3.103"
|
|
15
15
|
}
|
|
@@ -12,7 +12,7 @@ from .storage import ENGINEERING_STORAGE_SCHEMA_VERSION
|
|
|
12
12
|
|
|
13
13
|
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
|
14
14
|
CONTRACT = re.compile(r"^(\d{4})\.(0[1-9]|1[0-2])$")
|
|
15
|
-
CURRENT_PLATFORM_VERSION = "2.3.
|
|
15
|
+
CURRENT_PLATFORM_VERSION = "2.3.103"
|
|
16
16
|
MANIFEST_FIELDS = frozenset(
|
|
17
17
|
{
|
|
18
18
|
"platform_version",
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""Product-owned preserve/purge/restore lifecycle for EP Server instances."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import fcntl
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import stat
|
|
12
|
+
from typing import Callable, Mapping, Protocol
|
|
13
|
+
|
|
14
|
+
from . import operational_installation_record
|
|
15
|
+
from . import system_installation_topology
|
|
16
|
+
from . import system_provider_context
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CONTRACT = "engineering-platform.system-instance-lifecycle/v1"
|
|
20
|
+
_PROVIDER_PRESERVED = "PRESERVED_REQUIRES_REVERIFICATION"
|
|
21
|
+
_OPERATION = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SystemInstanceLifecycleError(RuntimeError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ServiceController(Protocol):
|
|
29
|
+
def register(
|
|
30
|
+
self,
|
|
31
|
+
instance: system_installation_topology.SystemInstanceTopology,
|
|
32
|
+
interpreter: Path,
|
|
33
|
+
) -> Mapping[str, object]: ...
|
|
34
|
+
def loaded(self, instance: system_installation_topology.SystemInstanceTopology) -> bool: ...
|
|
35
|
+
def remove(self, instance: system_installation_topology.SystemInstanceTopology) -> Mapping[str, object]: ...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
RuntimeInstaller = Callable[
|
|
39
|
+
[object], system_installation_topology.RuntimeSlot
|
|
40
|
+
]
|
|
41
|
+
ServiceInterpreter = Callable[
|
|
42
|
+
[system_installation_topology.SystemInstanceTopology], Path | None
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _Lock:
|
|
47
|
+
def __init__(self, path: Path) -> None:
|
|
48
|
+
self.path = path
|
|
49
|
+
self.fd: int | None = None
|
|
50
|
+
|
|
51
|
+
def __enter__(self) -> "_Lock":
|
|
52
|
+
self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
53
|
+
self.fd = os.open(self.path, os.O_RDWR | os.O_CREAT, 0o600)
|
|
54
|
+
try:
|
|
55
|
+
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
56
|
+
except OSError as error:
|
|
57
|
+
os.close(self.fd)
|
|
58
|
+
self.fd = None
|
|
59
|
+
raise SystemInstanceLifecycleError("another operation owns the exact EP instance lifecycle") from error
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(self, *_: object) -> None:
|
|
63
|
+
if self.fd is not None:
|
|
64
|
+
fcntl.flock(self.fd, fcntl.LOCK_UN)
|
|
65
|
+
os.close(self.fd)
|
|
66
|
+
self.fd = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _json_digest(value: Mapping[str, object]) -> str:
|
|
70
|
+
return "sha256:" + hashlib.sha256(
|
|
71
|
+
json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
72
|
+
).hexdigest()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_json(path: Path) -> dict[str, object]:
|
|
76
|
+
try:
|
|
77
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
78
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
79
|
+
raise SystemInstanceLifecycleError(f"EP lifecycle evidence is unavailable: {path.name}") from error
|
|
80
|
+
if not isinstance(value, dict):
|
|
81
|
+
raise SystemInstanceLifecycleError(f"EP lifecycle evidence is invalid: {path.name}")
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _atomic_json(path: Path, value: Mapping[str, object]) -> None:
|
|
86
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
87
|
+
temporary = path.with_name(f".{path.name}.tmp-{os.getpid()}")
|
|
88
|
+
try:
|
|
89
|
+
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
90
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
91
|
+
json.dump(value, stream, indent=2, sort_keys=True)
|
|
92
|
+
stream.write("\n")
|
|
93
|
+
stream.flush()
|
|
94
|
+
os.fsync(stream.fileno())
|
|
95
|
+
os.replace(temporary, path)
|
|
96
|
+
finally:
|
|
97
|
+
temporary.unlink(missing_ok=True)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _regular_digest(path: Path) -> str:
|
|
101
|
+
details = path.lstat()
|
|
102
|
+
if path.is_symlink() or not stat.S_ISREG(details.st_mode):
|
|
103
|
+
raise SystemInstanceLifecycleError(f"EP preserved instance contains unsafe entry: {path}")
|
|
104
|
+
digest = hashlib.sha256()
|
|
105
|
+
with path.open("rb") as stream:
|
|
106
|
+
while True:
|
|
107
|
+
payload = stream.read(1024 * 1024)
|
|
108
|
+
if not payload:
|
|
109
|
+
break
|
|
110
|
+
digest.update(payload)
|
|
111
|
+
return "sha256:" + digest.hexdigest()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _tree_digest(root: Path, *, ignored_roots: tuple[Path, ...] = ()) -> str:
|
|
115
|
+
try:
|
|
116
|
+
details = root.lstat()
|
|
117
|
+
except OSError as error:
|
|
118
|
+
raise SystemInstanceLifecycleError("EP preserved instance root is unavailable") from error
|
|
119
|
+
if root.is_symlink() or not stat.S_ISDIR(details.st_mode):
|
|
120
|
+
raise SystemInstanceLifecycleError("EP preserved instance root is unsafe")
|
|
121
|
+
ignored: tuple[tuple[str, ...], ...] = tuple(
|
|
122
|
+
path.relative_to(root).parts for path in ignored_roots
|
|
123
|
+
)
|
|
124
|
+
if any(not parts for parts in ignored):
|
|
125
|
+
raise SystemInstanceLifecycleError("EP lifecycle digest cannot ignore the instance root")
|
|
126
|
+
|
|
127
|
+
def ignored_path(path: Path) -> bool:
|
|
128
|
+
parts = path.relative_to(root).parts
|
|
129
|
+
return any(parts[:len(prefix)] == prefix for prefix in ignored)
|
|
130
|
+
|
|
131
|
+
entries: list[dict[str, object]] = []
|
|
132
|
+
for directory, names, files in os.walk(root, topdown=True, followlinks=False):
|
|
133
|
+
parent = Path(directory)
|
|
134
|
+
names[:] = [name for name in names if not ignored_path(parent / name)]
|
|
135
|
+
for name in sorted(names):
|
|
136
|
+
path = parent / name
|
|
137
|
+
info = path.lstat()
|
|
138
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
|
|
139
|
+
raise SystemInstanceLifecycleError(f"EP preserved instance contains unsafe entry: {path}")
|
|
140
|
+
entries.append({
|
|
141
|
+
"path": str(path.relative_to(root)),
|
|
142
|
+
"kind": "DIRECTORY",
|
|
143
|
+
"mode": stat.S_IMODE(info.st_mode),
|
|
144
|
+
})
|
|
145
|
+
for name in sorted(files):
|
|
146
|
+
path = parent / name
|
|
147
|
+
if ignored_path(path):
|
|
148
|
+
continue
|
|
149
|
+
info = path.lstat()
|
|
150
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
|
|
151
|
+
raise SystemInstanceLifecycleError(f"EP preserved instance contains unsafe entry: {path}")
|
|
152
|
+
entries.append({
|
|
153
|
+
"path": str(path.relative_to(root)),
|
|
154
|
+
"kind": "FILE",
|
|
155
|
+
"mode": stat.S_IMODE(info.st_mode),
|
|
156
|
+
"size": info.st_size,
|
|
157
|
+
"digest": _regular_digest(path),
|
|
158
|
+
})
|
|
159
|
+
return "sha256:" + hashlib.sha256(
|
|
160
|
+
json.dumps(entries, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
161
|
+
).hexdigest()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _mark_provider_auth_preserved(
|
|
165
|
+
instance: system_installation_topology.SystemInstanceTopology,
|
|
166
|
+
) -> list[str]:
|
|
167
|
+
providers: list[str] = []
|
|
168
|
+
for context in system_provider_context.provider_contexts(instance):
|
|
169
|
+
if context.auth_receipt.is_symlink() or context.descriptor.is_symlink():
|
|
170
|
+
raise SystemInstanceLifecycleError("EP provider lifecycle evidence contains a symbolic link")
|
|
171
|
+
auth = _read_json(context.auth_receipt)
|
|
172
|
+
if (
|
|
173
|
+
auth.get("provider") != context.provider
|
|
174
|
+
or auth.get("instance_id") != instance.instance_id
|
|
175
|
+
or auth.get("state") not in {"READY", _PROVIDER_PRESERVED}
|
|
176
|
+
):
|
|
177
|
+
raise SystemInstanceLifecycleError("EP provider authentication evidence cannot be preserved")
|
|
178
|
+
auth["state"] = _PROVIDER_PRESERVED
|
|
179
|
+
_atomic_json(context.auth_receipt, auth)
|
|
180
|
+
providers.append(context.provider)
|
|
181
|
+
return sorted(providers)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class SystemInstanceLifecycle:
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
product: system_installation_topology.SystemInstallationTopology,
|
|
188
|
+
controller: ServiceController,
|
|
189
|
+
service_interpreter: ServiceInterpreter,
|
|
190
|
+
) -> None:
|
|
191
|
+
self.product = product
|
|
192
|
+
self.controller = controller
|
|
193
|
+
self.service_interpreter = service_interpreter
|
|
194
|
+
|
|
195
|
+
def _root(self, instance_id: str) -> Path:
|
|
196
|
+
identity = system_installation_topology.validate_instance_id(instance_id)
|
|
197
|
+
return self.product.system_root / "instance-lifecycle-v1" / identity
|
|
198
|
+
|
|
199
|
+
def _operation_root(self, instance_id: str, operation_id: str) -> Path:
|
|
200
|
+
if _OPERATION.fullmatch(operation_id) is None:
|
|
201
|
+
raise SystemInstanceLifecycleError("EP lifecycle operation ID is invalid")
|
|
202
|
+
return self._root(instance_id) / "operations" / operation_id
|
|
203
|
+
|
|
204
|
+
def _state(
|
|
205
|
+
self,
|
|
206
|
+
instance_id: str,
|
|
207
|
+
operation_id: str,
|
|
208
|
+
operation: str,
|
|
209
|
+
request: Mapping[str, object],
|
|
210
|
+
) -> tuple[Path, dict[str, object]]:
|
|
211
|
+
root = self._operation_root(instance_id, operation_id)
|
|
212
|
+
path = root / "state.json"
|
|
213
|
+
if path.exists():
|
|
214
|
+
state = _read_json(path)
|
|
215
|
+
if (
|
|
216
|
+
state.get("contract") != CONTRACT
|
|
217
|
+
or state.get("operation") != operation
|
|
218
|
+
or state.get("request") != dict(request)
|
|
219
|
+
or state.get("request_digest") != _json_digest(dict(request))
|
|
220
|
+
):
|
|
221
|
+
raise SystemInstanceLifecycleError("EP lifecycle operation identity conflict")
|
|
222
|
+
return root, state
|
|
223
|
+
state = {
|
|
224
|
+
"contract": CONTRACT,
|
|
225
|
+
"operation": operation,
|
|
226
|
+
"operation_id": operation_id,
|
|
227
|
+
"instance_id": instance_id,
|
|
228
|
+
"request": dict(request),
|
|
229
|
+
"request_digest": _json_digest(dict(request)),
|
|
230
|
+
"phase": "PREPARED",
|
|
231
|
+
}
|
|
232
|
+
_atomic_json(path, state)
|
|
233
|
+
return root, state
|
|
234
|
+
|
|
235
|
+
def _save(self, root: Path, state: Mapping[str, object], phase: str, **evidence: object) -> dict[str, object]:
|
|
236
|
+
updated = {**state, **evidence, "phase": phase}
|
|
237
|
+
_atomic_json(root / "state.json", updated)
|
|
238
|
+
return updated
|
|
239
|
+
|
|
240
|
+
def _receipt(
|
|
241
|
+
self,
|
|
242
|
+
root: Path,
|
|
243
|
+
state: Mapping[str, object],
|
|
244
|
+
evidence: Mapping[str, object],
|
|
245
|
+
) -> dict[str, object]:
|
|
246
|
+
path = root / "receipt.json"
|
|
247
|
+
receipt = {
|
|
248
|
+
"contract": CONTRACT,
|
|
249
|
+
"operation": state["operation"],
|
|
250
|
+
"operation_id": state["operation_id"],
|
|
251
|
+
"instance_id": state["instance_id"],
|
|
252
|
+
"request_digest": state["request_digest"],
|
|
253
|
+
"state": "COMPLETE",
|
|
254
|
+
"evidence": dict(evidence),
|
|
255
|
+
}
|
|
256
|
+
receipt["receipt_sha256"] = _json_digest(receipt)
|
|
257
|
+
if path.exists():
|
|
258
|
+
existing = _read_json(path)
|
|
259
|
+
digest = existing.get("receipt_sha256")
|
|
260
|
+
unsigned = {key: value for key, value in existing.items() if key != "receipt_sha256"}
|
|
261
|
+
if (
|
|
262
|
+
digest != _json_digest(unsigned)
|
|
263
|
+
or existing.get("contract") != CONTRACT
|
|
264
|
+
or existing.get("operation") != state["operation"]
|
|
265
|
+
or existing.get("request_digest") != state["request_digest"]
|
|
266
|
+
):
|
|
267
|
+
raise SystemInstanceLifecycleError("EP lifecycle terminal receipt conflict")
|
|
268
|
+
receipt = existing
|
|
269
|
+
else:
|
|
270
|
+
_atomic_json(path, receipt)
|
|
271
|
+
self._save(root, state, "COMPLETE", receipt_sha256=receipt["receipt_sha256"])
|
|
272
|
+
return receipt
|
|
273
|
+
|
|
274
|
+
def _terminal(self, root: Path, state: Mapping[str, object]) -> dict[str, object]:
|
|
275
|
+
receipt = _read_json(root / "receipt.json")
|
|
276
|
+
digest = receipt.get("receipt_sha256")
|
|
277
|
+
unsigned = {key: value for key, value in receipt.items() if key != "receipt_sha256"}
|
|
278
|
+
if (
|
|
279
|
+
digest != _json_digest(unsigned)
|
|
280
|
+
or receipt.get("contract") != CONTRACT
|
|
281
|
+
or receipt.get("operation") != state.get("operation")
|
|
282
|
+
or receipt.get("request_digest") != state.get("request_digest")
|
|
283
|
+
or state.get("receipt_sha256") != digest
|
|
284
|
+
):
|
|
285
|
+
raise SystemInstanceLifecycleError("EP lifecycle terminal receipt is invalid")
|
|
286
|
+
return receipt
|
|
287
|
+
|
|
288
|
+
def _preserve_receipt(self, instance_id: str, preserve_operation_id: str) -> dict[str, object]:
|
|
289
|
+
root = self._operation_root(instance_id, preserve_operation_id)
|
|
290
|
+
receipt = _read_json(root / "receipt.json")
|
|
291
|
+
digest = receipt.get("receipt_sha256")
|
|
292
|
+
unsigned = {key: value for key, value in receipt.items() if key != "receipt_sha256"}
|
|
293
|
+
evidence = receipt.get("evidence")
|
|
294
|
+
if (
|
|
295
|
+
digest != _json_digest(unsigned)
|
|
296
|
+
or receipt.get("contract") != CONTRACT
|
|
297
|
+
or receipt.get("operation") != "PRESERVE"
|
|
298
|
+
or receipt.get("instance_id") != instance_id
|
|
299
|
+
or not isinstance(evidence, Mapping)
|
|
300
|
+
or evidence.get("lifecycle_state") != "UNINSTALLED_DATA_PRESERVED"
|
|
301
|
+
or evidence.get("restorable") is not True
|
|
302
|
+
):
|
|
303
|
+
raise SystemInstanceLifecycleError("EP preserve evidence does not authorize restore")
|
|
304
|
+
return receipt
|
|
305
|
+
|
|
306
|
+
def preserve(
|
|
307
|
+
self,
|
|
308
|
+
instance: system_installation_topology.SystemInstanceTopology,
|
|
309
|
+
descriptor: Mapping[str, object],
|
|
310
|
+
operation_id: str,
|
|
311
|
+
*,
|
|
312
|
+
confirm_instance_id: str,
|
|
313
|
+
) -> Mapping[str, object]:
|
|
314
|
+
if confirm_instance_id != instance.instance_id:
|
|
315
|
+
raise SystemInstanceLifecycleError("EP preserve confirmation mismatch")
|
|
316
|
+
if (self._root(instance.instance_id) / "purged.json").exists():
|
|
317
|
+
raise SystemInstanceLifecycleError("EP instance identity was permanently purged")
|
|
318
|
+
request = {
|
|
319
|
+
"instance_id": instance.instance_id,
|
|
320
|
+
"descriptor_digest": _json_digest(dict(descriptor)),
|
|
321
|
+
"selected_runtime": descriptor.get("selected_runtime"),
|
|
322
|
+
}
|
|
323
|
+
root, state = self._state(instance.instance_id, operation_id, "PRESERVE", request)
|
|
324
|
+
if state.get("phase") == "COMPLETE":
|
|
325
|
+
receipt = self._terminal(root, state)
|
|
326
|
+
if _tree_digest(instance.root) != receipt["evidence"]["mutable_instance_data_digest"]:
|
|
327
|
+
raise SystemInstanceLifecycleError("EP preserved instance changed after terminal preserve")
|
|
328
|
+
return receipt
|
|
329
|
+
with _Lock(instance.lifecycle_lock):
|
|
330
|
+
self.controller.remove(instance)
|
|
331
|
+
if self.controller.loaded(instance) or self.service_interpreter(instance) is not None:
|
|
332
|
+
raise SystemInstanceLifecycleError("EP preserved service was not fully removed")
|
|
333
|
+
providers = _mark_provider_auth_preserved(instance)
|
|
334
|
+
tree_digest = _tree_digest(instance.root)
|
|
335
|
+
state = self._save(
|
|
336
|
+
root,
|
|
337
|
+
state,
|
|
338
|
+
"VERIFIED",
|
|
339
|
+
mutable_instance_data_digest=tree_digest,
|
|
340
|
+
providers=providers,
|
|
341
|
+
)
|
|
342
|
+
return self._receipt(root, state, {
|
|
343
|
+
"lifecycle_state": "UNINSTALLED_DATA_PRESERVED",
|
|
344
|
+
"instance_identity": "PRESERVED",
|
|
345
|
+
"mutable_instance_data": "PRESERVED",
|
|
346
|
+
"mutable_instance_data_digest": tree_digest,
|
|
347
|
+
"restorable": True,
|
|
348
|
+
"service_state": "REMOVED_OR_INACTIVE",
|
|
349
|
+
"shared_immutable_runtime_slots": "PRESERVED",
|
|
350
|
+
"provider_auth_state": _PROVIDER_PRESERVED,
|
|
351
|
+
"selected_runtime": descriptor.get("selected_runtime"),
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
def restore(
|
|
355
|
+
self,
|
|
356
|
+
instance: system_installation_topology.SystemInstanceTopology,
|
|
357
|
+
descriptor: Mapping[str, object],
|
|
358
|
+
operation_id: str,
|
|
359
|
+
preserve_operation_id: str,
|
|
360
|
+
release: object,
|
|
361
|
+
runtime_installer: RuntimeInstaller,
|
|
362
|
+
) -> Mapping[str, object]:
|
|
363
|
+
if (self._root(instance.instance_id) / "purged.json").exists():
|
|
364
|
+
raise SystemInstanceLifecycleError("EP purged instance identity cannot be restored")
|
|
365
|
+
preserved = self._preserve_receipt(instance.instance_id, preserve_operation_id)
|
|
366
|
+
preserved_evidence = preserved["evidence"]
|
|
367
|
+
release_identity = release.identity().payload()
|
|
368
|
+
selected = descriptor.get("selected_runtime")
|
|
369
|
+
if not isinstance(selected, Mapping) or any(
|
|
370
|
+
selected.get(key) != value for key, value in release_identity.items()
|
|
371
|
+
):
|
|
372
|
+
raise SystemInstanceLifecycleError("EP restore release does not match preserved runtime identity")
|
|
373
|
+
request = {
|
|
374
|
+
"instance_id": instance.instance_id,
|
|
375
|
+
"preserve_operation_id": preserve_operation_id,
|
|
376
|
+
"preserve_receipt_sha256": preserved["receipt_sha256"],
|
|
377
|
+
"descriptor_digest": _json_digest(dict(descriptor)),
|
|
378
|
+
"release": release_identity,
|
|
379
|
+
}
|
|
380
|
+
root, state = self._state(instance.instance_id, operation_id, "RESTORE", request)
|
|
381
|
+
if state.get("phase") == "COMPLETE":
|
|
382
|
+
return self._terminal(root, state)
|
|
383
|
+
with _Lock(instance.lifecycle_lock):
|
|
384
|
+
if self.controller.loaded(instance):
|
|
385
|
+
raise SystemInstanceLifecycleError("EP preserved service unexpectedly became active")
|
|
386
|
+
current_digest = _tree_digest(instance.root)
|
|
387
|
+
if current_digest != preserved_evidence.get("mutable_instance_data_digest"):
|
|
388
|
+
raise SystemInstanceLifecycleError("EP preserved instance data no longer matches restore evidence")
|
|
389
|
+
slot = runtime_installer(release)
|
|
390
|
+
if str(slot.interpreter) != selected.get("interpreter"):
|
|
391
|
+
raise SystemInstanceLifecycleError("EP restored runtime slot does not match preserved interpreter")
|
|
392
|
+
replaying_verified = state.get("phase") == "VERIFIED"
|
|
393
|
+
state = self._save(
|
|
394
|
+
root,
|
|
395
|
+
state,
|
|
396
|
+
"VERIFIED",
|
|
397
|
+
mutable_instance_data_digest=current_digest,
|
|
398
|
+
expected_service_interpreter=str(slot.interpreter),
|
|
399
|
+
)
|
|
400
|
+
configured = self.service_interpreter(instance)
|
|
401
|
+
if configured is None:
|
|
402
|
+
service = self.controller.register(instance, slot.interpreter)
|
|
403
|
+
elif replaying_verified and configured == slot.interpreter:
|
|
404
|
+
service = {
|
|
405
|
+
"result": "REGISTERED",
|
|
406
|
+
"label": instance.service_label,
|
|
407
|
+
"replayed": True,
|
|
408
|
+
}
|
|
409
|
+
else:
|
|
410
|
+
raise SystemInstanceLifecycleError("EP restore found a foreign service definition")
|
|
411
|
+
return self._receipt(root, state, {
|
|
412
|
+
"lifecycle_state": "RESTORED_REQUIRES_PROVIDER_REVERIFICATION",
|
|
413
|
+
"instance_identity": "PRESERVED",
|
|
414
|
+
"mutable_instance_data": "PRESERVED",
|
|
415
|
+
"mutable_instance_data_digest": current_digest,
|
|
416
|
+
"restored_from_preserve_operation": preserve_operation_id,
|
|
417
|
+
"restored": True,
|
|
418
|
+
"restorable": False,
|
|
419
|
+
"service_state": "REGISTERED_INACTIVE",
|
|
420
|
+
"shared_immutable_runtime_slots": "PRESERVED",
|
|
421
|
+
"provider_auth_state": _PROVIDER_PRESERVED,
|
|
422
|
+
"ready": False,
|
|
423
|
+
"release": release_identity,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
def prepare_purge(
|
|
427
|
+
self,
|
|
428
|
+
instance: system_installation_topology.SystemInstanceTopology,
|
|
429
|
+
descriptor: Mapping[str, object],
|
|
430
|
+
operation_id: str,
|
|
431
|
+
*,
|
|
432
|
+
confirm_instance_id: str,
|
|
433
|
+
) -> Mapping[str, object]:
|
|
434
|
+
if confirm_instance_id != instance.instance_id:
|
|
435
|
+
raise SystemInstanceLifecycleError("EP purge confirmation mismatch")
|
|
436
|
+
request = {
|
|
437
|
+
"instance_id": instance.instance_id,
|
|
438
|
+
"descriptor_digest": _json_digest(dict(descriptor)),
|
|
439
|
+
}
|
|
440
|
+
root, state = self._state(instance.instance_id, operation_id, "PURGE", request)
|
|
441
|
+
if state.get("phase") == "COMPLETE":
|
|
442
|
+
return state
|
|
443
|
+
with _Lock(instance.lifecycle_lock):
|
|
444
|
+
self.controller.remove(instance)
|
|
445
|
+
if self.controller.loaded(instance) or self.service_interpreter(instance) is not None:
|
|
446
|
+
raise SystemInstanceLifecycleError("EP purge service was not fully removed")
|
|
447
|
+
tree_digest = _tree_digest(
|
|
448
|
+
instance.root,
|
|
449
|
+
ignored_roots=(instance.operations_root / operation_id,),
|
|
450
|
+
)
|
|
451
|
+
if state.get("phase") == "PURGE_READY":
|
|
452
|
+
if state.get("mutable_instance_data_digest") != tree_digest:
|
|
453
|
+
raise SystemInstanceLifecycleError("EP purge target changed after preparation")
|
|
454
|
+
return state
|
|
455
|
+
return self._save(
|
|
456
|
+
root,
|
|
457
|
+
state,
|
|
458
|
+
"PURGE_READY",
|
|
459
|
+
mutable_instance_data_digest=tree_digest,
|
|
460
|
+
descriptor_digest=request["descriptor_digest"],
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def resume_purge_target(
|
|
464
|
+
self,
|
|
465
|
+
instance: system_installation_topology.SystemInstanceTopology,
|
|
466
|
+
descriptor: Mapping[str, object],
|
|
467
|
+
operation_id: str,
|
|
468
|
+
) -> None:
|
|
469
|
+
root = self._operation_root(instance.instance_id, operation_id)
|
|
470
|
+
state = _read_json(root / "state.json")
|
|
471
|
+
if (
|
|
472
|
+
state.get("contract") != CONTRACT
|
|
473
|
+
or state.get("operation") != "PURGE"
|
|
474
|
+
or state.get("phase") != "PURGE_READY"
|
|
475
|
+
or state.get("instance_id") != instance.instance_id
|
|
476
|
+
or state.get("descriptor_digest") != _json_digest(dict(descriptor))
|
|
477
|
+
):
|
|
478
|
+
raise SystemInstanceLifecycleError("EP purge recovery evidence is invalid")
|
|
479
|
+
with _Lock(instance.lifecycle_lock):
|
|
480
|
+
self.controller.remove(instance)
|
|
481
|
+
if self.controller.loaded(instance) or self.service_interpreter(instance) is not None:
|
|
482
|
+
raise SystemInstanceLifecycleError("EP purge recovery service was not fully removed")
|
|
483
|
+
if _tree_digest(
|
|
484
|
+
instance.root,
|
|
485
|
+
ignored_roots=(instance.operations_root / operation_id,),
|
|
486
|
+
) != state.get("mutable_instance_data_digest"):
|
|
487
|
+
raise SystemInstanceLifecycleError("EP purge recovery target changed after preparation")
|
|
488
|
+
expected = self.product.system_root / "instances" / instance.instance_id
|
|
489
|
+
if (
|
|
490
|
+
instance.root != expected
|
|
491
|
+
or instance.root.is_symlink()
|
|
492
|
+
or not instance.root.is_relative_to(self.product.system_root / "instances")
|
|
493
|
+
):
|
|
494
|
+
raise SystemInstanceLifecycleError("EP purge recovery target is invalid")
|
|
495
|
+
shutil.rmtree(instance.root)
|
|
496
|
+
|
|
497
|
+
def record_purge(
|
|
498
|
+
self,
|
|
499
|
+
instance_id: str,
|
|
500
|
+
operation_id: str,
|
|
501
|
+
remove_receipt: Mapping[str, object],
|
|
502
|
+
) -> Mapping[str, object]:
|
|
503
|
+
root = self._operation_root(instance_id, operation_id)
|
|
504
|
+
state_path = root / "state.json"
|
|
505
|
+
if state_path.exists():
|
|
506
|
+
state = _read_json(state_path)
|
|
507
|
+
if (
|
|
508
|
+
state.get("contract") != CONTRACT
|
|
509
|
+
or state.get("operation") != "PURGE"
|
|
510
|
+
or state.get("instance_id") != instance_id
|
|
511
|
+
or state.get("phase") not in {"PURGE_READY", "COMPLETE"}
|
|
512
|
+
):
|
|
513
|
+
raise SystemInstanceLifecycleError("EP purge operation evidence is invalid")
|
|
514
|
+
else:
|
|
515
|
+
request = {
|
|
516
|
+
"instance_id": instance_id,
|
|
517
|
+
"adopted_terminal_remove_receipt_sha256": remove_receipt.get("receipt_sha256"),
|
|
518
|
+
}
|
|
519
|
+
root, state = self._state(instance_id, operation_id, "PURGE", request)
|
|
520
|
+
if state.get("phase") == "COMPLETE":
|
|
521
|
+
return self._terminal(root, state)
|
|
522
|
+
instance_root = self.product.system_root / "instances" / instance_id
|
|
523
|
+
if instance_root.exists() or instance_root.is_symlink():
|
|
524
|
+
raise SystemInstanceLifecycleError("EP destructive remove did not purge instance root")
|
|
525
|
+
tombstone = {
|
|
526
|
+
"contract": CONTRACT,
|
|
527
|
+
"instance_id": instance_id,
|
|
528
|
+
"lifecycle_state": "PURGED",
|
|
529
|
+
"restorable": False,
|
|
530
|
+
"purge_operation_id": operation_id,
|
|
531
|
+
"legacy_remove_receipt_sha256": remove_receipt.get("receipt_sha256"),
|
|
532
|
+
}
|
|
533
|
+
tombstone["tombstone_sha256"] = _json_digest(tombstone)
|
|
534
|
+
tombstone_path = self._root(instance_id) / "purged.json"
|
|
535
|
+
if tombstone_path.exists():
|
|
536
|
+
if _read_json(tombstone_path) != tombstone:
|
|
537
|
+
raise SystemInstanceLifecycleError("EP purge tombstone conflicts with prior evidence")
|
|
538
|
+
else:
|
|
539
|
+
_atomic_json(tombstone_path, tombstone)
|
|
540
|
+
return self._receipt(root, state, {
|
|
541
|
+
"lifecycle_state": "PURGED",
|
|
542
|
+
"instance_identity": "RETIRED",
|
|
543
|
+
"mutable_instance_data": "REMOVED",
|
|
544
|
+
"restorable": False,
|
|
545
|
+
"service_state": "REMOVED",
|
|
546
|
+
"shared_immutable_runtime_slots": "PRESERVED",
|
|
547
|
+
"provider_auth_state": "REMOVED_WITH_INSTANCE_DATA",
|
|
548
|
+
"legacy_remove_receipt_sha256": remove_receipt.get("receipt_sha256"),
|
|
549
|
+
"purge_tombstone_sha256": tombstone["tombstone_sha256"],
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
def status(self, instance_id: str, operation_id: str) -> Mapping[str, object]:
|
|
553
|
+
root = self._operation_root(instance_id, operation_id)
|
|
554
|
+
state = _read_json(root / "state.json")
|
|
555
|
+
result: dict[str, object] = {
|
|
556
|
+
"contract": CONTRACT,
|
|
557
|
+
"operation": state.get("operation"),
|
|
558
|
+
"operation_id": operation_id,
|
|
559
|
+
"instance_id": instance_id,
|
|
560
|
+
"phase": state.get("phase"),
|
|
561
|
+
"state": "IN_PROGRESS",
|
|
562
|
+
}
|
|
563
|
+
if state.get("phase") == "COMPLETE":
|
|
564
|
+
receipt = self._terminal(root, state)
|
|
565
|
+
evidence = receipt["evidence"]
|
|
566
|
+
result.update({
|
|
567
|
+
"state": "COMPLETE",
|
|
568
|
+
"lifecycle_state": evidence.get("lifecycle_state"),
|
|
569
|
+
"restorable": evidence.get("restorable"),
|
|
570
|
+
"receipt_sha256": receipt.get("receipt_sha256"),
|
|
571
|
+
})
|
|
572
|
+
return result
|