forge-autonomy 2.7.36__tar.gz → 2.7.38__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 (337) hide show
  1. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/PKG-INFO +1 -1
  2. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/installed_lifecycle.py +16 -0
  3. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/preserved_lifecycle.py +3 -2
  4. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/qualification/server_runtime.py +47 -0
  5. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge_autonomy.egg-info/PKG-INFO +1 -1
  6. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/product-version.json +1 -1
  7. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_installed_forge_update.py +376 -4
  8. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_installed_lifecycle.py +46 -0
  9. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_preserved_lifecycle.py +42 -0
  10. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/MANIFEST.in +0 -0
  11. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/README.md +0 -0
  12. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/__init__.py +0 -0
  13. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/__main__.py +0 -0
  14. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/_version.py +0 -0
  15. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/action_derivation_canary_closure.py +0 -0
  16. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/action_derivation_evidence.py +0 -0
  17. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/action_derivation_qualification.py +0 -0
  18. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/action_derivation_reattempt.py +0 -0
  19. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/ai_architect/__init__.py +0 -0
  20. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/ai_architect/provider_registry.py +0 -0
  21. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/api/installed-health-component-registry-1.0.json +0 -0
  22. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/api/operations-read-openapi-v1.json +0 -0
  23. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/api/operations-read-postman-v1.json +0 -0
  24. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/api/server-openapi-v1.json +0 -0
  25. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/api/server-postman-v1.json +0 -0
  26. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/architecture/__init__.py +0 -0
  27. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/architecture/advisor.py +0 -0
  28. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/architecture/presentation.py +0 -0
  29. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/architecture/workspace.py +0 -0
  30. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/autonomous_orchestrator.py +0 -0
  31. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/business/__init__.py +0 -0
  32. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/business/advisor.py +0 -0
  33. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/business/ingress.py +0 -0
  34. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/business/presentation.py +0 -0
  35. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/business/workspace.py +0 -0
  36. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/canary.py +0 -0
  37. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/capabilities/__init__.py +0 -0
  38. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/capabilities/delegation.py +0 -0
  39. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/completion/__init__.py +0 -0
  40. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/completion/assessor.py +0 -0
  41. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/completion/host_control_observer.py +0 -0
  42. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/completion/mission.py +0 -0
  43. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/completion/repository_observer.py +0 -0
  44. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/constitutional/__init__.py +0 -0
  45. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/constitutional/assessor.py +0 -0
  46. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/core/__init__.py +0 -0
  47. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/core/json_store.py +0 -0
  48. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/decision_evidence/__init__.py +0 -0
  49. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/decision_evidence/repository.py +0 -0
  50. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/dispatcher/__init__.py +0 -0
  51. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/dispatcher/dispatcher.py +0 -0
  52. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/ep_simulator.py +0 -0
  53. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/execution/__init__.py +0 -0
  54. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/execution/loop.py +0 -0
  55. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/execution/presentation.py +0 -0
  56. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/execution_host_configuration.py +0 -0
  57. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/foundation/__init__.py +0 -0
  58. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/foundation/loader.py +0 -0
  59. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/generation_two.py +0 -0
  60. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/generation_two_execution.py +0 -0
  61. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/governance/__init__.py +0 -0
  62. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/governance/execution_policy.py +0 -0
  63. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/governance/profiles.py +0 -0
  64. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/governance_authority.py +0 -0
  65. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/installed_health.py +0 -0
  66. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/intake.py +0 -0
  67. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/integration/__init__.py +0 -0
  68. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/integration/coordinator.py +0 -0
  69. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/integration/repository.py +0 -0
  70. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/knowledge/__init__.py +0 -0
  71. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/knowledge/consumer.py +0 -0
  72. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/knowledge/registry.py +0 -0
  73. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/lifecycle/__init__.py +0 -0
  74. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/lifecycle/store.py +0 -0
  75. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/mission_amendment.py +0 -0
  76. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/mission_cli.py +0 -0
  77. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/mission_scheduler.py +0 -0
  78. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/__init__.py +0 -0
  79. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/action.py +0 -0
  80. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/action_derivation.py +0 -0
  81. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/agent_policy.py +0 -0
  82. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/ai_architect_provider.py +0 -0
  83. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/ai_architect_session.py +0 -0
  84. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/ai_provider_registry.py +0 -0
  85. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/architecture_mission.py +0 -0
  86. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/architecture_reasoning.py +0 -0
  87. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/architecture_review.py +0 -0
  88. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/codex_runtime_prompt.py +0 -0
  89. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/constitutional_validation.py +0 -0
  90. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/criterion_assessment.py +0 -0
  91. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/criterion_observation.py +0 -0
  92. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/decision_evidence.py +0 -0
  93. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/execution_host.py +0 -0
  94. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/foundation.py +0 -0
  95. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/historical_intent.py +0 -0
  96. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/integration.py +0 -0
  97. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/intent.py +0 -0
  98. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/intent_authoring.py +0 -0
  99. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/mission.py +0 -0
  100. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/mission_candidate.py +0 -0
  101. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/mission_completion.py +0 -0
  102. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/mission_planner.py +0 -0
  103. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/mission_recommendation.py +0 -0
  104. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/phase_completion.py +0 -0
  105. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/planning.py +0 -0
  106. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/producer.py +0 -0
  107. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/prompt_artifact.py +0 -0
  108. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/proposal.py +0 -0
  109. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/runtime_prompt.py +0 -0
  110. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/models/solution_template.py +0 -0
  111. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/operations_read_api.py +0 -0
  112. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/operator_identity.py +0 -0
  113. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planner/__init__.py +0 -0
  114. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planner/action_derivation.py +0 -0
  115. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planner/codex_cli_session.py +0 -0
  116. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planner/durable_derivation.py +0 -0
  117. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planner/engine.py +0 -0
  118. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planner/openai_responses.py +0 -0
  119. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planner/provider_adapter.py +0 -0
  120. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planning/__init__.py +0 -0
  121. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planning/loader.py +0 -0
  122. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/planning/registry.py +0 -0
  123. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/portfolio_intelligence.py +0 -0
  124. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/programme_authorization.py +0 -0
  125. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/prompts/__init__.py +0 -0
  126. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/prompts/codex_cli.py +0 -0
  127. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/prompts/generator.py +0 -0
  128. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/proposals/__init__.py +0 -0
  129. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/proposals/generator.py +0 -0
  130. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/provider_context.py +0 -0
  131. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/provider_security.py +0 -0
  132. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/qualification/__init__.py +0 -0
  133. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/qualification/bootstrap_sequence.py +0 -0
  134. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/qualification/criterion_completion.py +0 -0
  135. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/qualification/generation_one.py +0 -0
  136. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/qualification/installed_smoke.py +0 -0
  137. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/recommendations/__init__.py +0 -0
  138. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/recommendations/engine.py +0 -0
  139. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/repository_truth.py +0 -0
  140. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/review/__init__.py +0 -0
  141. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/review/engine.py +0 -0
  142. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/__init__.py +0 -0
  143. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/bootstrap.py +0 -0
  144. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/data_root.py +0 -0
  145. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/database.py +0 -0
  146. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/dynamic_mission.py +0 -0
  147. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/evidence.py +0 -0
  148. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/execution_context.py +0 -0
  149. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/health.py +0 -0
  150. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/mission_controller.py +0 -0
  151. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/operational_reset.py +0 -0
  152. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/runner.py +0 -0
  153. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/runtime/service.py +0 -0
  154. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/scheduler/__init__.py +0 -0
  155. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/scheduler/adapter.py +0 -0
  156. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/scheduler/ep_http_adapter.py +0 -0
  157. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/scheduler/ep_v12.py +0 -0
  158. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/scheduler/scheduler.py +0 -0
  159. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/capability-registry-1.0.schema.json +0 -0
  160. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/capability.schema.json +0 -0
  161. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/common.schema.json +0 -0
  162. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/decision-evidence-1.0.schema.json +0 -0
  163. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-goal-0.5.schema.json +0 -0
  164. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-increment-proposal-0.5.schema.json +0 -0
  165. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-mode.schema.json +0 -0
  166. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-plan-0.5.schema.json +0 -0
  167. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-platform-peer-configuration-1.0.schema.json +0 -0
  168. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-platform-peer-configuration-1.1.schema.json +0 -0
  169. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-prompt-artifact-0.7.schema.json +0 -0
  170. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/engineering-proposal-0.6.schema.json +0 -0
  171. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/evidence-reference-0.5.schema.json +0 -0
  172. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/foundation-document.schema.json +0 -0
  173. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/governance-profile.schema.json +0 -0
  174. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/knowledge-source-0.4.schema.json +0 -0
  175. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/knowledge-source.schema.json +0 -0
  176. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/mission-candidate-1.0.schema.json +0 -0
  177. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/mission-candidate-1.1.schema.json +0 -0
  178. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/phase-completion-1.0.schema.json +0 -0
  179. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/planning-document-0.5.schema.json +0 -0
  180. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/repository-catalog.schema.json +0 -0
  181. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/repository.schema.json +0 -0
  182. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/solution-template-1.0.schema.json +0 -0
  183. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/schemas/workspace.schema.json +0 -0
  184. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/secure_store.py +0 -0
  185. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/server_runtime.py +0 -0
  186. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/solutions/__init__.py +0 -0
  187. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/solutions/catalogue.py +0 -0
  188. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/solutions/generator.py +0 -0
  189. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/state/__init__.py +0 -0
  190. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/state/mission_state.py +0 -0
  191. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge/workspace/__init__.py +0 -0
  192. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge_autonomy.egg-info/SOURCES.txt +0 -0
  193. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge_autonomy.egg-info/dependency_links.txt +0 -0
  194. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge_autonomy.egg-info/entry_points.txt +0 -0
  195. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/forge_autonomy.egg-info/top_level.txt +0 -0
  196. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/pyproject.toml +0 -0
  197. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/capability-registry-1.0.schema.json +0 -0
  198. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/capability.schema.json +0 -0
  199. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/common.schema.json +0 -0
  200. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/decision-evidence-1.0.schema.json +0 -0
  201. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/engineering-goal-0.5.schema.json +0 -0
  202. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/engineering-increment-proposal-0.5.schema.json +0 -0
  203. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/engineering-mode.schema.json +0 -0
  204. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/engineering-plan-0.5.schema.json +0 -0
  205. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/engineering-prompt-artifact-0.7.schema.json +0 -0
  206. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/engineering-proposal-0.6.schema.json +0 -0
  207. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/evidence-reference-0.5.schema.json +0 -0
  208. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/foundation-document.schema.json +0 -0
  209. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/governance-profile.schema.json +0 -0
  210. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/knowledge-source-0.4.schema.json +0 -0
  211. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/knowledge-source.schema.json +0 -0
  212. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/mission-candidate-1.0.schema.json +0 -0
  213. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/mission-candidate-1.1.schema.json +0 -0
  214. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/phase-completion-1.0.schema.json +0 -0
  215. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/planning-document-0.5.schema.json +0 -0
  216. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/repository-catalog.schema.json +0 -0
  217. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/repository.schema.json +0 -0
  218. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/solution-template-1.0.schema.json +0 -0
  219. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/schemas/workspace.schema.json +0 -0
  220. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/setup.cfg +0 -0
  221. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_action_derivation.py +0 -0
  222. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_action_derivation_canary_closure.py +0 -0
  223. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_action_derivation_evidence.py +0 -0
  224. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_action_derivation_qualification.py +0 -0
  225. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_adaptive_action_sizing_design.py +0 -0
  226. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_agent_policy.py +0 -0
  227. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_aggregate_health_roadmap.py +0 -0
  228. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_ai_architect_provider.py +0 -0
  229. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_ai_architect_session.py +0 -0
  230. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_ai_development_projection.py +0 -0
  231. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_ai_provider_registry.py +0 -0
  232. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_architecture_reasoning.py +0 -0
  233. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_architecture_review_engine.py +0 -0
  234. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_architecture_workspace.py +0 -0
  235. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_autonomous_orchestrator.py +0 -0
  236. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_bootstrap_adapter.py +0 -0
  237. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_bootstrap_mission_canary.py +0 -0
  238. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_bootstrap_mission_runner.py +0 -0
  239. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_bootstrap_mission_scheduler.py +0 -0
  240. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_bootstrap_sequence_qualification.py +0 -0
  241. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_business_governance_ingress.py +0 -0
  242. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_business_workspace.py +0 -0
  243. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_canonical_data_root.py +0 -0
  244. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_capability_delegation.py +0 -0
  245. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_codex_cli_runtime_prompt.py +0 -0
  246. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_codex_cli_session.py +0 -0
  247. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_constitutional_validation.py +0 -0
  248. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_criterion_assessment_contract.py +0 -0
  249. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_criterion_ep_http_boundary.py +0 -0
  250. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_criterion_planning_context.py +0 -0
  251. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_decision_evidence.py +0 -0
  252. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_default_project_quality_policy.py +0 -0
  253. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_distribution_identity.py +0 -0
  254. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_durable_action_derivation.py +0 -0
  255. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_dynamic_mission_capability.py +0 -0
  256. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_engineering_action_architecture.py +0 -0
  257. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_engineering_intent_authoring.py +0 -0
  258. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_engineering_intent_lifecycle.py +0 -0
  259. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_engineering_mission.py +0 -0
  260. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_engineering_planning.py +0 -0
  261. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_engineering_prompt_artifacts.py +0 -0
  262. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_engineering_proposals.py +0 -0
  263. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_ep_http_adapter.py +0 -0
  264. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_ep_simulator.py +0 -0
  265. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_ep_simulator_rolling_planning.py +0 -0
  266. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_execution_context.py +0 -0
  267. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_execution_host_configuration.py +0 -0
  268. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_execution_host_contract.py +0 -0
  269. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_execution_loop.py +0 -0
  270. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_existing_release_reconciliation.py +0 -0
  271. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_forge_console_admin_parity_contract.py +0 -0
  272. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_forge_console_configuration_contract.py +0 -0
  273. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_forge_console_hosting_contract.py +0 -0
  274. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_forge_operations_console_roadmap.py +0 -0
  275. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_forge_outer_loop_ci_roadmap.py +0 -0
  276. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_forge_v1_bootstrap_scheduler_contract.py +0 -0
  277. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_forge_v1_implementation_dag.py +0 -0
  278. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_foundation_loader.py +0 -0
  279. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_foundation_models.py +0 -0
  280. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_generation_one_bootstrap_qualification.py +0 -0
  281. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_generation_two_execution.py +0 -0
  282. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_generation_two_mission_intake.py +0 -0
  283. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_governance_authority.py +0 -0
  284. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_governed_coverage.py +0 -0
  285. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_historical_engineering_intent.py +0 -0
  286. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_host_control_completion.py +0 -0
  287. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_inner_loop_ci_contract.py +0 -0
  288. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_installed_dynamic_mission_runtime.py +0 -0
  289. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_integration_coordination.py +0 -0
  290. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_knowledge_consumption.py +0 -0
  291. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_live_project_roadmap_contract.py +0 -0
  292. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_completion_evaluator.py +0 -0
  293. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_controller.py +0 -0
  294. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_dispatcher.py +0 -0
  295. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_planner.py +0 -0
  296. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_recommendation_engine.py +0 -0
  297. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_recommendation_lifecycle.py +0 -0
  298. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_runtime_projection.py +0 -0
  299. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_runtime_scheduler.py +0 -0
  300. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_state_store.py +0 -0
  301. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_mission_status.py +0 -0
  302. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_openai_action_derivation.py +0 -0
  303. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_operational_reset.py +0 -0
  304. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_operations_health.py +0 -0
  305. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_operations_read_api.py +0 -0
  306. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_operator_identity.py +0 -0
  307. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_phase_completion.py +0 -0
  308. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_platform_contract_documents.py +0 -0
  309. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_portfolio_intelligence_operation.py +0 -0
  310. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_producer_contract.py +0 -0
  311. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_product_version_operations.py +0 -0
  312. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_production_release_workflow.py +0 -0
  313. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_productization_reconciliation.py +0 -0
  314. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_programme_authorization.py +0 -0
  315. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_provider_context.py +0 -0
  316. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_provider_security.py +0 -0
  317. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_pypi_distribution_readback.py +0 -0
  318. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_recovered_partial_completion.py +0 -0
  319. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_release_operation.py +0 -0
  320. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_repository_truth.py +0 -0
  321. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_role_aware_conversations_contract.py +0 -0
  322. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_bootstrap.py +0 -0
  323. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_database.py +0 -0
  324. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_evidence.py +0 -0
  325. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_health.py +0 -0
  326. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_instance_persistence.py +0 -0
  327. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_prompt_generation.py +0 -0
  328. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_service.py +0 -0
  329. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_runtime_status_projection.py +0 -0
  330. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_scheduler_dependency_boundary.py +0 -0
  331. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_schemas.py +0 -0
  332. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_secure_store.py +0 -0
  333. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_server_api_contract.py +0 -0
  334. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_server_runtime.py +0 -0
  335. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_solution_templates.py +0 -0
  336. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_substantive_mission_completion.py +0 -0
  337. {forge_autonomy-2.7.36 → forge_autonomy-2.7.38}/tests/test_terminal_continuation_replay.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forge-autonomy
3
- Version: 2.7.36
3
+ Version: 2.7.38
4
4
  Summary: Forge mission planning, producer contracts, and evidence interpretation runtime.
5
5
  Author: Forge maintainers
6
6
  License-Expression: Apache-2.0
@@ -52,6 +52,9 @@ SUPPORTED_TRANSITIONS = {
52
52
  ("2.7.32", "2.7.33"): (39, 39),
53
53
  ("2.7.33", "2.7.34"): (39, 39),
54
54
  ("2.7.34", "2.7.35"): (39, 39),
55
+ ("2.7.35", "2.7.38"): (39, 39),
56
+ ("2.7.36", "2.7.38"): (39, 39),
57
+ ("2.7.37", "2.7.38"): (39, 39),
55
58
  }
56
59
  SAME_SCHEMA_39_TRANSITIONS = frozenset(
57
60
  transition for transition, schemas in SUPPORTED_TRANSITIONS.items() if schemas == (39, 39)
@@ -498,6 +501,13 @@ def _exclusive_locks(paths: tuple[Path, ...]) -> Iterator[None]:
498
501
 
499
502
 
500
503
  def _assert_tree_has_no_links(root: Path) -> str:
504
+ """Prove one mutable instance tree is exclusively product-owned and safe."""
505
+ _assert_no_symlink_components(root)
506
+ root_info = root.lstat()
507
+ if not stat.S_ISDIR(root_info.st_mode):
508
+ raise InstalledLifecycleError("instance root is not a directory")
509
+ if stat.S_IMODE(root_info.st_mode) & 0o022:
510
+ raise InstalledLifecycleError("instance root is group/world writable")
501
511
  entries: list[dict[str, object]] = []
502
512
  for directory, names, files in os.walk(root, topdown=True, followlinks=False):
503
513
  parent = Path(directory)
@@ -508,8 +518,14 @@ def _assert_tree_has_no_links(root: Path) -> str:
508
518
  raise InstalledLifecycleError(f"instance tree contains an unsafe symbolic link: {path}")
509
519
  if not (stat.S_ISDIR(info.st_mode) or stat.S_ISREG(info.st_mode)):
510
520
  raise InstalledLifecycleError(f"instance tree contains an unsupported filesystem entry: {path}")
521
+ if stat.S_IMODE(info.st_mode) & 0o022:
522
+ raise InstalledLifecycleError(f"instance tree contains a group/world writable entry: {path}")
523
+ if stat.S_ISREG(info.st_mode) and info.st_nlink != 1:
524
+ raise InstalledLifecycleError(f"instance tree contains a non-exclusive hardlinked file: {path}")
511
525
  entries.append({
512
526
  "path": str(path.relative_to(root)), "mode": stat.S_IFMT(info.st_mode),
527
+ "permissions": stat.S_IMODE(info.st_mode),
528
+ "links": info.st_nlink if stat.S_ISREG(info.st_mode) else None,
513
529
  "size": info.st_size if stat.S_ISREG(info.st_mode) else None,
514
530
  })
515
531
  return _digest_bytes(_json_bytes(entries))
@@ -22,6 +22,7 @@ from .installed_lifecycle import (
22
22
  _assert_identity,
23
23
  _assert_no_symlink_components,
24
24
  _assert_quiescent,
25
+ _assert_tree_has_no_links,
25
26
  _atomic_json,
26
27
  _digest_bytes,
27
28
  _exclusive_locks,
@@ -41,8 +42,8 @@ PROVIDER_AUTH_PRESERVED = "PRESERVED_REQUIRES_REVERIFICATION"
41
42
 
42
43
 
43
44
  def _content_tree_digest(root: Path) -> str:
44
- """Bind every regular byte in one exact, link-free instance tree."""
45
- _assert_no_symlink_components(root)
45
+ """Bind every regular byte after shared exclusive-ownership admission."""
46
+ _assert_tree_has_no_links(root)
46
47
  entries: list[dict[str, object]] = []
47
48
  for directory, names, files in os.walk(root, topdown=True, followlinks=False):
48
49
  parent = Path(directory)
@@ -22,6 +22,7 @@ from urllib.request import Request, urlopen
22
22
  from forge._version import canonical_version
23
23
  from forge.ep_simulator import EpSimulatorServer, EpSimulatorState
24
24
  from forge.installed_lifecycle import (
25
+ InstalledLifecycleError,
25
26
  InstalledUninstallDispatcher,
26
27
  UninstallRequest,
27
28
  UpdateAssessmentRequest,
@@ -249,6 +250,7 @@ def run(*, candidate_wheel: Path | None = None, source_revision: str | None = No
249
250
  "preserve": "NOT_REQUESTED",
250
251
  "restore": "NOT_REQUESTED",
251
252
  "purge": "NOT_REQUESTED",
253
+ "filesystem_security": "NOT_REQUESTED",
252
254
  }
253
255
  if candidate_wheel is not None:
254
256
  if source_revision is None or len(source_revision) != 40:
@@ -265,6 +267,50 @@ def run(*, candidate_wheel: Path | None = None, source_revision: str | None = No
265
267
  raise RuntimeError("installed lifecycle update assessment did not bind the exact artifact")
266
268
  lifecycle["update_assessment"] = "PASS"
267
269
 
270
+ def unsafe_lifecycle_request(data_root: Path, operation_id: str) -> InstanceLifecycleRequest:
271
+ database = RuntimeBootstrap(data_root=data_root, forge_version=version).open()
272
+ try:
273
+ runtime_id = database.runtime_identity.runtime_id
274
+ installation_id = database.metadata["installation_id"]
275
+ finally:
276
+ database.close()
277
+ return InstanceLifecycleRequest(
278
+ operation_id=operation_id,
279
+ instance_id=runtime_id,
280
+ runtime_id=runtime_id,
281
+ installation_id=installation_id,
282
+ installed_version=version,
283
+ installed_source=source_revision,
284
+ installed_artifact_digest=wheel_digest,
285
+ data_root=str(data_root),
286
+ instances_root=str(root.resolve()),
287
+ )
288
+
289
+ hardlink_root = root / "unsafe-hardlink-instance"
290
+ hardlink_request = unsafe_lifecycle_request(hardlink_root, "installed-hardlink-negative")
291
+ foreign = root / "foreign-hardlink-source"
292
+ foreign.write_text("foreign", encoding="utf-8")
293
+ (hardlink_root / "unsafe-hardlink").hardlink_to(foreign)
294
+ try:
295
+ InstalledPreserveDispatcher(hardlink_request).run()
296
+ except InstalledLifecycleError as error:
297
+ if "hardlinked" not in str(error):
298
+ raise RuntimeError("installed lifecycle rejected hardlink for an unexpected reason") from error
299
+ else:
300
+ raise RuntimeError("installed lifecycle accepted a foreign hardlink")
301
+
302
+ permissive_root = root / "unsafe-permissive-instance"
303
+ permissive_request = unsafe_lifecycle_request(permissive_root, "installed-permissive-negative")
304
+ permissive_root.chmod(0o777)
305
+ try:
306
+ InstalledPreserveDispatcher(permissive_request).run()
307
+ except InstalledLifecycleError as error:
308
+ if "group/world writable" not in str(error):
309
+ raise RuntimeError("installed lifecycle rejected permissive root for an unexpected reason") from error
310
+ else:
311
+ raise RuntimeError("installed lifecycle accepted a group/world writable root")
312
+ lifecycle["filesystem_security"] = "PASS"
313
+
268
314
  preserve_request = InstanceLifecycleRequest(
269
315
  operation_id="forge-server-runtime-installed-preserve",
270
316
  instance_id=identities[0],
@@ -399,6 +445,7 @@ def run(*, candidate_wheel: Path | None = None, source_revision: str | None = No
399
445
  "lifecycle_preserve": lifecycle["preserve"],
400
446
  "lifecycle_restore": lifecycle["restore"],
401
447
  "lifecycle_purge": lifecycle["purge"],
448
+ "lifecycle_filesystem_security": lifecycle["filesystem_security"],
402
449
  }
403
450
 
404
451
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forge-autonomy
3
- Version: 2.7.36
3
+ Version: 2.7.38
4
4
  Summary: Forge mission planning, producer contracts, and evidence interpretation runtime.
5
5
  Author: Forge maintainers
6
6
  License-Expression: Apache-2.0
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "product": "forge",
3
3
  "schema_version": 1,
4
- "version": "2.7.36"
4
+ "version": "2.7.38"
5
5
  }
@@ -3,8 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import base64
6
+ from contextlib import redirect_stdout
6
7
  import importlib.util
7
8
  from hashlib import sha256
9
+ from io import StringIO
8
10
  import json
9
11
  import os
10
12
  from pathlib import Path
@@ -18,6 +20,7 @@ import zipfile
18
20
 
19
21
  from forge.runtime import RuntimeBootstrap
20
22
  import forge.runtime.database as runtime_database
23
+ from forge.installed_lifecycle import SUPPORTED_TRANSITIONS as PACKAGED_TRANSITIONS
21
24
  from forge.runtime.operational_reset import MAINTENANCE_TABLES
22
25
 
23
26
 
@@ -294,7 +297,7 @@ class InstalledForgeUpdateTests(unittest.TestCase):
294
297
  "github_release": {"draft": False},
295
298
  },
296
299
  }, sort_keys=True), encoding="utf-8")
297
- if version in {"2.7.25", "2.7.26", "2.7.27", "2.7.28", "2.7.29", "2.7.30", "2.7.31", "2.7.32", "2.7.33", "2.7.34", "2.7.35"}:
300
+ if version in {"2.7.25", "2.7.26", "2.7.27", "2.7.28", "2.7.29", "2.7.30", "2.7.31", "2.7.32", "2.7.33", "2.7.34", "2.7.35", "2.7.38"}:
298
301
  # Captured from the actual installed-composition command used by
299
302
  # both workflow stages; only the synthetic wheel binding changes.
300
303
  summary = json.loads((Path(__file__).parent / "fixtures" /
@@ -304,7 +307,7 @@ class InstalledForgeUpdateTests(unittest.TestCase):
304
307
  document = json.loads(receipt.read_text())
305
308
  document["qualification"]["criterion_completion"] = summary
306
309
  document["publication_receipt"]["criterion_completion"] = summary
307
- if version in {"2.7.34", "2.7.35"}:
310
+ if version in {"2.7.34", "2.7.35", "2.7.38"}:
308
311
  server_runtime = {
309
312
  "qualification": "FORGE_SERVER_RUNTIME_V1_INSTALLED_ARTIFACT",
310
313
  "version": version,
@@ -317,12 +320,19 @@ class InstalledForgeUpdateTests(unittest.TestCase):
317
320
  "production_ep_contacted": False,
318
321
  "production_provider_contacted": False,
319
322
  }
320
- if version == "2.7.35":
323
+ if version in {"2.7.35", "2.7.38"}:
321
324
  server_runtime.update({
322
325
  "lifecycle_update_assessment": "PASS",
323
326
  "lifecycle_uninstall_dispatcher": "PASS",
324
327
  "lifecycle_uninstall_replay": "PASS",
325
328
  })
329
+ if version == "2.7.38":
330
+ server_runtime.update({
331
+ "lifecycle_preserve": "PASS",
332
+ "lifecycle_restore": "PASS",
333
+ "lifecycle_purge": "PASS",
334
+ "lifecycle_filesystem_security": "PASS",
335
+ })
326
336
  document["qualification"]["server_runtime"] = server_runtime
327
337
  document["publication_receipt"]["server_runtime"] = server_runtime
328
338
  receipt.write_text(json.dumps(document, sort_keys=True))
@@ -339,6 +349,364 @@ class InstalledForgeUpdateTests(unittest.TestCase):
339
349
  })
340
350
  return request
341
351
 
352
+ def test_standalone_transition_registry_matches_packaged_policy(self):
353
+ self.assertEqual(update.SUPPORTED_TRANSITIONS, PACKAGED_TRANSITIONS)
354
+
355
+ def test_2735_2736_and_2737_have_direct_same_schema_paths_to_2738(self):
356
+ for existing in ("2.7.35", "2.7.36", "2.7.37"):
357
+ with self.subTest(existing=existing):
358
+ request = self._normal_release_request("2.7.38", existing)
359
+ request = update.UpdateRequest(**{
360
+ **request.__dict__,
361
+ "installed_source": "d" * 40,
362
+ "installed_artifact_digest": "sha256:" + "e" * 64,
363
+ "assessment_digest": "sha256:" + "f" * 64,
364
+ })
365
+ self.assertEqual(update.transition_schemas(request), (39, 39))
366
+ self.assertEqual(update.validate_qualified_artifact(request)["release_route"], "NORMAL")
367
+
368
+ def _assessment_request_2738(self, existing_version="2.7.35"):
369
+ self._same_schema39_transition(
370
+ existing_version=existing_version,
371
+ target_version="2.7.38",
372
+ )
373
+ self.request = update.UpdateRequest(**{
374
+ **self.request.__dict__,
375
+ "installed_source": "d" * 40,
376
+ "installed_artifact_digest": "sha256:" + "e" * 64,
377
+ })
378
+ return self.request
379
+
380
+ def test_2738_standalone_assessment_is_read_only_and_exactly_bound(self):
381
+ request = self._assessment_request_2738()
382
+ before = {
383
+ path.relative_to(self.data_root).as_posix(): update.file_digest(path)
384
+ for path in self.data_root.rglob("*") if path.is_file()
385
+ }
386
+ result = update.assess_update(request)
387
+ after = {
388
+ path.relative_to(self.data_root).as_posix(): update.file_digest(path)
389
+ for path in self.data_root.rglob("*") if path.is_file()
390
+ }
391
+ self.assertEqual(result["state"], "UPDATE_AVAILABLE")
392
+ self.assertFalse(result["mutating"])
393
+ self.assertEqual(result["operation_id"], request.operation_id)
394
+ self.assertEqual(result["request_digest"], request.assessment_request_digest)
395
+ self.assertEqual(result["selected_installation"]["source_revision"], "d" * 40)
396
+ self.assertEqual(result["candidate"]["version"], "2.7.38")
397
+ self.assertEqual(before, after)
398
+
399
+ def test_2738_stale_assessment_fails_before_operation_or_resolver_effect(self):
400
+ request = self._assessment_request_2738()
401
+ assessment = update.assess_update(request)
402
+ stale = update.UpdateRequest(**{
403
+ **request.__dict__,
404
+ "installed_source": "f" * 40,
405
+ "assessment_digest": assessment["assessment_digest"],
406
+ })
407
+ resolver_before = self.resolver.read_bytes()
408
+ controller = update.InstalledForgeUpdateController(stale, process_reader=lambda: ())
409
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "assessment"):
410
+ controller.run()
411
+ self.assertFalse(controller.operation_root.exists())
412
+ self.assertEqual(self.resolver.read_bytes(), resolver_before)
413
+
414
+ def test_2738_assessment_rejects_permissive_and_hardlinked_instance_entries(self):
415
+ request = self._assessment_request_2738()
416
+ unsafe = self.data_root / "unsafe-file"
417
+ unsafe.write_text("unsafe", encoding="utf-8")
418
+ unsafe.chmod(0o666)
419
+ result = update.assess_update(request)
420
+ self.assertEqual(result["state"], "UNKNOWN")
421
+ self.assertIn("group/world-writable", result["evidence"]["error"])
422
+ unsafe.chmod(0o600)
423
+ foreign = self.root / "foreign"
424
+ foreign.write_text("foreign", encoding="utf-8")
425
+ unsafe.unlink()
426
+ unsafe.hardlink_to(foreign)
427
+ result = update.assess_update(request)
428
+ self.assertEqual(result["state"], "UNKNOWN")
429
+ self.assertIn("hardlinked", result["evidence"]["error"])
430
+
431
+ def test_2738_assessment_schema_and_foreign_artifact_controls_fail_closed(self):
432
+ request = self._assessment_request_2738()
433
+ with sqlite3.connect(self.data_root / "forge.db") as connection:
434
+ connection.execute("PRAGMA user_version=38")
435
+ schema = update.assess_update(request)
436
+ self.assertEqual(schema["state"], "INCOMPATIBLE")
437
+ self.assertEqual(schema["reason_codes"], ["RUNTIME_SCHEMA_OUTSIDE_BOUNDED_TRANSITION"])
438
+ changed = update.UpdateRequest(**{
439
+ **request.__dict__,
440
+ "wheel_sha256": "sha256:" + "0" * 64,
441
+ })
442
+ foreign = update.assess_update(changed)
443
+ self.assertEqual(foreign["state"], "UNKNOWN")
444
+ self.assertIn("wheel digest", foreign["evidence"]["error"])
445
+
446
+ def test_2738_run_orchestration_binds_assessment_and_terminal_receipt(self):
447
+ request = self._assessment_request_2738()
448
+ assessment = update.assess_update(request)
449
+ request = update.UpdateRequest(**{
450
+ **request.__dict__, "assessment_digest": assessment["assessment_digest"],
451
+ })
452
+ controller = update.InstalledForgeUpdateController(request, process_reader=lambda: ())
453
+ before = update.database_snapshot(self.data_root / "forge.db")
454
+
455
+ def advance(state, phase, **evidence):
456
+ return {**state, **evidence, "phase": phase}
457
+
458
+ with (
459
+ patch.object(update, "reconcile_terminal_dispatcher_for_update",
460
+ side_effect=lambda _request, snapshot, _database: snapshot),
461
+ patch.object(controller, "_stage", side_effect=lambda state: advance(state, "STAGED")),
462
+ patch.object(controller, "_adopt_resolver",
463
+ side_effect=lambda state: advance(state, "ADOPTED", before=before)),
464
+ patch.object(controller, "_backup", side_effect=lambda state, _before: advance(
465
+ state, "BACKED_UP", backup={"status": "PASS", "path": str(controller.backup_path)},
466
+ )),
467
+ patch.object(controller, "_qualify_copy", side_effect=lambda state, _before: advance(
468
+ state, "MIGRATION_QUALIFIED", migration_qualification={"status": "PASS"},
469
+ )),
470
+ patch.object(controller, "_migrate_live", side_effect=lambda state, _before: (
471
+ advance(state, "MIGRATED", live_migration={"status": "PASS"}), before,
472
+ )),
473
+ patch.object(controller, "_activate", side_effect=lambda state, _after: advance(
474
+ state, "ACTIVATED", installed_readback={
475
+ "preservation": {"status": "PASS"},
476
+ "database_snapshot_digest": before["snapshot_digest"],
477
+ },
478
+ )),
479
+ ):
480
+ receipt = controller.run()
481
+
482
+ self.assertEqual(receipt["state"], "COMPLETE")
483
+ self.assertEqual(receipt["assessment_digest"], assessment["assessment_digest"])
484
+ self.assertEqual(receipt["installed_source"], request.installed_source)
485
+ self.assertEqual(receipt["installed_artifact_digest"], request.installed_artifact_digest)
486
+ self.assertEqual(receipt["credential_disposition"], "PRESERVED_UNCHANGED")
487
+ self.assertEqual(update._read_json(controller.receipt_path), receipt)
488
+ self.assertEqual(update._read_json(controller.state_path)["phase"], "COMPLETE")
489
+
490
+ def test_cli_assessment_success_mutation_success_and_error_are_machine_readable(self):
491
+ request = self._assessment_request_2738()
492
+ options = {
493
+ "operation-id": request.operation_id,
494
+ "version": request.version,
495
+ "product-source": request.product_source,
496
+ "wheel": request.wheel,
497
+ "wheel-sha256": request.wheel_sha256,
498
+ "qualification-receipt": request.qualification_receipt,
499
+ "qualification-receipt-sha256": request.qualification_receipt_sha256,
500
+ "controller-source": request.controller_source,
501
+ "controller-sha256": request.controller_sha256,
502
+ "data-root": request.data_root,
503
+ "runtime-root": request.runtime_root,
504
+ "runtime-id": request.runtime_id,
505
+ "installation-id": request.installation_id,
506
+ "peer-configuration-digest": request.peer_configuration_digest,
507
+ "resolver": request.resolver,
508
+ "resolver-sha256": request.resolver_sha256,
509
+ "existing-interpreter": request.existing_interpreter,
510
+ "existing-version": request.existing_version,
511
+ "base-python": request.base_python,
512
+ "installed-source": request.installed_source,
513
+ "installed-artifact-digest": request.installed_artifact_digest,
514
+ }
515
+ argv = [item for key, value in options.items() for item in (f"--{key}", value)]
516
+ assessment = update.assess_update(request)
517
+ output = StringIO()
518
+ with redirect_stdout(output):
519
+ self.assertEqual(update.main([*argv, "--assess-only"]), 0)
520
+ self.assertEqual(json.loads(output.getvalue()), assessment)
521
+
522
+ mutation_argv = [*argv, "--assessment-digest", assessment["assessment_digest"]]
523
+ completed = {"state": "COMPLETE", "operation_id": request.operation_id}
524
+ output = StringIO()
525
+ with (
526
+ patch.object(update.InstalledForgeUpdateController, "run", return_value=completed),
527
+ redirect_stdout(output),
528
+ ):
529
+ self.assertEqual(update.main(mutation_argv), 0)
530
+ self.assertEqual(json.loads(output.getvalue()), completed)
531
+
532
+ output = StringIO()
533
+ with (
534
+ patch.object(update.InstalledForgeUpdateController, "run",
535
+ side_effect=update.InstalledForgeUpdateError("blocked")),
536
+ redirect_stdout(output),
537
+ ):
538
+ self.assertEqual(update.main([*mutation_argv, "--reconcile-staged-controller"]), 1)
539
+ self.assertEqual(json.loads(output.getvalue()), {"status": "ERROR", "error": "blocked"})
540
+
541
+ def test_controller_input_and_utility_failures_are_fail_closed(self):
542
+ invalid = (
543
+ ({"operation_id": "../escape"}, "filesystem-safe"),
544
+ ({"wheel_sha256": "bad"}, "wheel digest"),
545
+ ({"product_source": "short"}, "source revisions"),
546
+ ({"wheel": "relative.whl"}, "absolute"),
547
+ ({"version": "2.7.38", "installed_source": "short",
548
+ "installed_artifact_digest": "sha256:" + "e" * 64}, "installed source"),
549
+ ({"version": "2.7.38", "installed_source": "d" * 40,
550
+ "installed_artifact_digest": "bad"}, "installed artifact"),
551
+ )
552
+ for changes, message in invalid:
553
+ with self.subTest(changes=changes):
554
+ request = update.UpdateRequest(**{**self.request.__dict__, **changes})
555
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, message):
556
+ request.validate_structure()
557
+
558
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "absolute"):
559
+ update._safe_directory(Path("relative"))
560
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "unavailable"):
561
+ update._safe_directory(self.root / "missing")
562
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "not a symlink"):
563
+ update._resolved_link(self.resolver)
564
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "required regular file"):
565
+ update.file_digest(self.root / "missing.bin")
566
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "required regular file"):
567
+ update._read_regular_bytes(self.root)
568
+
569
+ guarded = self.root / "guarded-tree"
570
+ guarded.mkdir()
571
+ (guarded / "link").symlink_to(self.resolver)
572
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "symbolic link"):
573
+ update._assert_private_owned_tree(guarded)
574
+ (guarded / "link").unlink()
575
+ os.mkfifo(guarded / "special")
576
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "special entry"):
577
+ update._assert_private_owned_tree(guarded)
578
+
579
+ malformed = self.root / "malformed.json"
580
+ malformed.write_text("{", encoding="utf-8")
581
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "unreadable"):
582
+ update._read_json(malformed)
583
+ malformed.write_text("[]", encoding="utf-8")
584
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "not an object"):
585
+ update._read_json(malformed)
586
+
587
+ failure = update.subprocess.CalledProcessError(
588
+ 1, ["tool"], output="bounded stdout", stderr="bounded stderr",
589
+ )
590
+ with patch.object(update.subprocess, "run", side_effect=failure):
591
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "bounded stderr"):
592
+ update._run(("tool",), cwd=self.root)
593
+ with patch.object(update, "_run", return_value=SimpleNamespace(stdout="not-json")):
594
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "malformed"):
595
+ update.installed_identity(Path(sys.executable), cwd=self.root)
596
+ with patch.object(update, "_run", return_value=SimpleNamespace(stdout="[]")):
597
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "malformed"):
598
+ update.installed_identity(Path(sys.executable), cwd=self.root)
599
+
600
+ def test_validated_wheel_installation_is_exact_and_tamper_evident(self):
601
+ wheel_bytes, manifest = update._validated_wheel(self.request)
602
+ slot = self.root / "candidate-slot"
603
+ site_packages = slot / "lib" / "python3.14" / "site-packages"
604
+ site_packages.mkdir(parents=True)
605
+ (slot / "bin").mkdir()
606
+ update._install_validated_wheel(slot, wheel_bytes, manifest)
607
+ evidence = update._verify_candidate_files(slot, manifest)
608
+ self.assertEqual(evidence["installed_file_count"], len(manifest))
609
+ self.assertEqual((slot / "bin" / "forge").stat().st_mode & 0o777, 0o700)
610
+ (site_packages / "forge" / "__init__.py").write_text("tampered", encoding="utf-8")
611
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "changed="):
612
+ update._verify_candidate_files(slot, manifest)
613
+
614
+ def test_2738_qualification_copy_is_durable_and_replayable(self):
615
+ request = self._assessment_request_2738()
616
+ controller = update.InstalledForgeUpdateController(
617
+ update.UpdateRequest(**{
618
+ **request.__dict__,
619
+ "assessment_digest": update.assess_update(request)["assessment_digest"],
620
+ }),
621
+ process_reader=lambda: (),
622
+ )
623
+ before = update.database_snapshot(self.data_root / "forge.db")
624
+ update._copy_sqlite_backup(self.data_root / "forge.db", controller.backup_path)
625
+ state = controller._state()
626
+ with patch.object(update, "_candidate_migrate", return_value={
627
+ "initialized": True, "storage_schema": "39", "product_version": "2.7.38",
628
+ }):
629
+ qualified = controller._qualify_copy(state, before)
630
+ replayed = controller._qualify_copy(qualified, before)
631
+ self.assertEqual(qualified["phase"], "MIGRATION_QUALIFIED")
632
+ self.assertEqual(qualified["migration_qualification"]["status"], "PASS")
633
+ self.assertEqual(replayed, qualified)
634
+
635
+ def test_2738_activation_uses_exact_candidate_and_readiness_identity(self):
636
+ request = self._assessment_request_2738()
637
+ request = update.UpdateRequest(**{
638
+ **request.__dict__,
639
+ "assessment_digest": update.assess_update(request)["assessment_digest"],
640
+ })
641
+ controller = update.InstalledForgeUpdateController(request, process_reader=lambda: ())
642
+ candidate = controller.slot / "bin" / "forge"
643
+ candidate.parent.mkdir(parents=True)
644
+ candidate.write_text("candidate", encoding="utf-8")
645
+ python = controller.slot / "bin" / "python"
646
+ python.write_text("python", encoding="utf-8")
647
+ controller.stable_resolver.parent.mkdir(parents=True)
648
+ controller.stable_resolver.symlink_to("../current")
649
+ self.resolver.unlink()
650
+ self.resolver.symlink_to(controller.stable_resolver)
651
+ before = update.database_snapshot(self.data_root / "forge.db")
652
+ state = {"phase": "MIGRATED", "before": before}
653
+ identity = {
654
+ "version": "2.7.38",
655
+ "distribution_version": "2.7.38",
656
+ "sys_executable": str(python),
657
+ "module": str(controller.slot / "forge" / "__init__.py"),
658
+ }
659
+ status = {
660
+ "product_version": "2.7.38",
661
+ "data_root": str(self.data_root),
662
+ "instance_id": self.runtime_id,
663
+ "storage_schema": "39",
664
+ }
665
+ with (
666
+ patch.object(update, "installed_identity", return_value=identity),
667
+ patch.object(update, "_run", side_effect=(
668
+ SimpleNamespace(stdout="2.7.38\n"),
669
+ SimpleNamespace(stdout=json.dumps(status)),
670
+ )),
671
+ patch.object(update, "verify_preservation", return_value={"status": "PASS"}),
672
+ ):
673
+ activated = controller._activate(state, before)
674
+ self.assertEqual(activated["phase"], "ACTIVATED")
675
+ self.assertEqual(activated["installed_readback"]["status"], status)
676
+ self.assertEqual(self.resolver.resolve(), candidate.resolve())
677
+
678
+ def test_completed_replay_rechecks_receipt_backup_and_slot_evidence(self):
679
+ request = self._assessment_request_2738()
680
+ request = update.UpdateRequest(**{
681
+ **request.__dict__,
682
+ "assessment_digest": update.assess_update(request)["assessment_digest"],
683
+ })
684
+ controller = update.InstalledForgeUpdateController(request, process_reader=lambda: ())
685
+ update._copy_sqlite_backup(self.data_root / "forge.db", controller.backup_path)
686
+ receipt = {
687
+ "state": "COMPLETE",
688
+ "request_digest": request.digest,
689
+ "backup": {
690
+ "path": str(controller.backup_path),
691
+ "sha256": update.file_digest(controller.backup_path),
692
+ },
693
+ }
694
+ update._atomic_json(controller.receipt_path, receipt)
695
+ state = {
696
+ "phase": "COMPLETE",
697
+ "request_digest": request.digest,
698
+ "receipt_sha256": update.file_digest(controller.receipt_path),
699
+ }
700
+ with patch.object(update, "_qualified_artifact", return_value=(
701
+ {"wheel_manifest_digest": "sha256:" + "a" * 64}, b"", {},
702
+ )):
703
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "required regular file"):
704
+ controller._verify_complete(state, receipt)
705
+
706
+ controller.database = self.root / "missing-forge.db"
707
+ with self.assertRaisesRegex(update.InstalledForgeUpdateError, "database is unavailable"):
708
+ controller._restore_database_writable()
709
+
342
710
  def test_2725_requires_exact_qualifying_and_published_installed_composition(self):
343
711
  request = self._normal_release_request("2.7.25", "2.7.24")
344
712
  self.assertEqual(update.validate_qualified_artifact(request)["release_route"], "NORMAL")
@@ -395,8 +763,12 @@ class InstalledForgeUpdateTests(unittest.TestCase):
395
763
  wrong_transition = update.UpdateRequest(**{
396
764
  **request.__dict__, "existing_version": "2.7.21",
397
765
  })
766
+ self.assertEqual(
767
+ update.validate_qualified_artifact(wrong_transition)["release_route"],
768
+ "NORMAL",
769
+ )
398
770
  with self.assertRaises(update.InstalledForgeUpdateError):
399
- update.validate_qualified_artifact(wrong_transition)
771
+ wrong_transition.validate()
400
772
 
401
773
  def test_real_schema37_to_38_migration_preserves_history_and_bindings(self) -> None:
402
774
  self._installed_schema37()
@@ -137,6 +137,23 @@ class UpdateAssessmentTests(InstalledLifecycleFixture):
137
137
  def test_valid_unsupported_transition_is_incompatible(self) -> None:
138
138
  self.assertEqual(self.assessment("3.0.0")["state"], "INCOMPATIBLE")
139
139
 
140
+ def test_declared_server_baselines_assess_directly_to_2738(self) -> None:
141
+ wheel, digest = _write_wheel(self.root, "2.7.38")
142
+ for installed in ("2.7.35", "2.7.36", "2.7.37"):
143
+ data_root = self.instances_root / installed
144
+ database = RuntimeBootstrap(data_root=data_root, forge_version=installed).open()
145
+ runtime_id = database.runtime_identity.runtime_id
146
+ installation_id = database.metadata["installation_id"]
147
+ database.close()
148
+ result = assess_update(UpdateAssessmentRequest(
149
+ data_root=str(data_root), runtime_id=runtime_id, installation_id=installation_id,
150
+ installed_version=installed, installed_source="a" * 40,
151
+ installed_artifact_digest="sha256:" + "b" * 64,
152
+ candidate_version="2.7.38", candidate_source="c" * 40,
153
+ candidate_wheel=str(wheel), candidate_artifact_digest=digest,
154
+ ))
155
+ self.assertEqual(result["state"], "UPDATE_AVAILABLE", installed)
156
+
140
157
  def test_identity_or_artifact_uncertainty_fails_closed(self) -> None:
141
158
  self.assertEqual(self.assessment(runtime_id="runtime-wrong")["state"], "UNKNOWN")
142
159
  result = self.assessment(candidate_artifact_digest="sha256:" + "0" * 64)
@@ -251,6 +268,35 @@ class UninstallDispatcherTests(InstalledLifecycleFixture):
251
268
  InstalledUninstallDispatcher(self.uninstall_request()).run()
252
269
  self.assertTrue(self.data_root.exists())
253
270
 
271
+ def test_hardlinked_file_fails_closed(self) -> None:
272
+ foreign = self.root / "foreign-hardlink-source"
273
+ foreign.write_text("foreign", encoding="utf-8")
274
+ (self.data_root / "unsafe-hardlink").hardlink_to(foreign)
275
+ self.assertEqual((self.data_root / "unsafe-hardlink").stat().st_nlink, 2)
276
+ with self.assertRaisesRegex(InstalledLifecycleError, "hardlinked"):
277
+ InstalledUninstallDispatcher(self.uninstall_request()).run()
278
+ self.assertTrue(self.data_root.exists())
279
+
280
+ def test_group_or_world_writable_tree_fails_closed(self) -> None:
281
+ self.data_root.chmod(0o777)
282
+ with self.assertRaisesRegex(InstalledLifecycleError, "group/world writable"):
283
+ InstalledUninstallDispatcher(self.uninstall_request(operation_id="unsafe-root")).run()
284
+ self.data_root.chmod(0o700)
285
+
286
+ unsafe_directory = self.data_root / "unsafe-directory"
287
+ unsafe_directory.mkdir(mode=0o700)
288
+ unsafe_directory.chmod(0o772)
289
+ with self.assertRaisesRegex(InstalledLifecycleError, "group/world writable"):
290
+ InstalledUninstallDispatcher(self.uninstall_request(operation_id="unsafe-directory")).run()
291
+ unsafe_directory.chmod(0o700)
292
+
293
+ unsafe_file = self.data_root / "unsafe-file"
294
+ unsafe_file.write_text("unsafe", encoding="utf-8")
295
+ unsafe_file.chmod(0o666)
296
+ with self.assertRaisesRegex(InstalledLifecycleError, "group/world writable"):
297
+ InstalledUninstallDispatcher(self.uninstall_request(operation_id="unsafe-file")).run()
298
+ self.assertTrue(self.data_root.exists())
299
+
254
300
  @unittest.skipIf(fcntl is None, "POSIX lock qualification")
255
301
  def test_live_server_lock_blocks_uninstall(self) -> None:
256
302
  lock = (self.data_root / "locks" / "forge-server-runtime.lock").open("a+")