engineering-platform 2.3.101__tar.gz → 2.3.102__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 (175) hide show
  1. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/PKG-INFO +1 -1
  2. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/pyproject.toml +2 -1
  3. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +1 -1
  4. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +4 -4
  5. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_admission.py +27 -5
  6. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_composition.py +44 -11
  7. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_plan.py +19 -1
  8. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_preparation.py +54 -3
  9. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/platform_version.py +1 -1
  10. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_readiness.py +23 -6
  11. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/providers.py +22 -1
  12. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/server.py +27 -8
  13. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/server_console_services.py +9 -6
  14. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/system_installation_topology.py +277 -6
  15. engineering_platform-2.3.102/src/engineering_platform/system_instance_provisioner.py +1049 -0
  16. engineering_platform-2.3.102/src/engineering_platform/system_provider_context.py +260 -0
  17. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/system_server_service.py +338 -8
  18. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/templates/workspace-config.json +1 -1
  19. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform.egg-info/PKG-INFO +1 -1
  20. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform.egg-info/SOURCES.txt +2 -0
  21. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform.egg-info/entry_points.txt +1 -0
  22. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/README.md +0 -0
  23. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/setup.cfg +0 -0
  24. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/__init__.py +0 -0
  25. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/__main__.py +0 -0
  26. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/agent_state.py +0 -0
  27. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/agent_trust.py +0 -0
  28. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/dashboard.css +0 -0
  29. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/dashboard.js +0 -0
  30. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/dashboard_locales.mjs +0 -0
  31. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/dashboard_status_store.mjs +0 -0
  32. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/dashboard_translation.mjs +0 -0
  33. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  34. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  35. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/operations-console/icon-dark.png +0 -0
  36. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/operations-console/icon-light.png +0 -0
  37. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  38. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assets/operations-console/manifest.webmanifest +0 -0
  39. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/assurance_scope.py +0 -0
  40. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/capability_preflight.py +0 -0
  41. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/capability_review.py +0 -0
  42. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/central_data_transfer.py +0 -0
  43. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/central_database.py +0 -0
  44. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/central_operational_reset.py +0 -0
  45. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/central_store_migration.py +0 -0
  46. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/codex_capacity.py +0 -0
  47. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/codex_chat.py +0 -0
  48. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/codex_observability.py +0 -0
  49. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/component_lock.py +0 -0
  50. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/component_logging.py +0 -0
  51. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/console_presentation.py +0 -0
  52. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/console_route_ownership.py +0 -0
  53. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/contracts/__init__.py +0 -0
  54. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/contracts/ep_consumer.py +0 -0
  55. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/contracts/models.py +0 -0
  56. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/contracts/projection.py +0 -0
  57. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/dashboard_browser_validation.py +0 -0
  58. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/dashboard_state.py +0 -0
  59. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/dashboard_supervisor.swift +0 -0
  60. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/dashboard_translation.py +0 -0
  61. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/dependabot_producer.py +0 -0
  62. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/development_profile.py +0 -0
  63. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/drift_diagnostics.py +0 -0
  64. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/emergency_recovery.py +0 -0
  65. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/engineering_memory.py +0 -0
  66. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/ep_consumer_credentials.py +0 -0
  67. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/evidence_projection.py +0 -0
  68. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_activity.py +0 -0
  69. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_context.py +0 -0
  70. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_errors.py +0 -0
  71. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_evidence.py +0 -0
  72. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_executor.py +0 -0
  73. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_finalization.py +0 -0
  74. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_host.py +0 -0
  75. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_host_evidence.py +0 -0
  76. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_lease.py +0 -0
  77. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_lifecycle.py +0 -0
  78. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_models.py +0 -0
  79. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_readiness.py +0 -0
  80. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_reporting.py +0 -0
  81. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_repository.py +0 -0
  82. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_timeout_policy.py +0 -0
  83. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_timing.py +0 -0
  84. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/execution_transaction.py +0 -0
  85. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/external_producer_binding.py +0 -0
  86. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/file_inbox.py +0 -0
  87. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/forensic_attribution.py +0 -0
  88. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/forensic_attribution_v2.py +0 -0
  89. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/forensic_delta.py +0 -0
  90. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/golden_scenario.py +0 -0
  91. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/historical_dashboard_configuration.py +0 -0
  92. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/host_admin.py +0 -0
  93. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/host_preflight.py +0 -0
  94. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_relocation.py +0 -0
  95. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_activation.py +0 -0
  96. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_backup.py +0 -0
  97. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_executor.py +0 -0
  98. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_migration.py +0 -0
  99. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/installation_update_operation.py +0 -0
  100. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/investigation_ledger.py +0 -0
  101. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/legacy_inbox_migration.py +0 -0
  102. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/legacy_installation_adoption.py +0 -0
  103. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/lifecycle_worker.py +0 -0
  104. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/live_status.py +0 -0
  105. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/local_api.py +0 -0
  106. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/local_api_keychain.py +0 -0
  107. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/local_repository_binding.py +0 -0
  108. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/managed_autonomy.py +0 -0
  109. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/managed_codex_runtime.py +0 -0
  110. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/managed_workspace_readiness.py +0 -0
  111. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/managed_workspace_recovery.py +0 -0
  112. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/merge_delegation.py +0 -0
  113. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/operational_installation.py +0 -0
  114. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/operational_installation_lock.py +0 -0
  115. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/operational_installation_record.py +0 -0
  116. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/owner_credential_recovery.py +0 -0
  117. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/parity_context.py +0 -0
  118. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/parity_lifecycle_dispatcher.py +0 -0
  119. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/platform_admin.py +0 -0
  120. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/platform_api.py +0 -0
  121. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/platform_bootstrap.py +0 -0
  122. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/platform_components.py +0 -0
  123. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/pr_check_repair.py +0 -0
  124. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/pr_evidence_backfill.py +0 -0
  125. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/producer.py +0 -0
  126. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/product_installation_readback.py +0 -0
  127. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/project_agent.py +0 -0
  128. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/project_agent_service.py +0 -0
  129. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/project_topology.py +0 -0
  130. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/prompt_history.py +0 -0
  131. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_context.py +0 -0
  132. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_context_benchmark.py +0 -0
  133. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_context_scope.py +0 -0
  134. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_interruption.py +0 -0
  135. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_process_identity.py +0 -0
  136. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_recovery.py +0 -0
  137. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/provider_usage.py +0 -0
  138. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/qualification.py +0 -0
  139. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/qualification_runtime.py +0 -0
  140. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/recommendation_handoff.py +0 -0
  141. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/reconciliation_adoption.py +0 -0
  142. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/release_operation.py +0 -0
  143. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/report_analysis.py +0 -0
  144. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/repository_attachment.py +0 -0
  145. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/repository_handoff.py +0 -0
  146. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/resources.py +0 -0
  147. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/reviewer_evidence.py +0 -0
  148. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/revision_binding.py +0 -0
  149. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/schemas/producer-readback-v1.2.schema.json +0 -0
  150. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/schemas/producer-readback-v1.3.schema.json +0 -0
  151. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/schemas/producer-readback-v1.schema.json +0 -0
  152. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/schemas/repository-attachment.schema.json +0 -0
  153. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/server_relay.py +0 -0
  154. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/server_service.py +0 -0
  155. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/status_model.py +0 -0
  156. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/status_reconciliation.py +0 -0
  157. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/storage.py +0 -0
  158. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/submission_cli.py +0 -0
  159. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/submission_intake.py +0 -0
  160. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/submission_service.py +0 -0
  161. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/telemetry.py +0 -0
  162. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/telemetry_contract.py +0 -0
  163. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/telemetry_export.py +0 -0
  164. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/telemetry_metrics.py +0 -0
  165. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/terminal_evidence_reconciliation.py +0 -0
  166. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/validation_identity.py +0 -0
  167. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/validation_profile.py +0 -0
  168. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/version_preparation_delivery.py +0 -0
  169. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/workspace_preflight.py +0 -0
  170. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/worktree_provenance.py +0 -0
  171. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform/worktree_tooling.py +0 -0
  172. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform.egg-info/dependency_links.txt +0 -0
  173. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/src/engineering_platform.egg-info/top_level.txt +0 -0
  174. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/tests/test_adaptive_action_sizing_design.py +0 -0
  175. {engineering_platform-2.3.101 → engineering_platform-2.3.102}/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.101
3
+ Version: 2.3.102
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.101"
7
+ version = "2.3.102"
8
8
  description = "Local-first Engineering Platform execution operations runtime"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14,<3.15"
@@ -20,6 +20,7 @@ engineering-platform = "engineering_platform.submission_cli:main"
20
20
  engineering-project-agent = "engineering_platform.project_agent:main"
21
21
  engineering-reconciliation-adopt = "engineering_platform.reconciliation_adoption:main"
22
22
  engineering-platform-maintenance = "engineering_platform.central_operational_reset:main"
23
+ engineering-platform-system-provisioner = "engineering_platform.system_instance_provisioner:main"
23
24
 
24
25
  [tool.setuptools]
25
26
  package-dir = {"" = "src"}
@@ -3,7 +3,7 @@
3
3
  "platform": {
4
4
  "id": "engineering-platform",
5
5
  "name": "Engineering Platform",
6
- "version": "2.3.101",
6
+ "version": "2.3.102",
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.101",
4
+ "dashboard_version": "2.3.102",
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.101",
9
+ "platform_version": "2.3.102",
10
10
  "report_format": 2,
11
- "runner_version": "2.3.101",
11
+ "runner_version": "2.3.102",
12
12
  "status_model": 1,
13
13
  "storage_schema": 45,
14
- "watcher_version": "2.3.101"
14
+ "watcher_version": "2.3.102"
15
15
  }
@@ -13,6 +13,7 @@ import json
13
13
  from pathlib import Path
14
14
  import subprocess
15
15
  from typing import Mapping
16
+ from typing import Callable
16
17
 
17
18
  from . import (
18
19
  installation_update_activation,
@@ -39,6 +40,9 @@ class InstallationUpdateAdmissionError(ValueError):
39
40
  """The exact prepared candidate cannot enter EP pre-cleanup execution."""
40
41
 
41
42
 
43
+ ServiceInterpreterResolver = Callable[[Path], Path | None]
44
+
45
+
42
46
  @dataclass(frozen=True)
43
47
  class ExecutionAdmission:
44
48
  """Durable, token-free proof for exactly one non-operational candidate."""
@@ -208,10 +212,15 @@ def _require_activated_service(
208
212
  candidate: PreparedUpdateCandidate,
209
213
  *,
210
214
  service_home: Path | None = None,
215
+ service_interpreter: ServiceInterpreterResolver | None = None,
211
216
  ) -> None:
212
217
  """Keep cleanup bound to the exact activated service and data root."""
213
218
  try:
214
- selected = server_service.configured_interpreter(Path(plan.data_root), home=service_home)
219
+ selected = (
220
+ service_interpreter(Path(plan.data_root))
221
+ if service_interpreter is not None
222
+ else server_service.configured_interpreter(Path(plan.data_root), home=service_home)
223
+ )
215
224
  except server_service.ServerServiceError as error:
216
225
  raise InstallationUpdateAdmissionError(
217
226
  "activated service does not bind the admitted candidate"
@@ -338,6 +347,7 @@ def admitted_candidate(
338
347
  *,
339
348
  runner: object = subprocess.run,
340
349
  service_home: Path | None = None,
350
+ service_interpreter: ServiceInterpreterResolver | None = None,
341
351
  ) -> PreparedUpdateCandidate:
342
352
  """Reopen the one candidate already admitted for a resumable update.
343
353
 
@@ -397,7 +407,10 @@ def admitted_candidate(
397
407
  raise InstallationUpdateAdmissionError("execution admission does not bind the exact prepared candidate")
398
408
  if state in {"VERIFIED", "CLEANUP_PENDING", "COMPLETE"}:
399
409
  _require_retained_candidate_package(candidate, runner=runner)
400
- _require_activated_service(plan, candidate, service_home=service_home)
410
+ _require_activated_service(
411
+ plan, candidate, service_home=service_home,
412
+ service_interpreter=service_interpreter,
413
+ )
401
414
  if admission.schema_version == 3:
402
415
  if plan.legacy_adoption is None:
403
416
  raise InstallationUpdateAdmissionError("execution admission legacy baseline is unavailable")
@@ -426,17 +439,26 @@ def admitted_candidate(
426
439
  _require_pre_activation_provenance(plan, admission, snapshot)
427
440
  elif record != expected:
428
441
  raise InstallationUpdateAdmissionError("activated installation does not bind the admitted legacy candidate")
429
- elif server_service.configured_interpreter(Path(plan.data_root), home=service_home) != Path(candidate.interpreter):
442
+ elif (
443
+ service_interpreter(Path(plan.data_root)) if service_interpreter is not None
444
+ else server_service.configured_interpreter(Path(plan.data_root), home=service_home)
445
+ ) != Path(candidate.interpreter):
430
446
  raise InstallationUpdateAdmissionError("activated service does not bind the admitted legacy candidate")
431
447
  elif state == "ACTIVATED":
432
448
  if record != expected:
433
449
  raise InstallationUpdateAdmissionError("activated installation does not bind the admitted legacy candidate")
434
- if server_service.configured_interpreter(Path(plan.data_root), home=service_home) != Path(candidate.interpreter):
450
+ if (
451
+ service_interpreter(Path(plan.data_root)) if service_interpreter is not None
452
+ else server_service.configured_interpreter(Path(plan.data_root), home=service_home)
453
+ ) != Path(candidate.interpreter):
435
454
  raise InstallationUpdateAdmissionError("activated service does not bind the admitted legacy candidate")
436
455
  elif state in {"VERIFIED", "CLEANUP_PENDING", "COMPLETE"}:
437
456
  if record != expected and not _finalized_replacement(record, expected):
438
457
  raise InstallationUpdateAdmissionError("activated installation does not bind the admitted legacy candidate")
439
- if server_service.configured_interpreter(Path(plan.data_root), home=service_home) != Path(candidate.interpreter):
458
+ if (
459
+ service_interpreter(Path(plan.data_root)) if service_interpreter is not None
460
+ else server_service.configured_interpreter(Path(plan.data_root), home=service_home)
461
+ ) != Path(candidate.interpreter):
440
462
  raise InstallationUpdateAdmissionError("activated service does not bind the admitted legacy candidate")
441
463
  else:
442
464
  raise InstallationUpdateAdmissionError("execution admission operation is invalid")
@@ -32,6 +32,10 @@ from .installation_update_plan import InstallationUpdatePlan
32
32
 
33
33
 
34
34
  MigrationRunner = Callable[..., subprocess.CompletedProcess[str]]
35
+ ActivationAction = Callable[
36
+ [InstallationUpdatePlan, Path, Mapping[str, object] | None],
37
+ Mapping[str, object],
38
+ ]
35
39
 
36
40
 
37
41
  class InstallationUpdateCompositionError(ValueError):
@@ -52,6 +56,8 @@ class InstallationUpdateOperationalActions:
52
56
  quiesce: EvidenceAction
53
57
  verify: EvidenceAction
54
58
  quiesce_preflight: EvidenceAction | None = None
59
+ activate: ActivationAction | None = None
60
+ service_interpreter: Callable[[Path], Path | None] | None = None
55
61
 
56
62
 
57
63
  def _evidence(value: Mapping[str, object], step: str) -> dict[str, object]:
@@ -72,11 +78,13 @@ def _admitted_target(
72
78
  admission: ExecutionAdmission,
73
79
  runner: MigrationRunner,
74
80
  service_home: Path | None = None,
81
+ service_interpreter: Callable[[Path], Path | None] | None = None,
75
82
  ) -> Path:
76
83
  """Resolve only the durable OI-4c candidate launcher for this operation."""
77
84
  try:
78
85
  candidate = installation_update_admission.admitted_candidate(
79
86
  plan, admission, runner=runner, service_home=service_home,
87
+ service_interpreter=service_interpreter,
80
88
  )
81
89
  except installation_update_admission.InstallationUpdateAdmissionError as error:
82
90
  raise InstallationUpdateCompositionError("installation update execution admission is invalid") from error
@@ -158,9 +166,13 @@ def _bound_target(
158
166
  admission: ExecutionAdmission,
159
167
  runner: MigrationRunner,
160
168
  service_home: Path | None = None,
169
+ service_interpreter: Callable[[Path], Path | None] | None = None,
161
170
  ) -> Path:
162
171
  """Require a recovery attempt to keep the target proven at inventory."""
163
- target = _admitted_target(plan, admission=admission, runner=runner, service_home=service_home)
172
+ target = _admitted_target(
173
+ plan, admission=admission, runner=runner, service_home=service_home,
174
+ service_interpreter=service_interpreter,
175
+ )
164
176
  try:
165
177
  events = status(Path(plan.data_root), plan.operation_id)["events"]
166
178
  except InstallationUpdateOperationError as error:
@@ -184,10 +196,12 @@ def _verify(
184
196
  action: EvidenceAction,
185
197
  runner: MigrationRunner,
186
198
  service_home: Path | None = None,
199
+ service_interpreter: Callable[[Path], Path | None] | None = None,
187
200
  ) -> Mapping[str, object]:
188
201
  """Keep verification bound to the runtime activated from the inventoried path."""
189
202
  expected = _bound_target(
190
203
  plan, admission=admission, runner=runner, service_home=service_home,
204
+ service_interpreter=service_interpreter,
191
205
  )
192
206
  try:
193
207
  record = operational_installation_record.load(Path(plan.data_root))
@@ -226,6 +240,7 @@ def compose(plan: InstallationUpdatePlan, *, admission: ExecutionAdmission,
226
240
  # repeats the same durable lookup while that executor owns the lock.
227
241
  admitted_target = _admitted_target(
228
242
  plan, admission=admission, runner=migration_runner, service_home=activation_home,
243
+ service_interpreter=actions.service_interpreter,
229
244
  )
230
245
  pre_activation_record = _admitted_pre_activation_record(admission)
231
246
  try:
@@ -246,6 +261,7 @@ def compose(plan: InstallationUpdatePlan, *, admission: ExecutionAdmission,
246
261
  _admitted_target(
247
262
  bound_plan, admission=admission, runner=migration_runner,
248
263
  service_home=activation_home,
264
+ service_interpreter=actions.service_interpreter,
249
265
  )
250
266
  return action(bound_plan)
251
267
  return invoke
@@ -255,6 +271,7 @@ def compose(plan: InstallationUpdatePlan, *, admission: ExecutionAdmission,
255
271
  _admitted_target(
256
272
  bound_plan, admission=admission, runner=migration_runner,
257
273
  service_home=activation_home,
274
+ service_interpreter=actions.service_interpreter,
258
275
  )
259
276
  _evidence(actions.quiesce(bound_plan), "retained quiescence")
260
277
  return action(bound_plan)
@@ -264,6 +281,7 @@ def compose(plan: InstallationUpdatePlan, *, admission: ExecutionAdmission,
264
281
  target = _bound_target(
265
282
  bound_plan, admission=admission, runner=migration_runner,
266
283
  service_home=activation_home,
284
+ service_interpreter=actions.service_interpreter,
267
285
  )
268
286
  _evidence(actions.quiesce(bound_plan), "retained quiescence")
269
287
  return installation_update_migration.migrate(
@@ -276,6 +294,7 @@ def compose(plan: InstallationUpdatePlan, *, admission: ExecutionAdmission,
276
294
  target=_admitted_target(
277
295
  bound_plan, admission=admission, runner=migration_runner,
278
296
  service_home=activation_home,
297
+ service_interpreter=actions.service_interpreter,
279
298
  ),
280
299
  admission=admission,
281
300
  action=actions.inventory,
@@ -284,16 +303,29 @@ def compose(plan: InstallationUpdatePlan, *, admission: ExecutionAdmission,
284
303
  quiesce=guarded(actions.quiesce),
285
304
  backup=after_quiescence(installation_update_backup.backup),
286
305
  migrate=migrate,
287
- activate=lambda bound_plan: installation_update_activation.activate(
288
- bound_plan,
289
- interpreter=_bound_target(
290
- bound_plan, admission=admission, runner=migration_runner,
291
- service_home=activation_home,
292
- ),
293
- pre_activation_record=pre_activation_record,
294
- home=activation_home,
295
- runner=activation_runner,
296
- package_runner=migration_runner,
306
+ activate=lambda bound_plan: (
307
+ actions.activate(
308
+ bound_plan,
309
+ _bound_target(
310
+ bound_plan, admission=admission, runner=migration_runner,
311
+ service_home=activation_home,
312
+ service_interpreter=actions.service_interpreter,
313
+ ),
314
+ pre_activation_record,
315
+ )
316
+ if actions.activate is not None
317
+ else installation_update_activation.activate(
318
+ bound_plan,
319
+ interpreter=_bound_target(
320
+ bound_plan, admission=admission, runner=migration_runner,
321
+ service_home=activation_home,
322
+ service_interpreter=actions.service_interpreter,
323
+ ),
324
+ pre_activation_record=pre_activation_record,
325
+ home=activation_home,
326
+ runner=activation_runner,
327
+ package_runner=migration_runner,
328
+ )
297
329
  ),
298
330
  verify=lambda bound_plan: _verify(
299
331
  bound_plan,
@@ -301,6 +333,7 @@ def compose(plan: InstallationUpdatePlan, *, admission: ExecutionAdmission,
301
333
  action=actions.verify,
302
334
  runner=migration_runner,
303
335
  service_home=activation_home,
336
+ service_interpreter=actions.service_interpreter,
304
337
  ),
305
338
  expected_activation=expected_activation,
306
339
  quiesce_preflight=(
@@ -33,6 +33,7 @@ class InstallationUpdatePlan:
33
33
  cleanup_targets: tuple[str, ...]
34
34
  steps: tuple[str, ...]
35
35
  legacy_adoption: dict[str, object] | None = None
36
+ target_runtime_venv: str | None = None
36
37
 
37
38
  def payload(self) -> dict[str, object]:
38
39
  value = asdict(self)
@@ -41,6 +42,8 @@ class InstallationUpdatePlan:
41
42
  # non-empty legacy evidence is always explicit and immutable.
42
43
  if value["legacy_adoption"] is None:
43
44
  del value["legacy_adoption"]
45
+ if value["target_runtime_venv"] is None:
46
+ del value["target_runtime_venv"]
44
47
  return value
45
48
 
46
49
 
@@ -72,7 +75,8 @@ def verify_exact_artifact(plan: InstallationUpdatePlan) -> dict[str, str]:
72
75
 
73
76
 
74
77
  def prepare(data_root: Path, *, operation_id: str, artifact: Path, target_version: str,
75
- target_digest: str, target_source_revision: str) -> InstallationUpdatePlan:
78
+ target_digest: str, target_source_revision: str,
79
+ target_runtime_venv: Path | None = None) -> InstallationUpdatePlan:
76
80
  """Bind an update to one record and wheel without changing the machine.
77
81
 
78
82
  Callers must execute this returned, exact plan under the EP installation
@@ -121,6 +125,19 @@ def prepare(data_root: Path, *, operation_id: str, artifact: Path, target_versio
121
125
  if _digest(exact_artifact) != target_digest:
122
126
  raise InstallationUpdatePlanError("target artifact does not match the requested digest")
123
127
  operation_root = root / "operations" / operation_id
128
+ runtime_venv: str | None = None
129
+ if target_runtime_venv is not None:
130
+ candidate = Path(target_runtime_venv).expanduser()
131
+ slot = "sha256-" + target_digest.removeprefix("sha256:")
132
+ if (
133
+ not candidate.is_absolute()
134
+ or candidate.name != "venv"
135
+ or candidate.parent.name != slot
136
+ or candidate.parent.parent.name != "slots"
137
+ or candidate.parent.parent.parent.name != "runtimes"
138
+ ):
139
+ raise InstallationUpdatePlanError("target runtime slot is invalid")
140
+ runtime_venv = str(candidate.absolute())
124
141
  cleanup = tuple(str(path) for path in (
125
142
  operation_root / "build", operation_root / "download", operation_root / "pip-cache",
126
143
  ))
@@ -133,4 +150,5 @@ def prepare(data_root: Path, *, operation_id: str, artifact: Path, target_versio
133
150
  steps=("INSTALLATION_LOCK", "INVENTORY_AND_COMPATIBILITY", "EXACT_ARTIFACT",
134
151
  "QUIESCE", "BACKUP_AND_MIGRATION", "ACTIVATE", "VERIFY", "CLEANUP"),
135
152
  legacy_adoption=legacy,
153
+ target_runtime_venv=runtime_venv,
136
154
  )
@@ -130,7 +130,11 @@ def _paths(plan: InstallationUpdatePlan) -> _OperationPaths:
130
130
  # the staged filename itself PEP 427-compatible so isolated ``pip`` can
131
131
  # install it without relying on an original temporary filename.
132
132
  staged = download / f"engineering_platform-{plan.target_version}-py3-none-any.whl"
133
- candidate = operation / _CANDIDATE_DIRECTORY
133
+ candidate = (
134
+ Path(plan.target_runtime_venv).expanduser().absolute()
135
+ if plan.target_runtime_venv is not None
136
+ else operation / _CANDIDATE_DIRECTORY
137
+ )
134
138
  paths = _OperationPaths(
135
139
  root=root,
136
140
  operation=operation,
@@ -145,11 +149,12 @@ def _paths(plan: InstallationUpdatePlan) -> _OperationPaths:
145
149
  ("update operation", paths.operation),
146
150
  ("operation download directory", paths.download),
147
151
  ("staged operation artifact", paths.staged_artifact),
148
- ("candidate runtime", paths.candidate),
149
152
  ("operation pip cache", paths.pip_cache),
150
153
  ("candidate runtime marker", paths.marker),
151
154
  ):
152
155
  _contained(root, path, label)
156
+ if plan.target_runtime_venv is None:
157
+ _contained(root, paths.candidate, "candidate runtime")
153
158
  return paths
154
159
 
155
160
 
@@ -250,13 +255,53 @@ def _ensure_marker(plan: InstallationUpdatePlan, paths: _OperationPaths, staged:
250
255
  _read_marker(plan, paths, staged)
251
256
  return
252
257
  if paths.candidate.exists() or paths.candidate.is_symlink():
253
- raise InstallationUpdatePreparationError("candidate runtime exists without operation identity")
258
+ if plan.target_runtime_venv is None:
259
+ raise InstallationUpdatePreparationError("candidate runtime exists without operation identity")
260
+ _read_runtime_slot_receipt(plan, paths)
254
261
  try:
255
262
  _write_marker(paths.marker, expected)
256
263
  except OSError as error:
257
264
  raise InstallationUpdatePreparationError("candidate runtime marker could not be retained") from error
258
265
 
259
266
 
267
+ def _runtime_slot_receipt(plan: InstallationUpdatePlan, paths: _OperationPaths) -> dict[str, object]:
268
+ return {
269
+ "schema_version": 1,
270
+ "version": plan.target_version,
271
+ "artifact_digest": plan.target_digest,
272
+ "source_revision": plan.target_source_revision,
273
+ "venv": str(paths.candidate),
274
+ }
275
+
276
+
277
+ def _runtime_slot_receipt_path(paths: _OperationPaths) -> Path:
278
+ return paths.candidate.parent / "runtime-slot.json"
279
+
280
+
281
+ def _read_runtime_slot_receipt(plan: InstallationUpdatePlan, paths: _OperationPaths) -> None:
282
+ path = _runtime_slot_receipt_path(paths)
283
+ if path.is_symlink() or not path.is_file():
284
+ raise InstallationUpdatePreparationError("shared runtime slot lacks exact artifact identity")
285
+ try:
286
+ observed = json.loads(path.read_text(encoding="utf-8"))
287
+ except (OSError, json.JSONDecodeError) as error:
288
+ raise InstallationUpdatePreparationError("shared runtime slot identity is unreadable") from error
289
+ if observed != _runtime_slot_receipt(plan, paths):
290
+ raise InstallationUpdatePreparationError("shared runtime slot identity mismatch")
291
+
292
+
293
+ def _ensure_runtime_slot_receipt(plan: InstallationUpdatePlan, paths: _OperationPaths) -> None:
294
+ if plan.target_runtime_venv is None:
295
+ return
296
+ path = _runtime_slot_receipt_path(paths)
297
+ expected = _runtime_slot_receipt(plan, paths)
298
+ if path.exists() or path.is_symlink():
299
+ _read_runtime_slot_receipt(plan, paths)
300
+ return
301
+ _write_marker(path, expected)
302
+ _read_runtime_slot_receipt(plan, paths)
303
+
304
+
260
305
  def _verified_candidate(
261
306
  plan: InstallationUpdatePlan,
262
307
  paths: _OperationPaths,
@@ -345,6 +390,10 @@ def staged_execution_plan(
345
390
  target_version=plan.target_version,
346
391
  target_digest=plan.target_digest,
347
392
  target_source_revision=plan.target_source_revision,
393
+ target_runtime_venv=(
394
+ None if plan.target_runtime_venv is None
395
+ else Path(plan.target_runtime_venv)
396
+ ),
348
397
  )
349
398
  except installation_update_plan.InstallationUpdatePlanError as error:
350
399
  raise InstallationUpdatePreparationError("staged candidate cannot be rebound to the registered installation") from error
@@ -352,6 +401,7 @@ def staged_execution_plan(
352
401
  "operation_id", "installation_id", "data_root", "current_version",
353
402
  "current_digest", "target_version", "target_digest",
354
403
  "target_source_revision", "cleanup_targets", "steps", "legacy_adoption",
404
+ "target_runtime_venv",
355
405
  )
356
406
  if any(getattr(rebound, field) != getattr(plan, field) for field in unchanged):
357
407
  raise InstallationUpdatePreparationError("registered installation changed before staged candidate binding")
@@ -517,6 +567,7 @@ def prepare_candidate(
517
567
  _ensure_marker(plan, paths, staged)
518
568
  _create_or_recover_venv(paths, builder=builder, runner=runner)
519
569
  package = _install_or_verify_candidate(plan, paths, staged=staged, runner=runner)
570
+ _ensure_runtime_slot_receipt(plan, paths)
520
571
  return PreparedUpdateCandidate(
521
572
  operation_id=plan.operation_id,
522
573
  installation_id=plan.installation_id,
@@ -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.101"
15
+ CURRENT_PLATFORM_VERSION = "2.3.102"
16
16
  MANIFEST_FIELDS = frozenset(
17
17
  {
18
18
  "platform_version",
@@ -6,18 +6,29 @@ retries authentication on behalf of an execution.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
9
+ import os
9
10
  from pathlib import Path
10
11
  import re
11
12
  import shutil
12
13
  import subprocess
13
14
 
14
- from .providers import CodexCliProvider, LocalProcessProvider, codex_cli_executable
15
+ from .providers import (
16
+ CodexCliProvider, LocalProcessProvider, codex_cli_executable,
17
+ github_cli_executable, MANAGED_GITHUB_CLI_EXECUTABLE_ENVIRONMENT,
18
+ )
15
19
 
16
20
 
17
21
  _VERSION = re.compile(r"\b\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.]+)?\b")
18
22
  CODEX_READINESS_TIMEOUT_SECONDS = 2
19
23
 
20
24
 
25
+ def _github_executable() -> str | None:
26
+ """Keep the legacy PATH observer while honoring a pinned system context."""
27
+ if MANAGED_GITHUB_CLI_EXECUTABLE_ENVIRONMENT in os.environ:
28
+ return github_cli_executable()
29
+ return shutil.which("gh")
30
+
31
+
21
32
  def _classify(result: subprocess.CompletedProcess[str] | None) -> str:
22
33
  if result is None:
23
34
  return "CHECK_FAILED"
@@ -58,7 +69,7 @@ def runtime_details(root: Path) -> dict[str, dict[str, str]]:
58
69
  codex_version = _version(CodexCliProvider().command("--version", timeout=CODEX_READINESS_TIMEOUT_SECONDS)) if codex_path else ""
59
70
  except (OSError, subprocess.TimeoutExpired):
60
71
  codex_version = ""
61
- github_path = shutil.which("gh") or ""
72
+ github_path = _github_executable() or ""
62
73
  try:
63
74
  github_version = _version(LocalProcessProvider().execute(root, (github_path, "--version"))) if github_path else ""
64
75
  except OSError:
@@ -98,7 +109,7 @@ def host_status(root: Path, *, require_github: bool = True) -> dict[str, dict[st
98
109
  }
99
110
  if not require_github:
100
111
  return result
101
- github_path = shutil.which("gh")
112
+ github_path = _github_executable()
102
113
  if github_path is None:
103
114
  result["github"] = {"provider": "GITHUB", "state": "UNAVAILABLE"}
104
115
  return result
@@ -117,11 +128,12 @@ def status(root: Path, *, require_github: bool = True) -> dict[str, dict[str, st
117
128
  result = host_status(root, require_github=False)
118
129
  if not require_github:
119
130
  return result
120
- if shutil.which("gh") is None:
131
+ github_path = _github_executable()
132
+ if github_path is None:
121
133
  result["github"] = {"provider": "GITHUB", "state": "UNAVAILABLE"}
122
134
  return result
123
135
  try:
124
- github_result = LocalProcessProvider().execute(root, ("gh", "auth", "status", "--hostname", "github.com"))
136
+ github_result = LocalProcessProvider().execute(root, (github_path, "auth", "status", "--hostname", "github.com"))
125
137
  except OSError:
126
138
  github_result = None
127
139
  github_state = _classify(github_result)
@@ -130,8 +142,13 @@ def status(root: Path, *, require_github: bool = True) -> dict[str, dict[str, st
130
142
  # `gh repo view --json` uses GitHub's GraphQL quota. Readiness only
131
143
  # needs a cheap repository-access proof, so use the REST endpoint
132
144
  # and avoid turning an exhausted GraphQL quota into a login repair.
145
+ repository_executable = (
146
+ github_path
147
+ if MANAGED_GITHUB_CLI_EXECUTABLE_ENVIRONMENT in os.environ
148
+ else "gh"
149
+ )
133
150
  repository_result = LocalProcessProvider().execute(
134
- root, ("gh", "api", "repos/{owner}/{repo}", "--jq", ".full_name")
151
+ root, (repository_executable, "api", "repos/{owner}/{repo}", "--jq", ".full_name")
135
152
  )
136
153
  except OSError:
137
154
  repository_result = None
@@ -347,6 +347,7 @@ class PrivateRemoteAccessProvider(Protocol):
347
347
 
348
348
 
349
349
  MANAGED_CODEX_CLI_PREFIX_ENVIRONMENT = "EP_MANAGED_CODEX_CLI_PREFIX"
350
+ MANAGED_GITHUB_CLI_EXECUTABLE_ENVIRONMENT = "EP_GITHUB_CLI_EXECUTABLE"
350
351
 
351
352
 
352
353
  def default_engineering_platform_codex_cli_prefix() -> Path:
@@ -380,6 +381,23 @@ def codex_cli_executable() -> str | None:
380
381
  return None
381
382
 
382
383
 
384
+ def github_cli_executable() -> str | None:
385
+ """Select an instance-owned gh executable when the service pins one.
386
+
387
+ User-owned Project Agents retain their ordinary user/PATH behavior. A
388
+ system Server LaunchDaemon always receives the environment variable and
389
+ therefore fails closed instead of falling back when its owned executable
390
+ is missing or substituted.
391
+ """
392
+ configured = os.environ.get(MANAGED_GITHUB_CLI_EXECUTABLE_ENVIRONMENT)
393
+ if configured is not None:
394
+ candidate = Path(configured).expanduser()
395
+ if candidate.is_absolute() and candidate.is_file() and os.access(candidate, os.X_OK):
396
+ return str(candidate.absolute())
397
+ return None
398
+ return shutil.which("gh")
399
+
400
+
383
401
  class ProviderOutputLimitExceeded(RuntimeError):
384
402
  """An advisory provider invocation exceeded its retained output budget."""
385
403
 
@@ -576,7 +594,10 @@ class GitHubProvider:
576
594
  return ProviderStatus("github", "configured", qualified, remote.stdout.strip() if qualified else "GitHub origin unavailable")
577
595
 
578
596
  def github(self, *args: str) -> str:
579
- completed = subprocess.run(("gh", *args), text=True, capture_output=True, check=False)
597
+ executable = github_cli_executable()
598
+ if executable is None:
599
+ raise RuntimeError("Engineering Platform GitHub CLI is unavailable")
600
+ completed = subprocess.run((executable, *args), text=True, capture_output=True, check=False)
580
601
  if completed.returncode:
581
602
  raise RuntimeError(completed.stderr.strip() or "GitHub provider command failed")
582
603
  return completed.stdout.strip()