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.
Files changed (176) hide show
  1. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/PKG-INFO +1 -1
  2. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/pyproject.toml +1 -1
  3. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +1 -1
  4. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +4 -4
  5. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_version.py +1 -1
  6. engineering_platform-2.3.103/src/engineering_platform/system_instance_lifecycle.py +572 -0
  7. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_instance_provisioner.py +104 -1
  8. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/templates/workspace-config.json +1 -1
  9. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/PKG-INFO +1 -1
  10. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/SOURCES.txt +1 -0
  11. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/README.md +0 -0
  12. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/setup.cfg +0 -0
  13. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/__init__.py +0 -0
  14. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/__main__.py +0 -0
  15. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/agent_state.py +0 -0
  16. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/agent_trust.py +0 -0
  17. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard.css +0 -0
  18. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard.js +0 -0
  19. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard_locales.mjs +0 -0
  20. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard_status_store.mjs +0 -0
  21. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/dashboard_translation.mjs +0 -0
  22. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  23. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  24. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/icon-dark.png +0 -0
  25. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/icon-light.png +0 -0
  26. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  27. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assets/operations-console/manifest.webmanifest +0 -0
  28. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/assurance_scope.py +0 -0
  29. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/capability_preflight.py +0 -0
  30. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/capability_review.py +0 -0
  31. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_data_transfer.py +0 -0
  32. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_database.py +0 -0
  33. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_operational_reset.py +0 -0
  34. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/central_store_migration.py +0 -0
  35. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/codex_capacity.py +0 -0
  36. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/codex_chat.py +0 -0
  37. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/codex_observability.py +0 -0
  38. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/component_lock.py +0 -0
  39. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/component_logging.py +0 -0
  40. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/console_presentation.py +0 -0
  41. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/console_route_ownership.py +0 -0
  42. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/__init__.py +0 -0
  43. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/ep_consumer.py +0 -0
  44. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/models.py +0 -0
  45. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/contracts/projection.py +0 -0
  46. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_browser_validation.py +0 -0
  47. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_state.py +0 -0
  48. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_supervisor.swift +0 -0
  49. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dashboard_translation.py +0 -0
  50. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/dependabot_producer.py +0 -0
  51. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/development_profile.py +0 -0
  52. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/drift_diagnostics.py +0 -0
  53. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/emergency_recovery.py +0 -0
  54. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/engineering_memory.py +0 -0
  55. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/ep_consumer_credentials.py +0 -0
  56. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/evidence_projection.py +0 -0
  57. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_activity.py +0 -0
  58. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_context.py +0 -0
  59. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_errors.py +0 -0
  60. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_evidence.py +0 -0
  61. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_executor.py +0 -0
  62. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_finalization.py +0 -0
  63. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_host.py +0 -0
  64. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_host_evidence.py +0 -0
  65. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_lease.py +0 -0
  66. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_lifecycle.py +0 -0
  67. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_models.py +0 -0
  68. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_readiness.py +0 -0
  69. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_reporting.py +0 -0
  70. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_repository.py +0 -0
  71. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_timeout_policy.py +0 -0
  72. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_timing.py +0 -0
  73. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/execution_transaction.py +0 -0
  74. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/external_producer_binding.py +0 -0
  75. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/file_inbox.py +0 -0
  76. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/forensic_attribution.py +0 -0
  77. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/forensic_attribution_v2.py +0 -0
  78. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/forensic_delta.py +0 -0
  79. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/golden_scenario.py +0 -0
  80. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/historical_dashboard_configuration.py +0 -0
  81. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/host_admin.py +0 -0
  82. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/host_preflight.py +0 -0
  83. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_relocation.py +0 -0
  84. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_activation.py +0 -0
  85. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_admission.py +0 -0
  86. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_backup.py +0 -0
  87. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_composition.py +0 -0
  88. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_executor.py +0 -0
  89. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_migration.py +0 -0
  90. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_operation.py +0 -0
  91. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_plan.py +0 -0
  92. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/installation_update_preparation.py +0 -0
  93. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/investigation_ledger.py +0 -0
  94. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/legacy_inbox_migration.py +0 -0
  95. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/legacy_installation_adoption.py +0 -0
  96. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/lifecycle_worker.py +0 -0
  97. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/live_status.py +0 -0
  98. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/local_api.py +0 -0
  99. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/local_api_keychain.py +0 -0
  100. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/local_repository_binding.py +0 -0
  101. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_autonomy.py +0 -0
  102. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_codex_runtime.py +0 -0
  103. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_workspace_readiness.py +0 -0
  104. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/managed_workspace_recovery.py +0 -0
  105. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/merge_delegation.py +0 -0
  106. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/operational_installation.py +0 -0
  107. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/operational_installation_lock.py +0 -0
  108. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/operational_installation_record.py +0 -0
  109. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/owner_credential_recovery.py +0 -0
  110. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/parity_context.py +0 -0
  111. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/parity_lifecycle_dispatcher.py +0 -0
  112. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_admin.py +0 -0
  113. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_api.py +0 -0
  114. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_bootstrap.py +0 -0
  115. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/platform_components.py +0 -0
  116. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/pr_check_repair.py +0 -0
  117. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/pr_evidence_backfill.py +0 -0
  118. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/producer.py +0 -0
  119. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/product_installation_readback.py +0 -0
  120. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/project_agent.py +0 -0
  121. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/project_agent_service.py +0 -0
  122. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/project_topology.py +0 -0
  123. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/prompt_history.py +0 -0
  124. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_context.py +0 -0
  125. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_context_benchmark.py +0 -0
  126. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_context_scope.py +0 -0
  127. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_interruption.py +0 -0
  128. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_process_identity.py +0 -0
  129. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_readiness.py +0 -0
  130. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_recovery.py +0 -0
  131. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/provider_usage.py +0 -0
  132. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/providers.py +0 -0
  133. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/qualification.py +0 -0
  134. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/qualification_runtime.py +0 -0
  135. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/recommendation_handoff.py +0 -0
  136. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/reconciliation_adoption.py +0 -0
  137. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/release_operation.py +0 -0
  138. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/report_analysis.py +0 -0
  139. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/repository_attachment.py +0 -0
  140. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/repository_handoff.py +0 -0
  141. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/resources.py +0 -0
  142. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/reviewer_evidence.py +0 -0
  143. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/revision_binding.py +0 -0
  144. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/producer-readback-v1.2.schema.json +0 -0
  145. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/producer-readback-v1.3.schema.json +0 -0
  146. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/producer-readback-v1.schema.json +0 -0
  147. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/schemas/repository-attachment.schema.json +0 -0
  148. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server.py +0 -0
  149. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server_console_services.py +0 -0
  150. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server_relay.py +0 -0
  151. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/server_service.py +0 -0
  152. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/status_model.py +0 -0
  153. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/status_reconciliation.py +0 -0
  154. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/storage.py +0 -0
  155. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/submission_cli.py +0 -0
  156. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/submission_intake.py +0 -0
  157. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/submission_service.py +0 -0
  158. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_installation_topology.py +0 -0
  159. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_provider_context.py +0 -0
  160. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/system_server_service.py +0 -0
  161. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry.py +0 -0
  162. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry_contract.py +0 -0
  163. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry_export.py +0 -0
  164. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/telemetry_metrics.py +0 -0
  165. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/terminal_evidence_reconciliation.py +0 -0
  166. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/validation_identity.py +0 -0
  167. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/validation_profile.py +0 -0
  168. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/version_preparation_delivery.py +0 -0
  169. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/workspace_preflight.py +0 -0
  170. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/worktree_provenance.py +0 -0
  171. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform/worktree_tooling.py +0 -0
  172. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/dependency_links.txt +0 -0
  173. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/entry_points.txt +0 -0
  174. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/src/engineering_platform.egg-info/top_level.txt +0 -0
  175. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/tests/test_adaptive_action_sizing_design.py +0 -0
  176. {engineering_platform-2.3.102 → engineering_platform-2.3.103}/tests/test_role_task_model_policy_design.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-platform
3
- Version: 2.3.102
3
+ Version: 2.3.103
4
4
  Summary: Local-first Engineering Platform execution operations runtime
5
5
  Author: Peter van Tol
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "engineering-platform"
7
- version = "2.3.102"
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"
@@ -3,7 +3,7 @@
3
3
  "platform": {
4
4
  "id": "engineering-platform",
5
5
  "name": "Engineering Platform",
6
- "version": "2.3.102",
6
+ "version": "2.3.103",
7
7
  "generation": 2,
8
8
  "documentation_namespace": "engineering-platform",
9
9
  "capability_registry_version": 1
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bootstrap_contract": "2026.12",
3
3
  "checkpoint_format": 1,
4
- "dashboard_version": "2.3.102",
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.102",
9
+ "platform_version": "2.3.103",
10
10
  "report_format": 2,
11
- "runner_version": "2.3.102",
11
+ "runner_version": "2.3.103",
12
12
  "status_model": 1,
13
13
  "storage_schema": 45,
14
- "watcher_version": "2.3.102"
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.102"
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