engineering-platform 2.3.81__tar.gz → 2.3.82__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 (168) hide show
  1. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/PKG-INFO +1 -1
  2. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/pyproject.toml +1 -1
  3. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +1 -1
  4. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +4 -4
  5. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/central_operational_reset.py +490 -12
  6. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/platform_version.py +1 -1
  7. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/templates/workspace-config.json +1 -1
  8. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform.egg-info/PKG-INFO +1 -1
  9. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/README.md +0 -0
  10. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/setup.cfg +0 -0
  11. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/__init__.py +0 -0
  12. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/__main__.py +0 -0
  13. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/agent_state.py +0 -0
  14. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/agent_trust.py +0 -0
  15. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/dashboard.css +0 -0
  16. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/dashboard.js +0 -0
  17. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/dashboard_locales.mjs +0 -0
  18. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/dashboard_status_store.mjs +0 -0
  19. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/dashboard_translation.mjs +0 -0
  20. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  21. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  22. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/operations-console/icon-dark.png +0 -0
  23. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/operations-console/icon-light.png +0 -0
  24. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  25. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/assets/operations-console/manifest.webmanifest +0 -0
  26. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/capability_preflight.py +0 -0
  27. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/capability_review.py +0 -0
  28. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/central_data_transfer.py +0 -0
  29. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/central_database.py +0 -0
  30. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/central_store_migration.py +0 -0
  31. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/codex_capacity.py +0 -0
  32. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/codex_chat.py +0 -0
  33. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/codex_observability.py +0 -0
  34. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/component_lock.py +0 -0
  35. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/component_logging.py +0 -0
  36. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/console_presentation.py +0 -0
  37. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/console_route_ownership.py +0 -0
  38. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/contracts/__init__.py +0 -0
  39. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/contracts/ep_consumer.py +0 -0
  40. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/contracts/models.py +0 -0
  41. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/contracts/projection.py +0 -0
  42. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/dashboard_browser_validation.py +0 -0
  43. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/dashboard_state.py +0 -0
  44. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/dashboard_supervisor.swift +0 -0
  45. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/dashboard_translation.py +0 -0
  46. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/dependabot_producer.py +0 -0
  47. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/development_profile.py +0 -0
  48. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/drift_diagnostics.py +0 -0
  49. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/emergency_recovery.py +0 -0
  50. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/engineering_memory.py +0 -0
  51. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/ep_consumer_credentials.py +0 -0
  52. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/evidence_projection.py +0 -0
  53. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_activity.py +0 -0
  54. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_context.py +0 -0
  55. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_errors.py +0 -0
  56. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_evidence.py +0 -0
  57. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_executor.py +0 -0
  58. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_finalization.py +0 -0
  59. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_host.py +0 -0
  60. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_host_evidence.py +0 -0
  61. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_lease.py +0 -0
  62. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_lifecycle.py +0 -0
  63. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_models.py +0 -0
  64. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_readiness.py +0 -0
  65. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_reporting.py +0 -0
  66. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_repository.py +0 -0
  67. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_timeout_policy.py +0 -0
  68. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_timing.py +0 -0
  69. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/execution_transaction.py +0 -0
  70. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/external_producer_binding.py +0 -0
  71. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/file_inbox.py +0 -0
  72. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/forensic_attribution.py +0 -0
  73. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/forensic_attribution_v2.py +0 -0
  74. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/forensic_delta.py +0 -0
  75. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/golden_scenario.py +0 -0
  76. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/historical_dashboard_configuration.py +0 -0
  77. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/host_admin.py +0 -0
  78. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/host_preflight.py +0 -0
  79. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_relocation.py +0 -0
  80. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_activation.py +0 -0
  81. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_admission.py +0 -0
  82. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_backup.py +0 -0
  83. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_composition.py +0 -0
  84. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_executor.py +0 -0
  85. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_migration.py +0 -0
  86. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_operation.py +0 -0
  87. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_plan.py +0 -0
  88. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/installation_update_preparation.py +0 -0
  89. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/investigation_ledger.py +0 -0
  90. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/legacy_inbox_migration.py +0 -0
  91. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/legacy_installation_adoption.py +0 -0
  92. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/lifecycle_worker.py +0 -0
  93. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/live_status.py +0 -0
  94. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/local_api.py +0 -0
  95. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/local_api_keychain.py +0 -0
  96. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/local_repository_binding.py +0 -0
  97. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/managed_autonomy.py +0 -0
  98. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/managed_codex_runtime.py +0 -0
  99. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/operational_installation.py +0 -0
  100. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/operational_installation_lock.py +0 -0
  101. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/operational_installation_record.py +0 -0
  102. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/owner_credential_recovery.py +0 -0
  103. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/parity_context.py +0 -0
  104. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/parity_lifecycle_dispatcher.py +0 -0
  105. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/platform_admin.py +0 -0
  106. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/platform_api.py +0 -0
  107. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/platform_bootstrap.py +0 -0
  108. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/platform_components.py +0 -0
  109. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/pr_check_repair.py +0 -0
  110. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/pr_evidence_backfill.py +0 -0
  111. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/producer.py +0 -0
  112. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/product_installation_readback.py +0 -0
  113. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/project_agent.py +0 -0
  114. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/project_agent_service.py +0 -0
  115. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/project_topology.py +0 -0
  116. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/prompt_history.py +0 -0
  117. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_context.py +0 -0
  118. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_context_benchmark.py +0 -0
  119. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_context_scope.py +0 -0
  120. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_interruption.py +0 -0
  121. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_process_identity.py +0 -0
  122. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_readiness.py +0 -0
  123. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_recovery.py +0 -0
  124. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/provider_usage.py +0 -0
  125. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/providers.py +0 -0
  126. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/qualification.py +0 -0
  127. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/qualification_runtime.py +0 -0
  128. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/recommendation_handoff.py +0 -0
  129. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/reconciliation_adoption.py +0 -0
  130. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/release_operation.py +0 -0
  131. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/report_analysis.py +0 -0
  132. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/repository_attachment.py +0 -0
  133. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/repository_handoff.py +0 -0
  134. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/resources.py +0 -0
  135. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/reviewer_evidence.py +0 -0
  136. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/revision_binding.py +0 -0
  137. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/schemas/producer-readback-v1.2.schema.json +0 -0
  138. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/schemas/producer-readback-v1.schema.json +0 -0
  139. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/schemas/repository-attachment.schema.json +0 -0
  140. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/server.py +0 -0
  141. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/server_console_services.py +0 -0
  142. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/server_relay.py +0 -0
  143. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/server_service.py +0 -0
  144. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/status_model.py +0 -0
  145. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/status_reconciliation.py +0 -0
  146. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/storage.py +0 -0
  147. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/submission_cli.py +0 -0
  148. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/submission_intake.py +0 -0
  149. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/submission_service.py +0 -0
  150. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/system_installation_topology.py +0 -0
  151. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/system_server_service.py +0 -0
  152. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/telemetry.py +0 -0
  153. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/telemetry_contract.py +0 -0
  154. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/telemetry_export.py +0 -0
  155. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/telemetry_metrics.py +0 -0
  156. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/terminal_evidence_reconciliation.py +0 -0
  157. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/validation_identity.py +0 -0
  158. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/validation_profile.py +0 -0
  159. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/version_preparation_delivery.py +0 -0
  160. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/workspace_preflight.py +0 -0
  161. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/worktree_provenance.py +0 -0
  162. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform/worktree_tooling.py +0 -0
  163. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform.egg-info/SOURCES.txt +0 -0
  164. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform.egg-info/dependency_links.txt +0 -0
  165. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform.egg-info/entry_points.txt +0 -0
  166. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/src/engineering_platform.egg-info/top_level.txt +0 -0
  167. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/tests/test_adaptive_action_sizing_design.py +0 -0
  168. {engineering_platform-2.3.81 → engineering_platform-2.3.82}/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.81
3
+ Version: 2.3.82
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.81"
7
+ version = "2.3.82"
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.81",
6
+ "version": "2.3.82",
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.81",
4
+ "dashboard_version": "2.3.82",
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.81",
9
+ "platform_version": "2.3.82",
10
10
  "report_format": 2,
11
- "runner_version": "2.3.81",
11
+ "runner_version": "2.3.82",
12
12
  "status_model": 1,
13
13
  "storage_schema": 45,
14
- "watcher_version": "2.3.81"
14
+ "watcher_version": "2.3.82"
15
15
  }
@@ -36,6 +36,14 @@ PROFILE = "EP_CENTRAL_OPERATIONAL_HISTORY_V1"
36
36
  PLAN_VERSION = 1
37
37
  SCHEMA_VERSION = 68
38
38
  _OPERATION = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{7,127}")
39
+ _INSTANCE_ID = re.compile(
40
+ r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
41
+ )
42
+ _UPDATE_STEPS = (
43
+ "INSTALLATION_LOCK", "INVENTORY_AND_COMPATIBILITY", "EXACT_ARTIFACT",
44
+ "QUIESCE", "BACKUP_AND_MIGRATION", "ACTIVATE", "VERIFY", "CLEANUP",
45
+ )
46
+ _UPDATE_CLEANUP_DIRECTORIES = ("build", "download", "pip-cache")
39
47
  _ACTIVE_STATES = frozenset({
40
48
  "PREPARING", "AUTHORIZED", "ARTIFACTS_ARCHIVING", "ARTIFACTS_ARCHIVED",
41
49
  "DB_APPLIED", "VERIFIED", "FAILED",
@@ -469,6 +477,10 @@ def _actor(data_root: Path) -> str:
469
477
  return f"uid:{uid}:{name}"
470
478
 
471
479
 
480
+ def _valid_instance_id(value: object) -> bool:
481
+ return isinstance(value, str) and _INSTANCE_ID.fullmatch(value) is not None
482
+
483
+
472
484
  def _identity(data_root: Path, connection: sqlite3.Connection) -> dict[str, object]:
473
485
  try:
474
486
  file_identity = json.loads((data_root / "runtime-identity.json").read_text(encoding="utf-8"))
@@ -478,16 +490,22 @@ def _identity(data_root: Path, connection: sqlite3.Connection) -> dict[str, obje
478
490
  metadata = connection.execute(
479
491
  "SELECT value FROM engineering_metadata WHERE key='installation.instance_id'"
480
492
  ).fetchone()
493
+ runtime_instance = file_identity.get("instance_id") if isinstance(file_identity, dict) else None
494
+ database_instance = row[0] if row is not None else None
495
+ metadata_instance = metadata[0] if metadata is not None else None
481
496
  if (
482
- not isinstance(file_identity, dict) or not isinstance(file_identity.get("instance_id"), str)
483
- or row is None or metadata is None or file_identity["instance_id"] != row[0] != ""
484
- or row[0] != metadata[0]
497
+ not _valid_instance_id(runtime_instance)
498
+ or not _valid_instance_id(database_instance)
499
+ or not _valid_instance_id(metadata_instance)
500
+ or runtime_instance != database_instance
501
+ or runtime_instance != metadata_instance
502
+ or database_instance != metadata_instance
485
503
  ):
486
504
  raise OperationalResetError("TARGET_IDENTITY_CONFLICT")
487
505
  database = (data_root / central_database.DATABASE_FILENAME).resolve()
488
506
  stat = database.stat()
489
507
  return {
490
- "product": "engineering-platform", "instance_id": str(row[0]),
508
+ "product": "engineering-platform", "instance_id": str(database_instance),
491
509
  "data_root": str(data_root), "database": str(database), "schema_version": int(row[1]),
492
510
  "database_device": stat.st_dev, "database_inode": stat.st_ino,
493
511
  }
@@ -584,11 +602,408 @@ def _safe_files(directory: Path, logical_root: str) -> list[dict[str, object]]:
584
602
  return rows
585
603
 
586
604
 
605
+ def _candidate_runtime_marker(
606
+ operation_root: Path, *, expected_installation_id: str,
607
+ require_staged_artifact: bool,
608
+ ) -> dict[str, object] | None:
609
+ """Read the updater's exact, closed, token-free candidate identity."""
610
+ if not _valid_instance_id(expected_installation_id):
611
+ return None
612
+ operation_id = operation_root.name
613
+ marker = _regular_json_object(operation_root / "candidate-runtime.json")
614
+ if marker is None or _OPERATION.fullmatch(operation_id) is None:
615
+ return None
616
+ expected_candidate = str(operation_root / "candidate-venv")
617
+ if (
618
+ set(marker) != {
619
+ "schema_version", "operation_id", "installation_id", "target_version",
620
+ "target_digest", "target_source_revision", "staged_artifact", "candidate_venv",
621
+ }
622
+ or marker.get("schema_version") != 1
623
+ or marker.get("operation_id") != operation_id
624
+ or not _valid_instance_id(marker.get("installation_id"))
625
+ or marker.get("installation_id") != expected_installation_id
626
+ or marker.get("candidate_venv") != expected_candidate
627
+ or re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)",
628
+ str(marker.get("target_version", ""))) is None
629
+ or re.fullmatch(r"sha256:[0-9a-f]{64}", str(marker.get("target_digest", ""))) is None
630
+ or re.fullmatch(r"[0-9a-f]{40}", str(marker.get("target_source_revision", ""))) is None
631
+ ):
632
+ return None
633
+ staged = marker.get("staged_artifact")
634
+ if not isinstance(staged, str):
635
+ return None
636
+ staged_path = Path(staged)
637
+ if (
638
+ not staged_path.is_absolute()
639
+ or staged_path != Path(os.path.normpath(staged))
640
+ or staged_path.parent != operation_root / "download"
641
+ or staged_path.name != (
642
+ f"engineering_platform-{marker['target_version']}-py3-none-any.whl"
643
+ )
644
+ ):
645
+ return None
646
+ if require_staged_artifact:
647
+ try:
648
+ metadata = staged_path.lstat()
649
+ except OSError:
650
+ return None
651
+ if (
652
+ not stat.S_ISREG(metadata.st_mode)
653
+ or _regular_file_digest(staged_path) != marker["target_digest"]
654
+ ):
655
+ return None
656
+ return marker
657
+
658
+
659
+ def _path_is_absent(path: Path) -> bool:
660
+ """Distinguish an absent path from a dangling link or unreadable entry."""
661
+ try:
662
+ path.lstat()
663
+ except FileNotFoundError:
664
+ return True
665
+ except OSError:
666
+ return False
667
+ return False
668
+
669
+
670
+ def _unbound_candidate_runtime_marker(
671
+ operation_root: Path, *, expected_installation_id: str,
672
+ ) -> dict[str, object] | None:
673
+ """Return a closed candidate marker only in the pre-journal crash window."""
674
+ if not _path_is_absent(operation_root / "operation.json"):
675
+ return None
676
+ return _candidate_runtime_marker(
677
+ operation_root, expected_installation_id=expected_installation_id,
678
+ require_staged_artifact=True,
679
+ )
680
+
681
+
682
+ def _opaque_update_directory_classification(
683
+ root_name: str, directory: Path, relative: str, *, expected_installation_id: str,
684
+ ) -> str | None:
685
+ """Recognize only an identity-bound updater runtime/staging directory.
686
+
687
+ Merely finding ``operation.json`` beside a venv-shaped directory is not
688
+ ownership evidence: an arbitrary file must not turn unknown application
689
+ data into an opaque preserve boundary. The journal and the updater's
690
+ candidate marker therefore have to bind the same operation, installation,
691
+ plan and exact paths before normal Python venv symlinks become opaque.
692
+ """
693
+ parts = Path(relative).parts
694
+ if (
695
+ root_name != "operations" or len(parts) != 2
696
+ or _OPERATION.fullmatch(parts[0]) is None
697
+ or parts[1] not in {"candidate-venv", "download", "pip-cache"}
698
+ ):
699
+ return None
700
+
701
+ operation_id = parts[0]
702
+ operation_root = directory.parent
703
+ unbound_marker = _unbound_candidate_runtime_marker(
704
+ operation_root, expected_installation_id=expected_installation_id,
705
+ )
706
+ if unbound_marker is not None:
707
+ return "INSTALLATION_RUNTIME_STAGING_UNBOUND"
708
+ if parts[1] != "candidate-venv":
709
+ return None
710
+ data_root = operation_root.parent.parent
711
+ journal = _regular_json_object(operation_root / "operation.json")
712
+ marker = _candidate_runtime_marker(
713
+ operation_root, expected_installation_id=expected_installation_id,
714
+ require_staged_artifact=False,
715
+ )
716
+ if journal is None:
717
+ return None
718
+ if marker is None:
719
+ return None
720
+ schema = journal.get("schema_version")
721
+ base_fields = {
722
+ "schema_version", "operation_id", "plan", "plan_digest", "state", "events",
723
+ }
724
+ fields_by_schema = {
725
+ 1: base_fields,
726
+ 2: base_fields | {"prepared_candidate", "prepared_candidate_digest"},
727
+ 3: base_fields | {
728
+ "prepared_candidate", "prepared_candidate_digest",
729
+ "prepared_record_provenance", "prepared_record_provenance_digest",
730
+ },
731
+ 4: base_fields | {
732
+ "prepared_candidate", "prepared_candidate_digest",
733
+ "prepared_record_provenance", "prepared_record_provenance_digest",
734
+ "execution_admission", "execution_admission_digest",
735
+ },
736
+ }
737
+ if (
738
+ type(schema) is not int
739
+ or set(journal) != fields_by_schema.get(schema)
740
+ or not isinstance(journal.get("events"), list)
741
+ or journal.get("state") not in {
742
+ "PREPARED", "INVENTORIED", "QUIESCING", "QUIESCED", "BACKED_UP",
743
+ "MIGRATED", "ACTIVATED", "VERIFIED", "CLEANUP_PENDING", "COMPLETE",
744
+ }
745
+ ):
746
+ return None
747
+ plan = journal.get("plan")
748
+ if not isinstance(plan, dict):
749
+ return None
750
+ plan_fields = {
751
+ "operation_id", "installation_id", "data_root", "current_version",
752
+ "current_digest", "target_version", "target_digest", "target_source_revision",
753
+ "artifact", "cleanup_targets", "steps",
754
+ }
755
+ installation_id = plan.get("installation_id")
756
+ expected_cleanup = [
757
+ str(operation_root / name) for name in _UPDATE_CLEANUP_DIRECTORIES
758
+ ]
759
+ if (
760
+ frozenset(plan) not in {frozenset(plan_fields), frozenset(plan_fields | {"legacy_adoption"})}
761
+ or journal.get("operation_id") != operation_id
762
+ or plan.get("operation_id") != operation_id
763
+ or installation_id != expected_installation_id
764
+ or plan.get("data_root") != str(data_root)
765
+ or plan.get("cleanup_targets") != expected_cleanup
766
+ or plan.get("steps") != list(_UPDATE_STEPS)
767
+ or re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)",
768
+ str(plan.get("current_version", ""))) is None
769
+ or re.fullmatch(r"sha256:[0-9a-f]{64}", str(plan.get("current_digest", ""))) is None
770
+ ):
771
+ return None
772
+ try:
773
+ canonical_plan = json.dumps(
774
+ plan, sort_keys=True, separators=(",", ":"),
775
+ ).encode("utf-8")
776
+ except (TypeError, ValueError):
777
+ return None
778
+ if journal.get("plan_digest") != "sha256:" + hashlib.sha256(canonical_plan).hexdigest():
779
+ return None
780
+
781
+ expected_candidate = str(directory)
782
+ expected_operation = str(operation_root)
783
+ expected_interpreter = str(directory / "bin" / "python")
784
+ if (
785
+ set(marker) != {
786
+ "schema_version", "operation_id", "installation_id", "target_version",
787
+ "target_digest", "target_source_revision", "staged_artifact", "candidate_venv",
788
+ }
789
+ or marker.get("schema_version") != 1
790
+ or marker.get("operation_id") != operation_id
791
+ or marker.get("installation_id") != installation_id
792
+ or marker.get("candidate_venv") != expected_candidate
793
+ or marker.get("target_version") != plan.get("target_version")
794
+ or marker.get("target_digest") != plan.get("target_digest")
795
+ or marker.get("target_source_revision") != plan.get("target_source_revision")
796
+ or marker.get("staged_artifact") != plan.get("artifact")
797
+ ):
798
+ return None
799
+ staged_artifact = marker.get("staged_artifact")
800
+ if (
801
+ not isinstance(staged_artifact, str)
802
+ or Path(staged_artifact).parent != operation_root / "download"
803
+ ):
804
+ return None
805
+
806
+ prepared = journal.get("prepared_candidate")
807
+ if prepared is None:
808
+ return (
809
+ "INSTALLATION_RUNTIME"
810
+ if journal.get("prepared_candidate_digest") is None else None
811
+ )
812
+ if not isinstance(prepared, dict):
813
+ return None
814
+ package = prepared.get("package")
815
+ if (
816
+ set(prepared) != {
817
+ "operation_id", "installation_id", "operation_root", "staged_artifact",
818
+ "artifact_digest", "candidate_venv", "interpreter", "pip_cache", "package",
819
+ }
820
+ or prepared.get("operation_id") != operation_id
821
+ or prepared.get("installation_id") != installation_id
822
+ or prepared.get("operation_root") != expected_operation
823
+ or prepared.get("candidate_venv") != expected_candidate
824
+ or prepared.get("interpreter") != expected_interpreter
825
+ or prepared.get("staged_artifact") != staged_artifact
826
+ or prepared.get("artifact_digest") != plan.get("target_digest")
827
+ or prepared.get("pip_cache") != str(operation_root / "pip-cache")
828
+ or not isinstance(package, dict)
829
+ or set(package) != {"interpreter", "version", "metadata", "package"}
830
+ or package.get("interpreter") != expected_interpreter
831
+ or package.get("version") != plan.get("target_version")
832
+ or not all(isinstance(package.get(field), str) and package[field]
833
+ for field in ("version", "metadata", "package"))
834
+ or not all(_lexically_within(directory, Path(str(package[field])))
835
+ for field in ("metadata", "package"))
836
+ ):
837
+ return None
838
+ try:
839
+ canonical_prepared = json.dumps(
840
+ prepared, sort_keys=True, separators=(",", ":"),
841
+ ).encode("utf-8")
842
+ except (TypeError, ValueError):
843
+ return None
844
+ if journal.get("prepared_candidate_digest") != (
845
+ "sha256:" + hashlib.sha256(canonical_prepared).hexdigest()
846
+ ):
847
+ return None
848
+ return "INSTALLATION_RUNTIME"
849
+
850
+
851
+ def _lexically_within(root: Path, candidate: Path) -> bool:
852
+ """Check an already persisted absolute path without resolving any link."""
853
+ if (
854
+ not candidate.is_absolute()
855
+ or candidate != Path(os.path.normpath(str(candidate)))
856
+ ):
857
+ return False
858
+ try:
859
+ candidate.relative_to(root)
860
+ except ValueError:
861
+ return False
862
+ return candidate != root
863
+
864
+
865
+ def _regular_json_object(path: Path) -> dict[str, object] | None:
866
+ """Read one bounded regular JSON file without following its final link."""
867
+ descriptor = -1
868
+ try:
869
+ descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
870
+ metadata = os.fstat(descriptor)
871
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > 4 * 1024 * 1024:
872
+ return None
873
+ with os.fdopen(descriptor, "r", encoding="utf-8") as stream:
874
+ descriptor = -1
875
+ value = json.load(stream)
876
+ except (OSError, UnicodeError, json.JSONDecodeError):
877
+ return None
878
+ finally:
879
+ if descriptor >= 0:
880
+ os.close(descriptor)
881
+ return value if isinstance(value, dict) else None
882
+
883
+
884
+ def _regular_file_digest(path: Path) -> str | None:
885
+ """Hash one regular file while refusing a final-component link swap."""
886
+ descriptor = -1
887
+ try:
888
+ descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
889
+ metadata = os.fstat(descriptor)
890
+ if not stat.S_ISREG(metadata.st_mode):
891
+ return None
892
+ digest = hashlib.sha256()
893
+ with os.fdopen(descriptor, "rb") as stream:
894
+ descriptor = -1
895
+ while chunk := stream.read(1024 * 1024):
896
+ digest.update(chunk)
897
+ return "sha256:" + digest.hexdigest()
898
+ except OSError:
899
+ return None
900
+ finally:
901
+ if descriptor >= 0:
902
+ os.close(descriptor)
903
+
904
+
905
+ def _walk_preserved_files(
906
+ directory: Path, *, root_name: str, code: str, expected_installation_id: str,
907
+ opaque_bindings: list[tuple[Path, str, int, int, int]],
908
+ ) -> tuple[list[Path], list[tuple[Path, str, int, int, int]]]:
909
+ """Walk preserved product data while treating exact updater venvs as opaque."""
910
+ if not directory.exists():
911
+ return [], []
912
+ if directory.is_symlink() or not directory.is_dir():
913
+ raise OperationalResetError(code)
914
+ files: list[Path] = []
915
+ opaque: list[tuple[Path, str, int, int, int]] = []
916
+ pending = [directory]
917
+ while pending:
918
+ current = pending.pop()
919
+ try:
920
+ entries = sorted(os.scandir(current), key=lambda entry: entry.name)
921
+ except OSError as error:
922
+ raise OperationalResetError(code) from error
923
+ for entry in entries:
924
+ try:
925
+ path = Path(entry.path)
926
+ metadata = path.lstat()
927
+ if stat.S_ISLNK(metadata.st_mode):
928
+ raise OperationalResetError(code)
929
+ if stat.S_ISDIR(metadata.st_mode):
930
+ relative = path.relative_to(directory).as_posix()
931
+ classification = _opaque_update_directory_classification(
932
+ root_name, path, relative,
933
+ expected_installation_id=expected_installation_id,
934
+ )
935
+ if classification is not None:
936
+ descriptor = _open_opaque_directory(
937
+ path, device=metadata.st_dev, inode=metadata.st_ino,
938
+ )
939
+ binding = (
940
+ path, classification, metadata.st_dev, metadata.st_ino,
941
+ descriptor,
942
+ )
943
+ opaque.append(binding)
944
+ opaque_bindings.append(binding)
945
+ else:
946
+ pending.append(path)
947
+ elif stat.S_ISREG(metadata.st_mode):
948
+ files.append(path)
949
+ else:
950
+ raise OperationalResetError(code)
951
+ except OSError as error:
952
+ raise OperationalResetError(code) from error
953
+ return sorted(files), sorted(opaque, key=lambda item: item[0])
954
+
955
+
956
+ def _open_opaque_directory(path: Path, *, device: int, inode: int) -> int:
957
+ """Pin the accepted real directory until the inventory is published."""
958
+ descriptor = -1
959
+ try:
960
+ descriptor = os.open(
961
+ path,
962
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
963
+ )
964
+ metadata = os.fstat(descriptor)
965
+ if (
966
+ not stat.S_ISDIR(metadata.st_mode)
967
+ or metadata.st_dev != device
968
+ or metadata.st_ino != inode
969
+ ):
970
+ raise OperationalResetError("EXTERNAL_SYMLINK_UNSAFE")
971
+ return descriptor
972
+ except OSError as error:
973
+ if descriptor >= 0:
974
+ os.close(descriptor)
975
+ raise OperationalResetError("EXTERNAL_SYMLINK_UNSAFE") from error
976
+ except BaseException:
977
+ if descriptor >= 0:
978
+ os.close(descriptor)
979
+ raise
980
+
981
+
982
+ def _recheck_opaque_directory(
983
+ path: Path, *, device: int, inode: int, descriptor: int,
984
+ ) -> None:
985
+ """Bind the final opaque receipt to the still-pinned original directory."""
986
+ try:
987
+ pinned = os.fstat(descriptor)
988
+ current = path.lstat()
989
+ except OSError as error:
990
+ raise OperationalResetError("EXTERNAL_SYMLINK_UNSAFE") from error
991
+ if (
992
+ not stat.S_ISDIR(pinned.st_mode)
993
+ or not stat.S_ISDIR(current.st_mode)
994
+ or pinned.st_dev != device or pinned.st_ino != inode
995
+ or current.st_dev != device or current.st_ino != inode
996
+ ):
997
+ raise OperationalResetError("EXTERNAL_SYMLINK_UNSAFE")
998
+
999
+
587
1000
  def _known_top_level(name: str) -> bool:
588
1001
  return name in _KNOWN_TOP_LEVEL or _VERSIONED_RECOVERY_BACKUP.fullmatch(name) is not None
589
1002
 
590
1003
 
591
- def _nested_preserved_classification(root_name: str, relative: str, path: Path) -> str | None:
1004
+ def _nested_preserved_classification(
1005
+ root_name: str, relative: str, path: Path, *, expected_installation_id: str,
1006
+ ) -> str | None:
592
1007
  """Classify only product-owned shapes inside preserved top-level roots."""
593
1008
  parts = Path(relative).parts
594
1009
  if root_name == "runtime":
@@ -605,12 +1020,22 @@ def _nested_preserved_classification(root_name: str, relative: str, path: Path)
605
1020
  return "INSTALLATION_OPERATION_AUDIT"
606
1021
  if parts and _OPERATION.fullmatch(parts[0]) is not None:
607
1022
  operation_root = path.parents[len(parts) - 2]
1023
+ if _unbound_candidate_runtime_marker(
1024
+ operation_root, expected_installation_id=expected_installation_id,
1025
+ ) is not None:
1026
+ if parts[1:] == ("candidate-runtime.json",):
1027
+ return "INSTALLATION_RUNTIME_STAGING_UNBOUND"
1028
+ # The exact staging subtrees are normally opaque. This branch
1029
+ # also keeps a race/retry readback classified if a leaf was
1030
+ # enumerated just before the boundary recognition.
1031
+ if len(parts) >= 3 and parts[1] in {"download", "pip-cache"}:
1032
+ return "INSTALLATION_RUNTIME_STAGING_UNBOUND"
608
1033
  if (operation_root / "operation.json").is_file() and len(parts) >= 2:
609
1034
  if parts[1] in {"operation.json", "candidate-runtime.json"} and len(parts) == 2:
610
1035
  return "INSTALLATION_OPERATION_AUDIT"
611
1036
  if parts[1:] == ("backup", "central.sqlite"):
612
1037
  return "FORENSIC_OR_RECOVERY"
613
- if parts[1] in {"build", "download", "pip-cache", "candidate-venv"}:
1038
+ if parts[1] in {"build", "download", "pip-cache"}:
614
1039
  return "INSTALLATION_RUNTIME_STAGING"
615
1040
  return None
616
1041
  if root_name == "migration":
@@ -651,8 +1076,9 @@ def _nested_preserved_classification(root_name: str, relative: str, path: Path)
651
1076
  return None
652
1077
 
653
1078
 
654
- def _external_inventory(
655
- data_root: Path,
1079
+ def _external_inventory_bound(
1080
+ data_root: Path, *, expected_installation_id: str,
1081
+ opaque_bindings: list[tuple[Path, str, int, int, int]],
656
1082
  ) -> tuple[list[dict[str, object]], list[dict[str, object]], list[str], list[str]]:
657
1083
  unknown = sorted(path.name for path in data_root.iterdir() if not _known_top_level(path.name))
658
1084
  rows: list[dict[str, object]] = []
@@ -693,11 +1119,30 @@ def _external_inventory(
693
1119
  "runtime", "operations", "recovery", "migration", "backups",
694
1120
  "operational-reset-archive",
695
1121
  }:
696
- for nested in _walk_regular_files(path, code="EXTERNAL_SYMLINK_UNSAFE"):
1122
+ nested_files, opaque_boundaries = _walk_preserved_files(
1123
+ path, root_name=path.name, code="EXTERNAL_SYMLINK_UNSAFE",
1124
+ expected_installation_id=expected_installation_id,
1125
+ opaque_bindings=opaque_bindings,
1126
+ )
1127
+ for boundary, classification, _device, _inode, _descriptor in opaque_boundaries:
1128
+ relative = boundary.relative_to(path).as_posix()
1129
+ entry = {
1130
+ "path": f"{path.name}/{relative}",
1131
+ "classification": classification,
1132
+ "effect": "PRESERVE", "kind": "directory",
1133
+ "opaque_boundary": True, "symlinks_followed": False,
1134
+ }
1135
+ if classification == "INSTALLATION_RUNTIME_STAGING_UNBOUND":
1136
+ entry["installation_update_state"] = "INCOMPLETE_NO_OPERATION_JOURNAL"
1137
+ preserved.append(entry)
1138
+ for nested in nested_files:
697
1139
  relative = nested.relative_to(path).as_posix()
698
1140
  if path.name == "runtime" and relative.startswith("central-data-imports/"):
699
1141
  continue
700
- classification = _nested_preserved_classification(path.name, relative, nested)
1142
+ classification = _nested_preserved_classification(
1143
+ path.name, relative, nested,
1144
+ expected_installation_id=expected_installation_id,
1145
+ )
701
1146
  if classification is None:
702
1147
  unknown.append(f"{path.name}/{relative}")
703
1148
  continue
@@ -711,6 +1156,34 @@ def _external_inventory(
711
1156
  return rows, preserved, sorted(set(unknown)), active_ingest_controls
712
1157
 
713
1158
 
1159
+ def _external_inventory(
1160
+ data_root: Path, *, expected_installation_id: str,
1161
+ ) -> tuple[list[dict[str, object]], list[dict[str, object]], list[str], list[str]]:
1162
+ """Inventory external data while pinning every published opaque boundary."""
1163
+ if not _valid_instance_id(expected_installation_id):
1164
+ raise OperationalResetError("TARGET_IDENTITY_CONFLICT")
1165
+ bindings: list[tuple[Path, str, int, int, int]] = []
1166
+ try:
1167
+ result = _external_inventory_bound(
1168
+ data_root, expected_installation_id=expected_installation_id,
1169
+ opaque_bindings=bindings,
1170
+ )
1171
+ # This is intentionally the final work before returning the receipt.
1172
+ # A path swapped after initial acceptance cannot be represented as the
1173
+ # pinned directory that was actually classified.
1174
+ for path, _classification, device, inode, descriptor in bindings:
1175
+ _recheck_opaque_directory(
1176
+ path, device=device, inode=inode, descriptor=descriptor,
1177
+ )
1178
+ return result
1179
+ finally:
1180
+ for _path, _classification, _device, _inode, descriptor in bindings:
1181
+ try:
1182
+ os.close(descriptor)
1183
+ except OSError:
1184
+ pass
1185
+
1186
+
714
1187
  def _runtime_activity(data_root: Path) -> dict[str, object]:
715
1188
  try:
716
1189
  payload = json.loads((data_root / "runtime.json").read_text(encoding="utf-8"))
@@ -807,7 +1280,9 @@ def preview(data_root: Path) -> dict[str, object]:
807
1280
  raise OperationalResetError("CENTRAL_INSPECTION_FAILED") from error
808
1281
  external_error: str | None = None
809
1282
  try:
810
- external, preserved_external, unknown_paths, active_ingest_controls = _external_inventory(root)
1283
+ external, preserved_external, unknown_paths, active_ingest_controls = _external_inventory(
1284
+ root, expected_installation_id=str(identity["instance_id"]),
1285
+ )
811
1286
  except OperationalResetError as error:
812
1287
  external, preserved_external, unknown_paths, active_ingest_controls = [], [], [], []
813
1288
  external_error = error.code
@@ -1725,6 +2200,7 @@ def _verify_under_lock(
1725
2200
  credentials = int(connection.execute("SELECT COUNT(*) FROM ep_consumer_credentials").fetchone()[0])
1726
2201
  projects = int(connection.execute("SELECT COUNT(*) FROM ep_project_registrations").fetchone()[0])
1727
2202
  repositories = int(connection.execute("SELECT COUNT(*) FROM ep_repository_registrations").fetchone()[0])
2203
+ expected_installation_id = str(_identity(root, connection)["instance_id"])
1728
2204
  archive = root / "operational-reset-archive" / operation_id
1729
2205
  boundary: dict[str, object] | None = None
1730
2206
  boundary_path_value = row.get("finish_boundary_path")
@@ -1750,7 +2226,9 @@ def _verify_under_lock(
1750
2226
  raise OperationalResetError("FINISH_BOUNDARY_INVALID")
1751
2227
  if not allow_boundary_recovery:
1752
2228
  frozen_modes = _verify_frozen_empty_active_roots(root)
1753
- external, _preserved, unknown, active_controls = _external_inventory(root)
2229
+ external, _preserved, unknown, active_controls = _external_inventory(
2230
+ root, expected_installation_id=expected_installation_id,
2231
+ )
1754
2232
  backup = verify_backup(
1755
2233
  Path(str(row["backup_path"])), operation_id=operation_id, plan_digest=plan_digest,
1756
2234
  expected_manifest_digest=str(row["backup_manifest_digest"]),
@@ -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.81"
15
+ CURRENT_PLATFORM_VERSION = "2.3.82"
16
16
  MANIFEST_FIELDS = frozenset(
17
17
  {
18
18
  "platform_version",
@@ -3,7 +3,7 @@
3
3
  "platform": {
4
4
  "id": "engineering-platform",
5
5
  "name": "Engineering Platform",
6
- "version": "2.3.81",
6
+ "version": "2.3.82",
7
7
  "generation": 2,
8
8
  "documentation_namespace": "engineering-platform",
9
9
  "capability_registry_version": 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engineering-platform
3
- Version: 2.3.81
3
+ Version: 2.3.82
4
4
  Summary: Local-first Engineering Platform execution operations runtime
5
5
  Author: Peter van Tol
6
6
  License: MIT